Socket与系统调用深度分析
一.系统调用原理:
系统调用是linux内核为用户态程序提供的主要功能接口。通过系统调用,用户态进程能够临时切换到内核态,使用内核态才能访问的硬件和资源完成特定功能。系统调用由linux内核和内核模块实现,内核在处理系统调用时还会检查系统调用请求和参数是否正确,保证对特权资源和硬件访问的正确性。通过这种方式,linux在提供内核和硬件资源访问接口的同时,保证了内核和硬件资源的使用正确性和安全性。
再从代码层面抽象地看一下应⽤程序、封装例程、系统调⽤处理程序及系统调⽤服务例程之间的关系
应用程序需要使用内核资源时需要通过系统调用进入内核态,调用内核函数使用相应的资源,完成相应的功能,上图中封装在库文件中的api,如xyz()是运行在用户态的相应函数,当这些函数需要使用内核资源是,通过执行int 0x80转到entry_INT80_32这个系统调用函数,系统调用函数通过系统调用号找到相应的内核函数,执行相应的内核函数sys_xyz()使用相应的内核资源完成相应的功能,在整个过程中需要相应的中断的支持,中断执行过程中需要现场保护,查找识别中断源,执行中断处理程序,恢复现场;其中查找识别中断源就是对应系统调用函数通过系统调用号找到相应的内核函数,执行中断函数就是执行相应的内核函数。
对于int ox80转到INT_80_32的过程是通过内核函数初始化的过程完成的。
内核的初始化完成了以下的函数调用过程:start_kernel > trap_init > idt_setup_traps, 其中start_kernal是内核启动的入口函数,它调用了trap_init,trap_init函数中的这行代码: SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32),使得int 0x80指令和entry_INT80_32在内核初始化的过程中完成了绑定,其内容写入idt中。在后续的执行过程中,一旦出现了int 0x80这条中断指令,就会直接跳转到entry_INT80_32去了。trap_init函数中又调用了idt_setup_traps,它完成对中断描述符表的初始化。idt将每个异常或中断向量分别与它们的处理过程联系起来。
二.socket对于应用函数系统调用过程中对内核函数的追踪:
由上面系统调用过程可知道,我们可以通过gdb在相应的socket内核函数处设置断点追踪内核函数的执行,首先我们利用上次实验的环境,在Linux中通过gdb对socket函数的追踪,可以发现socket应用函数在调用内核函数的具体的执行过程。
1.完成gdb与gdbserv的连接:
2.在对断点设置之前我们可以通过系统调用表发现与socket api相关的系统调用函数是sys_socketcall,我们在sys_socketcall处设置断点可以发现如下结果:
可以发现函数在执行到断点sys_socketcall 处停止,接着调用内核函数SYSCALL_DEFINE2。
3.通过查询SYSCALL_DEFINNE2函数源码如下:
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;
}
可以发现该函数通过case分支语句实现实现了socket内核的个函数,例如bind()、listen()、send()、recv()、close()等,下面可以对bind()、listen()等函数打上断点来测试内核函数的执行是否是这样.
由上图可以发现每次在进入__sys_bind、和__sys_listen时之前都要经过sys_socketcall,所有以上说明了之前是正确的,即socket对应的内核函数__sys_bind、__sys_listen等是系统调用函数sys_socketcall的调用SYSCALL_DEFINE2执行的,同理上图结果也说明通过在调用函数处设置断点追踪到socket api是如何执行相应的调用函数的。