socket与系统调用深度分析

一、什么是系统调用?

  系统调用是操作系统提供给应用程序的接口。为什么必须要使用系统调用呢?是这样,操作系统作为计算机硬件和软件的管理者,为了满足多用户程序的运行需要以及极大限度的利用cpu,必须要实现对硬件的接管,使得所有需要访问硬件的操作都要经过操作系统的把关,在操作系统的监管下合理分配资源、推进程序的运行

  调用系统调用和调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。调用自定义函数是通过call指令直接跳转到该函数的地址,继续运行。而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫API(Application Programming Interface)。 我们都知道,其实对一切需要访问硬件的操作的实现都必须要通过内核中的函数来实现,应用程序通过调用API执行系统调用进而调用相应的系统函数。

二、实验过程

本次实验是基于上次搭建的环境——Qemu中,完成socket系统调用的追踪

系统调用是一种软中断,是一种主动向内核发出请求,来让内核服务实现某种需求。既然是中断就必须有中断号和与之对应的中断处理函数

查阅资料,发现socket对应的系统调用函数是sys_socketcall,接下来就是通过gdb调试,跟踪这个函数来查看从用户态到内核态最终实现整个socket通信的流程。

cd ~/MenuOs/menu
qemu -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -append nokaslr -s (去掉-S)
或者是去掉Makefile中的-S,执行make rootfs

另起一个终端
gdb
file ~/MenuOS/linux-5.0.1/vmlinux
target remote:1234

 

 这里在sys_socketcall设置了断点,c运行;在qemu中输入replyhi,可以看到程序停在了sys_socketcall处,说明执行了这个函数;

c继续运行,可以看到又执行了三次,直到qemu提示输入hello,可以观察到每次函数的参数不同(call、args),由socket通信过程推断这几个函数可能分别对应socket、bind、listen、accept

输入hello,此时有连续调用了7次sys_socketcall

从上面可以看到,sys_socketcall系统调用对应的内核处理函数为SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)这个宏函数,

在net/socket.c找到他的具体实现:

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]);          #call=1
        break;
    case SYS_BIND:                                #call=2
        err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_CONNECT:                   #call=3
        err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
        break;
    case SYS_LISTEN:                               #call=4
        err = __sys_listen(a0, a1);
        break;
    case SYS_ACCEPT:                               #call=5
        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:                                #call=9
        err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
                   NULL, 0);
        break;
    case SYS_SENDTO:                              #call=10
        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;
}

   从上面这段代码可以很清楚的看到其实就是一个通过switch判断系统调用号,这个系统调用号是通过寄存器EAX在每次中断的时候传递的,比如bind函数,那么就将2放到eax寄存器中,然后通过int 0x80这条中断指令,使pc强制指向系统中断入口处,进入找到相应的中断处理函数。

  在本次实验中,replyhi服务端程序socket、bind、listen、accpet、send分别对应call=1,2,4,5 ,10,hello客户端 socket、connet、send、 rev分别对应1、3、10、9。

实验中遇到的问题:

  在输入hello之后,程序并没有继续运行,输入help发现没有hello这个命令,说明此时Menuos中并没有这个程序;查看了上次搭建环境的lab3中Makefile看到,其源代码并没有加入到menu中(lab2的Makefile中将test.c复制到了menu中,所有含有replyhi命令),所以需要将lab3中的main.c复制到menu中,并且更名为test.c,这是因为menu的makefile中指定的主函数是test.c,然后make,即可。

posted @ 2019-12-19 20:05  andyflyto  阅读(191)  评论(0编辑  收藏  举报