异常、中断与系统调用
概述
linux里面,有这样一些机制
- 中断(interrupt):异步,由外设或软件等触发
- 异常(exception):异步,往往由执行某些错误的指令,如缺页,除0等
- 系统调用(syscall):同步,程序通过一些指令主动进入,如int 0x80等
中断可能是由用户态进入,也可能是由内核态进入
但是系统调用一定是由用户态进入
中断
当触发一次中断时,cpu将根据不同架构做出不同的压栈操作
i386
CPU先将当前EFLAGS寄存器内容压栈,当前的CS:IP压栈(就是返回地址),如果中断是由异常引起的,还要有一个出错代码压栈。
如果需要改变特权级别,还需要更换堆栈,根据DPL中的数字,选择TSS中的对应的SS:ESP内容放入当前的SS:ESP寄存器中,此时,还需要将原来的堆栈指针压入新堆栈
1 2 3 4 5 6 7 8 9 10
| +--------------+ SP_TOP | SS | 用户态的堆栈 | ESP | +--------------+ | EFLAGS | 当前的状态信息 | CS | 返回地址 这两个是硬件自动压入的 | EIP | +--------------+ | ERRORCODE | 错误码(异常) 如果是异常的话,会有这个 +--------------+
|
然后是通用的中断处理,SAVE_ALL,保存现场,也就是内核中的pt_regs,在内核栈的栈底
DS和ES原来的内容被保存在栈中,其寄存器内容被切换为KERNEL_DS和KERNEL_ES,以标识他们进入了内核态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| +--------------+ SP_Button | SS | 用户态的堆栈 | ESP | +--------------+ | EFLAGS | 当前的状态信息 | CS | 返回地址 这两个是硬件自动压入的 | EIP | +--------------+ | 中断号-256 | 中断号 -256的意思是区分中断和系统调用(orig_eax) +--------------+ | ES | | DS | | EAX | | EBP | | EDI | | ESI | | EDX | | ECX | | EBX | +--------------+ ss:esp 系统堆栈指针SP_TOP
|
再将ret_from_intr的地址放入栈中,模拟压入了一个返回地址,然后调用do_IRQ,之后从do_IRQ返回时,将ret_from_intr的地址放入CS:EIP,就到了中断的恢复现场部分
这个do_IRQ的参数是一个pt_regs数据结构,而不是指针
因为进行函数调用时,会先将函数参数从右至左压栈,然后调用call指令,call指令会将当前指令的下一条指令放入栈中,作为函数返回地址
此时到了新函数中,首先会push ebp
,将上一个函数的栈帧保存在栈中,然后更新新函数的栈帧mov ebp esp
,此时ebp
指向新函数栈帧的栈底,esp
和ebp
一样,然后新函数的参数是ebp+xxx
来获取
1 2 3 4 5 6 7 8 9 10 11 12 13
| +-------------+ | param n-1 | | param n-2 | | ...... | | param 1 | +-------------+ | return addr | +-------------+ | ebp | 旧函数的ebp +-------------+ | | ebp esp 新函数的栈 | | +-------------+
|
异常
i386
异常比起中断来,orig_eax存放的是异常的错误号,而不是中断号
以缺页异常来说
系统触发缺页之后,在异常向量表里面找到缺页的项,那一项跳转到对应的汇编代码
1 2 3
| ENTRY(page_fault) pushl $SYMBOL_NAME(do_page_fault) jmp error_code
|
首先将缺页处理的函数地址压栈,然后使用jmp进行跳转,jmp不会压栈返回值
error_code是异常的通用处理代码
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
| error_code: pushl %ds # 压栈ds pushl %eax # 压栈eax xorl %eax, %eax # eax清零 pushl %ebp # 压栈ebp pushl %edi # 压栈edi pushl %esi # 压栈esi pushl %edx # 压栈edx decl %eax # eax = -1 因为 0 - 1 = -1 pushl %ecx # 压栈ecx pushl %ebx # 压栈ebx cld # 方向向下 movl %es, %ecx # 当前的es保存到ecx中 movl ORIG_EAX(%esp), %esi # 拿出orig_eax,放入到esi中 movl ES(%esp), %edi # pt_regs中保存的ES,do_page_fault,放在edi里面 movl %eax, ORIG_EAX(%esp) # eax,也就是-1,放在orig_eax地方 movl %ecx, ES(%esp) # 将ecx里面的内容,放在堆栈ES的位置,也就是当前的es寄存器的值 movl %esp, %edx # esp放在edx里面 pushl %esi # 压栈esi,esi当前是出错代码 pushl %edx # 压栈edx,edx当前是esp,也就是pt_regs movl $(__KERNEL_DS), %edx # edx里面放的是内核数据段了 movl %edx, %ds # edx放在ds寄存器里 movl %edx, %es # edx放在es寄存器里 GET_CURRENT(%ebx) # ebx放task_struct指针 call *%edi # 调用edi地方的函数 do_page_fault addl $8, %esp # 栈顶减8, jmp ret_from_exception # 跳转到异常尾声
|
注意上面,对比SAVE_ALL,没有先压栈es寄存器,那么,其作为pt_regs的时候,es的位置放的是什么呢?
还记得跳转到error_code之前,先压栈了do_page_fault的地址么?他就相当于es的位置,然后通过movl ES(%esp), %edi
这一句,将do_page_fault的地址放在了edi
里面,然后将前面eax算的-1放在orig_eax里面。
异常尾声
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ret_from_exception: #ifdef CONFIG_SMP GET_CURRENT(%ebx) movl processor(%ebx), %eax shll $CONFIG_X86_L1_CACHE_SHIFT, %eax movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active testl SYMBOL_NAME(irq_stat)+4(,%eax), %ecx #softirq_mask #else movl SYMBOL_NAME(irq_stat), %ecx # softirq_active testl SYMBOL_NAME(irq_stat)+4, %ecx # softirq_mask #endif jne handle_softirq
ENTRY(ret_from_intr) GET_CURRENT(%ebx) movl EFLAGS(%esp), %eax movb CS(%esp), %al testl $(VM_MASK | 3), %eax jne ret_with_reschedule jmp restore_all
|
系统调用
i386
指令
在i386中,通过int 0x80
进入系统调用
参数传递
系统调用号:eax
最多五个参数通过寄存器传递
ebx
:第一个参数
ecx
:第二个参数
edx
:第三个参数
esi
:第四个参数
edi
:第五个参数
返回值:eax
x86_64
指令
在x86_64中,通过syscall指令进入系统调用
参数传递
系统调用号:rax
最多六个参数通过寄存器传递
rdi
:第一个参数
rsi
:第二个参数
rdx
:第三个参数
rcx
:第四个参数
r8
:第五个参数
r9
:第六个参数
返回值:rax
arm64
指令
在arm64中,通过svc
指令进入系统调用
参数传递
系统调用号:x8
最多六个参数通过寄存器传递
x0
:第一个参数
x1
:第二个参数
x2
:第三个参数
x3
:第四个参数
x4
:第五个参数
x5
:第六个参数
返回值:x0