0 源代码
有两个版本的,一个是带中文注释,Intel格式的;一个是不带注释是AT&T格式的。
Linux 0.11 中文注释版
Linux 0.11 源码,基于《Linux内核完全注释》赵炯
1 asm.s 文件
我们先假设该文件处理的中断是无特权过渡的情况(具体是不是暂时不知道)。
我们看一下,当中断发生且被检测到之后,硬件做了什么。
我们可以看到,中断被检测到之后,一些必要的信息被压栈了。
在80386手册中提到,中断发生之后,一些必要的,为了中断程序结束之后返回原程序继续执行的信息,会被压栈,就像最开始那个图看到的内样,这里分成了4种情况,我们只讨论无特权转换的两种情况,分别输有出错码的异常和无处错码的异常。
这个压栈过程是硬件完成的,回顾一下我们之前的知识
在中断发生之后,80386执行了
- 保存断点(将EFLAGS,CS,EIP依次压栈)
- 识别中断源
至于怎么通过中断号,找到对应的中断服务程序,我们后面说。
2 无处错码中断
源代码在kernel/asm.s文件中
_divide_error: # int0 除法错中断,中断错误码是0pushl $_do_divide_error # 用于打印除法错中断的错误信息的函数,在traps.c实现# 中断处理程序的地址入栈
no_error_code:# 保护现场xchgl %eax,(%esp) # eax的值入栈,eax保存中断处理程序地址pushl %ebxpushl %ecxpushl %edxpushl %edipushl %esipushl %ebppush %ds # 占4字节而非2字节push %espush %fspushl $0 # "error code" 中断有无错误码,是有相关规定的lea 44(%esp),%edxpushl %edx# 设置内核数据段选择符,设置好数据寻址的基址movl $0x10,%edxmov %dx,%dsmov %dx,%esmov %dx,%fscall *%eax # 执行中断处理程序,C语言函数do_divide_error# 恢复现场addl $8,%esppop %fspop %espop %dspopl %ebppopl %esipopl %edipopl %edxpopl %ecxpopl %ebxpopl %eaxiret # 返回中断处理之前的程序,继续执行后续指令_debug:pushl $_do_int3 # _do_debugjmp no_error_code
我们通过画堆栈内存图的方式分析一下这个过程。
2.1 检测中断后,硬件完成的事情
硬件完成的事情其实就是压栈,压栈之后的内存图是:
2.2 进入中断服务程序
在识别到中断源之后,通过某种方式,CPU获取的中断服务程序的CS:EIP
,进入了终端服务程序,我们这里以debug
为例子。
注意,所有的无出错号的中断,默认当成出错号是0,会在保护现场的时候入栈,这个其实是配合实际出错号是0的除法错中断,先不管它。
pushl $_do_int3 # _do_debug 中断服务程序主体,中断处理程序
jmp no_error_code
执行完这两条指令,内存分布是:
然后就跳转到了no_error_code
。
它主要分成三大部分
- 保护现场
- 执行中断处理程序
- 恢复现场和返回原程序
下面是保护现场的部分
# 保护现场xchgl %eax,(%esp) # eax的值入栈,eax保存中断处理程序地址pushl %ebxpushl %ecxpushl %edxpushl %edipushl %esipushl %ebppush %ds # 占4字节而非2字节push %espush %fspushl $0 # "error code" 中断有无错误码,是有相关规定的lea 44(%esp),%edxpushl %edx
然后是执行中断处理程序的部分,会有数据段选择符是设定,先暂时不管,这个其实就是类似于8086设置ds
的值。
# 设置内核数据段选择符,设置好数据寻址的基址movl $0x10,%edxmov %dx,%dsmov %dx,%esmov %dx,%fscall *%eax # 执行中断处理程序,C语言函数do_debug
紧接着是恢复现场和返回原程序
# 恢复现场addl $8,%esppop %fspop %espop %dspopl %ebppopl %esipopl %edipopl %edxpopl %ecxpopl %ebxpopl %eaxiret # 返回中断处理之前的程序,继续执行后续指令
这里需要补充
iret的作用
中断门或陷阱门中断服务例程的返回,按照之前入栈的相反顺序将EIP、CS先后出栈;再将EFLAGS标志寄存器出栈,恢复现场(IRET指令)。
简而言之,就是把最开始保存断点的部分依次出栈,从而回到源程序。
我们看一下这个过程的内存分布
其中
esp0
对应push $0
esp1
对应pushl %edx
esp2
对应addl $8,%esp
esp3
对应popl %eax
还有一个比较值得关心的
call指令执行前后,寄存器的变化
这一点,使用C语言分析一下函数调用,看看前后变化,在学堂在线《Linux内核分析》中科大孟宁老师MOOC讲解很清晰。
后面的内些的无处错码的函数,与上面的例子完全一样
_debug:pushl $_do_int3 # _do_debugjmp no_error_code_nmi:pushl $_do_nmijmp no_error_code_int3:pushl $_do_int3jmp no_error_code_overflow:pushl $_do_overflowjmp no_error_code_bounds:pushl $_do_boundsjmp no_error_code_invalid_op:pushl $_do_invalid_opjmp no_error_code_coprocessor_segment_overrun:pushl $_do_coprocessor_segment_overrunjmp no_error_code_reserved:pushl $_do_reservedjmp no_error_code_irq13:pushl %eaxxorb %al,%aloutb %al,$0xF0movb $0x20,%aloutb %al,$0x20jmp 1f
1: jmp 1f
1: outb %al,$0xA0popl %eaxjmp _coprocessor_error
这些都一样,其中比较特别的是,Linux0.11实现了几个用户自定义中断,_irq13
就是int45
。
2.2.1 其他细节的说明
这里保存中断号0,以及保存最开始的堆栈地址esp
的值,似乎没啥用,以后再说吧。
这里比较奇怪的是,除法零中断,它的错误码是0,使用的是无处错码的处理程序,如果错误码在之前就被压入栈的话,iret
指令执行之前,指向的应该是error code 0
,而不是eip
,除非这个除法零中断的处理比较特殊,不将出错码压栈,与其他无处错码的中断等同对待,才比较合理。以后再看看这个细节,暂时先放放。
3 有出错码中断
结构与无处错码中断基本一致,这里简要说明一下区别。
首先,硬件处理有区别,压栈之后,会把出错码压栈。
然后,在保存现场的处理上,有一些不同。
_double_fault:pushl $_do_double_fault
error_code:xchgl %eax,4(%esp) # error code <-> %eaxxchgl %ebx,(%esp) # &function <-> %ebxpushl %ecxpushl %edxpushl %edipushl %esipushl %ebppush %dspush %espush %fspushl %eax # error codelea 44(%esp),%eax # offsetpushl %eaxmovl $0x10,%eaxmov %ax,%dsmov %ax,%esmov %ax,%fscall *%ebxaddl $8,%esppop %fspop %espop %dspopl %ebppopl %esipopl %edipopl %edxpopl %ecxpopl %ebxpopl %eaxiret
看最开始的指令
上图是将中断处理函数压栈之后。
然后执行
xchgl %eax,4(%esp) # error code <-> %eax
xchgl %ebx,(%esp) # &function <-> %ebx
分别将
- eax与栈内的error code交换,eax入栈
- ebx保存中断处理函数地址,ebx入栈
然后,在压入错误码的时候,压入的是pushl %eax # error code
,这个时候error code就是动态的了,取决于之前硬件压入的错误码是多少,所以,之前的除法零是静态的0,极大概率可能不压入错误码,当成固定机制处理,也可能与兼容性有关。
然后就是调用中断处理函数的时候,执行call *%ebx
,因为这次是ebx
存放函数地址。
其他的就没有差别了,需要注意的是,error code
最开始就被替换下来了,在后面才压栈。
错误码压栈的作用
好吧,看起来错误码压栈之后,好像根本没有使用就略过了啊,肯定不会的,我们看看手册。
某种类型的异常也导致错误码压栈。异常处理器也能够使用错误码帮助检测异常。
这一点说明了
- 错误码的功能:帮助检查异常
- 某些有错误码的异常会压栈,不是所有的都会把错误码压栈,比如除法零错误,很可能就不压栈