在我们写的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的层的传递过程,加深了我们对于此过程的了解。