linux系統調用實作機制詳解(核心4.14.4)前言
linux核心中設定了一組用于實作系統功能的子程式,稱為系統調用。和普通庫函數調用相似,隻是系統調用由作業系統核心提供,運作于核心态,而普通的函數調用由函數庫或使用者自己提供,運作于使用者态。
在Linux中,每個系統調用被賦予一個系統調用号。通過這個獨一無二的号就可以關聯系統調用。當使用者空間的程序執行一個系統調用的時候,這個系統調用号就被用來指明到底是要執行哪個系統調用。
系統調用号一旦配置設定就不能再有任何變更,否則編譯好的應用程式就會崩潰。Linux有一個“未實作”系統調用sys_ni_syscall(),它除了傳回一ENOSYS外不做任何其他工作,這個錯誤号就是專門針對無效的系統調用而設的。
結合具體源碼來看下實作機制。
具體号子配置設定在檔案arch/x86/entry/syscalls/syscall_64.tbl中定義,如下:
0 common read sys_read
1 common write sys_write
2 common open sys_open
3 common close sys_close
………
30 common shmat sys_shmat
31 common shmctl sys_shmctl
32 common dup sys_dup
33 common dup2 sys_dup2
34 common pause sys_pause
35 common nanosleep sys_nanosleep
36 common getitimer sys_getitimer
37 common alarm sys_alarm
38 common setitimer sys_setitimer
39 common getpid sys_getpid
40 common sendfile sys_sendfile64
41
common socket sys_socket
…….
也可以在arch/x86/include/generated/uapi/asm/unistd_64.h檔案中查找到系統調用号。
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
#define __NR_poll 7
#define __NR_lseek 8
……
在檔案(include/linux/syscalls.h)中定義了系統調用函數聲明,函數聲明中的asmlinkage限定詞,這用于通知編譯器僅從棧中提取該函數的參數。所有的系統調用都需要這個限定詞。例如系統調用getpid()在核心中被定義成sys_ getpid。這是Linux中所有系統調用都應該遵守的命名規則.
如下:
asmlinkage long sys_kill(pid_t pid, int sig);
不同的系統調用實作在不同的檔案中,例如sys_read
系統調用實作在fs/read_write.c檔案中,sys_socket定義在net/socket.c中。
例如sys_socket的原型如下:
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
其中3表示有3個參數,用于解析參數時候使用。
檢視宏SYSCALL_DEFINE3的定義,定義也在include/linux/syscalls.h中,如下:
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name,
__VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name,
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name,
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name,
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name,
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name,
#define SYSCALL_DEFINE_MAXARGS 6
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x,
__VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname,
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long
sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
\
__attribute__((alias(__stringify(SyS##name)))); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));
SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))
{
long ret =
SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x,
ret,__MAP(x,__SC_ARGS,__VA_ARGS__));
return ret; \
}
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
我們看到SYSCALL_DEFINE3指向SYSCALL_DEFINEx,而SYSCALL_DEFINEx指向__SYSCALL_DEFINEx,在__SYSCALL_DEFINEx宏中調用真正的原型,如sys_socket(其也定義在syscalls.h)。
是以SYSCALL_DEFINE3(socket,
int, family, int, type, int, protocol) 就是sys_socket函數。具體實作後續會在linux協定棧中進行介紹。
置于為什麼會這麼複雜,因為linux發展過程中難免碰到各種漏洞,有些則是因為修改漏洞需要,例如CVE-2009-0029漏洞
<a href="https://bugzilla.redhat.com/show_bug.cgi?id=479969">https://bugzilla.redhat.com/show_bug.cgi?id=479969</a>
之前在arch/x86/kernel/entry_64.S中實作了system_call的系統調用總接口。根據系統參數參數号來執行具體的系統調用。
現在所有socket相關的系統調用,都會使用sys_socketcall的系統調用,如下socketcall的代碼片段,根據參數進入switch…case…判斷操作碼,跳轉至對應的系統接口:
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
switch (call) {
case SYS_SOCKET:
err = sys_socket(a0,
a1, a[2]);
break;
case SYS_BIND:
err = sys_bind(a0, (struct sockaddr __user
*)a1, a[2]);
case SYS_CONNECT:
err =
sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_LISTEN:
err = sys_listen(a0,
a1);
case SYS_ACCEPT:
sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);
break;
case SYS_GETSOCKNAME:
sys_getsockname(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
case SYS_GETPEERNAME:
err =
sys_getpeername(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
這裡的變量定義在檔案include/uapi/linux/net.h中,如下
#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
#define SYS_ACCEPT4 18 /* sys_accept4(2) */
#define SYS_RECVMMSG 19 /* sys_recvmmsg(2) */
#define SYS_SENDMMSG 20 /* sys_sendmmsg(2) */
整體的系統調用的過程如下,由應用程式調用C庫提供的API函數,該API實作函數會調用核心的統一入口函數,具體到系統調用。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLzgTNzkDOidzN4UjNxMWZldjN2ADZiBjYhZGMiJzY0M2NyUGM0MjN48CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.png)
圖中邏輯為常用的系統調用。Socket相關的系統調用入口函數為sys_socketcall
如果出現錯誤,錯誤碼定義在檔案:
include/uapi/asm-generic/errno-base.h中。
具體看下節中的socket系統調用。
Socket 的API函數 socket
()(該函數定義在/usr/include/sys/socket.h檔案中)
extern int socket (int __domain, int __type, int __protocol) __THROW;
glibc庫對socket系統調用進行了封裝。位于檔案
sysdeps/unix/sysv/linux/i386/socket.S中
其中定義了# define __socket socket,調用__socket就是調用socket函數。
該函數是對socket函數的封裝,代碼中主要邏輯是調用sys_socketcall系統調用,參數為socket的調用号,然後用socketcall函數來進行調用socket。
整體邏輯看上方圖。
可以編譯一個使用socket系統調用的應用程式,進行gdb調試,運作到socket時候進行反彙編顯示如下,下面标紅的一行是移動0x29到eax,而0x29就是41,就是socket系統調用的系統号:
(gdb) disass socket
Dump of assembler code for function socket:
=> 0x00007ffff78f85a0 <+0>: mov $0x29,%eax
0x00007ffff78f85a5 <+5>: syscall
0x00007ffff78f85a7 <+7>: cmp
$0xfffffffffffff001,%rax
0x00007ffff78f85ad <+13>: jae
0x7ffff78f85b0 <socket+16>
0x00007ffff78f85af <+15>: retq
0x00007ffff78f85b0 <+16>: mov
0x2bb8c1(%rip),%rcx #
0x7ffff7bb3e78
0x00007ffff78f85b7 <+23>: neg
%eax
0x00007ffff78f85b9 <+25>: mov
%eax,%fs:(%rcx)
0x00007ffff78f85bc <+28>: or
$0xffffffffffffffff,%rax
0x00007ffff78f85c0 <+32>: retq
End of assembler dump.
編寫一個代碼如下:
#include <unistd.h>
#include <fcntl.h>
int main(){
int handle,bytes;
void * ptr;
handle=open("tmp/test.txt",O_RDONLY);
close(handle);
return 0;
}
編譯:gcc -o hell hello.c
使用strace指令進行跟蹤:
# strace -o log.txt ./hello
打開log.txt可以看到如下内容:
execve("./hello", ["./hello"], [/* 22 vars */]) = 0
brk(<b>NULL</b>) = 0xa1e000
access("/etc/ld.so.nohwcap", <b>F_OK</b>) = -1 <b>ENOENT</b> (No such file
or directory)
mmap(<b>NULL</b>, 8192, <b>PROT_READ</b>|<b>PROT_WRITE</b>, <b>MAP_PRIVATE</b>|<b>MAP_ANONYMOUS</b>, -1, 0) = 0x7fcc310ad000
access("/etc/ld.so.preload", <b>R_OK</b>) = -1 <b>ENOENT</b> (No such file
open("/etc/ld.so.cache", <b>O_RDONLY</b>|<b>O_CLOEXEC</b>) = 3
fstat(3, {st_mode=<b>S_IFREG</b>|0644, st_size=71985, ...}) = 0
mmap(<b>NULL</b>, 71985, <b>PROT_READ</b>, <b>MAP_PRIVATE</b>, 3, 0) = 0x7fcc3109b000
close(3) = 0
open("/lib/x86_64-linux-gnu/libc.so.6", <b>O_RDONLY</b>|<b>O_CLOEXEC</b>) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=<b>S_IFREG</b>|0755, st_size=1868984, ...}) = 0
mmap(<b>NULL</b>, 3971488, <b>PROT_READ</b>|<b>PROT_EXEC</b>, <b>MAP_PRIVATE</b>|<b>MAP_DENYWRITE</b>, 3, 0) = 0x7fcc30ac0000
mprotect(0x7fcc30c80000, 2097152, <b>PROT_NONE</b>) = 0
mmap(0x7fcc30e80000, 24576, <b>PROT_READ</b>|<b>PROT_WRITE</b>, <b>MAP_PRIVATE</b>|<b>MAP_FIXED</b>|<b>MAP_DENYWRITE</b>, 3, 0x1c0000) = 0x7fcc30e80000
mmap(0x7fcc30e86000, 14752, <b>PROT_READ</b>|<b>PROT_WRITE</b>, <b>MAP_PRIVATE</b>|<b>MAP_FIXED</b>|<b>MAP_ANONYMOUS</b>, -1, 0) = 0x7fcc30e86000
mmap(<b>NULL</b>, 4096, <b>PROT_READ</b>|<b>PROT_WRITE</b>, <b>MAP_PRIVATE</b>|<b>MAP_ANONYMOUS</b>, -1, 0) = 0x7fcc3109a000
mmap(<b>NULL</b>, 4096, <b>PROT_READ</b>|<b>PROT_WRITE</b>, <b>MAP_PRIVATE</b>|<b>MAP_ANONYMOUS</b>, -1, 0) = 0x7fcc31099000
mmap(<b>NULL</b>, 4096, <b>PROT_READ</b>|<b>PROT_WRITE</b>, <b>MAP_PRIVATE</b>|<b>MAP_ANONYMOUS</b>, -1, 0) = 0x7fcc31098000
arch_prctl(<b>ARCH_SET_FS</b>, 0x7fcc31099700) = 0
mprotect(0x7fcc30e80000, 16384, <b>PROT_READ</b>) = 0
mprotect(0x600000, 4096, <b>PROT_READ</b>) = 0
mprotect(0x7fcc310af000, 4096, <b>PROT_READ</b>) = 0
munmap(0x7fcc3109b000, 71985)
= 0
open("tmp/test.txt", <b>O_RDONLY</b>) = -1 <b>ENOENT</b> (No such file
close(-1) = -1 <b>EBADF</b> (Bad file descriptor)
exit_group(0) = ?
+++ exited with 0 +++
注意到最後幾行就是我們程式的系統中實作的系統調用。
看到open傳回的是-1(<b>ENOENT</b>),因為在tmp目錄中不存在test.txt檔案。而且我們程式中沒有對檔案打開與否進行判斷,導緻出錯了應用也不知道,隻能通過strace來進行跟蹤。
這個也是在(檔案include/uapi/asm-generic/errno-base.h中定義的:
#define ENOENT 2
/* No such file or directory */)
我們再來看一下之前的一大堆調用,這是為支援我們寫的程式運作,系統進行的程序建立、記憶體映射等等工作。我們在代碼中隻寫了幾行,但是系統卻在編譯連結以及加載到記憶體的時候做了非常多的事情。
是以,在開發應用程式的時候還會覺得麻煩麼?最麻煩的事情底層其實都已經幫我們做好了,實在是找不到借口和老闆說應用程式開發很麻煩了哦。
建立一個tmp/test.txt檔案,再調用發現最後三行如下:
open("tmp/test.txt", <b>O_RDONLY</b>) = 3
說明打開正确了。後續如果要診斷程式的系統調用問題可以使用strace函數。
由于網絡上關于系統的調用的介紹代碼引用比較分散切老舊對新步入的同學造成不同的費解,是以總結此文。
本文基于核心4.14.14代碼介紹了linux系統調用,将系統調用表、調用号所在源碼位置标出,同時梳理的系統調用的整個執行邏輯。最後剖析了socket使用者接口和sys_socket系統調用之間的關系。針對函數細節沒有進行深入,這個未來會有專項課題。
如有錯誤歡迎指正,祝大家玩的愉快。