百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

「内核知识」Linux下的系统调用write

liuian 2025-04-29 02:07 40 浏览

本文以x86_64平台为例,分析linux下的系统调用是如何被执行的。

假设目标系统调用是,其对应的内核源码为:

// fs/read_write.c
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
                size_t, count)
{
        return ksys_write(fd, buf, count);
}

这里主要看下SYSCALL_DEFINE3这个宏定义:

// include/linux/syscalls.h
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
...
#define SYSCALL_DEFINEx(x, sname, ...)                          \
        ...
        __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

该宏又引用了__SYSCALL_DEFINEx,继续看下:

// arch/x86/include/asm/syscall_wrapper.h
#define __SYSCALL_DEFINEx(x, name, ...)                                 \
        asmlinkage long __x64_sys##name(const struct pt_regs *regs);    \
        ...                                                             \
        static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));     \
        static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
        asmlinkage long __x64_sys##name(const struct pt_regs *regs)     \
        {                                                               \
                return __se_sys##name(SC_X86_64_REGS_TO_ARGS(x,__VA_ARGS__));\
        }                                                               \
        ...                                                             \
        static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))      \
        {                                                               \
                long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
                ...                                                     \
                return ret;                                             \
        }                                                               \
        static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

该宏的参数中,x为3,name为_write,...代表的__VA_ARGS__为unsigned int, fd, const char __user *, buf, size_t, count。

接着,在宏的定义中,先声明了三个函数,分别为__x64_sys_write、_se_sys_write、__do_sys_write,紧接着,定义了__x64_sys_write和_se_sys_write的实现,__x64_sys_write内调用_se_sys_write,_se_sys_write内调用__do_sys_write。

__do_sys_write只是一个方法头,它和最开始的write系统调用的方法体构成完整的方法。

由上可以看到,三个方法中,只有__x64_sys_write方法没有static,即只有它是外部可调用的,所以我们看下哪里引用了__x64_sys_write。

// arch/x86/entry/syscalls/syscall_64.tbl
#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
#
# The abi is "common", "64" or "x32" for this file.
#
0       common  read                    __x64_sys_read
1       common  write                   __x64_sys_write
...

我们会在一个非c文件中,找到了对__x64_sys_write方法的引用,但这个文件又是怎么被使用的呢?

根据
arch/x86/entry/syscalls/Makefile我们可以知道,是有对应的shell脚本,根据上面的文件来生成c版的头文件,比如下面两个。

kernel内部使用的:

// arch/x86/include/generated/asm/syscalls_64.h
#ifdef CONFIG_X86
__SYSCALL_64(0, __x64_sys_read, )
#else /* CONFIG_UML */
__SYSCALL_64(0, sys_read, )
#endif
#ifdef CONFIG_X86
__SYSCALL_64(1, __x64_sys_write, )
#else /* CONFIG_UML */
__SYSCALL_64(1, sys_write, )
#endif
...

给用户使用的:

// arch/x86/include/generated/uapi/asm/unistd_64.h
#define __NR_read 0
#define __NR_write 1
...

那生成的这两个头文件又是给谁使用的呢?看下下面这个文件:

// arch/x86/entry/syscall_64.c
#define __SYSCALL_64(nr, sym, qual) [nr] = sym,

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
        /*
         * Smells like a compiler bug -- it doesn't work
         * when the & below is removed.
         */
        [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

该文件中定义了一个const的数组变量sys_call_table,数组下标为系统调用的编号,值为该编号对应的系统调用方法。

最开始整个数组都初始化为sys_ni_syscall,该方法内会返回错误码ENOSYS,表示对应的方法未实现。

接着用#include <asm/syscalls_64.h>的方式再初始化存在的系统调用。

该include的文件就是上面生成的
arch/x86/include/generated/asm/syscalls_64.h,syscalls_64.h文件里调用__SYSCALL_64,为对应的系统下标赋值。

最后,sys_call_table[1] = __x64_sys_write。

到这里,我们基本可以猜测,肯定有个地方是根据系统调用的编号,到数组sys_call_table中找到对应方法,然后调用。

让我们来看下这段代码在哪里

// arch/x86/entry/common.c
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
        ...
        if (likely(nr < NR_syscalls)) {
                nr = array_index_nospec(nr, NR_syscalls);
                regs->ax = sys_call_table[nr](regs);
        }
        ...
}

上面的方法就是我们要找的方法。

我们再看下这个方法是在哪里被调用的。

// arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
        ...
        call    do_syscall_64           /* returns with IRQs disabled */
        ...

上面的就是对应的汇编代码了,这里为了简单,省略掉了该汇编方法的其他部分。

那这段汇编代码又是在哪里调用的呢?

// arch/x86/kernel/cpu/common.c
void syscall_init(void)
{
        ...
        wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
        ...
}

在上面的方法中,我们可以看到,汇编代码entry_SYSCALL_64被写到了MSR_LSTAR表示的寄存器中。

该寄存器的作用就是,当我们执行syscall机器指令时,MSR_LSTAR寄存器中存放的对应方法就会被执行,即在user space,我们只要执行syscall机器指令,给它对应的系统调用编号和参数,kernel space里对应的系统调用就会被执行了。

有兴趣的可以分析并执行下下面的汇编代码,好好体会下整个系统调用的流程。

# ----------------------------------------------------------------------------------------
# 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"

到这里,系统调用对应的kernel space部分就已经分析完毕了,下篇文章我们结合对应的c源码,看下user space的部分是如何实现的。

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

比如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下的系统调用write - 论坛 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛

相关推荐

驱动网卡(怎么从新驱动网卡)
驱动网卡(怎么从新驱动网卡)

网卡一般是指为电脑主机提供有线无线网络功能的适配器。而网卡驱动指的就是电脑连接识别这些网卡型号的桥梁。网卡只有打上了网卡驱动才能正常使用。并不是说所有的网卡一插到电脑上面就能进行数据传输了,他都需要里面芯片组的驱动文件才能支持他进行数据传输...

2026-01-30 00:37 liuian

win10更新助手装系统(微软win10更新助手)

1、点击首页“系统升级”的按钮,给出弹框,告诉用户需要上传IMEI码才能使用升级服务。同时给出同意和取消按钮。华为手机助手2、点击同意,则进入到“系统升级”功能华为手机助手华为手机助手3、在检测界面,...

windows11专业版密钥最新(windows11专业版激活码永久)

 Windows11专业版的正版密钥,我们是对windows的激活所必备的工具。该密钥我们可以通过微软商城或者通过计算机的硬件供应商去购买获得。获得了windows11专业版的正版密钥后,我...

手机删过的软件恢复(手机删除过的软件怎么恢复)
手机删过的软件恢复(手机删除过的软件怎么恢复)

操作步骤:1、首先,我们需要先打开手机。然后在许多图标中找到带有[文件管理]文本的图标,然后单击“文件管理”进入页面。2、进入页面后,我们将在顶部看到一行文本:手机,最新信息,文档,视频,图片,音乐,收藏,最后是我们正在寻找的[更多],单击...

2026-01-29 23:55 liuian

一键ghost手动备份系统步骤(一键ghost 备份)

  步骤1、首先把装有一键GHOST装系统的U盘插在电脑上,然后打开电脑马上按F2或DEL键入BIOS界面,然后就选择BOOT打USDHDD模式选择好,然后按F10键保存,电脑就会马上重启。  步骤...

怎么创建局域网(怎么创建局域网打游戏)

  1、购买路由器一台。进入路由器把dhcp功能打开  2、购买一台交换机。从路由器lan端口拉出一条网线查到交换机的任意一个端口上。  3、两台以上电脑。从交换机任意端口拉出网线插到电脑上(电脑设置...

精灵驱动器官方下载(精灵驱动手机版下载)

是的。驱动精灵是一款集驱动管理和硬件检测于一体的、专业级的驱动管理和维护工具。驱动精灵为用户提供驱动备份、恢复、安装、删除、在线更新等实用功能。1、全新驱动精灵2012引擎,大幅提升硬件和驱动辨识能力...

一键还原系统步骤(一键还原系统有哪些)

1、首先需要下载安装一下Windows一键还原程序,在安装程序窗口中,点击“下一步”,弹出“用户许可协议”窗口,选择“我同意该许可协议的条款”,并点击“下一步”。  2、在弹出的“准备安装”窗口中,可...

电脑加速器哪个好(电脑加速器哪款好)

我认为pp加速器最好用,飞速土豆太懒,急速酷六根本不工作。pp加速器什么网页都加速,太任劳任怨了!以上是个人观点,具体性能请自己试。ps:我家电脑性能很好。迅游加速盒子是可以加速电脑的。因为有过之...

任何u盘都可以做启动盘吗(u盘必须做成启动盘才能装系统吗)

是的,需要注意,U盘的大小要在4G以上,最好是8G以上,因为启动盘里面需要装系统,内存小的话,不能用来安装系统。内存卡或者U盘或者移动硬盘都可以用来做启动盘安装系统。普通的U盘就可以,不过最好U盘...

u盘怎么恢复文件(u盘文件恢复的方法)

开360安全卫士,点击上面的“功能大全”。点击文件恢复然后点击“数据”下的“文件恢复”功能。选择驱动接着选择需要恢复的驱动,选择接入的U盘。点击开始扫描选好就点击中间的“开始扫描”,开始扫描U盘数据。...

系统虚拟内存太低怎么办(系统虚拟内存占用过高什么原因)

1.检查系统虚拟内存使用情况,如果发现有大量的空闲内存,可以尝试释放一些不必要的进程,以释放内存空间。2.如果系统虚拟内存使用率较高,可以尝试增加系统虚拟内存的大小,以便更多的应用程序可以使用更多...

剪贴板权限设置方法(剪贴板访问权限)
剪贴板权限设置方法(剪贴板访问权限)

1、首先打开iphone手机,触碰并按住单词或图像直到显示选择选项。2、其次,然后选取“拷贝”或“剪贴板”。3、勾选需要的“权限”,最后选择开启,即可完成苹果剪贴板权限设置。仅参考1.打开苹果手机设置按钮,点击【通用】。2.点击【键盘】,再...

2026-01-29 21:37 liuian

平板系统重装大师(平板重装win系统)

如果你的平板开不了机,但可以连接上电脑,那就能好办,楼主下载安装个平板刷机王到你的个人电脑上,然后连接你的平板,平板刷机王会自动识别你的平板,平板刷机王上有你平板的我刷机包,楼主点击下载一个,下载完成...

联想官网售后服务网点(联想官网售后服务热线)

联想3c服务中心是联想旗下的官方售后,是基于互联网O2O模式开发的全新服务平台。可以为终端用户提供多品牌手机、电脑以及其他3C类产品的维修、保养和保险服务。根据客户需求层次,联想服务针对个人及家庭客户...