Socket与系统调用深度分析
Socket与系统调用深度分析
socket接口在用户态通过系统调用机制进入内核:
操作系统内核进入与退出的三种方式:系统调用、异常、中断
内核将系统调用作为一个特殊的中断来处理,即软中断(对应128号中断向量),使用int 0x80指令陷入到内核,128号中断向量对应的中断服务例程是 entry_INT80_32 。
中断向量是怎么初始化的?在内核初始化的时候(start_kernel)初始化了中断向量和对应的中断处理程序。把0x80中断与entry_INT80_32绑定,使得当用户态程序执行到int 0x80指令时程序能够跳转到entry_INT80_32。对于使用x86架构的32位系统,系统调用的初始化过程为:
start_kernel-->trap_init-->idt_setup_traps-->0x80与entry_INT80_32绑定
irq(interrupt request,中断请求)
idt(interrupt descriptor table,中断描述符表)
绑定后(初始化完),每次系统调用的底层汇编都会执行int 0x80,然后使用%eax寄存器传递系统调用号,操作系统根据系统调用号去系统调用表里找对应的服务例程。例如:用户态调用connect()函数,对应的汇编是先将362放到寄存器%eax中,然后执行int 0x80指令,进入内核,内核执行128号中断向量对应的服务例程entry_INT80_32 ,entry_INT80_32根据%eax的值362去系统调用表里找对应的服务例程为sys_connect,执行sys_connect函数,执行完毕后返回到用户态。当然这只是大致流程,具体的函数调用不会这么简单。
glibc提供的与socket有关的系统调用函数API、系统调用号及对应的内核处理函数:
系统调用号 | 系统调用API | 对应的内核处理函数 |
---|---|---|
102 | socektcall | sys_socketcall |
359 | socket | sys_socket |
361 | bind | sys_bind |
362 | connect | sys_connect |
363 | listen | sys_listen |
6 | close | sys_close |
364 | accept4 | sys_accept4 |
369 | sendto | sys_sendto |
371 | recvfrom | sys_recvfrom |
实验验证:
- 系统初始化过程:
断点设在:start_kernel、trap_init、idt_setup_traps,跟踪内核启动过程,
内核启动依次停在断点处,结果与分析一致。
- 跟踪socket系统调用直至内核处理函数
首先在sys_socketcall处设置断点:
在net/socket.c下找到该函数的定义:可以看到socketcall函数使用swith语句,根据call的值执行不同的内核处理函数,从而使不同的socket系统调用有统一的接口
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
unsigned long a[AUDITSC_ARGS];
unsigned long a0, a1;
int err;
unsigned int len;
if (call < 1 || call > SYS_SENDMMSG)
return -EINVAL;
call = array_index_nospec(call, SYS_SENDMMSG + 1);
len = nargs[call];
if (len > sizeof(a))
return -EINVAL;
/* copy_from_user should be SMP safe. */
if (copy_from_user(a, args, len))
return -EFAULT;
err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
if (err)
return err;
a0 = a[0];
a1 = a[1];
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]);
break;
case SYS_CONNECT:
err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_LISTEN:
err = __sys_listen(a0, a1);
break;
case SYS_ACCEPT:
err = __sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);
break;
case SYS_GETSOCKNAME:
err =
__sys_getsockname(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_GETPEERNAME:
err =
__sys_getpeername(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_SOCKETPAIR:
err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
break;
case SYS_SEND:
err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
NULL, 0);
break;
case SYS_SENDTO:
err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
(struct sockaddr __user *)a[4], a[5]);
break;
case SYS_RECV:
err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
NULL, NULL);
break;
case SYS_RECVFROM:
err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
(struct sockaddr __user *)a[4],
(int __user *)a[5]);
break;
case SYS_SHUTDOWN:
err = __sys_shutdown(a0, a1);
break;
case SYS_SETSOCKOPT:
err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
a[4]);
break;
case SYS_GETSOCKOPT:
err =
__sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
(int __user *)a[4]);
break;
case SYS_SENDMSG:
err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
a[2], true);
break;
case SYS_SENDMMSG:
err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
a[3], true);
break;
case SYS_RECVMSG:
err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
a[2], true);
break;
case SYS_RECVMMSG:
if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME))
err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
a[2], a[3],
(struct __kernel_timespec __user *)a[4],
NULL);
else
err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
a[2], a[3], NULL,
(struct old_timespec32 __user *)a[4]);
break;
case SYS_ACCEPT4:
err = __sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], a[3]);
break;
default:
err = -EINVAL;
break;
}
return err;
}
在qemu中执行命令replyhi后,服务端程序启动,使用gdb单步调试结果如下:
执行顺序为:call=1、call=2、call=4、call=5。在我们的实验环境中,socket接口的调用是通过给socket接口函数编号的方式通过112号系统调用来处理的。这些socket接口函数编号的宏定义见如下:
26#define SYS_SOCKET 1 /* sys_socket(2) */
27#define SYS_BIND 2 /* sys_bind(2) */
28#define SYS_CONNECT 3 /* sys_connect(2) */
29#define SYS_LISTEN 4 /* sys_listen(2) */
30#define SYS_ACCEPT 5 /* sys_accept(2) */
31#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
32#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
33#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
34#define SYS_SEND 9 /* sys_send(2) */
35#define SYS_RECV 10 /* sys_recv(2) */
36#define SYS_SENDTO 11 /* sys_sendto(2) */
37#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
38#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
39#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
40#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
41#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
42#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
43#define SYS_ACCEPT4 18 /* sys_accept4(2) */
44#define SYS_RECVMMSG 19 /* sys_recvmmsg(2) */
45#define SYS_SENDMMSG 20 /* sys_sendmmsg(2) */
查表可知内核函数执行顺序为:SYS_SOCKET->SYS_BIND->SYS_LISTEN->SYS_ACCEPT。然后服务端处于阻塞状态,等待客户端的连接。这与理论完全一致:
然后在qemu中输入命令hello,启动客户端程序:
客户端的执行顺序为:call=1、call=3、call=10、call=9。查表可知内核函数的执行次序为:SYS_SOCKET->SYS_CONNECT->SYS_RECV->SYS_SEND。
然后服务端执行:call=10、call=9、call=5。查表可知内核函数的执行次序为:SYS_RECV->SYS_SEND->SYS_ACCEPT。服务端又阻塞在accept状态,等待客户端的连接。