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

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

haoteby 2025-06-30 16:18 3 浏览

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

五、系统调用编号

在示例程序中,我们使用了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<n>/COMPAT_SYSCALL_DEFINE<n>
  */
 #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 系统调用实现细节(完结篇)

相关推荐

JAVA零基础入门:JDK的概述及安装(jdk完整安装教程)

一.什么是jdkJDK(JavaDevelopmentToolKit)是Java开发工具包,JDK是整个JAVA的核心,包括了Java运行环境(JavaRuntimeEnvirnment),一...

开源、强大的工作流引擎:camunda入门介绍

原创不易,请多多支持!对Java技术感兴趣的童鞋请关注我,后续技术分享更精彩。简介CamundaisaJava-basedframeworksupportingBPMNforwork...

Centos8搭建Java环境(JDK1.8+Nginx+Tomcat9+Redis+Mysql)

一、开篇1.1目的每次换新的服务器,都要找资料配下环境,所以我写这篇文章,重新梳理了一下,方便了自己,希望也能给大家带来一些帮助。安装的软件有:JDK1.8+Nginx+Tomcat9+...

记录一次tomcat的升级过程(tomcat6升级tomcat8)

原因:ApacheTomcat资源管理错误漏洞(CVE-2021-42340)版本:ApacheTomcat/9.0.46,tomcat解决方法:升级tomcat9到最新版本9.0.581.官...

Tomcat10安装与配置图文教程(tomcat安装及配置)

Tomcat10安装与配置图文教程1、百度搜索“tomcat下载”,进入官网下载https://tomcat.apache.org/index.html...

VS2022配置x86/x64调用32位和64位汇编语言动态库环境

配置X86MASM汇编环境1.创建项目打开VS2022创建新项目,新建asm文件(注意要手动修改cpp文件后缀名为asm文件后缀名)。2.设置入口点选择菜单栏中的“调试”-“demo调试属性”-...

ARM版Win10用户狂喜 微软全新补丁让应用不再不兼容

Windows10onARM仅支持模拟32位的X86应用程序,这意味着大多数的桌面应用是无法在这一平台上运行的,这在很大程度上限制该平台的发展。为了解决这一问题,微软在内部开发频道推出可用于AR...

分享收藏的 oracle 11.2.0.4各平台的下载地址

概述oracle11.2.0.4是目前生产环境用的比较多的版本,同时也是很稳定的一个版本。目前官网上已经找不到下载链接了,有粉丝在头条里要求分享一下下载地址。一、各平台下载地址...

Android-x86现已基于5.1.1 Lollipop:支持UEFI和64位内核

采用Linux内核的Android-x86,旨在为PC带来最新的Android移动操作系统体验。而近日,该操作系统已经发布了Android-x865.1的首个候选发布(RC)版本。发行说明中提到:A...

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

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

树莓派4B安装win10后实测,CPU秒杀AMD Athlon64 3200+

在上一篇文章介绍了如何给树莓派4B安装win10系统,这篇就简单对系统进行测试,上一篇文章链接https://www.toutiao.com/i7015518822056886821/因为树莓派是a...

一键离线部署x86、arm64 RabbitMQ,花了2天去验证整理,直接拿去

最近有一个项目,客户是内网网络,只能离线部署,采用的麒麟ARM64服务器系统,不能远程部署,需要提前准备离线部署包让客户IT拷备上去再现场部署,部署时间就只有1天。自家系统采用的vue+springb...

Linux软件包管理(linux系统软件包的安装方法,并简要说明其特点)

Linux系统如果需要安装软件怎么办?如何安装,大概有以下几种方式1.二级制软件包管理(RPM、YUM)...

Tachyum要做全球最强64位处理器:性能比X86强,面积比ARM小

全球半导体芯片研发、生产最强的国家非美国莫属,如果有某家美国公司宣布要开发性能超强的芯片,大家不会意外,但要是一家斯洛伐克初创公司宣布要研发超级芯片呢?Tachyum公司就是这样一家公司,成立于201...

Android L 64位模拟器终于来了:x86独享

GoogleI/O2014大会已经过去了很久,64位的AndroidL依然停留在纸面上,但现在至少可以让开发者们先行品尝品尝了:64位的AndroidL模拟器已经发布。这次公布的模拟器镜像是专...