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

Linux Kernel源码阅读: x86-64 系统调用实现细节(二)

ahcoder 2025-04-05 16:23 4 浏览

特别说明:该文章前两天发布过,但一直在审核中。看头条网友说字数太多可能一直处于审核中状态,我把该文章拆分成几个章节发布,如影响阅读体验还请见谅。

五、系统调用编号

在示例程序中,我们使用了writeexit系统调用,并通过%rax传递了系统调用号。在Linux中,32位系统和64位系统有不同的系统调用编号。32位系统调用号定义在
arch/x86/syscalls/syscall_32.tbl文件;64位系统调用号定义在
arch/x86/syscalls/syscall_64.tbl文件。

下面列出了64位系统的部分系统调用及编号,可以看到,write()的系统调用编号为 1 ,exit()系统调用编号为 60。

 0   common  read            sys_read
 1   common  write           sys_write           # write 系统调用
 2   common  open            sys_open
 3   common  close           sys_close
 
 ......
   
 59  64  execve          sys_execve
 60  common  exit            sys_exit           # exit 系统调用
 61  common  wait4           sys_wait4
 62  common  kill            sys_kill
 ......

六、系统调用表及其初始化

linux内核中包含一个被称为系统调用表的数据结构。64位系统调用表定义在
arch/x86/kernel/syscall_64.c文件中:

 // file: arch/x86/kernel/syscall_64.c
 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>
 };

可以看到,sys_call_table是一个包含__NR_syscall_max+1个元素的数组。__NR_syscall_max是一个宏,在64位模式下其值为542,该宏定义于
include/generated/asm-offsets.h
文件,这个文件是Kbuild编译后生成的。

 // file: include/generated/asm-offsets.h
 #define __NR_syscall_max 542 /* sizeof(syscalls_64) - 1 # */

系统调用表的元素类型为sys_call_ptr_t,这是通过typedef定义的函数指针。

 // file: arch/x86/kernel/syscall_64.c
 typedef void (*sys_call_ptr_t)(void);

sys_ni_syscall表示一个未实现的系统调用,其定义如下:

 // file: kernel/sys_ni.c
 asmlinkage long sys_ni_syscall(void)
 {
     return -ENOSYS;
 }

sys_ni_syscall直接返回一个错误码-ENOSYSENOSYS值为38,表示调用了一个未实现的函数。

 // file: include/uapi/asm-generic/errno.h
 #define ENOSYS      38  /* Function not implemented */

符号 ...是GCC编译器的的一个扩展--Designated Initializers,该扩展允许我们以任意顺序初始化成员元素。正如我们看到的,sys_call_table先用sys_ni_syscall进行初始化,然后再用<asm/syscalls_64.h>头文件中的内容对数组进行填充。该头文件是使用
arch/x86/syscalls/syscalltbl.sh脚本读取syscall_64.tbl后生成的,它包含以下宏:

 // file: arch/x86/include/generated/asm/syscalls_64.h
 __SYSCALL_COMMON(0, sys_read, sys_read)
 __SYSCALL_COMMON(1, sys_write, sys_write)
 __SYSCALL_COMMON(2, sys_open, sys_open)
 
 ......
     
 __SYSCALL_X32(540, compat_sys_process_vm_writev, compat_sys_process_vm_writev)
 __SYSCALL_X32(541, compat_sys_setsockopt, compat_sys_setsockopt)
 __SYSCALL_X32(542, compat_sys_getsockopt, compat_sys_getsockopt)

__SYSCALL_COMMON宏定义如下,该宏被扩展成__SYSCALL_64宏,最终被扩展成函数定义。

 // file: arch/x86/kernel/syscall_64.c
 #define __SYSCALL_COMMON(nr, sym, compat) __SYSCALL_64(nr, sym, compat)
 #define __SYSCALL_64(nr, sym, compat) [nr] = sym,

最终,sys_call_table被扩展成了下面的格式,各系统调用号关联的函数指针被填充到该数组中;其它所有未实现的系统调用号都指向了sys_ni_syscall函数,该函数只是简单返回一个错误码-ENOSYS

 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,
     [0] = sys_read,
     [1] = sys_write,
     [2] = sys_open,
     ......
 };

七、系统调用的定义

下面我们以示例程序中使用的write系统调用为例,来看看系统调用是如何定义的。

write系统调用函数原型如下,可以通过 man 2 write命令查看。

 ssize_t write(int fd, const void *buf, size_t count);

在linux内核中,write系统调用定义在fs/read_write.c文件中。由于write有3个参数,所以是用SYSCALL_DEFINE3宏定义的。

 // file: fs/read_write.c
 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
         size_t, count)
 {
     struct fd f = fdget(fd);
     ssize_t ret = -EBADF;
 
     if (f.file) {
         loff_t pos = file_pos_read(f.file);
         ret = vfs_write(f.file, buf, count, &pos);
         file_pos_write(f.file, pos);
         fdput(f);
     }
 
     return ret;
 }

SYSCALL_DEFINE3宏定义在 include/linux/syscalls.h中。可以看到,linux 内核一共定义了7个宏,每个宏后面都有一个数字,表示入参数量。

 // file: include/linux/syscalls.h
 #define SYSCALL_DEFINE0(sname)                  \
     SYSCALL_METADATA(_##sname, 0);              \
     asmlinkage long sys_##sname(void)
 
 #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_METADATA(sname, x, __VA_ARGS__)         \
     __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

SYSCALL_DEFINE3被扩展成了SYSCALL_DEFINEx宏,该宏又扩展成了SYSCALL_METADATA__SYSCALL_DEFINEx

write为例,看下扩展过程:

 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)

扩展成:

 SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)

注意,扩展后,函数名前面多个了下划线”_“。”##“是连接操作符,在宏扩展时,可以把2个符号合并成一个,具体使用见 gcc 文档 3.5 Concatenation。

继续扩展:

 SYSCALL_METADATA(_write, 3, unsigned int, fd, const char *, buf, size_t, count)         \
 __SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)

SYSCALL_METADATA宏的实现,由Kbuild时配置的选项CONFIG_FTRACE_SYSCALLS来决定,只有设置CONFIG_FTRACE_SYSCALLS选项时,该宏才有实际意义。从选项名称就能够看出来,它主要是用来对系统调用过程进行追踪的。 关于调试和追踪方面的细节,本文暂不涉及,我们主要来看下__SYSCALL_DEFINEx宏的实现。

7.1 __SYSCALL_DEFINEx

__SYSCALL_DEFINEx宏定义于 include/linux/syscalls.h文件:

 // file: include/linux/syscalls.h
 #define __SYSCALL_DEFINEx(x, name, ...)                 \
     asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));  \
     static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));  \
     asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))   \
     {                               \
         long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));  \
         __MAP(x,__SC_TEST,__VA_ARGS__);             \
         __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));   \
         return ret;                     \
     }                               \
     SYSCALL_ALIAS(sys##name, SyS##name);                \
     static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))

7.1.1 __MAP

__MAP宏会根据参数数量和映射函数做适当的扩展。其中n表示参数数量,m代表映射函数,其它参数都是成对出现的,t表示参数类型,a表示参数值。从注释中也可以看到,__MAP(n, m, t1, a1, t2, a2, ..., tn, an)会被扩展成m(t1, a1), m(t2, a2), ..., m(tn, an)

 // file: include/linux/syscalls.h
 /*
  * __MAP - apply a macro to syscall arguments
  * __MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
  *    m(t1, a1), m(t2, a2), ..., m(tn, an)
  * The first argument must be equal to the amount of type/name
  * pairs given.  Note that this list of pairs (i.e. the arguments
  * of __MAP starting at the third one) is in the same format as
  * for SYSCALL_DEFINE/COMPAT_SYSCALL_DEFINE
  */
 #define __MAP0(m,...)
 #define __MAP1(m,t,a) m(t,a)
 #define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
 #define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
 #define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
 #define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
 #define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
 #define __MAP(n,...) __MAP##n(__VA_ARGS__)

7.1.2 __SC_DECL、__SC_LONG、__SC_CAST、__SC_TEST、__SC_ARGS

这些宏是作为__MAP宏的映射函数存在的,这些宏中的t表示参数类型(type),a表示参数值(argument)。其中__SC_DECL__SC_CAST__SC_ARGS这三个宏比较简单,就不做说明了,重点说说其它宏。

 // file: include/linux/syscalls.h
 #define __SC_DECL(t, a) t a
 #define __SC_CAST(t, a) (t) a
 #define __SC_ARGS(t, a) a
 
 #define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
 #define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))
 #define __TYPE_IS_LL(t) (__same_type((t)0, 0LL) || __same_type((t)0, 0ULL))

7.1.2.1 __SC_LONG

7.1.2.1.1 __TYPE_IS_LL

__SC_LONG宏中引用了__TYPE_IS_LL宏,而__TYPE_IS_LL宏又引用了__same_type函数。__same_type函数定义如下:

 // file: include/linux/compiler.h
 /* Are two types/vars the same type (ignoring qualifiers)? */
 #ifndef __same_type
 # define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
 #endif

__same_type函数通过gcc 内建函数
__builtin_types_compatible_p
来判断2个入参的类型是否一致,如果一致,返回1,否则返回0。
__builtin_types_compatible_p
函数说明如下:

You can use the built-in function __builtin_types_compatible_p to determine whether two types are the same.

This built-in function returns 1 if the unqualified versions of the types type1 and type2 (which are types, not expressions) are compatible, 0 otherwise. The result of this built-in function can be used in integer constant expressions.

综上所述,__TYPE_IS_LL(t)的作用是判断给定的类型t是否是Logg LongUnsigned Long Long类型,如果是其值为1,否则为0。

7.1.2.1. __builtin_choose_expr

__builtin_choose_expr也是一个gcc 内建函数,该函数有3个参数,第一个参数是一个常量表达式(const_exp)。其作用类似于三元操作符”?:“,如果第一参数非0,则返回第2个参数,否则返回第3个参数。

Built-in Function: type __builtin_choose_expr (const_exp, exp1, exp2)

You can use the built-in function __builtin_choose_expr to evaluate code depending on the value of a constant expression. This built-in function returns exp1 if const_exp, which is an integer constant expression, is nonzero. Otherwise it returns exp2.

7.1.2.1.3 结论

经过以上分析,宏__SC_LONG(t, a)的作用就是把”LL“或”ULL“类型的参数,转换为”LL“类型;其它类型的参数,转换成”L“类型

7.1.2.2 __SC_TEST

 #define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))

从名称也可以看到,宏__SC_TEST(t, a)主要用于测试目的。该宏又引用了BUILD_BUG_ON_ZERO,其定义如下。

 // file: include/linux/bug.h
 /* Force a compilation error if condition is true, but also produce a
    result (of value 0 and type size_t), so the expression can be used
    e.g. in a structure initializer (or where-ever else comma expressions
    aren't permitted). */
 /* sizeof(struct { int:-!!(e); } 用法参考: https://stackoverflow.com/questions/9229601/what-is-in-c-code */
 #define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))

这是一种使用技巧,它主要用来进行编译时检查。

 sizeof(struct { int: -!!(e); }))

执行流程如下,详见What is ":-!!" in C code?:

(e): Compute expression e.

!!(e): Logically negate twice: 0 if e == 0; otherwise 1.

-!!(e): Numerically negate the expression from step 2: 0 if it was 0; otherwise -1.

struct{int: -!!(0);} --> struct{int: 0;}: If it was zero, then we declare a struct with an anonymous integer bitfield that has width zero. Everything is fine and we proceed as normal.

struct{int: -!!(1);} --> struct{int: -1;}: On the other hand, if it isn't zero, then it will be some negative number. Declaring any bitfield with negative width is a compilation error.

综上,__SC_TEST(t, a)的作用就是当参数类型t不是LL类型,但其类型大小却超过L类型时,强制编译器报错。说白了就是进行类型检测。

7.1.3 SYSCALL_ALIAS

SYSCALL_ALIAS宏定义如下:

 // file: include/linux/linkage.h
 #ifndef SYSCALL_ALIAS
 #define SYSCALL_ALIAS(alias, name) asm(         \
     ".globl " VMLINUX_SYMBOL_STR(alias) "\n\t"  \
     ".set   " VMLINUX_SYMBOL_STR(alias) ","     \
           VMLINUX_SYMBOL_STR(name))
 #endif

VMLINUX_SYMBOL_STR定义如下:

 // file: include/linux/export.h
 /*
  * Export symbols from the kernel to modules.  Forked from module.h
  * to reduce the amount of pointless cruft we feed to gcc when only
  * exporting a simple symbol or two.
  *
  * Try not to add #includes here.  It slows compilation and makes kernel
  * hackers place grumpy comments in header files.
  */
 /* Indirect, so macros are expanded before pasting. */
 #define VMLINUX_SYMBOL(x) __VMLINUX_SYMBOL(x)
 #define VMLINUX_SYMBOL_STR(x) __VMLINUX_SYMBOL_STR(x)
 
 #define __VMLINUX_SYMBOL(x) x
 #define __VMLINUX_SYMBOL_STR(x) #x

实际效果是给name设置了个别名alias,本例中是给SyS_write设置了别名sys_write

7.1.4 最终扩展

我们继续往下分析,刚才分析到了如下代码:

 __SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)

所以我们知道,在宏内部x值为3,__VA_ARGS__参数类型和值列表。

根据__MAP__SC_DECL宏定义,__MAP(x,__SC_DECL,__VA_ARGS__)被扩展成为:

 unsigned int fd, const char * buf, size_t count

根据__MAP__SC_LONG宏定义,__MAP(x,__SC_LONG,__VA_ARGS__)被扩展成:

 long fd, long buf, long count

__MAP(x,__SC_CAST,__VA_ARGS__)被扩展成:

 (unsigned int) fd, (const char *) buf, (size_t) count

__MAP(x,__SC_ARGS,__VA_ARGS__) 被扩展成:

 fd, buf, count

所以,__SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)最终扩展如下:

 
     asmlinkage long sys_write(unsigned int fd, const char * buf, size_t count); \
     static inline long SYSC_write(unsigned int fd, const char * buf, size_t count); \
     asmlinkage long SyS_write(long fd, long buf, long count)    \
     {                               \
         long ret = SYSC_write((unsigned int) fd, (const char *) buf, (size_t) count);   \
         __MAP(x,__SC_TEST,__VA_ARGS__);             \   # 用于测试,不涉及
         __PROTECT(x, ret, fd, buf, count);  \
         return ret;                     \
     }                               \
     SYSCALL_ALIAS(sys_write, SyS_write);                \
     static inline long SYSC_write(unsigned int fd, const char * buf, size_t count)   

再结合write函数具体实现,完整的write系统调用扩展如下:

     asmlinkage long sys_write(unsigned int fd, const char * buf, size_t count); \
     static inline long SYSC_write(unsigned int fd, const char * buf, size_t count); \
     asmlinkage long SyS_write(long fd, long buf, long count)    \
     {                               \
         long ret = SYSC_write((unsigned int) fd, (const char *) buf, (size_t) count);   \
         __MAP(x,__SC_TEST,__VA_ARGS__);             \   # 用于测试,不涉及
         __PROTECT(x, ret, fd, buf, count);  \
         return ret;                     \
     }                               \
     SYSCALL_ALIAS(sys_write, SyS_write);                \
     static inline long SYSC_write(unsigned int fd, const char * buf, size_t count)  
     {
         struct fd f = fdget(fd);
         ssize_t ret = -EBADF;
 
         if (f.file) {
             loff_t pos = file_pos_read(f.file);
             ret = vfs_write(f.file, buf, count, &pos);
             file_pos_write(f.file, pos);
             fdput(f);
         }
 
         return ret;
     }

这段代码先声明了2个入参相同的函数sys_writeSYSC_write;然后定义了函数SyS_write,该函数内部调用了SYSC_write;给SyS_write设置了一个别名sys_writeSYSC_writewrite系统调用的具体实现。

7.1.5 总结

总结一下实现流程:

  • 内部实现函数为SYSC_write
  • SyS_write函数对SYSC_write进行了封装,增加了编译时类型检查及参数保护;
  • SyS_write设置了别名sys_write

关联链接:

Linux Kernel源码阅读: x86-64 系统调用实现细节(一)

Linux Kernel源码学习: x86-64 系统调用实现细节(完结篇)

相关推荐

Linux 6.15将更新媒体子系统:高通Iris驱动首次亮相

IT之家3月24日消息,科技媒体phoronix昨日(3月23日)发布博文,报道称Linux6.15内核将于本周启动合并窗口,首批提交的PullRequest请求之一,就...

Ubuntu 25.04发行版登场:Linux 6.14内核,带来多项技术革新

IT之家4月18日消息,科技媒体linuxiac昨日(4月17日)发布博文,报道称代号为PluckyPuffin的Ubuntu25.04发行版正式上线,搭载最新Linu...

WordPress 6.8 版本发布:聚焦性能升级,代号“Cecil”

WordPress6.8版本发布:聚焦性能升级,代号“Cecil”全球最受欢迎的内容管理系统(CMS)WordPress正式推出最新版本6.8,代号“塞西尔”(Cecil),以此致敬传奇钢琴家...

Linus Torvalds接受微软Hyper-V升级 下一代Linux启动会更快

虽然Windows的粉丝和Linux的粉丝经常喜欢进行激烈的键盘大战,但操作系统的制造商们自己也了解彼此的优缺点。毫无疑问,微软也明白这一点,事实上,它甚至鼓励用户尝试Linux,尽管是使用...

Debian 12发布:Linux内核升级6.1(debian更新内核)

IT之家6月11日消息,Debian是最古老的GNU/Linux发行版之一,也是许多其他基于Linux的操作系统的基础,包括Ubuntu、Kali、MX和树莓派OS等。这...

Linux Mint预告新功能:升级Nemo搜索、LMDE 7支持OEM安装

IT之家4月10日消息,LinuxMint发布了最新月度简讯,宣布增强Nemo文件管理器的文件搜索功能、Cinnamon桌面环境开始支持Wayland、LMDE7(LinuxM...

Linux 6.2合并大量网络系统更新:推进Wi-Fi 7和800 Gbps网络

IT之家12月15日消息,Linux6.2合并窗口期内已经确认将会合并大量网络子系统更新。和以往版本相同,Linux6.2内核更新周期在网络功能上有大量的改进,更多的细节可以访问这条...

Linux内核升级实践指南(linux内核怎么升级)

Linux内核升级是一个需要谨慎操作的过程,但掌握正确方法后可以显著提升系统性能、安全性或硬件兼容性。以下是一份详细的实践指南,涵盖主流方法及注意事项:一、准备工作查看当前内核版本bashuname...

Linux又将迎来大版本更新 5.20版可能会被称为Linux 6.0

如果你错过了昨天Linux5.19的发布公告,那么这一消息需要回顾一下:被称为Linux5.20的开发中的内核很可能会被称为Linux6.0。根据LinusTorvalds对现代版本的划分,L...

周六的娱乐就是安装一台OracleLinux虚拟机(一)

每个人有每个人的娱乐方式。老父亲可能喜欢摆弄一些机械工具修修补补。我趁着周六安装一套OracleLinux,寓学于乐。在oracle网站上,下载VirtualBox,现在最新版本是7.0.8。在安装过...

Linux系统下Gaussian 16的安装(linux系统如何安装新软件)

Gaussian是目前使用最多的量子化学计算软件,当前最新的版本为Gaussian16C.01。理论上来说Gaussian并不需要安装,因为高斯一般是不出售源代码的,只有二进制文件,以Gaussi...

「Linux基础」VMWare虚拟机安装CentOS后配置静态ip

在VMWare中安装本地虚拟机CentOS7操作系统,动态IP地址会经常变化,不便于管理与实验。为了便于实验,考虑设置成静态IP地址,目标是本地局域网可以通过NAT网关互相访问,且能连接互联网。由于是...

Win10虚拟机,Hyper-V安装CentOS,一次成功

打开控制面板,选择程序选择启用或关闭Windows功能选中Hyper-V下所有的组件。提示我们需要,重启计算机,保存我们的文件,之后点击“立即重新启动”选择开始菜单,输入Hy,就可以打开虚拟机管理器了...

如何在Windows 10中的Hyper-V虚拟机上安装CentOS Linux

注意双重引导的一种可行且相当不错的替代方法是在虚拟机上安装各种操作系统。Microsoft正式支持CentOS作为Hyper-V的来宾OS,并且在安装时可以很好地集成。自CentOS6.4版以来,用...

安装Linux虚拟机的5个理由,以及不安装的5个原因

虚拟机(VM)允许你在不永久更改计算机的情况下探索Linux。如果你对Linux很好奇,但犹豫是否要尝试,VM提供了一个安全、灵活的解决方案。如果这是你第一次听说虚拟机,虚拟机就像“计算机中的计算机”...