作者:小牛呼噜噜 | https://xiaoniuhululu.github.io
计算机内功、源码解析、科技故事、项目实战、面试八股等更多硬核文章,首发于公众号「小牛呼噜噜」
哈喽,大家好呀,我是呼噜噜,好久没有更新old linux了,本文接着上一篇文章Linux0.12内核源码解读(6)-main.c
,继续我们的内核探索之路
trap_init()
上一篇我们讲解了main.c
中的mem_init()
,我们接着来看看trap_init陷阱门初始化
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| mem_init(main_memory_start,memory_end); trap_init(); blk_dev_init(); chr_dev_init(); tty_init(); time_init(); sched_init(); buffer_init(buffer_memory_end); hd_init(); floppy_init(); sti(); move_to_user_mode(); if (!fork()) { init(); }
|
trap_init()
具体实现是在kernel/traps.c
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| 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); void alignment_check(void);
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); 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); set_trap_gate(17,&alignment_check); for (i=18;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); }
|
上面这一大段是陷阱中断程序初始化的子程序,是不是看起来很多没有头绪
set_trap_gate和set_system_gate
别怕都是纸老虎,我们先来看第一个set_trap_gate(0,÷_error)
,set_trap_gate()
函数的具体实现是在/include/asm/system.h
文件中,这部分的汇编还是写的非常绕的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #define _set_gate(gate_addr,type,dpl,addr) \ __asm__ ( "movw %%dx,%%ax\n\t" \ "movw %0,%%dx\n\t" \ "movl %%eax,%1\n\t" \ "movl %%edx,%2" \ : \ : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ "o" (*((char *) (gate_addr))), \ "o" (*(4+(char *) (gate_addr))), \ "d" ((char *) (addr)),"a" (0x00080000))
#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)
|
_set_gate
这个宏是内联汇编的写法,通俗点讲就是在C语言中嵌入汇编指令,将操作数统一编号来引用,比如这里的%0,%1,%2
;%%
是因为GCC在编译时会将%
视为特殊字符,%%
仅仅是为了汇编的%
不被GCC全部转译
另外i,o,d,a
表示寄存器加载代码,”i”表示立即数,”o”表示使用内存地址并可以加偏移值,”d”使用寄存器edx,”a”使用寄存器eax,这段汇编参数如下:
1 2 3 4 5 6 7 8
| 参数:gate_addr -描述符地址;type -描述符类型;dpl -描述符特权级;addr -偏移地址
%0 - (由dpl,type 组合成的类型标志字); %1 - (描述符低4 字节地址); %2 - (描述符高4 字节地址); %3 - edx(程序偏移地址,(char *) (addr)); %4 - eax(高字中含有段选择符,0x00080000)
|
其中&idt[n]
不就是操作系统前面设置的中断描述符表IDT嘛,IDT 是一个 8 字节描述符数组(在保护模式下),最多有256个中断或异常向量,它记录着0~255的中断号和调用函数之间的关系
再结合上图是IDT的结构参数图,所以这里我们可以知道set_trap_gate
函数中参数n代表中断向量号,参数addr代表中断服务函数的地址,addr
是异常处理函数入口点的偏移地址,因为内核代码段的线性基址是0,所以偏移地址就是线性地址,我们知道1M内的低端内存线性地址就等于物理地址,所以addr就是中断服务函数的地址
movw %%dx,%%ax
表示把低16位的dx值赋值给低16位的ax中,此时eax中的值就是0x0008+addr偏移地址。0x0008
是段选择符(选择子),二进制就是0b1000
,索引是1(索引0默认是null)
, TI=0(GDT)
, 请求特权级别RPL=0
,其中TI=0
选择子是在GDT中,TI=1
选择子是在LDT中;最终表示在GDT中查找,索引为1的值。
"a" (0x00080000)
把0x00080000
放到eax;高16位0x0008
是段选择符,低16位会被放在edx中的过程偏移低16位代替,目前就是0,那为什么设置这个值呢?
笔者翻了下之前的源码,发现在head.s中有这相关的参数设置,当时还不太理解movl $0x00080000,%eax
,原来是和这样前后呼应了啊
1 2 3 4 5 6 7 8 9 10 11 12
| setup_idt: lea ignore_int,%edx #将 ignore_int 的有效地址(偏移值)值 赋值给 edx 寄存器 movl $0x00080000,%eax # 将段选择符 0x0008 置入 eax 的高 16 位中!!!
movw %dx,%ax # 偏移值的低 16 位置入 eax 的低 16 位中。此时 eax 含有门描述符低 4 字节的值 movw $0x8E00,%dx #此时 edx 含有门描述符高 4 字节的值
lea _idt,%edi # _idt 是中断描述符表的地址, 取idt的偏移给edi mov $256,%ecx #循环256次
|
_set_gate(gate_addr,type,dpl,addr)
此宏就是用于设置门描述符,即填写IDT数组中的一项;所以set_trap_gate(0,÷_error)
就是设置int0中断描述符
,其对应的中断函数divide_error
,divide_error
函数我们暂时不讲,稍后下文再讲
我们再回到源码处,发现后面的代码其实都是类似的,例如
- 30~47行(这里的行数仅以本文截取的代码段行数为准): 依此设置IDT的某一项中断描述符,
set_trap_gate、set_system_gate
函数引用了同一个宏定义:_set_gate
,它们的主要区别就是前者的特权级DPL = 0,后者的特权级DPL = 3
,相同的是type=15
表示陷阱门描述符,IDT还可以存放其他类型的门描述符,比如调用门、中断门、任务门等。这里我们先做简单介绍,后面系列文章我们再详细讲,敬请期待
- 48~49行:把
int17 ~ int48
的陷阱门先设置为reserved
,后面各个硬件初始化时会重新设置自己的陷阱门
- 50~53行:设置协处理器
int45(0x20+13)
陷阱门描述符,并允许其产生中断请求;0x21
是 8259A 主片命令字OCW1的端口地址,用于对其中断屏蔽寄存器 IMR 进行读/写操作;0xA1
则是 8259A 从片命令字OCW1的端口地址
当执行完trap_init()
,我们来看下此时的内存分布图:
从divide_error来看中断处理函数的调用
当进行除以零的操作时产生中断,执行divide_error
,我们再回到set_trap_gate(0,÷_error)
中divide_error
这个函数定义在/kernel/asm.s
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| .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 .globl _alignment_check
_divide_error: pushl $_do_divide_error # 首先把将要调用的函数地址入栈;_do_divide_error是C函数do_divide_error被编译后的名字 no_error_code: # 保存被中断的进程的上下文 xchgl %eax,(%esp) #_do_divide_error的地址→eax,eax被交换入栈 pushl %ebx pushl %ecx pushl %edx pushl %edi pushl %esi pushl %ebp push %ds # 16 位的段寄存器入栈后也要占用 4 个字节 push %es push %fs pushl $0 # "error code",将数值 0 作为出错码入栈 lea 44(%esp),%edx # 取有效地址,即栈中原调用返回地址处的栈指针位置 pushl %edx # 并压入堆栈(即esp0 指针入栈)
# 所有段寄存器都设置为内核数据段选择符,设置好数据寻址的基址 movl $0x10,%edx # 初始化段寄存器ds、es和fs,加载内核数据段选择符 mov %dx,%ds mov %dx,%es mov %dx,%fs call *%eax #* 号表示调用操作数指定地址处的函数,称为间接调用,这里就是执行C语言函数do_divide_error
# 恢复被中断进程的上下文 addl $8,%esp pop %fs pop %es pop %ds popl %ebp popl %esi popl %edi popl %edx popl %ecx popl %ebx popl %eax # 弹出原来eax中的内容 iret # 返回中断处理之前的程序,继续执行后续指令
|
C语言函数do_divide_error
在kernel/traps.c
,它的参数都在栈中, esp
就是do_divide_error
调用前的栈顶,用于返回之前的状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| void do_divide_error(long esp, long error_code) { die("divide error",esp,error_code); }
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); }
|
其他中断函数都是类似的,比如debug,nmi,breakpoint
等,大家感兴趣地,可以自行去看看,这些中断处理函数的作用都列下图中:
根据上述源码,我们来总结一下linux内核是如何调用中断处理函数流程
需要注意的是,中断处理函数使用的栈都是内核的内存空间,而内核代码段的选择符值是0x08,早在前文gdt初始化中设置了; 若无出错码时就使用0;另外内核中的栈每次起始地址都是一样的,当函数调用结束后,栈又会变空,因此内核栈是可以重复利用的
参考资料:
https://elixir.bootlin.com/linux/0.12/source/init/main.c
https://elixir.bootlin.com/linux/0.12/source/kernel/traps.c
《Linux内核完全注释5.0》
英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A-英特尔®
系列文章目录
- 聊聊x86计算机启动发生的事?
- Linux0.12内核源码解读(2)-Bootsect.S
- Linux0.12内核源码解读(3)-Setup.S
- 图解CPU的实模式与保护模式
- Linux0.12内核源码解读(5)-head.s
- Linux0.12内核源码解读(6)-main.c
全文完,感谢您的阅读,如果我的文章对你有所帮助的话,还请点个免费的赞,你的支持会激励我输出更高质量的文章,感谢!
计算机内功、源码解析、科技故事、项目实战、面试八股等更多硬核文章,首发于公众号「小牛呼噜噜」,我们下期再见!