Socket与系统调用深度分析

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

1. 系统调用的初始化git

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

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

系统调用初始化完成后,咱们的TCP/IP协议栈怎么加载进内核的呢?咱们看看rest_init源码:app

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.mdsocket

至此系统调用初始化完成并且socket系统调用的中断处理程序(TCP/IP协议栈)也已经注册好了。tcp

2. 用户态程序发起系统调用函数

当用户程序调用socket()这个API时,glibc库中会转为调用  __socket()函数,为何要作这样一步呢?觉得这样可让你们统一只包括glic这个库不用再关心其余头文件,而实现glic这个脏活累活交给系统级开发人员干。_socket()这个函数定义在socket.s这个汇编文件中,它完成参数的传递,而后ENTER_KERNEL进入内核,代码以下:spa

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  出中止运行呢?为何名称不同???

相关文章
相关标签/搜索