本文将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在X86 64环境下Linux5.0以上的内核中进一步跟踪验证。
1. 系统调用的初始化
在加电启动BootLoader运行后,BootLoader对硬件初始化并把内核加载进内存然后将参数传给内核后,内核将接过系统控制权开始运行,而内核运行的第一个函数入口就是start_kernel这个入口函数。它将进行一系列初始化,其中就包括系统调用初始化,内核最后创建系统的第0号进程rest_init,它做的一件事情是从根文件系统寻找init函数作为系统第1号进程运行,rest_init则退化为系统空闲时候运行的idle进程。内核启动过程流程大致如下:
而在start_kernel众多初始化中,有一项初始化 tarp_init()即系统调用初始化,涉及到一些初始化中断向量,可以看到它在set_intr_gate
设置到很多的中断门,很多的硬件中断,其中有一个系统陷阱门,进行系统调用的。之后还有idt_setup_tarps()即初始化中断描述表初始化。使用gdb验证如下:
系统调用初始化完成后,我们的TCP/IP协议栈怎么加载进内核的呢?我们看看rest_init源码:
393static noinline void __init_refok rest_init(void) 394{ 395 int pid; 396 397 rcu_scheduler_starting(); 398 /* 399 * We need to spawn init first so that it obtains pid 1, however 400 * the init task will end up wanting to create kthreads, which, if 401 * we schedule it before we create kthreadd, will OOPS. 402 */ 403 kernel_thread(kernel_init, NULL, CLONE_FS); 404 numa_default_policy(); 405 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); 406 rcu_read_lock(); 407 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); 408 rcu_read_unlock(); 409 complete(&kthreadd_done); 410 411 /* 412 * The boot idle thread must execute schedule() 413 * at least once to get things moving: 414 */ 415 init_idle_bootup_task(current); 416 schedule_preempt_disabled(); 417 /* Call into cpu_idle with preempt disabled */ 418 cpu_startup_entry(CPUHP_ONLINE); 419}
通过rest_init()
新建kernel_init
、kthreadd
内核线程。403行代码 kernel_thread(kernel_init, NULL, CLONE_FS);
,由注释得调用 kernel_thread()
创建1号内核线程(在kernel_init
函数正式启动),kernel_init
函数启动了init用户程序。另外405行代码 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
调用kernel_thread
执行kthreadd
,创建PID为2的内核线程。rest_init()
最后调用cpu_idle()
演变成了idle进程。更多细节参考:https://github.com/mengning/net/blob/master/doc/tcpip.md
至此系统调用初始化完成而且socket系统调用的中断处理程序(TCP/IP协议栈)也已经注册好了。
2. 用户态程序发起系统调用
当用户程序调用socket()这个API时,glibc库中会转为调用 __socket()函数,为什么要做这样一步呢?以为这样可以让大家统一只包括glic这个库不用再关心其他头文件,而实现glic这个脏活累活交给系统级开发人员干。_socket()这个函数定义在socket.s这个汇编文件中,它完成参数的传递,然后ENTER_KERNEL进入内核,代码如下:
movl $SYS_ify(socketcall), %eax /* System call number in %eax. */ /* Use ## so `socket' is a separate token that might be #define'd. */ movl $P(SOCKOP_,socket), %ebx /* Subcode is first arg to syscall. */ lea 4(%esp), %ecx /* Address of args is 2nd arg. */ /* Do the system call trap. */ ENTER_KERNEL
其中
SYS_ify宏定义为
#define SYS_ify(syscall_name) __NR_##syscall_name;
P宏定义为
#define P(a, b) P2(a, b) #define P2(a, b) a##b
##为连接符号。
#define __NR_socketcall 102
#define SOCKOP_socket 1
因此,中断号是102,子中断号是1;
而ENTER_KERNEL是什么呢?为啥它就能进入内呢?请看下面定义:
# define ENTER_KERNEL int $0x80
int $0x80是x86的软中断指令,使用它会使系统进入内核模式,也就是所谓的内陷。
该指令会跳转到system_call中断入口在kernel/arch/x86/kernel/entry_32.S:
syscall_call: call *sys_call_table(,%eax,4)
该指令又会跳转到对应的
中断向量表102号中断:
.long sys_socketcall
进入sys_socketcall()函数,根据子中断号(socket是1)以决定走哪个分支:kernel/net/Socket.c:
switch (call) { case : break; case SYS_BIND: …...
上面这张图是open()的系统调用示意图,socket与之类似。
3.gdb跟踪验证
由于我们已经在menu os中集成了replyhi和hello两个程序,这两个通信程序就使用了socket()API,如下图:
int Replyhi() { char szBuf[MAX_BUF_LEN] = "\0"; char szReplyMsg[MAX_BUF_LEN] = "hi\0"; int sockfd = -1; struct sockaddr_in serveraddr; struct sockaddr_in clientaddr; socklen_t addr_len = sizeof(struct sockaddr); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(PORT); serveraddr.sin_addr.s_addr = inet_addr(IP_ADDR); memset(&serveraddr.sin_zero, 0, 8); sockfd = socket(PF_INET,SOCK_STREAM,0); int ret = bind( sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr)); if(ret == -1) { fprintf(stderr,"Bind Error,%s:%d\n", __FILE__,__LINE__); close(sockfd); return -1; } listen(sockfd,MAX_CONNECT_QUEUE); while(1) { int newfd = accept( sockfd, (struct sockaddr *)&clientaddr, &addr_len); if(newfd == -1) { fprintf(stderr,"Accept Error,%s:%d\n", __FILE__,__LINE__); } ret = recv(newfd,szBuf,MAX_BUF_LEN,0); if(ret > 0) { printf("recv \"%s\" from %s:%d\n", szBuf, (char*)inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); \ } ret = send(newfd,szReplyMsg,strlen(szReplyMsg),0); if(ret > 0) { printf("rely \"hi\" to %s:%d\n", (char*)inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); \ } close(newfd); } close(sockfd); return 0; }
其中使用了socket,bind, listen, accpet, recv, send, close等API,都会发生系统调用,我们只跟踪socket()这个API的调用栈来验证即可。运行如下命令:
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -append nokaslr -s -S
然后,按照我们前面的socketAPI系统调用栈来打断点
gdb file vmlinux b sys_call b sys_socketcall b call *syacall_call_table(,%eax,4) b SYS_SOCKET target remote:1234
最后发现
只有sys_socketcall断点成功,其他都直接编译处理掉了。继续运行内核,出现如下结果:
可以看到内核启动过程三次调用sys_socketcall,而且都是子终端号call=1,即SYS_SOCKET中断处理程序分配了3个socket套接字,用于Bring up interface:lo 和 Bring up interface: etho和List all interfaces。(具体我也不知道干啥的,以后再探究)
然后内核加载完,我们在menu os里面运行 replyhi 程序,可以看到又发生4次系统调用:
上面要对着子中断号call具体是什么,再结合replyhi程序源码来分析,这里就先到这儿,至少系统调用栈的验证算是成功了。
不过这里有一处好奇的地方:我打的断点是sys_socketcall, 为什么实际上它在__se_sys_socketcall 出停止运行呢?为什么名称不一样???