结合源代码分析一个完整的中断过程【转】
转自:http://blog.csdn.net/rosetta/article/details/49454021
转载请注明出处:http://blog.csdn.net/rosetta
结合源代码分析一个完整的中断过程
此文详细描述了中断产生到中断处理程序执行、中断处理程序返回以及中断描述符初始化整个过程,结合linux-0.00源代码片断学习将会更直观易懂。
中断是指当前执行程序或任务在执行到某处时出现一个事件,该事件需要程序处理。这种事件会导致程序会从当前运行程序转移到被称为中断(或异常)处理程序中执行。中断可以由硬件发生,也可以由软件调用int n指令产生。异常发生在CPU执行一条指令时,检测到一个出错条件时发生,例如被0除出错。
那么中断产生时,当CPU去执行中断处理程序之前如何保存当前程序的信息?需要保存的信息有哪些,以便在处理完中断处理程序后恢复执行之前的程序?再者CPU怎么去找中断处理程序的?
下面以int n产生中断为例,以linux-0.00中的head.s源码为实例,并使用bochs单步调试,理论联系实际,分析从中断产生到执行并返回整个过程。
Boch调试环境及linux-0.00运行环境搭建可参考《Linux-0.00运行环境搭建》。
从head.s如下代码处开始分析。完整的代码可参考《一个简单多任务内核实例的分析 》
- 236 task0:
- 237 movl $0x17, %eax
- 238 movw %ax, %ds
- 239 movb $65, %al /*print 'A' 65*/
- 240 int $0x80
- 241 movl $0xfff, %ecx
- 242 1: loop 1b
- 243 jmp task0
运行bochs 调试脚本,按以下方法找到task0的地址,因为head程序在物理地址0x00处,所以使用objdump显示的偏移地址+段地址0x00还是其本身,即最终的断点位置。
在源代码目录执行:
- [root@xxx linux-0.00-rh9]# objdump -d -j.text -t ./head.o|grep task0
- 000010e0 l .text 00000000 task0
所以在bochs中直接下断到0x10e0处。
在执行int 0x80前先停下观察cpu相应寄存器及栈的数据。
栈中状态:
可以看到此时的cs=0x000f(62行iret返回后设置的cs值),eip=0x10e9(当前即将执行的int 0x80代码),如果这里不产生中断的话下一条代码将是执行0x10eb,但这里使用int 0x80 产生了一个中断,从而去调用IDT中断描述符表中索引为0x80(第128个)的描述符。
使用跟进函数指令s跟进中断处理程序并显示当前cpu、栈的情况。
寄存器中主要变化的是cs,eip,ss,esp,而栈的变化也很大,那么他们是如何变化的呢?
1. eip=0x10e9 => 0x0166
eip的变化很好理解,调用int0x80之前的指令0x10e9处跳到了现在的0x0166处,之前指令的后一条将要执行的指令地址0x10eb存在了栈中(用于中断返回)。
2. cs=0x000f => 0x0008
那么cs为何变成了0x0008呢?注意此时是保护模式下,所以0x0008其实为段选择符。
因为此时已经跳转到了IDT中第0x80个描述符,那么0x80处的描述符长何样?
- 36 # setup timer & system call interruptdescriptors.
- 37 movl $0x00080000, %eax
- 38 movw $timer_interrupt, %ax
- 39 movw $0x8E00, %dx
- 40 movl $0x08, %ecx # The PC default timer int.
- 41 lea idt(,%ecx,8), %esi
- 42 movl %eax,(%esi)
- 43 movl %edx,4(%esi)
- 44 movw$system_interrupt, %ax
- 45 movw $0xef00, %dx
- 46 movl $0x80, %ecx
- 47 lea idt(,%ecx,8), %esi
- 48 movl %eax,(%esi)
- 49 movl %edx,4(%esi)
如上代码,因为IDT中的每一项中断描述符都为8字节,以上第37行,44~49行就是设置IDT表中第0x80个位置处的中断描述符,其内容:eax为低4字节,其中eax的高16位为段选择符0x0008(即刚才cs的变化,由0x000f => 0x0008),eax的低16位为中断处理程序system_interrupt偏移地址;dx为中断描述符类型及DPL等,此时的中断描述符类型为0xf=1111为陷阱门描述符。
3.ss=0x0017=> 0x0010, esp=0x0bd8 => 0xe4c
代码中的tss0内容为:
- 205tss0: .long 0 /* back link */
- 206 .long krn_stk0, 0x10 /* esp0, ss0 */
- 207 .long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */
- 208 .long 0, 0, 0, 0, 0 /* eip, eflags, eax, ecx, edx */
- 209 .long 0, 0, 0, 0, 0 /* ebx esp, ebp, esi, edi */
- 210 .long 0, 0, 0, 0, 0, 0 /* es, cs, ss, ds, fs, gs */
- 211 .long LDT0_SEL, 0x8000000 /* ldt, trace bitmap */
- 212
- 213 .fill 128,4,0
- 214 krn_stk0:
- 215 # .long 0
使用objdump查看head.o中的krn_stak0地址:
- [root@plmlinux-0.00-rh9]# objdump -d -j .text -t ./head.o|grep krn_stk0
- 00000e60 l .text 00000000krn_stk0
- 00000e60<krn_stk0>:
4.栈的变化
ss的值加载了tss任务段描述符中的ss0字段,为0x10;esp也是加载其中的esp0字段,所以应该是0xe60,但是因为该栈已经被使用过了,所以导致esp减至0xe4c。
栈中从0xe60开始至0xe4c处都是什么内容?此时看下system_interrupt是如何返回的?
- 152 .align 2
- 153system_interrupt:
- 154 push %ds
- 155 pushl %edx
- 156 pushl %ecx
- 157 pushl %ebx
- 158 pushl %eax
- 159 movl $0x10, %edx
- 160 mov %dx, %ds
- 161 call write_char
- 162 popl %eax
- 163 popl %ebx
- 164 popl %ecx
- 165 popl %edx
- 166 pop %ds
- 167 iret
发现其最后是iret,而执行此指令会导致:
- eip <= ss:esp esp=esp+4 ; eip <= 0x10eb
- cs <= ss:esp esp=esp+4; cs <= 0x000f
- eflags <= ss:esp esp=esp+4; eflags <= 246
- esp <= ss:esp esp=esp+4; esp <= 0x0bd8
- ss <= ss:esp esp=esp+4; ss <= 0x0017
所以,栈中保存的内容都是中断前的信息,以便返回时使用。
System_interrupt中断处理程序调用iret返回后将继续执行int 0x80后面的指令cs:eip=0x00f:0x10eb。
此文仅描述中断方面的知识,对于任务task的切换和加载、tss任务段描述符未做说明,此部分内容将在下一篇文章中记录。
参考文献:
《Linux内核完全剖析》赵炯