用戶程序需要系統提供服務的時候,會通過系統調用產生一個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個宏來提供了系統調用統一的定義接口。