Socket与系统调用深度分析

系统调用是什么:

  • 计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用。

为什么需要系统调用

  • linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于内核态,而普通的函数调用由函数库或用户自己提供,运行于用户态。
  • 一般的,进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。CPU硬件决定了这些(这就是为什么它被称作“保护模式”。
  • 为了和用户空间上运行的进程进行交互,内核提供了一组接口。透过该接口,应用程序可以访问硬件设备和其他操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求,而内核负责满足这些请求(或者让应用程序暂时搁置)。实际上提供这组接口主要是为了保证系统稳定可靠,避免应用程序肆意妄行,惹出大麻烦。
  • 系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个:
    • 它为用户空间提供了一种统一的硬件的抽象接口。比如当需要读些文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型。
    • 系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限和其他一些规则对需要进行的访问进行裁决。举例来说,这样可以避免应用程序不正确地使用硬件设备,窃取其他进程的资源,或做出其他什么危害系统的事情。
    • 每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。如果应用程序可以随意访问硬件而内核又对此一无所知的话,几乎就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。在Linux中,系统调用是用户空间访问内核的惟一手段;除异常和中断外,它们是内核惟一的合法入口。

基本机制

  • Linux下的系统调用是通过0x80实现的,但是我们知道操作系统会有多个系统调用(Linux下有319个系统调用),而对于同一个中断号是如何处理多个不同的系统调用的?最简单的方式是对于不同的系统调用采用不同的中断号,但是中断号明显是一种稀缺资源,Linux显然不会这么做;还有一个问题就是系统调用是需要提供参数,并且具有返回值的,这些参数又是怎么传递的?也就是说,对于系统调用我们要搞清楚两点:
    • 系统调用的函数名称转换。
    • 系统调用的参数传递。
  • 首先看第一个问题。实际上,Linux中每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,sys_call_table,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在x86上,系统调用号是通过eax寄存器传递给内核的。
  • 用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。
  • 通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。
  • 新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。

Socket的跟踪调用

Replyhi&hello中的Socket系统调用追踪

  • 依次给可能用到的系统调用加上断点,包括:socket、bind、listen、accept、recv、send、connect。
(gdb) b __sys_socket
(gdb) b __sys_bind
(gdb) b __sys_listen
(gdb) b __sys_accept4
(gdb) b __sys_recvfrom
(gdb) b __sys_recvmsg
(gdb) b __sys_sendto
(gdb) b __sys_sendmsg
(gdb) b __sys_connect
  • 断点信息如下;
    1.png
  • 接下来,运行一次replyhi程序,查看其调用情况:
    • 可以看到replyhi分别调用了__sys_socket、__sys_bind函数。
      2.png
  • 接下来,运行一次hello程序,查看其调用情况:
    3.png

Socket系统调用的函数栈

  • 查看调用__sys_socket、__sys_bind的函数堆栈:
    • 可以看到__sys_socket、__sys_bind都是被ia32_compat_sys_socketcall、do_syscall_32_irqs_on、do_fast_syscall_32 、entry_SYSCALL_compat 所调用,而再之上的函数gdb就无法读取到了。于是可以知道,Linux的Socket系统调用的函数或函数指针栈即为ia32_compat_sys_socketcall、do_syscall_32_irqs_on、do_fast_syscall_32 、entry_SYSCALL_compat。用户程序从函数指针entry_SYSCALL_compat找到系统调用函数入口后遍依次进入后面的函数直到调用到所期望的系统调用。
Breakpoint 12, __sys_socket (family=2, type=1, protocol=0) at net/socket.c:1327
1327    {
(gdb) bt
#0  __sys_socket (family=2, type=1, protocol=0) at net/socket.c:1327
#1  0xffffffff8180bd52 in __do_compat_sys_socketcall (args=<optimized out>, call=<optimized out>) at net/compat.c:863
#2  __se_compat_sys_socketcall (args=<optimized out>, call=<optimized out>) at net/compat.c:838
#3  __ia32_compat_sys_socketcall (regs=<optimized out>) at net/compat.c:838
#4  0xffffffff8100261d in do_syscall_32_irqs_on (regs=<optimized out>) at arch/x86/entry/common.c:326
#5  do_fast_syscall_32 (regs=0x2 <irq_stack_union+2>) at arch/x86/entry/common.c:397
#6  0xffffffff81c013d1 in entry_SYSCALL_compat () at arch/x86/entry/entry_64_compat.S:257

Breakpoint 3, __sys_bind (fd=4, umyaddr=0xffed075c, addrlen=16) at net/socket.c:1469
1469    {
(gdb) bt
#0  __sys_bind (fd=4, umyaddr=0xffed075c, addrlen=16) at net/socket.c:1469
#1  0xffffffff8180bd60 in __do_compat_sys_socketcall (args=<optimized out>, call=<optimized out>) at net/compat.c:866
#2  __se_compat_sys_socketcall (args=<optimized out>, call=<optimized out>) at net/compat.c:838
#3  __ia32_compat_sys_socketcall (regs=<optimized out>) at net/compat.c:838
#4  0xffffffff8100261d in do_syscall_32_irqs_on (regs=<optimized out>) at arch/x86/entry/common.c:326
#5  do_fast_syscall_32 (regs=0x4 <irq_stack_union+4>) at arch/x86/entry/common.c:397
#6  0xffffffff81c013d1 in entry_SYSCALL_compat () at arch/x86/entry/entry_64_compat.S:257
#7  0x0000000000000000 in ?? ()

Socket系统调用函数源码

  • 以__sys_socket、__sys_bind为例,查看其源码。
int __sys_socket(int family, int type, int protocol)
{
    int retval;
    struct socket *sock;
    int flags;

    /* Check the SOCK_* constants for consistency.  */
    BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
    BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
    BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
    BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

    flags = type & ~SOCK_TYPE_MASK;
    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
        return -EINVAL;
    type &= SOCK_TYPE_MASK;

    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        return retval;

    return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        err = move_addr_to_kernel(umyaddr, addrlen, &address);
        if (!err) {
            err = security_socket_bind(sock,
                           (struct sockaddr *)&address,
                           addrlen);
            if (!err)
                err = sock->ops->bind(sock,
                              (struct sockaddr *)
                              &address, addrlen);
        }
        fput_light(sock->file, fput_needed);
    }
    return err;
}

posted @ 2019-12-19 14:35  Guo_r  阅读(118)  评论(0编辑  收藏  举报