以 Linux 2.6.36 为例
1. 将中断号与系统调用函数绑定
系统调用函数 (system_call) 是系统调用的总入口,将其与中断号 0x80绑定。
arch/x86/kernel/traps.c
#define SYSCALL_VECTOR 0x80
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
2. 系统调用函数的实现
该中断,由于需要从用户态栈切换到内核态栈,需要栈切换,因此CPU会将用户态的栈相关的参数(oldss, oldesp)压栈,再调用system_call。
在 system_call 内部:
- 将所有寄存器压入内核栈,其中 ebx, ecx, edx 存放程序的参数
- 以 eax 为偏移量,在 sys_call_table 中找到指定的系统调用的地址,其中sys_call_table 定义了所有的系统函数的地址。
从以上可以看出参数传递的方式:
- 从用户态到内核态通过寄存器传递
- 内核函数通过栈读取,即通过栈传递
arch/x86/kernel/entry_32.S
.macro SAVE_ALL
cld
PUSH_GS
pushl %fs
pushl %es
pushl %ds
pushl %eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
pushl %ecx
pushl %ebx
movl $(__USER_DS), %edx
movl %edx, %ds
movl %edx, %es
movl $(__KERNEL_PERCPU), %edx
movl %edx, %fs
SET_KERNEL_GS %edx
.endm
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp) # system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax,PT_EAX(%esp) # store the return value
arch/x86/kernel/syscall_table_32.S
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long ptregs_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
...
.long sys_rt_tgsigqueueinfo /* 335 */
.long sys_perf_event_open
.long sys_recvmmsg
.long sys_fanotify_init
.long sys_fanotify_mark
.long sys_prlimit64 /* 340 */
3. 系统函数原型
以 read 函数为例,系统调用函数原型前面都有限定符 asmlinkage 修饰,这个修饰符的作用是限定函数的参数不通过寄存器传递,即通过栈传递。
结合前面可以看出,系统调用函数都是在内核栈上读取函数的参数。
include/linux/syscalls.h
asmlinkage long sys_read(unsigned int fd, char __user *buf, size_t count);
arch/x86/include/asm/linkage.h
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))