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

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

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

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

五、系统调用编号

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

相关推荐

一日一技:用Python程序将十进制转换为二进制

用Python程序将十进制转换为二进制通过将数字连续除以2并以相反顺序打印其余部分,将十进制数转换为二进制。在下面的程序中,我们将学习使用递归函数将十进制数转换为二进制数,代码如下:...

十进制转化成二进制你会吗?#数学思维

六年级奥赛起跑线:抽屉原理揭秘。同学们好,我是你们的奥耀老师。今天一起来学习奥赛起跑线第三讲二进制计数法。例一:把十进制五十三化成二进制数是多少?首先十进制就是满十进一,二进制就是满二进一。二进制每个...

二进制、十进制、八进制和十六进制,它们之间是如何转换的?

在学习进制时总会遇到多种进制转换的时候,学会它们之间的转换方法也是必须的,这里分享一下几种进制之间转换的方法,也分享两个好用的转换工具,使用它们能够大幅度的提升你的办公和学习效率,感兴趣的小伙伴记得点...

c语言-2进制转10进制_c语言 二进制转十进制

#include<stdio.h>intmain(){charch;inta=0;...

二进制、八进制、十进制和十六进制数制转换

一、数制1、什么是数制数制是计数进位的简称。也就是由低位向高位进位计数的方法。2、常用数制计算机中常用的数制有二进制、八进制、十进制和十六进制。...

二进制、十进制、八进制、十六进制间的相互转换函数

二进制、十进制、八进制、十六进制间的相互转换函数1、输入任意一个十进制的整数,将其分别转换为二进制、八进制、十六进制。2、程序代码如下:#include<iostream>usingna...

二进制、八进制、十进制和十六进制等常用数制及其相互转换

从大学开始系统的接触计算机专业,到现在已经过去十几年了,今天整理一下基础的进制转换,希望给还在上高中的表妹一个入门的引导,早日熟悉这个行业。一、二进制、八进制、十进制和十六进制是如何定义的?二进制是B...

二进制如何转换成十进制?_二进制如何转换成十进制例子图解

随着社会的发展,电器维修由继电器时代逐渐被PLC,变频器,触摸屏等工控时代所替代,特别是plc编程,其数据逻辑往往涉及到数制二进制,那么二进制到底是什么呢?它和十进制又有什么区别和联系呢?下面和朋友们...

二进制与十进制的相互转换_二进制和十进制之间转换

很多同学在刚开始接触计算机语言的时候,都会了解计算机的世界里面大多都是二进制来表达现实世界的任何事物的。当然现实世界的事务有很多很多,就拿最简单的数字,我们经常看到的数字大多都是十进制的形式,例如:我...

十进制如何转换为二进制,二进制如何转换为十进制

用十进制除以2,除的断的,商用0表示;除不断的,商用1表示余0时结束假如十进制用X表示,用十进制除以2,即x/2除以2后为整数的(除的断的),商用0表示;除以2除不断的,商用1表示除完后的商0或1...

十进制数如何转换为二进制数_十进制数如何转换为二进制数举例说明

我们经常听到十进制数和二进制数,电脑中也经常使用二进制数来进行计算,但是很多人却不清楚十进制数和二进制数是怎样进行转换的,下面就来看看,十进制数转换为二进制数的方法。正整数转二进制...

二进制转化为十进制,你会做吗?一起来试试吧

今天孩子问把二进制表示的110101改写成十进制数怎么做呀?,“二进制”简单来说就是“满二进一”,只用0和1共两个数字表示,同理我们平常接触到的“十进制”是“满十进一”,只用0-9共十个数字表示。如果...

Mac终于能正常打游戏了!苹果正逐渐淘汰Rosetta转译

Mac玩家苦转译久矣!WWDC2025苹果正式宣判Rosetta死刑,原生游戏时代终于杀到。Metal4光追和AI插帧技术直接掀桌,连Steam都连夜扛着ARM架构投诚了。看到《赛博朋克2077》...

怎么把视频的声音提出来转为音频?音频提取,11款工具实测搞定

想把视频里的声音单独保存为音频文件(MP3/AAC/WAV/FLAC)用于配音、播客、听课或二次剪辑?本文挑出10款常用工具,给出实测可复现的操作步骤、优缺点和场景推荐。1)转换猫mp3转换器(操作门...

6个mp4格式转换器测评:转换速度与质量并存!

MP4视频格式具有兼容性强、视频画质高清、文件体积较小、支持多种编码等特点,适用于网络媒体传播。如果大家想要将非MP4格式的视频转换成MP4的视频格式的话,可以使用MP4格式转换器更换格式。本文分别从...