Linux内核源码分析 - 系统调用 . 续

上一篇文章 Linux内核源码分析 - 系统调用 中分析了linux下的系统调用在kernel space层是如何实现的,现在我们来分析下user space层的实现。

上篇结尾讲到我们可以使用syscall机器指令来调用系统调用,那如何指定系统调用的编号及参数,以及如何获取返回值呢?

详细介绍可以参考这篇文章:

http://man7.org/linux/man-pages/man2/syscall.2.html

简而言之就是通过一定的约定来实现指定系统调用编号和传递参数及返回值。

比如x86_64平台,在执行syscall机器码之前,系统调用的编号要先放到rax寄存器,参数要分别放到rdi、rsi、rdx、r10、r8、r9寄存器中,这样kernel中的代码就会从这些地方取值,然后继续执行逻辑,当kernel部分的逻辑完成之后,结果会再放到rax寄存器中,这样user space的部分就可以从rax寄存器中拿到返回值。

下面我们再来看下上篇文章最后的例子:

# ----------------------------------------------------------------------------------------
# Writes "Hello, World" to the console using only system calls. Runs on 64-bit Linux only.
# To assemble and run:
#
#     gcc -c hello.s && ld hello.o && ./a.out
#
# or
#
#     gcc -nostdlib hello.s && ./a.out
# ----------------------------------------------------------------------------------------

        .global _start

        .text
_start:
        # write(1, message, 13)
        mov     $1, %rax                # system call 1 is write
        mov     $1, %rdi                # file handle 1 is stdout
        mov     $message, %rsi          # address of string to output
        mov     $13, %rdx               # number of bytes
        syscall                         # invoke operating system to do the write

        # exit(0)
        mov     $60, %rax               # system call 60 is exit
        xor     %rdi, %rdi              # we want return code 0
        syscall                         # invoke operating system to exit
message:
        .ascii  "Hello, world\n"

现在就非常明白了吧,比如第一个write系统调用,因为其编号为1,所以先将1放到rax里,之后将标准输出文件描述符到到rdi里,再之后将message地址放到rsi里,再之后将message的长度13放到rdx里,最后调用syscall机器码,这样就会转到对应kernel space部分的代码。

从汇编角度我们已经讲明白了,那在c语言中我们又是如何调用呢?总不能在c中嵌入汇编代码吧?

其实本质上就是在c中嵌入汇编代码,只是不是我们来做,而是glibc来帮我做。

再来看个例子:

#include <unistd.h>

int main(int argc, char *argv[]) {
  write(STDOUT_FILENO, "Hello, World\n", 13);
  return 60;
}

这个例子就是上面汇编代码对应的c实现,编译执行之后也是会输出同样的内容。

注意,这里的write并不是kernel内部的系统调用write,而是glibc中的一个wrapper,这个wrapper里面再帮我们调用真正的系统调用write。

我们再来看下对应的glibc的代码:

// sysdeps/unix/sysv/linux/write.c
/* Write NBYTES of BUF to FD.  Return the number written, or -1.  */
ssize_t
__libc_write (int fd, const void *buf, size_t nbytes)
{
  return SYSCALL_CANCEL (write, fd, buf, nbytes);
}
...
weak_alias (__libc_write, write)
...

这里需要注意的是,write方法其实是__lib_write的一个weak alias,当我们调用write时,其实相当于我们在调用__lib_write。

继续看下SYSCALL_CANCEL宏:

// sysdeps/unix/sysdep.h
#define SYSCALL_CANCEL(...) \
  ({                                                                         \
    long int sc_ret;                                                         \
    if (SINGLE_THREAD_P)                                                     \
      sc_ret = INLINE_SYSCALL_CALL (__VA_ARGS__);                            \
    else                                                                     \
      {
        ...                                                                  \
      }                                                                      \
    sc_ret;                                                                  \
  })

这个宏里面又调用了INLINE_SYSCALL_CALL,INLINE_SYSCALL_CALL里又调用了很多其他的宏,这里就不一一展开了,有兴趣的朋友可以留言,我们再一起交流。

最终,会调用下面的宏。

// sysdeps/unix/sysv/linux/x86_64/sysdep.h
#define internal_syscall3(number, err, arg1, arg2, arg3)                \
({                                                                      \
    unsigned long int resultvar;                                        \
    TYPEFY (arg3, __arg3) = ARGIFY (arg3);                              \
    TYPEFY (arg2, __arg2) = ARGIFY (arg2);                              \
    TYPEFY (arg1, __arg1) = ARGIFY (arg1);                              \
    register TYPEFY (arg3, _a3) asm ("rdx") = __arg3;                   \
    register TYPEFY (arg2, _a2) asm ("rsi") = __arg2;                   \
    register TYPEFY (arg1, _a1) asm ("rdi") = __arg1;                   \
    asm volatile (                                                      \
    "syscall\n\t"                                                       \
    : "=a" (resultvar)                                                  \
    : "0" (number), "r" (_a1), "r" (_a2), "r" (_a3)                     \
    : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);                        \
    (long int) resultvar;                                               \
})

是不是很熟悉,这就是我们上面手写的汇编代码啊。

到此,整个流程就全部通了。

我们在写c时(其他语言也一样),调用的其实是glibc里的wrapper,glibc里的wrapper再帮我们调用对应的系统调用,之后再将结果从rax中取出,返回给我们,这样我们使用起来就非常方便了。

完。

原文发布于微信公众号 - Linux内核及JVM底层相关技术研究(ytcode)

原文发表时间:2019-05-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

编辑于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券