【Linux 0.12】内核代码——中断与异常处理
内核代码
在 init/main.c 的 main 函数中,调用了
trap_init()
函数对硬件中断向量进行初始化。trap_init()
位于 kernel/traps.c,内部负责设置向量号对应的处理程序。而处理程序的定义则位于 asm.s 中。
asm.s 程序
asm.c 程序包括大部分 CPU 探测到的异常故障处理的底层代码,也包括数学协处理器的异常处理。
应当重点关注故障处理时的内核堆栈变化。
以 divide_error
为例(divide_error
无出错号):
个人理解:出错号是传递给中断服务程序的一个参数。
1 | _divide_error: |
- 在开始执行相应的中断服务程序之前,堆栈指针 esp 指向中断返回地址。(类似函数调用)
- 然后压入处理程序地址
push dword ptr _do_divide_error
,病调用xchg
将处理程序地址保存至 eax,eax 的值则入栈,完美。 - 保存其他各种寄存器的值
- 调用中断服务程序
- 恢复寄存器,中断返回
事实上,中断服务程序的代码并没有显示,asm.c 似乎只定义了保存现场环境的功能。
trap.c 程序
主要包括一些 asm.c 中调用的相应的 C 函数。其中 die()
函数用于在中断处理中显示详细的出错信息。
trap_init()
函数负责初始化中断向量。
1 | //// 设置陷阱门函数。 |
trap_init()
函数里使用了上述两个宏定义来初始化中断向量。他们的区别在于特权级不同。set_system_gate
特权级是 3,可以由用户态程序发出。
sys_call.s 程序
系统调用使用中断 int 0x80 和功能号(保存在 eax) 来使用内核提供的各种功能服务。
对于所有系统调用的实现函数,内核将他们按照系统调用功能号按顺序排列成一张函数指针表,然后在 int 0x80 的处理过程中根据 eax 的功能号调用对应系统调用函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 // 位于 include/linux/sys.h
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};
sys_call.s 主要实现了:
- int 0x80 的入口处理过程以及信号检测处理
- 实现了两个系统功能的底层接口(sys_execve 和 sys_fork)
- 其他非系统调用的中断处理程序,包括 int16, int7, int32 等
对于软中断,主要是先为 C 函数处理程序做准备,压入参数,或者设置寄存器,然后调用对应的服务程序(C 语言编写)。处理完成后检测当前任务的信号位图,处理信号。
对于硬件中断, 。。。。。
对于系统调用,基本上对应的程序都是 C 语言程序:
- 首先检查系统调用号 (eax) 是否有效
- 将一些会用到的寄存器保存到栈中。(linux 内核默认把段寄存器 ds, es 用于内核数据段,fs 用于用户数据段)
- 通过一个地址跳转表
sys_call_table
调用对应的系统调用函数 - 函数返回后,将返回值压入堆栈保存起来
- 后续会执行调度程序,处理信号等等
附:系统调用发生的全过程
在系统启动时,会在 sched_init(void)
函数中调用 set_system_gate(0x80,&system_call)
,设置中断向量号0x80的中断描述符:
1 | // kernel/sched.c sched_init 函数 |
而 system_call
函数则定义于 kernel/system_call.s
注意:在进入 system_call()
函数之前,eax,ebx,ecx,edx
- 首先检查系统调用号 (eax) 是否有效
1 | _system_call: |
- 将一些会用到的寄存器保存到栈中。(linux 内核默认把段寄存器 ds, es 用于内核数据段,fs 用于用户数据段)
1 | push ds ;// 保存原段寄存器值。 |
- 通过一个地址跳转表
sys_call_table
调用对应的系统调用函数
1 | mov edx,10h ;// set up ds,es to kernel space |
为什么 0x10h 指向全局描述符表中的数据段描述符?
为什么 0x17h 指向局部描述符表中的数据段描述符?
- 函数返回后,将返回值压入堆栈保存起来
1 | push eax ;// 把系统调用号入栈。 |
- 检查当前任务运行的状态。如果不在就绪状态的话,则需要执行调度程序,选取新的任务执行。如果已经是就绪态,但是时间片 counter 为 0,那也需要执行调度程序。
1 | mov eax,_current |
进行到这里,说明当前任务是活跃的,且还有时间片支持运行。但是在返回原程序执行之前,还需要处理信号:
如果当前程序是初始任务 task[0],那么就直接返回
1
2
3mov eax,_current ;// task[0] cannot have signals
cmp eax,_task
je l1 ;// 向前(forward)跳转到标号l1。l1是程序的返回代码,恢复了寄存器并执行 iretd如果当前用户是超级用户,那么就直接返回到原程序执行。(超级用户不被抢占?内核态程序不抢占原则?)
1
2cmp word ptr [R_CS+esp],0fh ;// was old code segment supervisor ?
jne l1如果原堆栈段选择符不是 0x17(即原来的堆栈不在用户数据段),那么也直接返回
1
2cmp word ptr [OLR_DSS+esp],17h ;// was stack segment = 0x17 ?
jne l1如果不是上述三种情况,则需要处理信号。
获取信号位图,查看当前进程收到了哪些信号,然后获取信号屏蔽码,过滤不允许的信号,然后获取数值最小的被允许的信号。将该信号值作为参数之一,调用
do_signal()
。