【Linux】GNU C庫 accept函數的逐層下調的研究

        在我們寫的socket程序中,一般都會使用網絡套接字API,比如send()、accept()、receive()等函數,那麼這些函數在Linux操作系統中是如何實現的呢?我們將通過分析代碼對其進行深入的研究。本文中所使用的Glibc庫版本爲2.0.111,Linux內核版本爲Linux 1.2.13。

1.用戶層代碼

         在程序中我們使用了accept()函數,這個函數的實現是在Glibc庫,即GNU的C庫中實現的。具體代碼是:

1 //glibc-2.0.111\sysdeps\unix\sysv\linux\accept.S
2 #define    socket    accept
3 #define    __socket __libc_accept
4 #define    NARGS    3
5 #include <socket.S>

         這段與socket.S是accept()從用戶態進入內核態的關鍵代碼。accept.S中將accept定義爲socket,__socket定義爲__libc_accpet,NARGS定義爲3,表示調用參數有3個。接下來包含了socket.S文件。Socket.S的作用與功能按照源文件中的註釋所述:

/* The socket-oriented system calls are handled unusally in Linux.
   They are all gated through the single `socketcall' system call number.
   `socketcall' takes two arguments: the first is the subcode, specifying
   which socket function is being called; and the second is a pointer to
   the arguments to the specific function.

   The .S files for the other calls just #define socket and #include this.  */

        翻譯成中文的大概意思是:

    socket系列的系統函數經常被調用。
    他們都通過單一的一個socketcall系統調用號(進行調用)。
    Socketcall有兩個參數:第一個是子調用碼,指定了哪一個socket函數被調用;第二個參數是一個指向被調用的socket函數所需參數的指針。

    其他的(socket系列的)函數的.S文件只需要#define socket 爲某個值和#include 這個文件(指此socket.S)即可

         在socket.S中進行了進一步的調用,socket從用戶態進行相應參數的設置,然後使用int指令自陷,調用操作系統提供的中斷服務程序,在內核態執行相應的系統服務,我們將整個函數的代碼粘貼進來,在具體的語句上進行註釋解釋:

 1 // glibc-2.0.111\sysdeps\unix\sysv\linux\i386\socket.S
 2 #include <sysdep.h>
 3 #include <socketcall.h>
 4 // 定義了P(a,b)與P2(a,b)兩個宏,他們的作用都是將a與b連接到一起。
 5 #define P(a, b) P2(a, b)
 6 #define P2(a, b) a##b
 7 
 8     .text
 9 
10 #ifndef __socket
11 #ifndef NO_WEAK_ALIAS
12 #define __socket P(__,socket)     
13 #else
14 #define __socket socket
15 #endif
16 #endif
17 
18 .globl __socket
19 ENTRY (__socket)   //這裏開始進行函數的處理
20 
21 
22     /* 保存ebx的值  */
23     movl %ebx, %edx
24 
25     // SYS_ify宏在sysdep.h中定義。一會兒詳細瞭解它的作用
26     // 下面一條語句的作用是將socketcall的調用號存入寄存器eax
27     movl $SYS_ify(socketcall), %eax    /* System call number in %eax.  */
28 
29     /* 子調用號放入ebx中,關於下面一條語句的將在下面有詳細解釋  */
30     movl $P(SOCKOP_,socket), %ebx    /* Subcode is first arg to syscall.  */
31     /* 指向調用參數的指針放入ecx中  */
32     lea 4(%esp), %ecx        /* Address of args is 2nd arg.  */
33 
34         /* 0x80中斷,自陷進入內核態 */
35     int $0x80
36 
37     /* 恢復ebx寄存器的值 */
38     movl %edx, %ebx
39 
40     /* eax是返回值,如果<0則表示調用出錯,就跳到錯誤處理的代碼中去  */
41     cmpl $-125, %eax
42     jae SYSCALL_ERROR_LABEL
43 
44     /* 成功的話就返回相應的返回值  */
45 L(pseudo_end):
46     ret
47 
48 PSEUDO_END (__socket)
49 
50 #ifndef NO_WEAK_ALIAS
51 weak_alias (__socket, socket)
52 #endif

         我們首先看movl $SYS_ify(socketcall), %eax這一條語句。SYS_ify在sysdep.h中定義,但是有兩個不同文件夾下的sysdep.h文件。

(1)

        按照文件層次來講,應該是按照如下的代碼進行:

1 // glibc-2.0.111\sysdeps\unix\sysv\linux\i386\sysdep.h
2 .....
3 #undef SYS_ify
4 #define SYS_ify(syscall_name)    __NR_##syscall_name
5 .....

         在這段代碼之前有一段註釋:

/* For Linux we can use the system call table in the header file
    /usr/include/asm/unistd.h
   of the kernel.  But these symbols do not follow the SYS_* syntax
   so we have to redefine the `SYS_ify' macro here.  */

         中文的大概意思是:

   對於Linux系統,我們可以使用在/usr/include/asm/unistd.h頭文件中的內核系統調用表。
   但是這些符號並不是以SYS_符號爲前綴的,所以這裏我們必須重定義SYS_ify宏。

         可以看到,通過SYS_ify(socketcall),我們得到了__NR_socketcall。

 

(2)

         按照另外一本書上所講的,在下列位置中存在另外一套代碼:

1 // glibc-2.0.111\sysdeps\unix\sysdep.h
2 ……
3 #ifdef __STDC__
4 #define SYS_ify(syscall_name) SYS_##syscall_name
5 #else
6 #define SYS_ify(syscall_name) SYS_/**/syscall_name
7 #endif
8 ……

         如果是經由這段代碼的處理,那麼我們將得到SYS_socketcall,那麼這又是一個什麼呢?我們查看源代碼是看不到的。而在實際的操作系統(筆者所使用的是Fedora 14)中,/usr/include /bits/syscall.h中則有相應的答案,這個文件是libc在構建時候根據具體的操作系統而生成的。在其中,會有:

 1 #ifndef _SYSCALL_H
 2 # error "Never use <bits/syscall.h> directly; include <sys/syscall.h> instead."
 3 #endif
 4 
 5 #define SYS__llseek __NR__llseek
 6 #define SYS__newselect __NR__newselect
 7 #define SYS__sysctl __NR__sysctl
 8 #define SYS_access __NR_access
 9 #define SYS_acct __NR_acct
10 ……
11 #define SYS_socketcall __NR_socketcall
12 ……

         可以看到,通過這一部分的處理之後,最後依然會得到__NR_socketcall。

2.內核態代碼

         瞭解Linux系統的人都知道,在/linux/include/linux/unistd.h中,我們可以看到,這些內容:

 1 // linux/include/linux/unistd.h
 2 ……
 3 #define __NR_setup          0    /* used only by init, to get system going */
 4 #define __NR_exit          1
 5 #define __NR_fork          2
 6 #define __NR_read          3
 7 #define __NR_write          4
 8 ……
 9 #define __NR_socketcall        102
10 ……

         我們可以看到,__NR_socketcall被定義爲102,上面一行的代碼即是將eax的值賦成102,即此係統調用的調用號。

         下面我們看movl $P(SOCKOP_,socket), %ebx這一句。在socketcall.h中有相應的定義:

 1 // glibc-2.0.111\sysdeps\unix\sysv\linux\socketcall.h
 2 ……
 3 #define SOCKOP_socket        1
 4 #define SOCKOP_bind        2
 5 #define SOCKOP_connect        3
 6 #define SOCKOP_listen        4
 7 #define SOCKOP_accept        5
 8 #define SOCKOP_getsockname    6
 9 #define SOCKOP_getpeername    7
10 #define SOCKOP_socketpair    8
11 #define SOCKOP_send        9
12 #define SOCKOP_recv        10
13 #define SOCKOP_sendto        11
14 #define SOCKOP_recvfrom        12
15 #define SOCKOP_shutdown        13
16 #define SOCKOP_setsockopt    14
17 #define SOCKOP_getsockopt    15
18 #define SOCKOP_sendmsg        16
19 #define SOCKOP_recvmsg        17
20 ……

         這一句的意思就是將相應的操作碼賦予ebx,此例中是5。下面我們進入操作系統中的代碼進行分析,在entry.S中,有一段中斷處理函數:

 1 // linux/arch/i386/kernel/entry.S
 2 _system_call:
 3 // 保存eax的值
 4     pushl %eax            # save orig_eax
 5 // 保存所有寄存器的值
 6     SAVE_ALL
 7     movl $-ENOSYS,EAX(%esp)
 8 // 比較eax中的調用號是否超過了限定的數值,NR_syscalls,默認是256。
 9     cmpl $(NR_syscalls),%eax  # compare whether eax>NR_syscalls
10     jae ret_from_sys_call
11 //從系統調用表中找到對應的入口地址,放入eax中
12     movl _sys_call_table(,%eax,4),%eax
13     testl %eax,%eax
14     je ret_from_sys_call
15 // 子調用號放入ebx中
16     movl _current,%ebx
17     andl $~CF_MASK,EFLAGS(%esp)    # clear carry - assume no errors
18     movl $0,errno(%ebx)
19     movl %db6,%edx
20     movl %edx,dbgreg6(%ebx)  # save current hardware debugging status
21     testb $0x20,flags(%ebx)        # PF_TRACESYS
22     jne 1f
23 // 進行系統調用
24     call *%eax
25     movl %eax,EAX(%esp)        # save the return value
26     movl errno(%ebx),%edx
27     negl %edx
28     je ret_from_sys_call
29     movl %edx,EAX(%esp)
30     orl $(CF_MASK),EFLAGS(%esp)    # set carry to indicate error
31     jmp ret_from_sys_call

         具體的語句的作用已經在代碼中進行了標註。我們接下來可以查看處理socket調用的系統函數socket.c:

 1 // linux/net/socket.c
 2 ……
 3 asmlinkage int sys_socketcall(int call, unsigned long *args)
 4 {
 5     int er;
 6     switch(call) 
 7     {
 8         case SYS_SOCKET:
 9             er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
10             if(er)
11                 return er;
12             return(sock_socket(get_fs_long(args+0),
13                 get_fs_long(args+1),
14                 get_fs_long(args+2)));
15 ……
16         case SYS_ACCEPT:
17             er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
18             if(er)
19                 return er;
20             return(sock_accept(get_fs_long(args+0),
21                 (struct sockaddr *)get_fs_long(args+1),
22                 (int *)get_fs_long(args+2)));
23 ……

         這個sys_socketcall函數是socket系列函數的分發函數,根據具體調用號,調用不同的處理函數進行處理,至此,我們看到了整個從應用層socket函數到BSDsocket的層的傳遞過程,加深了我們對於此過程的瞭解。

 

       

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章