socket与系统调用深度分析(代码片段)

zaihua zaihua     2023-05-03     432

关键词:

1. 前言

本文主要阐述C语言socket api追踪至系统调用的详细过程。追踪过程分为用户态的追踪和内核态的追踪。

  • 目录
  • 用户态追踪
    • 系统调用定义
    • 系统调用初始化的过程
    • 系统调用的执行过程(以socket为例的证明过程)
  • 内核态追踪
    • 分析replyhi和hello程序
    • gdb跟踪
    • sys_socket()调用栈

2. 用户态追踪

2.1 系统调用定义

操作系统通过系统调用为运行于其上的进程提供服务。
当用户态进程发起一个系统调用, CPU 将切换到内核态并开始执行一个内核函数 。 内核函数负责响应应用程序的要求,例如操作文件、进行网络通讯或者申请内存资源等。

2.2 系统调用的初始化

  • 实验环境:内核Linux-5.0.1,架构x86_32
  • 在x86-32位系统下,系统调用初始化过程为:start_kernel --> trap_init --> idt_setup_traps
  • 验证过程:分别在以上函数处打断点,启动内核
    技术图片
    技术图片

  • start_kernel是Linux内核的起点,位于init/main.c,功能是对内核的主要模块进行初始化工作。其中有一个trap_init函数调用。
537 asmlinkage __visible void __init start_kernel(void)
538 
            ....
595     trap_init();
596     mm_init();
            ....
740 
  • trap_init()在arch/x86/kernel/traps.c中。可以看到idt_setup_traps()被调用
929 void __init trap_init(void)
930 
931     /* Init cpu_entry_area before IST entries are set up */
932     setup_cpu_entry_areas();
933
934     idt_setup_traps();
            ....
955 
  • idt_setup_traps()在arch/x86/kernel/idt.c中。在这里设置各种中断门。
    操作系统通过“门”机制向用户态程序提供必要的服务。在x86种有四种门:中断门、陷阱门、调用门、任务门,这些是cpu从硬件层提供的支持。
    这四个门就是让CPU找到到哪里去执行异常或中断的处理代码,是中断和异常处理机制。
    根据处理异常或中断的区别,选择合适的门来处理。

/* Interrupt gate 中断门*/
#define INTG(_vector, _addr)                    G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS)

/* System interrupt gate  系统中断门 */
#define SYSG(_vector, _addr)                    G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)

/* Task gate 任务门*/
#define TSKG(_vector, _gdt)                 G(_vector, NULL, DEFAULT_STACK, GATE_TASK, DPL0, _gdt << 3)

// def_idt的定义
// 可以看到int 0x80对应的中断服务例程是entry_INT80_32。这就是系统调用的中断门
static const __initconst struct idt_data def_idts[] = 
    INTG(X86_TRAP_DE,       divide_error),
    INTG(X86_TRAP_NMI,      nmi),
    INTG(X86_TRAP_BR,       bounds),
    INTG(X86_TRAP_UD,       invalid_op),
    INTG(X86_TRAP_NM,       device_not_available),
    INTG(X86_TRAP_OLD_MF,       coprocessor_segment_overrun),
    INTG(X86_TRAP_TS,       invalid_TSS),
    INTG(X86_TRAP_NP,       segment_not_present),
    INTG(X86_TRAP_SS,       stack_segment),
    INTG(X86_TRAP_GP,       general_protection),
    INTG(X86_TRAP_SPURIOUS,     spurious_interrupt_bug),
    INTG(X86_TRAP_MF,       coprocessor_error),
    INTG(X86_TRAP_AC,       alignment_check),
    INTG(X86_TRAP_XF,       simd_coprocessor_error),
#ifdef CONFIG_X86_32
    TSKG(X86_TRAP_DF,       GDT_ENTRY_DOUBLEFAULT_TSS),
#else
    INTG(X86_TRAP_DF,       double_fault),
#endif
    INTG(X86_TRAP_DB,       debug),
#ifdef CONFIG_X86_MCE
    INTG(X86_TRAP_MC,       &machine_check),
#endif

    SYSG(X86_TRAP_OF,       overflow),
#if defined(CONFIG_IA32_EMULATION)
    SYSG(IA32_SYSCALL_VECTOR,   entry_INT80_compat),
#elif defined(CONFIG_X86_32)
    SYSG(IA32_SYSCALL_VECTOR,   entry_INT80_32),
#endif
;


/**
 * idt_setup_traps - Initialize the idt table with default traps
 */
void __init idt_setup_traps(void)

    idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);


static void
idt_setup_from_table(gate_desc *idt, const struct idt_data *t, int size, bool sys)

    gate_desc desc;

    for (; size > 0; t++, size--) 
        idt_init_desc(&desc, t);
        write_idt_entry(idt, t->vector, &desc);
        if (sys)
            set_bit(t->vector, system_vectors);
    

1.3 系统调用的执行流程

  1. 应用程序代码调用系统调用,该函数是一个包装系统调用的库函数 ;
  2. 库函数负责准备向内核传递的参数,并触发软中断以切换到内核;
  3. CPU被软中断打断后,执行中断处理函数,即系统调用处理函数(system_call);
  4. 系统调用处理函数调用系统调用服务例程sys_XXX,真正开始处理该系统调用;

其中,第2步的从用户态到内核态的切换比较复杂,又分为以下几个步骤

  1. 在用户态把参数放到对应的寄存器,其中eax存放系统调用号。执行int 0x80指令触发软中断。(参数通过ebx/ecx/edx/est/edi传递)
  2. CPU 被软中断打断后,执行对应的中断处理函数,这时便已进入内核态 ;
  3. 系统调用处理函数准备内核执行栈,并保存所有寄存器
  4. 系统调用处理函数根据系统调用号调用对应的 C 函数—— 系统调用服务例程 ;
  5. 系统调用处理函数准备返回值并从内核栈中恢复寄存器 ;
  6. 系统调用处理函数执行ret指令切换回用户态 ;

证明过程如下:
因为我们无法在menuOS里使用gdb,意味着我们无法在socket api打断点来进行追踪。
但从上面的步骤我们可知,程序是通过int 0x80来实现软中断,进而实现系统调用的。
以socket()函数为例。如果我们可以通过软中断的方式实现创建socket,则可证明以上系统调用执行的步骤是正确的。
在menu/test.c里增加menuOS命令和实现。
技术图片
实现一个sockettest函数,用最简单的socket()函数创建一个socket,判断是否成功并打印。
然后再写一个sockettestASM函数。这里我们不再用socket函数了,而是写入汇编代码,通过寄存器传参,通过"int 0x80来实现软中断"。
技术图片
其中,%ebx存放PF_INET=2,表示用IP协议。%ecx存放SOCK_STRAEM=1,表示用TCP协议。%edx传0。%eax存放sys_socket系统调用号,为0x167=359。

//系统调用号:arch/x86/include/generated/uapi/asm/unistd_32.h
...
#define __NR_socketcall 102
...
#define __NR_socket 359
#define __NR_socketpair 360
#define __NR_bind 361
#define __NR_connect 362
#define __NR_listen 363
#define __NR_accept4 364
#define __NR_getsockopt 365
#define __NR_setsockopt 366
#define __NR_getsockname 367
#define __NR_getpeername 368
#define __NR_sendto 369
#define __NR_sendmsg 370
#define __NR_recvfrom 371
#define __NR_recvmsg 372
#define __NR_shutdown 373

在menuOS中添加sockettest和sockettestasm命令。
技术图片
分别运行这两个命令。比较运行结果。
技术图片
可以看到两种方式都成功创建了socket。可以证明用户态下是通过软中断的方式


3. socket api系统调用分析

3.1 调用socket api的程序

用上次实验的replyhi和hello来跟踪socket api的系统调用
在/lab3/main.c中找到replyhi和hello的实现,并在syswarpper.h里找到每个函数具体调用哪个socket api

#include"syswrapper.h"
#define MAX_CONNECT_QUEUE   1024
// StartReplyhi创建了一个子进程来执行Replyhi
int Replyhi()

    char szBuf[MAX_BUF_LEN] = "";
    char szReplyMsg[MAX_BUF_LEN] = "hi";
    InitializeService();  // socket() -> bind() -> listen() 
    while (1)
    
        ServiceStart();   // accept()
        RecvMsg(szBuf);  // recv() 收到hello
        SendMsg(szReplyMsg);  // send()回复hi
        ServiceStop();  // close()
    
    ShutdownService();
    return 0;


int Hello(int argc, char *argv[])

    char szBuf[MAX_BUF_LEN] = "";
    char szMsg[MAX_BUF_LEN] = "hello";
    OpenRemoteService();  // socket() -> connect()
    SendMsg(szMsg);          // send() 发送hello
    RecvMsg(szBuf);            // recv()  收到hi
    CloseRemoteService();  // close()
    return 0;

可以看到这两个程序就是通过socket api,严格按照服务端和客户端的执行流程来编写的。再次打出socket 客户端和服务端建立连接过程,顺便复习一下。
技术图片
系统调用表:arch/x86/include/generated/uapi/asm/unistd_32.h

3.2 gdb打断点跟踪

在gdb中把断点设在sys_socketcall,在menuOS里执行replyhi
技术图片
可以看到进入到了sys_socketcall里,并调用了SYSCALL_DEFINE2函数
在net/socket.c中可以找到sys_socketcall和SYSCALL_DEFINE2。查看实现

/*
 *  System call vectors.
 *
 *  Argument checking cleaned up. Saved 20% in size.
 *  This function doesn't need to set the kernel lock because
 *  it is set by the callees.
 */

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)

        ...
        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: ...
    case SYS_GETPEERNAME: ...   
    case SYS_SOCKETPAIR: ...
    case SYS_SEND: ...
    case SYS_SENDTO: ...
    case SYS_RECV: ...
    case SYS_RECVFROM: ...
    case SYS_SHUTDOWN: ...
    case SYS_SETSOCKOPT: ...
    case SYS_GETSOCKOPT: ...
    case SYS_SENDMSG: ...
    case SYS_SENDMMSG: ...
    case SYS_RECVMSG: ...
    case SYS_RECVMMSG: ...
    case SYS_ACCEPT4: ...
    default:
        err = -EINVAL;
        break;
    
    return err;

由此可知socket api的系统调用入口为sys_socketcall,然后调用SYSCALL_DEFINE2并传递call参数。在SYSCALL_DEFINE2里通过switch语句和call的值调用不同的socket系统调用。
这些socket接口函数编号的宏定义见include/uapi/linux/net.h

#define SYS_SOCKET  1       /* sys_socket(2)        */
#define SYS_BIND    2       /* sys_bind(2)          */
#define SYS_CONNECT 3       /* sys_connect(2)       */
#define SYS_LISTEN  4       /* sys_listen(2)        */
#define SYS_ACCEPT  5       /* sys_accept(2)        */
#define SYS_GETSOCKNAME 6       /* sys_getsockname(2)       */
#define SYS_GETPEERNAME 7       /* sys_getpeername(2)       */
#define SYS_SOCKETPAIR  8       /* sys_socketpair(2)        */
#define SYS_SEND    9       /* sys_send(2)          */
#define SYS_RECV    10      /* sys_recv(2)          */
#define SYS_SENDTO  11      /* sys_sendto(2)        */
#define SYS_RECVFROM    12      /* sys_recvfrom(2)      */
#define SYS_SHUTDOWN    13      /* sys_shutdown(2)      */
#define SYS_SETSOCKOPT  14      /* sys_setsockopt(2)        */
#define SYS_GETSOCKOPT  15      /* sys_getsockopt(2)        */
#define SYS_SENDMSG 16      /* sys_sendmsg(2)       */
#define SYS_RECVMSG 17      /* sys_recvmsg(2)       */
#define SYS_ACCEPT4 18      /* sys_accept4(2)       */
#define SYS_RECVMMSG    19      /* sys_recvmmsg(2)      */
#define SYS_SENDMMSG    20      /* sys_sendmmsg(2)      */

以socket为例,从sys_socketcall开始函数调用栈如下

socket()
--------内核态---------
    | -->sys_socketcall
        |--> SYSCALL_DEFINE2(call=1)
            |-->sock_create(family, type, protocol, &sock)
                |-->__sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
                    |-->sock_alloc();
                        |-->return sock = SOCKET_I(inode);
                            |-->return &container_of(inode, struct socket_alloc, vfs_inode)->socket
                    |-->pf->create()
                            |-->inet_create()
            |-->sock_map_fd()                
                |-->sock_alloc_fd()
                |-->sock_attach_fd()
                |-->fd_install

socket与系统调用深度分析(代码片段)

Socket与系统调用深度分析SocketAPI编程接口之上可以编写基于不同网络协议的应用程序;Socket接口在用户态通过系统调用机制进入内核;内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;socket相关... 查看详情

socket与系统调用深度分析(代码片段)

一、系统调用与socket编程系统调用是操作系统为用户态进程与硬件设备进行交互提供了一组接口。系统调用通过软中断向内核发出中断请求,int指令(interrupt)+具体的系统调用号触发中断请求。Socket的功能通过调用SocketAPI来实... 查看详情

socket与系统调用深度分析(代码片段)

一、Socket和系统调用  操作系统是计算机资源的管理者,他保证资源被所有的进程共享,并且进程之间不会有干扰,为了达到这个目的,进程不会拥有操作硬件的功能,即进程在计算机上运行是受限制的。而操作系统为了对程... 查看详情

socket与系统调用深度分析(代码片段)

Socket与系统调用系统调用是操作系统为用户态进程与硬件设备之间进行交互提供的一组接口,其实现是通过一个软中断(trap)使系统从用户态切换为内核态。Socket的功能通过调用SocketAPI来实现,而API(applicationprograminterface)实际上... 查看详情

socket与系统调用深度分析(代码片段)

一、实验环境准备uname-a 在本机编译linux5.0.1X86-64内核,重新按照64位方式编译,步骤同上一篇博客。makex86_64_defconfigmakemenuconfigmake#编译内核二、Socket与系统调用1.socketSocketAPI编程接口之上可以编写基于不同网络协议的应用程序... 查看详情

socket与系统调用深度分析(代码片段)

Socket与系统调用深度分析可以想象的是,当应用程序调用socket()接口,请求操作系统提供服务时,必然会系统调用,内核根据发起系统调用时传递的系统调用号,判断要执行的程序,若为socket对应的编号,则执行socket对应的中断... 查看详情

socket系统调用socket与系统调用深度分析(代码片段)

Socket与系统调用深度分析系统调用在一开始,应用程序是可以直接控制硬件的,这就需要程序员有很高的编程能力,否则一旦程序出了问题,会将整个系统Crash。在现在的操作系统中,用户程序运行在用户态,而要进行诸如Socket... 查看详情

socket与系统调用深度分析(代码片段)

本实验以上一次实验为基础,在构建好的menuOS之上,对replyhi进行分析。当在应用中调用到socket()函数时,便会发生系统调用,所有与socket相关的操作都会被映射到 sys_socketcall 这个系统调用中(32位)。给sys_socketcall打上断... 查看详情

socket与系统调用深度分析(代码片段)

在linux中,将程序的运行空间分为内核空间与用户空间(内核态和用户态),在逻辑上它们之间是相互隔离的,因此用户程序不能访问内核数据,也无法使用内核函数。当用户进程必须访问内核或使用某个内核函数时,就得使用... 查看详情

socket与系统调用深度分析(代码片段)

实验要求 SocketAPI编程接口之上可以编写基于不同网络协议的应用程序;Socket接口在用户态通过系统调用机制进入内核;内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;socket相关系统调用的... 查看详情

socket与系统调用深度分析(代码片段)

实验要求:SocketAPI编程接口之上可以编写基于不同网络协议的应用程序;Socket接口在用户态通过系统调用机制进入内核;内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;socket相关系统调用的内... 查看详情

socket与系统调用深度分析(代码片段)

一、linux系统调用原理操作系统通过系统调用为运行于其上的进程提供服务。当用户态进程发起一个系统调用, CPU 将切换到 内核态 并开始执行一个 内核函数 。内核函数负责响应应用程序的要求,例如操... 查看详情

socket与系统调用深度分析(代码片段)

在Linux里面,可通过创建Socket,使得进程之间进行网络通信,可通过TCP或者UDP的方式进行交互。 scoket系统调用主要完成socket的创建,必要字段的初始化,关联传输控制块,绑定文件等任务,完成返回socket绑定的文件描述符;/... 查看详情

socket与系统调用深度分析(代码片段)

本次实验是在X8664环境下Ubuntu18.04.3以及Linux5.0以上的内核中进行。实验将SocketAPI编程接口、系统调用机制及内核中系统调用相关源代码、socket相关系统调用的内核处理函数结合起来分析实验原理:SocketAPI编程接口之上可以编写基... 查看详情

socket与系统调用深度分析(代码片段)

Socket与系统调用深度分析socket接口在用户态通过系统调用机制进入内核:操作系统内核进入与退出的三种方式:系统调用、异常、中断内核将系统调用作为一个特殊的中断来处理,即软中断(对应128号中断向量),使用int0x80指... 查看详情

socket与系统调用深度分析(代码片段)

Socket与系统调用深度分析 1.系统调用:在系统中真正被所有进程都使用的内核通信方式是系统调用。例如当进程请求内核服务时,就使用的是系统调用。一般情况下,进程是不能够存取系统内核的。它不能存取内核使用的内... 查看详情

socket与系统调用深度分析(代码片段)

1.前言本文主要阐述C语言socketapi追踪至系统调用的详细过程。追踪过程分为用户态的追踪和内核态的追踪。目录用户态追踪系统调用定义系统调用初始化的过程系统调用的执行过程(以socket为例的证明过程)内核态追踪分析replyh... 查看详情

socket与系统调用深度分析(代码片段)

一、系统调用什么是系统调用?简单的说,系统调用是操作系统提供给应用程序的接口。为什么必须要使用系统调用呢?是这样,操作系统作为计算机硬件和软件的管理者,为了满足多用户程序的运行需要以及极大限度的利用cpu... 查看详情