Linux系统调用
用户程序需要系统提供服务的时候,会通过系统调用产生一个int 0x80的软中断,就会进入到系统调用的入口函数,入口函数存放在以下文件当中:
以下是系统调用的入口:(arch/x86/kernel/entry_32.S)
http://www.cs.fsu.edu/~baker/devices/lxr/http/source/linux/arch/x86/kernel/entry_32.S
517 ENTRY(system_call) 518 RING0_INT_FRAME # can't unwind into user space anyway 519 pushl %eax # save orig_eax #将系统调用号压入堆栈 520 CFI_ADJUST_CFA_OFFSET 4 521 SAVE_ALL 522 GET_THREAD_INFO(%ebp) 523 # system call tracing in operation / emulation 524 testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) 525 jnz syscall_trace_entry 526 cmpl $(nr_syscalls), %eax 527 jae syscall_badsys 528 syscall_call: 529 call *sys_call_table(,%eax,4) 530 movl %eax,PT_EAX(%esp) # store the return value
521行的SAVE_ALL:将寄存器的值压入堆栈当中,压入堆栈的顺序对应着结构体struct pt_regs ,当出栈的时候,就将这些值传递到结构体struct pt_regs里面的成员,从而实现从汇编代码向C程序传递参数。struct pt_regs 位于:linux/arch/x86/include/asm/ptrace.h。
522行的GET_THREAD_INFO 宏获得当前进程的thread_info结构的地址,获取当前进程的信息。(后面会有文章讲task_struct 和 thread_info等内核线程的数据结构和相关内容)
525行的jnz syscall_trace_entry比较结果不为零的时候跳转。对用户态进程传递过来的系统调用号的合法性进行检查。如果不合法则跳转到syscall_badsys标记的命令处。
525行和526行,比较结果大于或者等于最大的系统调用号的时候跳转,合法则跳转到相应系统调用号所对应的服务例程当中,也就是在sys_call_table表中找到了相应的函数入口点。由于sys_call_table表的表项占4字节,因此获得服务例程指针的具体方法是将由eax保存的系统调用号乘以4再与sys_call_table表的基址相加。
接下来,会进入到系统调用表查找到系统调用服务程序的入口函数的地址,再进行跳转,整个过程大概是这样:system_call -> 4x%eax ->sys_call_table ->sys_xxx
sys_call_table位于:linux/arch/x86/kernel/syscall_table_32.S
http://www.cs.fsu.edu/~baker/devices/lxr/http/source/linux/arch/x86/kernel/syscall_table_32.S
1 ENTRY(sys_call_table) 2 .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ 3 .long sys_exit 4 .long ptregs_fork 5 .long sys_read 6 .long sys_write 7 .long sys_open /* 5 */ 8 .long sys_close 9 .long sys_waitpid 10 .long sys_creat 11 .long sys_link 12 .long sys_unlink /* 10 */ 13 .long ptregs_execve 14 .long sys_chdir 15 .long sys_time 16 .long sys_mknod 17 .long sys_chmod /* 15 */ 18 .long sys_lchown16 19 .long sys_ni_syscall /* old break syscall holder */ 20 .long sys_stat 21 .long sys_lseek 22 .long sys_getpid /* 20 */ 23 . long sys_mount …. 144 .long sys_select /* 142 */ ….
这里我们结合select()函数来讲一下系统调用陷入内核的过程。
select()系统调用号存于:linux/arch/x86/include/asm/unistd_32.h
http://www.cs.fsu.edu/~baker/devices/lxr/http/source/linux/arch/x86/include/asm/unistd_32.h
1 #ifndef _ASM_X86_UNISTD_32_H 2 #define _ASM_X86_UNISTD_32_H 3 4 /* 5 * This file contains the system call numbers. 6 */ 7 8 #define __NR_restart_syscall 0 9 #define __NR_exit 1 10 #define __NR_fork 2 11 #define __NR_read 3 12 #define __NR_write 4 13 #define __NR_open 5 14 #define __NR_close 6 15 #define __NR_waitpid 7 16 #define __NR_creat 8 17 #define __NR_link 9 18 #define __NR_unlink 10 19 #define __NR_execve 11 20 #define __NR_chdir 12 21 #define __NR_time 13 … 150 #define __NR__newselect 142 …
系统调用原型在:linux/include/linux/syscalls.h
http://www.cs.fsu.edu/~baker/devices/lxr/http/source/linux/include/linux/syscalls.h
568 asmlinkage long sys_select(int n, fd_set __user *inp, fd_set __user *outp, 569 fd_set __user *exp, struct timeval __user *tvp);
其中这里使用了一个宏asmlinkage ,我们再看一下它在系统里的定义:(位于:linux/arch/x86/include/asm/linkage.h)
http://www.cs.fsu.edu/~baker/devices/lxr/http/source/linux/arch/x86/include/asm/linkage.h
9 #ifdef CONFIG_X86_32
10 #define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
后面的 __attribute__((regparm(0)))表示的是不通过寄存器来传递参数,通过栈来传递。所以系统调用的入口函数里面
ENTRY(system_call)
SAVE_ALL 将寄存器的值压入堆栈当中,压入堆栈的顺序对应着结构体struct pt_regs ,当出栈的时候,就将这些值传递到结构体struct pt_regs里面的成员,从而实现从汇编代码向C程序传递参数。
定义了这个SAVE_ALL是将参数压倒堆栈里面,然后通过堆栈来进行参数的传递。经过了系统调用入口函数之后,会调用到
syscall_call:
call *sys_call_table(,%eax,4)
sys_call_table每一项占用4个字节。system_call函数可以读取eax寄存器获得当前系统调用的系统调用号,将其乘以4生成偏移地址,然后以sys_call_table为基址,基址加上偏移地址所指向的内容即是应该执行的系统调用服务例程的地址。
由上面的sys_call_table 可以看出 sys_select 是 select 系统调用的入口地址。哈哈,貌似万事大吉,已经跟踪到了,但是我发现我在内核里无论如何也找不到函数sys_select的定义。
原来在linux/include/linux/syscalls.h 中定义了如下的宏:
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__) #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__) #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__) #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__) #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__) #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
还有:
#define SYSCALL_DEFINEx(x, sname, ...) \ __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) #define __SYSCALL_DEFINEx(x, name, ...) \ asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__)); \ static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__)); \ asmlinkage long SyS##name(__SC_LONG##x(__VA_ARGS__)) \ { \ __SC_TEST##x(__VA_ARGS__); \ return (long) SYSC##name(__SC_CAST##x(__VA_ARGS__)); \ } \ SYSCALL_ALIAS(sys##name, SyS##name); \ static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__))
看懂这些宏定义又是一个问题,C语言还是算写的熟悉,但是很少用到那么大量的宏。而且宏里面的“#”和“##”操作也不熟悉(后面会有文章介绍C语言在Linux中的宏)。
现在首先来解释一下SYSCALL_DEFINEx 中的x 表示 上层应用函数的参数个数,比如select()函数有5个参数,因此会对应到SYSCALL_DEFINE5(如何对应过来的下面会讲)。sname表示的就是上层应用程序中函数的名字,如select。
这里首先给出sys_select()的定义,在select.c中(linux/fs/select.c)
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp, fd_set __user *, exp, struct timeval __user *, tvp) { struct timespec end_time, *to = NULL; struct timeval tv; int ret; if (tvp) { if (copy_from_user(&tv, tvp, sizeof(tv))) return -EFAULT; to = &end_time; if (poll_select_set_timeout(to, tv.tv_sec + (tv.tv_usec / USEC_PER_SEC), (tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC)) return -EINVAL; } ret = core_sys_select(n, inp, outp, exp, to); ret = poll_select_copy_remaining(&end_time, tvp, 1, ret); return ret; }
为什么呢,我们来看一个简单的程序(也许你已经懂了,但是我当时就是没懂):
#include<stdio.h>
#define SYS_CALL(x) \
void print(x)
SYS_CALL(int x) //这种写法比较诡异,内核中就是这样写的
{
printf("syscall %d",x);
}
int main()
{
print(10);
return 0;
}
使用gcc –E 选项进行预编译展开(这里只给出关键部分):
void print(int x)
{
printf("syscall %d",x);
}
int main()
{
print(10);
return 0;
}
也就是说SYS_CALL 直接被替换为了print。
结合上面的宏定义SYSCALL_DEFINEx相关的宏定义。在内核中SYSCALL_DEFINE5,也会直接被展开为sys_select,而且根据参数个数和前面提到过的sname,很快就能确定sys_select的定义为上面给出的SYSCALL_DEFINE5的那一部分。
在较新的内核中相当于使用了5个宏来提供了系统调用统一的定义接口。