kernel源码(十)asm.s和trap.c
0 回顾
在main.c中我们对各个设备进行了初始化动作,并创建了进程0,通过进程0创建了其他进程,现在操作系统内核已经初步初始化完成。但是在main.c中,我们讲解时对于如何初始化没有展开细讲,因为这些初始化动作分布在内核代码的各个文件中,从这一篇文章开始,我们讲解内核初始化的各个细节,可能会持续多篇文章。
现在开始讲解硬件中断(注意这里是硬件中断)的设置过程
1 硬件中断的主要流程
当外设发生中断请求,中断请求会发送到8259A中断处理芯片,8259A芯片会暂存这个中断信号到自己的寄存器中,同时8259A芯片的输出引脚连接着CPU的INTR引脚,CPU在处理完当前指令后,会检查INTR引脚是否有中断信号到达,如果有中断信号到达,则CPU会发送信号给8259A的INTA引脚,8259A收到信号后,会把缓存的中断信号通过8259A的D0-d7引脚将中断信号发送到数据总线,CPU在数据总线上识别到中断信号,保护现场,中断描述符表idtr寄存器获取中断描述附表idt地址,在中断描述符表中根据中断类型获取中断处理程序地址(比如上面的÷_error),处理中断,处理完中断恢复现场。
在CPU的INTR引脚检测到中断信号的同时,在同一时钟周期中会把下图黄色的寄存器(原ss,原sp,原eflags,cs,ip)压入当前进程的内核栈(非用户栈)。然后,根据中断控制芯片8259A发来的中断向量号查找中断描述符表idt,得到中断处理程序地址并压入内核栈(esp1处)。然后 jmp no_error_code 跳转到这里执行,在no_error_code中保护现场(esp1-esp2处),然后执行 call *%eax 开始执行中断处理程序,执行完毕,恢复现场(恢复当前进程在用户空间的执行状态)。最后执行iret命令完成硬件中断,弹出下图黄色部分标志的寄存器,返回用户态当前进程中断前的状态。
2 Makefile
我们现在讲解kernel文件夹,该文件夹下有一个Makefile文件,这个文件描述了生成kernel.o的过程(kernel文件夹下的所有文件最终会被编译成一个kernel.o文件,这也是为什么asm.s能够调用trap.c中代码的原因)
AR =gar AS =gas LD =gld LDFLAGS =-s -x CC =gcc CFLAGS =-Wall -O -fstrength-reduce -fomit-frame-pointer -fcombine-regs \ -finline-functions -mstring-insns -nostdinc -I../include CPP =gcc -E -nostdinc -I../include .c.s: $(CC) $(CFLAGS) \ -S -o $*.s $< .s.o: $(AS) -c -o $*.o $< .c.o: $(CC) $(CFLAGS) \ -c -o $*.o $< OBJS = sched.o system_call.o traps.o asm.o fork.o \ panic.o printk.o vsprintf.o sys.o exit.o \ signal.o mktime.o kernel.o: $(OBJS) $(LD) -r -o kernel.o $(OBJS) sync
3 asm.s
在阅读接下来内容之前,我们先复习一下进程的内核栈。通过https://www.cnblogs.com/zhenjingcool/p/15996841.html 3.3.1 进程中的内核栈,我们可以了解到:一个用户进程有两个栈,分别是用户栈和内核栈,分别存在于用户空间和内核空间。当进程在用户空间运行时,CPU的ss和sp寄存器指向的是用户栈;当进程在内核空间运行时,ss和sp指向的是进程的内核栈。
当发生中断或系统调用而陷入内核态执行时,进程会将原ss(进程用户栈基址)、原sp(进程用户栈sp)、原eflags(进程用户栈eflags)压入内核栈,然后将ss、sp为内核栈地址,这就完成了用户态向内核态的转变。
当进程从内核态恢复到用户态执行时,在内核态执行的最后将保存在内核栈里面的原ss、原sp恢复,这样就实现了内核态向用户态的转变。
下面进行asm.s的讲解
asm.s这个程序包括了大部分cpu能够探测到的异常的处理代码
下图是发生中断时堆栈的变化,可帮助我们理解代码
注:上图黄色部分是硬件中断发生时CPU自动的行为。然后,中断发生时,CPU从中断控制器8092A中获取到中断号,然后内核查找中断向量表,找到中断处理函数压入栈中,也就是上图中的esp1处,然后esp1到esp2是保存现场。然后将error_code压入栈,接下来一步esp3处为什么这样做暂不清楚,好像不做这一步照样没问题。接下来执行call eax调用中断处理函数(call指令会将当前程序计数器PC的内容入栈,并将要调用的中断处理函数的地址送入PC,于是CPU的下一条指令就会转去执行中断处理函数)
call执行完毕后,返回到esp3处,然后通过+8到达esp2处,然后就是一系列pop指令,恢复现场。pop完毕后iret返回,至此,本次硬件中断结束。这里的中断处理函数都比较简单,只是打印一些信息,后面会对这些中断处理函数进行重写(比如向特定进程发送signal,然后通过软中断处理比较耗时的操作)。
源码
/* * linux/kernel/asm.s * * (C) 1991 Linus Torvalds */ /* * asm.s contains the low-level code for most hardware faults. * page_exception is handled by the mm, so that isn't here. This * file also handles (hopefully) fpu-exceptions due to TS-bit, as * the fpu must be properly saved/resored. This hasn't been tested. */ .globl _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op .globl _double_fault,_coprocessor_segment_overrun .globl _invalid_TSS,_segment_not_present,_stack_segment .globl _general_protection,_coprocessor_error,_irq13,_reserved _divide_error: pushl $_do_divide_error no_error_code: xchgl %eax,(%esp) pushl %ebx pushl %ecx pushl %edx pushl %edi pushl %esi pushl %ebp push %ds push %es push %fs pushl $0 # "error code" lea 44(%esp),%edx pushl %edx movl $0x10,%edx mov %dx,%ds mov %dx,%es mov %dx,%fs call *%eax addl $8,%esp pop %fs pop %es pop %ds popl %ebp popl %esi popl %edi popl %edx popl %ecx popl %ebx popl %eax iret _debug: pushl $_do_int3 # _do_debug jmp no_error_code _nmi: pushl $_do_nmi jmp no_error_code _int3: pushl $_do_int3 jmp no_error_code _overflow: pushl $_do_overflow jmp no_error_code _bounds: pushl $_do_bounds jmp no_error_code _invalid_op: pushl $_do_invalid_op jmp no_error_code _coprocessor_segment_overrun: pushl $_do_coprocessor_segment_overrun jmp no_error_code _reserved: pushl $_do_reserved jmp no_error_code _irq13: pushl %eax xorb %al,%al outb %al,$0xF0 movb $0x20,%al outb %al,$0x20 jmp 1f 1: jmp 1f 1: outb %al,$0xA0 popl %eax jmp _coprocessor_error _double_fault: pushl $_do_double_fault error_code: xchgl %eax,4(%esp) # error code <-> %eax xchgl %ebx,(%esp) # &function <-> %ebx pushl %ecx pushl %edx pushl %edi pushl %esi pushl %ebp push %ds push %es push %fs pushl %eax # error code lea 44(%esp),%eax # offset pushl %eax movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %ax,%fs call *%ebx addl $8,%esp pop %fs pop %es pop %ds popl %ebp popl %esi popl %edi popl %edx popl %ecx popl %ebx popl %eax iret _invalid_TSS: pushl $_do_invalid_TSS jmp error_code _segment_not_present: pushl $_do_segment_not_present jmp error_code _stack_segment: pushl $_do_stack_segment jmp error_code _general_protection: pushl $_do_general_protection jmp error_code
首先,定义了全局的标号
.globl _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op
.globl _double_fault,_coprocessor_segment_overrun
.globl _invalid_TSS,_segment_not_present,_stack_segment
.globl _general_protection,_coprocessor_error,_irq13,_reserved
0号中断_int0,把do_divide_error函数地址入栈(在trap.c中定义)(注:汇编中带下划线,c中不带下划线)
_divide_error: pushl $_do_divide_error
接着往下看,对照上图查看
no_error_code: xchgl %eax,(%esp) //交换eax和esp,交换后见上图显示。因为do_devide_error已经入栈,所以这里执行后esp位于图中esp1处,且堆栈里存放的是eax而且也是do_devide_error的地址 pushl %ebx //ebx入栈 pushl %ecx //ecx入栈 pushl %edx //edx入栈 pushl %edi //edi入栈 pushl %esi //esi入栈 pushl %ebp //ebp入栈 push %ds //ds入栈 push %es //es入栈 push %fs //fs入栈 pushl $0 # 错误码入栈,这里没有错误码,写0 lea 44(%esp),%edx //lea为取有效地址,esp+44赋给edx pushl %edx //edx入栈,edx指向esp0处,也就是指向发生中断前的位置。 movl $0x10,%edx mov %dx,%ds //代码段寄存器设置为0x10,在保护模式下段寄存器的唯一作用是存放段选择符,根据段选择符的结构,index=2,TI=0,RPL=0,我们知道这个数据段指向GDT的,1个段描述符(从0开始) mov %dx,%es //附加段寄存器设置为0x10 mov %dx,%fs //标志段寄存器设置为0x10 call *%eax //eax中存储的是do_divide_error的地址,因此,这里是调用do_divide_error函数 addl $8,%esp //do_divide_error函数返回后,esp+8(注意堆栈是向下生长的),esp+8后堆栈指针到达esp2处 pop %fs //弹出fs pop %es //弹出es pop %ds //弹出ds popl %ebp //弹出ebp popl %esi popl %edi popl %edx popl %ecx popl %ebx popl %eax iret //返回
1号中断,调试中断的入口点
_debug: pushl $_do_int3 # _do_debug jmp no_error_code
和0号中断类似,区别是执行的中断处理函数不再是do_divide_error,而是do_int3
2号中断
_nmi: pushl $_do_nmi jmp no_error_code
3号中断
_int3: pushl $_do_int3 jmp no_error_code
其他中断
_overflow: //4号中断 pushl $_do_overflow jmp no_error_code _bounds: //5号中断 pushl $_do_bounds jmp no_error_code _invalid_op: //6号中断 pushl $_do_invalid_op jmp no_error_code _coprocessor_segment_overrun: //9号中断 pushl $_do_coprocessor_segment_overrun jmp no_error_code _reserved: //其他保留的中断 pushl $_do_reserved jmp no_error_code
45号中断
_irq13: pushl %eax xorb %al,%al outb %al,$0xF0 movb $0x20,%al outb %al,$0x20 jmp 1f 1: jmp 1f 1: outb %al,$0xA0 popl %eax jmp _coprocessor_error
以上讲的中断都是无错误码的中断
下面开始是有错误码的中断
_double_fault: //8号中断_int8 pushl $_do_double_fault error_code: xchgl %eax,4(%esp) # 错误码放到eax中 xchgl %ebx,(%esp) # 函数地址放到ebx中 pushl %ecx pushl %edx pushl %edi pushl %esi pushl %ebp push %ds push %es push %fs pushl %eax # error code lea 44(%esp),%eax # offset pushl %eax movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %ax,%fs call *%ebx addl $8,%esp pop %fs pop %es pop %ds popl %ebp popl %esi popl %edi popl %edx popl %ecx popl %ebx popl %eax iret _invalid_TSS: //无效的任务状态段,10号中断 pushl $_do_invalid_TSS jmp error_code _segment_not_present: //段不存在,11号中断 pushl $_do_segment_not_present jmp error_code _stack_segment: //堆栈错误,12号中断 pushl $_do_stack_segment jmp error_code _general_protection: //13号中断,一般保护错误 pushl $_do_general_protection jmp error_code
总结:asm.s定义了几个中断的处理方式。
4 traps.c
该文件主要作用是实现陷阱门的初始化。
具体为:在head.s中我们对中断描述符表idt进行了初始化,其默认中断处理函数为ignore_int,即所有硬件中断都会使用这个中断处理程序处理,打印“Unknown interrupt”。
在本文件中,对一些陷阱门进一步初始化,更新中断描述符表idt,替代默认的中断处理函数为真正的中断处理函数。注意此处处理的是陷阱门和系统门,中断门在sched.c中初始化的(比如时钟中断:set_intr_gate(0x20,&timer_interrupt);)。
/* * linux/kernel/traps.c * * (C) 1991 Linus Torvalds */ /* * 'Traps.c' handles hardware traps and faults after we have saved some * state in 'asm.s'. Currently mostly a debugging-aid, will be extended * to mainly kill the offending process (probably by giving it a signal, * but possibly by killing it outright if necessary). */ #include <string.h> #include <linux/head.h> #include <linux/sched.h> #include <linux/kernel.h> #include <asm/system.h> #include <asm/segment.h> #include <asm/io.h> #define get_seg_byte(seg,addr) ({ \ register char __res; \ __asm__("push %%fs;mov %%ax,%%fs;movb %%fs:%2,%%al;pop %%fs" \ :"=a" (__res):"0" (seg),"m" (*(addr))); \ __res;}) #define get_seg_long(seg,addr) ({ \ register unsigned long __res; \ __asm__("push %%fs;mov %%ax,%%fs;movl %%fs:%2,%%eax;pop %%fs" \ :"=a" (__res):"0" (seg),"m" (*(addr))); \ __res;}) #define _fs() ({ \ register unsigned short __res; \ __asm__("mov %%fs,%%ax":"=a" (__res):); \ __res;}) int do_exit(long code); void page_exception(void); void divide_error(void); void debug(void); void nmi(void); void int3(void); void overflow(void); void bounds(void); void invalid_op(void); void device_not_available(void); void double_fault(void); void coprocessor_segment_overrun(void); void invalid_TSS(void); void segment_not_present(void); void stack_segment(void); void general_protection(void); void page_fault(void); void coprocessor_error(void); void reserved(void); void parallel_interrupt(void); void irq13(void); static void die(char * str,long esp_ptr,long nr) { long * esp = (long *) esp_ptr; int i; printk("%s: %04x\n\r",str,nr&0xffff); printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n", esp[1],esp[0],esp[2],esp[4],esp[3]); printk("fs: %04x\n",_fs()); printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17)); if (esp[4] == 0x17) { printk("Stack: "); for (i=0;i<4;i++) printk("%p ",get_seg_long(0x17,i+(long *)esp[3])); printk("\n"); } str(i); printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i); for(i=0;i<10;i++) printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0]))); printk("\n\r"); do_exit(11); /* play segment exception */ } void do_double_fault(long esp, long error_code) { die("double fault",esp,error_code); } void do_general_protection(long esp, long error_code) { die("general protection",esp,error_code); } void do_divide_error(long esp, long error_code) { die("divide error",esp,error_code); } void do_int3(long * esp, long error_code, long fs,long es,long ds, long ebp,long esi,long edi, long edx,long ecx,long ebx,long eax) { int tr; __asm__("str %%ax":"=a" (tr):"0" (0)); printk("eax\t\tebx\t\tecx\t\tedx\n\r%8x\t%8x\t%8x\t%8x\n\r", eax,ebx,ecx,edx); printk("esi\t\tedi\t\tebp\t\tesp\n\r%8x\t%8x\t%8x\t%8x\n\r", esi,edi,ebp,(long) esp); printk("\n\rds\tes\tfs\ttr\n\r%4x\t%4x\t%4x\t%4x\n\r", ds,es,fs,tr); printk("EIP: %8x CS: %4x EFLAGS: %8x\n\r",esp[0],esp[1],esp[2]); } void do_nmi(long esp, long error_code) { die("nmi",esp,error_code); } void do_debug(long esp, long error_code) { die("debug",esp,error_code); } void do_overflow(long esp, long error_code) { die("overflow",esp,error_code); } void do_bounds(long esp, long error_code) { die("bounds",esp,error_code); } void do_invalid_op(long esp, long error_code) { die("invalid operand",esp,error_code); } void do_device_not_available(long esp, long error_code) { die("device not available",esp,error_code); } void do_coprocessor_segment_overrun(long esp, long error_code) { die("coprocessor segment overrun",esp,error_code); } void do_invalid_TSS(long esp,long error_code) { die("invalid TSS",esp,error_code); } void do_segment_not_present(long esp,long error_code) { die("segment not present",esp,error_code); } void do_stack_segment(long esp,long error_code) { die("stack segment",esp,error_code); } void do_coprocessor_error(long esp, long error_code) { if (last_task_used_math != current) return; die("coprocessor error",esp,error_code); } void do_reserved(long esp, long error_code) { die("reserved (15,17-47) error",esp,error_code); } void trap_init(void) { int i; set_trap_gate(0,÷_error); set_trap_gate(1,&debug); set_trap_gate(2,&nmi); set_system_gate(3,&int3); /* int3-5 can be called from all */ set_system_gate(4,&overflow); set_system_gate(5,&bounds); set_trap_gate(6,&invalid_op); set_trap_gate(7,&device_not_available); set_trap_gate(8,&double_fault); set_trap_gate(9,&coprocessor_segment_overrun); set_trap_gate(10,&invalid_TSS); set_trap_gate(11,&segment_not_present); set_trap_gate(12,&stack_segment); set_trap_gate(13,&general_protection); set_trap_gate(14,&page_fault); set_trap_gate(15,&reserved); set_trap_gate(16,&coprocessor_error); for (i=17;i<48;i++) set_trap_gate(i,&reserved); set_trap_gate(45,&irq13); outb_p(inb_p(0x21)&0xfb,0x21); outb(inb_p(0xA1)&0xdf,0xA1); set_trap_gate(39,¶llel_interrupt); }
取段的一个字节
#define get_seg_byte(seg,addr) ({ \ register char __res; \ __asm__("push %%fs;mov %%ax,%%fs;movb %%fs:%2,%%al;pop %%fs" \ :"=a" (__res):"0" (seg),"m" (*(addr))); \ __res;})
程序退出的函数,这里是个函数原型,函数定义在exit.c中定义
int do_exit(long code);
页异常函数,在page相关的代码中定义
void page_exception(void);
下面这些函数原型,其定义都在本文件中
void divide_error(void); void debug(void); void nmi(void); void int3(void); void overflow(void); void bounds(void); void invalid_op(void); void device_not_available(void); void double_fault(void); void coprocessor_segment_overrun(void); void invalid_TSS(void); void segment_not_present(void); void stack_segment(void); void general_protection(void); void page_fault(void); void coprocessor_error(void); void reserved(void); void parallel_interrupt(void); void irq13(void);
下面这个函数是用来打印出错或中断信息的
static void die(char * str,long esp_ptr,long nr) { long * esp = (long *) esp_ptr; int i; printk("%s: %04x\n\r",str,nr&0xffff); printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n", esp[1],esp[0],esp[2],esp[4],esp[3]); printk("fs: %04x\n",_fs()); printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17)); if (esp[4] == 0x17) { printk("Stack: "); for (i=0;i<4;i++) printk("%p ",get_seg_long(0x17,i+(long *)esp[3])); printk("\n"); } str(i); printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i); for(i=0;i<10;i++) printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0]))); printk("\n\r"); do_exit(11); /* play segment exception */ }
下面的函数都是通过调用die函数对中断做出处理的
void do_double_fault(long esp, long error_code) { die("double fault",esp,error_code); } void do_general_protection(long esp, long error_code) { die("general protection",esp,error_code); } void do_divide_error(long esp, long error_code) { die("divide error",esp,error_code); }
在main.c中我们讲过,在main函数中调用了trap_init函数,这个函数就是在这里定义的。
在这个函数中,我们设置了各个中断的处理程序,比如 set_trap_gate(0,÷_error); 表示_int0的中断处理程序是 divide_error , divide_error 和其他中断处理程序都是在asm.s中定义的。 set_trap_gate 函数在其他文件中定义的,在以后讲解。
set_trap_gate 和 set_system_gate 的区别是,前者特权优先级为0,后者特权优先级为3.
void trap_init(void) { int i; set_trap_gate(0,÷_error); set_trap_gate(1,&debug); set_trap_gate(2,&nmi); set_system_gate(3,&int3); /* int3-5 can be called from all */ set_system_gate(4,&overflow); set_system_gate(5,&bounds); set_trap_gate(6,&invalid_op); set_trap_gate(7,&device_not_available); set_trap_gate(8,&double_fault); set_trap_gate(9,&coprocessor_segment_overrun); set_trap_gate(10,&invalid_TSS); set_trap_gate(11,&segment_not_present); set_trap_gate(12,&stack_segment); set_trap_gate(13,&general_protection); set_trap_gate(14,&page_fault); set_trap_gate(15,&reserved); set_trap_gate(16,&coprocessor_error); for (i=17;i<48;i++) set_trap_gate(i,&reserved); set_trap_gate(45,&irq13); outb_p(inb_p(0x21)&0xfb,0x21); //允许中断处理芯片8259A芯片的IRQ2中断请求 outb(inb_p(0xA1)&0xdf,0xA1); //允许8259A芯片的IRQ13 set_trap_gate(39,¶llel_interrupt); }
set_trap_gate在system.h中定义,如下。在这里设置idt表项。
#define set_intr_gate(n,addr) \ _set_gate(&idt[n],14,0,addr) #define set_trap_gate(n,addr) \ _set_gate(&idt[n],15,0,addr) #define set_system_gate(n,addr) \ _set_gate(&idt[n],15,3,addr)
我们把int17到48都设置为保留的
for (i=17;i<48;i++) set_trap_gate(i,&reserved);
至此,中断门初始化完成,当外设发生中断请求,中断请求会发送到8259A中断处理芯片,8259A芯片会暂存这个中断信号到自己的寄存器中,同时8259A芯片的输出引脚连接着CPU的INTR引脚,CPU在处理完当前指令后,会检查INTR引脚是否有中断信号到达,如果有中断信号到达,则CPU会发送信号给8259A的INTA引脚,8259A收到信号后,会把缓存的中断信号通过8259A的D0-d7引脚将中断信号发送到数据总线,CPU在数据总线上识别到中断信号,保护现场,中断描述符表idtr寄存器获取中断描述附表idt地址,在中断描述符表中根据中断类型获取中断处理程序地址(比如上面的÷_error),处理中断,处理完中断恢复现场。