简介
- 基于Linux0.11内核代码,主要参考资料为Linux0.11内核注释。
- 系统调用的一些基本概念。
- 系统调用的处理流程。
- 源码分析。
基本概念
- 如何触发,何时触发。
调用系统调用函数(如fork)时触发, 此时会进一步调用_syscall0. _syscall0是一个宏,可生成如下函数, 以fork举例.
int fork(void)
{
long __res;
// 执行指令int $0x80 输入eax=_NR_fork (在unistd.h中定义为2) 输入结果在ax寄存器中, 将ax寄存器的值赋值给__res变量.
__asm__ voliatile("int $0x80" : "=a" (__res) : "0" (_NR_fork));
if (__res >= 0)
return (int) __res;
errno = -__res;
return -1;
}
由上可知, 要调用系统调用函数, 先调用封装好的C函数, C函数中会调用0x80中断.
- 如何传递参数。
上面的函数可知, 调用0x80中断时, 需要将系统调用的枚举值传入,给eax寄存器, 并且在返回时将返回值(存在eax寄存器中)赋值给__res, 再赋值给errno.
在封装的C函数有参数传入时, 怎么处理呢, 以参数最多的C函数write函数举例. write通过_syscall3宏实现, 该宏最终生成的write函数定义如下:
int write(int fd, const char* buf, off_t count)
{
long __res;
// 执行指令int $0x80 输入eax =_NR_fork (在unistd.h中定义为2) 输入结果在ax寄存器中, 将ax寄存器的值赋值给__res变量.
__asm__ voliatile("int $0x80" : "=a" (__res)
: "0" (_NR_write), "b" ((long)(a)), "c" ((long)(b)), "d", ((long)(c)));
if (__res >= 0)
return (int) __res;
errno = -__res;
return -1;
}
由上可知, 当C函数有参数传入时, 依次将函数参数传给寄存器ebx, ecx, edx.
- 堆栈的位置。
系统调用使用的堆栈在内核中, 给进程分配task_struct时, 会分配一整页(4KB)的内存, 除了task_struct占用的内存, 其余内存用作系统调用的栈. - 系统调用表。
系统调用通过int 0x80触发, 不同的系统调用传入的系统调用号不同, 该值存储在eax寄存器中, 在内核态时通过eax寄存器的值选择要调用的系统调用. 所有合法有效的系统调用存储在系统调用表(数组)中, eax寄存器的值对应系统调用在数组中的下表. 系统调用表(在sys.h文件中)如下:
typedef int (*fn_ptr)()
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 }
处理流程
- 调用封装的C函数.
- 在C函数中调用int 0x80系统调用, 并且将参数和系统调用号传入指定寄存器中.
- 调用int 0x80中断的处理函数system_call.
- 保存中断进程的上下文.
- 根据系统调用号找到处理函数.
- 调用系统调用处理函数.
- 此时结果在eax寄存器, 通过push将其放入栈中.
- 判断当前进程的状态或者剩余的tick. 如果不满足运行条件, 调用reschedule调度运行其它进程.
- 判断进程是否为task0(初始进程), 是则直接返回.
- 通过CS寄存器和DS寄存器判断本次调用是否为用户代码, 如果不是则直接返回.
- 判断该进程是否有信号投递过来, 有则调用do_signal处理信号.
- 从栈中恢复eax寄存器的值, 和fs, es, ds等段寄存器.
总结: 系统调用函数除了用户代码触发, task0可以触发, 系统调用中的其它函数也可以触发. 因此在系统调用中要处理一些特殊情况.
源码分析
- 在上面的分析中, 我们知道系统调用函数最终都会调用system_call函数, system_call函数是由汇编编写的函数, 函数定义在system_call.s文件中. 定义如下:
system_call:
cmpl $nr_system_calls-1,%eax # 调用号如果超出范围的话就在eax中置-1并退出
ja bad_sys_call
push %ds # 保存原段寄存器值
push %es
push %fs
# 一个系统调用最多可带有3个参数,也可以不带参数。下面入栈的ebx、ecx和edx中放着系统
# 调用相应C语言函数的调用函数。这几个寄存器入栈的顺序是由GNU GCC规定的,
# ebx 中可存放第1个参数,ecx中存放第2个参数,edx中存放第3个参数。
# 系统调用语句可参见头文件include/unistd.h中的系统调用宏。
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds # 将ds es段寄存器切换到内核空间
mov %dx,%es
# fs指向局部数据段(局部描述符表中数据段描述符),即指向执行本次系统调用的用户程序的数据段。
# 注意,在Linux 0.11 中内核给任务分配的代码和数据内存段是重叠的,他们的段基址和段限长相同。
movl $0x17,%edx # fs points to local data space
mov %dx,%fs # fs段寄存器切换到内核空间
# 下面这句操作数的含义是:调用地址=[_sys_call_table + %eax * 4]
# sys_call_table[]是一个指针数组,定义在include/linux/sys.h中,该指针数组中设置了所有72
# 个系统调用C处理函数地址。
call sys_call_table(,%eax,4) # 间接调用指定功能C函数
pushl %eax # 把系统调用返回值入栈
# 下面几行查看当前任务的运行状态。如果不在就绪状态(state != 0)就去执行调度程序。如果该
# 任务在就绪状态,但其时间片已用完(counter = 0),则也去执行调度程序。例如当后台进程组中的
# 进程执行控制终端读写操作时,那么默认条件下该后台进程组所有进程会收到SIGTTIN或SIGTTOU
# 信号,导致进程组中所有进程处于停止状态。而当前进程则会立刻返回。
movl current,%eax # 取当前任务(进程)数据结构地址→eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
# 以下这段代码执行从系统调用C函数返回后,对信号进行识别处理。其他中断服务程序退出时也
# 将跳转到这里进行处理后才退出中断过程,例如后面的处理器出错中断int 16.
ret_from_sys_call:
# 首先判别当前任务是否是初始任务task0,如果是则不比对其进行信号量方面的处理,直接返回。
movl current,%eax # task[0] cannot have signals
cmpl task,%eax
je 3f # 向前(forward)跳转到标号3处退出中断处理
# 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。
# 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。这里比较选择符是否
# 为用户代码段的选择符0x000f(RPL=3,局部表,第一个段(代码段))来判断是否为用户任务。如果不是
# 则说明是某个中断服务程序跳转到上面的,于是跳转退出中断程序。如果原堆栈段选择符不为
# 0x17(即原堆栈不在用户段中),也说明本次系统调用的调用者不是用户任务,则也退出。
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
# 下面这段代码用于处理当前任务中的信号。首先取当前任务结构中的信号位图(32位,每位代表1种
# 信号),然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值最小的信号值,
# 再把原信号位图中该信号对应的位复位(置0),最后将该信号值作为参数之一调用do_signal().
# do_signal()在kernel/signal.c中,其参数包括13个入栈信息。
movl signal(%eax),%ebx # 取信号位图→ebx,每1位代表1种信号,共32个信号
movl blocked(%eax),%ecx # 取阻塞(屏蔽)信号位图→ecx
notl %ecx # 每位取反
andl %ebx,%ecx # 获得许可信号位图
bsfl %ecx,%ecx # 从低位(位0)开始扫描位图,看是否有1的位,若有,则ecx保留该位的偏移值
je 3f # 如果没有信号则向前跳转退出
btrl %ecx,%ebx # 复位该信号(ebx含有原signal位图)
movl %ebx,signal(%eax) # 重新保存signal位图信息→current->signal.
incl %ecx # 将信号调整为从1开始的数(1-32)
pushl %ecx # 信号值入栈作为调用do_signal的参数之一
call do_signal # 调用C函数信号处理程序(kernel/signal.c)
popl %eax # 弹出入栈的信号值
3: popl %eax # eax中含有上面入栈系统调用的返回值
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
总结
- 通过int 0x80中断使进程进入内核中断.
- 通过寄存器传递要调用的系统调用号和需要的参数.
- 调用系统调用的除了用户代码, 还可以是task0或者其它系统调用.
- 每次系统调用结束时, 会检查进程状态或者剩余的tick, 判断是否有继续运行的权限.
- 每次系统调用结束, 会判断进程是否有信号需要处理, 有则会触发do_signal, 进程转而处理信号.
- 系统调用的返回值通过eax寄存器返回, 并且如果异常, 会赋值给errno.