目录
目录
1. 中断是什么?
一个程序正在执行自己的过程中,突然被一个事件给打扰了,并且去处理这个事件,处理完后又执行自己。用比较形象的例子描述,就是我正在认真看书学习,突然我妈喊我吃饭,然后我去吃饭了,吃完饭我又回来学习。如图是中断的执行过程。
那么对于顺序执行的程序来说,CPU是如何处理中断的呢?CPU虽然是一直取指令,执行指令;但是CPU并不是不管外部事件的,它会每执行一条指令后查询有没有外部事件发生,如果有就到该事件的处理函数中执行,执行完就回到顺序执行的程序中。就拿如下的程序片段举例子。
int main()
{
L1: int a=1;
L2: int b=2;
L3: int c=a+b;
return c;
}
void ISR()
{
cout<<"I am a interrupt";
}
正常的执行顺序是L1,L2,L3;假设突然来了中断,中断发生的时刻是在L1和L2之间,那么程序“隐式”的执行结果类似下面的样子。
int main()
{
L1: int a=1;
ISR();
L2: int b=2;
L3: int c=a+b;
return c;
}
void ISR()
{
cout<<"I am a interrupt";
}
2. 中断现场状态具体指什么?
在L1和L2之间被CPU自动的加入了一段ISR()函数的调用,这种过程很类似函数调用,但是并不是函数调用。在用户程序执行中是感受不到中断的存在,因为中断作为一个事件随时可能发生,那么谁统一管理用户程序的执行和中断的执行呢?这个责任自然放在了CPU上。这里需要解决的核心问题就是,中断现场的保护和恢复。用户程序执行的过程中有它的状态,状态主要表现在三个方面:1)进行逻辑或者算术计算的通用状态,表现在通用寄存器EAX,EBX,ECX等。2)下一条需要执行的指令位置CS\IP等。3)栈位置栈顶ESP和栈底EBP。
3. 函数调用栈实现
前两个状态很好理解,这里解释下栈状态。从C语言提出函数过程的概念后,就引入了栈的技术,栈底EBP指向某个函数第一行代码内存位置,栈顶ESP指向这个函数内部某行局部变量内存地址,拿以下函数调用举例子。在下面的例子执行过程中,函数传参和局部变量均使用栈内存作为变量的存储空间。因此栈的状态代表了函数的传参和局部变量的存储状态,这就是函数调用栈的目的。
int main()
{
L1: int a=1;
L2: int b=2;
L3: fun(a);
L4: int c=a+b;
return c;
}
void ISR()
{
cout<<"I am a interrupt";
}
void int fun(int aa)
{
int p = 1;
int q = 2;
aa = p+q;
}
对应的汇编代码如下
//%ebp=0,%esp=0
label main:
push %ebp;//main’s EBP
mov %esp,%ebp;
pushal;//main’s saved register
push $1;//Argument#a
push $2;//Argument#b
mov $1,%eax;
push %eax;//Argument#aa
push L4;//Return Address L4
jump fun;
push %ebp;//fun's EBP
mov %esp,%ebp;
pushal;//fun’s saved register
push $1;//Argument#p,
push $2;//Argument#q,
mov 0x0c(%ebp),%ebx;
mov 0x10(%ebp),%edx;
add %ebx,%edx;
mov %ebx,-0x08(%ebp);//aa=p+q
main函数和fun函数调用栈对应如下
上面代码显示了main()函数中调用fun()函数过程,其中局部变量a\b\p\q均压入栈内存中,形式参数aa压入栈中。函数调用是通过栈顶指针ESP和栈底指针EBP来实现的。函数传参是通过访问栈变量内存位置实现的,如汇编代码在mov %ebx,-0x08(%ebp)。对于函数中的局部变量和形参寻址,在高级语言C中并不能看到这样的过程,然而在汇编语言中就能看到,这是因为从高级语言到汇编语言经过了重要的编译步骤,这其中需要解决局部变量存储和形参传递,这些都是编译器帮助我们完成的。如果不使用编译器,想要实现一个函数过程调用,就需要程序设计者自己实现函数局部参数入栈出栈、形参的内存寻址等。
4. 中断栈实现
除了函数调用栈,用栈来描述传参的形参寻址和局部变量寻址,那么从用户程序到中断程序是否也有类似的地方呢?答案是肯定的,就是中断的实现也要在栈上实现。
由于中断发生的时刻不确定,中断打断的代码位置也是未知的,所以栈的位置不确定。栈的位置只有中断发生时的CPU知道,因此CPU会自动进行入栈出栈如下信息:1)栈位置栈顶ESP和栈底EBP。2)下一条需要执行的指令位置CS\IP等。
当栈的位置确定后,当前时刻的逻辑或者算术计算的通用状态(通用寄存器EAX,EBX,ECX等),用户可以通过汇编指令进行入栈出栈操作。
假设不考虑内核状态和用户状态,看看如下代码突然来了中断,中断发生的时刻是在L1和L2之间,那么程序“隐式”的执行结果类似下面的样子。
int main()
{
L1: int a=1;
ISR_timer();
L2: int b=2;
L3: int c=a+b;
return c;
}
void ISR_timer()
{
cout<<"I am a interrupt";
}
这段程序的中断栈执行流程是什么?他又是如何与代码相关联起来的呢?
4.1 执行main函数压栈
4.2 X86硬件自动压栈
4.3 中断向量压栈
中断执行代码
.globl vector32
vector32:
pushl $0
pushl $32
jmp __alltraps
压栈结果如下
4.4 压入通用状态寄存器
汇编代码如下
.text
.globl __alltraps
__alltraps:
# push registers to build a trap frame
# therefore make the stack look like a struct trapframe
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
# load GD_KDATA into %ds and %es to set up data segments for kernel
movl $GD_KDATA, %eax
movw %ax, %ds
movw %ax, %es
# push %esp to pass a pointer to the trapframe as an argument to trap()
pushl %esp
# call trap(tf), where tf=%esp
call trap
# pop the pushed stack pointer
L27:popl %esp
压栈结果
4.5 压入trapframe指针,以及Call函数调用
汇编代码如下
.text
.globl __alltraps
__alltraps:
……
# push %esp to pass a pointer to the trapframe as an argument to trap()
pushl %esp
# call trap(tf), where tf=%esp
call trap
# pop the pushed stack pointer
L27:popl %esp
备注:
call指令
call 0x12345
调用0x12345这个地址,可分解为:
pushl %eip ——> 将cpu下一条要执行的指令压入栈中
movl $0x12345, %eip ——> eip = 0x12345
注意:CPU下一条指令将会从地址0x12345中取。
压栈结果
4.6 C语言访问trapframe指针
根据函数调用栈中的分析结果,每个形式参数位置是固定的,因此trapframe的指针tf位置也是固定的。这个固定位置在汇编指令中是-8(EBP),那么tf指针代表了整个trapframe结构体的内容,也就不难理解tf=%esp了。
5. 中断形式的内核进入与退出
在第4节中讨论了一般的内核态程序中断栈的执行过程,但是对于X86来说,具有用户态和内核态程序的区别。如果用户态的程序来了中断大致是怎样的流程呢?大致的流程如下:
5.1 CPU正在执行用户程序,突然来了中断
此时用户态的栈要切换到内核状态,与第4节不同的是在内核栈中压入用户态栈的位置信息,其他的过程基本上是相同的。
5.2 CPU正在执行用户程序,用户调用系统调用
触发中断处理,此时用户态的栈要切换到内核状态,与第4节不同的是在内核栈中压入用户态栈的位置信息,其他的过程基本上是相同的。
5.3 内核栈出栈
不管是什么形式触发的内核栈压栈,当内核中断程序执行完成之后,内核栈就会执行栈的退出和切换。在图中,tf_regs/gs/fs/es/ds是__trapret函数中内核程序出栈;图中iret指令触发CPU让内核栈出栈操作,将tf_ss/tf_esp/tf_eflags/tf_cs/tf_eip/tf_err出栈。