异常、中断与系统调用

异常、中断与系统调用

概述

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指向新函数栈帧的栈底,espebp一样,然后新函数的参数是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


异常、中断与系统调用
https://yill-z.github.io/2025/01/19/异常,中断,和系统调用/
作者
Yill Zhang
发布于
2025年1月19日
许可协议