Socket与系统调用深度分析

本文将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在X86 64环境下Linux5.0以上的内核中进一步跟踪验证。

1. 系统调用的初始化

在加电启动BootLoader运行后,BootLoader对硬件初始化并把内核加载进内存然后将参数传给内核后,内核将接过系统控制权开始运行,而内核运行的第一个函数入口就是start_kernel这个入口函数。它将进行一系列初始化,其中就包括系统调用初始化,内核最后创建系统的第0号进程rest_init,它做的一件事情是从根文件系统寻找init函数作为系统第1号进程运行,rest_init则退化为系统空闲时候运行的idle进程。内核启动过程流程大致如下:

Socket与系统调用深度分析_第1张图片

而在start_kernel众多初始化中,有一项初始化 tarp_init()即系统调用初始化,涉及到一些初始化中断向量,可以看到它在set_intr_gate设置到很多的中断门,很多的硬件中断,其中有一个系统陷阱门,进行系统调用的。之后还有idt_setup_tarps()即初始化中断描述表初始化。使用gdb验证如下:

Socket与系统调用深度分析_第2张图片

系统调用初始化完成后,我们的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_initkthreadd内核线程。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:  
        …...  

Socket与系统调用深度分析_第3张图片

 

上面这张图是open()的系统调用示意图,socket与之类似。

3.gdb跟踪验证

由于我们已经在menu os中集成了replyhi和hello两个程序,这两个通信程序就使用了socket()API,如下图:

Socket与系统调用深度分析_第4张图片

 

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

最后发现

Socket与系统调用深度分析_第5张图片

 

只有sys_socketcall断点成功,其他都直接编译处理掉了。继续运行内核,出现如下结果:

Socket与系统调用深度分析_第6张图片

可以看到内核启动过程三次调用sys_socketcall,而且都是子终端号call=1,即SYS_SOCKET中断处理程序分配了3个socket套接字,用于Bring  up  interface:lo  和  Bring  up  interface:  etho和List  all  interfaces。(具体我也不知道干啥的,以后再探究)

然后内核加载完,我们在menu os里面运行 replyhi 程序,可以看到又发生4次系统调用:

Socket与系统调用深度分析_第7张图片

Socket与系统调用深度分析_第8张图片

Socket与系统调用深度分析_第9张图片

上面要对着子中断号call具体是什么,再结合replyhi程序源码来分析,这里就先到这儿,至少系统调用栈的验证算是成功了。

不过这里有一处好奇的地方:我打的断点是sys_socketcall,  为什么实际上它在__se_sys_socketcall  出停止运行呢?为什么名称不一样???

你可能感兴趣的:(Socket与系统调用深度分析)