C++开发者是如何理解系统调用的?
系统调用是什么在 Linux 中按照特权等级进程的运行空间被划分为了用户空间和内核空间引入了保护环Protection Ring的概念根据执行的权限等级通常分为 Ring 0-3 四个级别如下所示内核空间运行在 Ring 0具有最高的权限可以访问所有的资源。用户空间运行在 Ring 3不能直接访问内存等硬件资源。如果想要访问内核空间的资源就需要通过系统调用System Call来实现。比如应用程序读取一个磁盘文件、打印一行日志到终端、分配一块内存等都需要通过系统调用来完成。在 Linux 上可以通过 strace 命令来跟踪进程执行的系统调用比如cat /dev/null的 strace 结果如下所示$ strace cat /dev/null ... open(/dev/null, O_RDONLY) 3 fstat(3, {st_modeS_IFCHR|0666, st_rdevmakedev(1, 3), ...}) 0 fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) 0 read(3, , 65536) 0 ...上面输出中 open、fstat、read 等都是 cat 命令执行过程中使用的系统调用。传统系统调用传说中的 int 0x80 指令传统的 x86-32 平台预留了一个特殊的软中断号 128 (0x80)通过执行一条中断指令int 0x80就可以使得用户空间的程序进入内核执行特定的系统调用。这里的 int 是 interrupt中断的缩写不是表示整数 integer这个值是在 glibc 里的sysdeps/unix/sysv/linux/i386/sysdep.h定义的。# define ENTER_KERNEL int $0x80int 0x80指令解决了怎么触发系统调用的问题除此之外还需要两个最基本的信息内核怎么知道要执行哪一个系统调用系统调用的参数怎么传值Linux 做了一个约定使用 %EAX 寄存器存储系统调用编号另外使用额外六个寄存器存储传入系统调用的参数这几个寄存器是%EBX arg1%ECX arg2%EDX arg3%ESI arg4%EDI arg5%EBP arg6有了这些基础我们来尝试用汇编实现一个程序输出 Hello, World! 到终端。为了搞清楚如何在终端中输出字符串我们先来写一段 C 语言的实现#include stdio.h int main() { char *str Hello, World!\n; printf(%s, str); }这段代码非常好懂那更接近底层一点的写法是更接近系统调用层的写法是#include unistd.h int main() { int stdout_fd 1; char* str Hello, World!\n; int length 14; write(stdout_fd, str, length); }Unix 的设计哲学一切皆文件一个程序运行以后都至少包含三个文件描述符file descriptor简称 fd标准输入 stdin(0)标准输出 stdout(1)错误输出 stderr(2)在终端执行程序输出字符串实际上就是往标准输出 stdout 文件描述符写数据stdout 的 fd 值等于 1。write 是一个系统调用把数据写入到文件它的函数签名如下ssize_t write(int fd, void * buffer, size_t count)第一个参数 fd 表示要写入的文件描述符第二个参数 buffer 表示要写入文件中数据的内存地址第三个参数表示从 buffer 写入文件的数据字节数。因此在标准输出中输出Hello, World!\n实际上是调用 write 系统调用往 fd 为 1 的文件描述符写入 14 个字节的字符串。编译并执行上面的 C 代码就可以看到输出了 Hello, World! 字符串gcc main.c -o main ./main Hello, World!有了上面的基本概念我们来看用汇编如何来实现。.section .data msg: .ascii Hello, World!\n .section .text .globl _start _start: movl $4, %eax # write 系统调用的编号 4 movl $1, %ebx # write 的第 1 个参数 fd: 1 movl $msg, %ecx # write 的第 2 个参数 buffer: Hello, World!\n movl $14, %edx # write 的第 3个参数 count: 14 int $0x80 # 执行系统调用: write(fd, buffer, count) movl $0, %ebx # exit 的第一个参数 0 movl $1, %eax # exit 系统调用编号 1 int $0x80 # 执行系统调用: exit(status)在汇编中任何以点.开头的都不会被直接翻译为机器指令.section将汇编代码划分为多个段.section .data是数据段的开始数据段中存储后面程序需要用到的数据相当于一个全局变量。在数据段中我们定义了一个 msgascii 编码表示的内容是 Hello, World!\n。接下来的.section .text表示是文本段的开始文本段是存放程序指令的地方。接下来的指令是.globl _start这里并没有拼错不是 global_start是一个标签。接下来是真正的汇编指令部分了。前面介绍过执行 write 系统调用时eax寄存器存储 write 的系统调用号 4ebx存储标准输出的 fdecx存储着输出 buffer 的地址。edx存储字节数。所以看到_start便签后有四个 movl 指令movl 指令的格式是movl src dst比如movl $4, %eax指令是将常量 4 存储到寄存器eax中数字 4 前面的 $ 表示「立即寻址」汇编的其它寻址方式这里先不展开只需要知道立即寻址是本身就包含要访问的数据。接下来指令是int $0x80这是一条中断触发指令把执行流程交给内核继续处理应用程序不用关心内核是如何处理的内核处理完会把执行流程还给应用程序同时根据执行成功与否设置全局变量 errno 的值。一般情况下在 linux 上系统调用成功会返回非负值发送错误时会返回负值。接下来的指令实际上执行 exit(0) 退出程序指令和逻辑与之前的一样不再赘述。下面来编译和执行上面的汇编代码。在 Linux 上可以使用 as 和 ld 汇编和链接程序as $helloworld.s -o helloworld.o ld $helloworld.o -o helloworld ./helloworld Hello, World!通过这个例子我们就非常清晰地看清楚了系统调用过程以及寄存器是如何传值的。详细的过程如下图所示内核使用一个 sys_call_table 数组来存储系统调用编号和实现函数之间的对应关系。内核通过 %eax 寄存器获取系统调用编号然后在这个数组中查找对应的处理函数这里为 sys_write通过其他寄存器存储的参数就可以完成此次函数调用了。快速系统调用传统系统调用采用软中断的方式效率不是很高后面就推出了新的称为快速系统调用Fast System Call的方式32 位系统和 64 位系统指令不太一样32 位sysenter 和 sysexit。64 位syscall 和 sysret。接下来的内容我们只介绍 64 位系统的指令。上面的说明了触发系统调用使用什么指令还需要弄清楚如何传值系统调用编号的存储使用 %rax 寄存器使用 %rdi、%rsi、%rdx、%r10、%r8、%r9 寄存器存储系统调用的参数。同前面一样我们还是以输出 Hello, World! 字符串到终端为例汇编的代码如下.section .data msg: .ascii Hello, World!\n .section .text .globl _start _start: movq $1, %rax # write 系统调用本身的数字标识1 movq $1, %rdi # write 的第 1 个参数 fd: 1 movq $msg, %rsi # write 的第 2 个参数 buffer: Hello, World!\n movq $14, %rdx # write 的第 3个参数 count: 14 syscall # 执行系统调用: write(fd, buffer, count) movq $0, %rdi # status: 0 movq $60, %rax # 函数: exit syscall # 执行系统调用: exit(status)编译运行上面的程序as helloworld_v2.s -o helloworld_v2.o ld helloworld_v2.o -o helloworld_v2 ./helloworld Hello, World!需要注意的是 64 位与 32 位系统的系统的系统编号值是不一样的64 位系统 write 系统调用的变化是 1在 32 为系统上这个值为 4。使用 GDB 来探究系统调用mmap 系统调用有六个参数我们就拿 mmap 系统调用来验证寄存器与传值。测试代码如下#include stdio.h #include fcntl.h #include stdlib.h #include fcntl.h #include sys/mman.h int main() { struct stat file_stat; int src_fd open(/home/ya/watch.sh, O_RDONLY); fstat(src_fd, file_stat); int file_len file_stat.st_size; char *src mmap(NULL, file_len, PROT_READ, MAP_PRIVATE, src_fd, 0); printf(%p\n, src); return 0; }编译这个程序然后使用 gdb 调试运行$ gcc -g syscall_test.c $ gdb a.out先断点到 12 行也就是 mmap 之前然后运行到断点处。(gdb) b 12 Breakpoint 1 at 0x40064f: file syscall_test.c, line 12 (gdb) r Starting program: /home/ya/dev/linux_study/process/syscall/a.out Breakpoint 1, main () at syscall_test.c:12 12 char *src mmap(NULL, file_len, PROT_READ, MAP_PRIVATE, src_fd, 0);然后断点到 glibc 的 __mmap 方法上然后输出 c执行到断点处。(gdb) b __mmap Breakpoint 2 at 0x7ffff7b05d10: __mmap. (2 locations) (gdb) c Continuing. Breakpoint 2, __mmap (addr0x0, len11, prot1, flags2, fd7, offset0) at ../sysdeps/unix/sysv/linux/wordsize-64/mmap.c:32在 gdb 中输入 disas 查看汇编代码在当前汇编指令不远处出现了 syscall这个就是真正执行系统调用的地方。Dump of assembler code for function __mmap: 0x00007ffff7b05d10 0: push %r15 ... 0x00007ffff7b05d31 33: mov %r13,%r9 0x00007ffff7b05d34 36: mov %r14,%r8 0x00007ffff7b05d37 39: movslq %r12d,%r10 0x00007ffff7b05d3a 42: mov %r15,%rdx 0x00007ffff7b05d3d 45: mov %rbp,%rsi 0x00007ffff7b05d40 48: mov %rbx,%rdi 0x00007ffff7b05d43 51: mov $0x9,%eax 0x00007ffff7b05d48 56: syscall ...然后断点到 syscall 汇编代码处执行 c 运行到此次。(gdb) b *0x00007ffff7b05d48 (gdb) c Continuing.此时已经是执行系统调用前的最后一步了可以使用 gdb 来查看各个寄存器的状态。(gdb) p $rax $1 9 (gdb) p $rdi $2 0 (gdb) p $rsi $3 11 (gdb) p $rdx $4 1 (gdb) p $r10 $5 2 (gdb) p $r8 $6 7 (gdb) p $r9 $7 0mmap 系统调用对应的函数是 sys_mmap它的函数签名如下long sys_mmap(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, off_t pgoff);其中 rax 寄存器的值为 mmap 系统调用的编号 9。rdi 是第一个参数 addr这里为 0也就是 NULL。rsi 是第二个参数 len这里值为 11表示文件真实的大小 11 字节。rdx 是第三个参数 prot这里为 1表示 PROT_READ(0x1)它在内核中的定义如下#define PROT_READ 0x1 /* page can be read */ #define PROT_WRITE 0x2 /* page can be written */ #define PROT_EXEC 0x4 /* page can be executed */r10 是第四个参数 flags这里为 2表示 MAP_PRIVATE(0x02)它在内核中的定义如下#define MAP_SHARED 0x01 /* Share changes */ #define MAP_PRIVATE 0x02 /* Changes are private */ #define MAP_TYPE 0x0f /* Mask for type of mapping */ #define MAP_FIXED 0x10 /* Interpret addr exactly */r8 是第五个参数 fd这里为 7表示/home/ya/watch.sh文件的文件句柄可以通过/proc/pid/fd来进行确认。$ ls -l /proc/11835/fd lrwx------. 1 ya ya 64 Dec 11 07:15 0 - /dev/pts/5 lrwx------. 1 ya ya 64 Dec 11 07:30 1 - /dev/pts/5 lrwx------. 1 ya ya 64 Dec 11 07:30 2 - /dev/pts/5 ... lr-x------. 1 ya ya 64 Dec 11 07:30 7 - /home/ya/watch.shr9 是第六个参数 pgoff这里的 offset 值为 0。通过 gdb我们就再次验证了 syscall 系统调用的调用方式。一些问题只是用 6 个寄存器传值会不会有超过 6 个参数的情况不存在这个问题目前已有的系统调用最多也只有 6 个参数反正 linux 自己定规则大家必须遵守。系统调用涉及 CPU 上下文切换吗答案是肯定的一次系统调用的过程会发生了两次 CPU 上下文切换。一次是保存用户态的线程寄存器、程序计数器然后执行内核空间代码。系统调用结束后还需要恢复用户态现场切换回用户空间代码处继续执行。

相关新闻

为什么你的ChatGPT总“答非所问”?揭秘LLM理解机制底层逻辑:3步定位Prompt失效根因,5分钟完成精准调优

为什么你的ChatGPT总“答非所问”?揭秘LLM理解机制底层逻辑:3步定位Prompt失效根因,5分钟完成精准调优

更多请点击: https://kaifayun.com 第一章:为什么你的ChatGPT总“答非所问”?揭秘LLM理解机制底层逻辑:3步定位Prompt失效根因,5分钟完成精准调优 大型语言模型并非“听懂”自然语言,而是基于概率分布对to…

2026/6/29 4:22:51阅读更多 →
【宝塔面板排障】服务启动失败?三步精准定位并修复“Panel服务”卡死难题

【宝塔面板排障】服务启动失败?三步精准定位并修复“Panel服务”卡死难题

1. 问题现象与初步判断 最近在Windows系统上使用宝塔面板时,不少朋友都遇到了一个典型问题:点击启动宝塔面板后,界面长时间卡在"正在启动Panel服务"的提示界面,进度条一动不动,就像被冻住了一样。这种情况我…

2026/6/29 4:22:51阅读更多 →
终极指南:如何用smcFanControl解决Mac过热降频问题

终极指南:如何用smcFanControl解决Mac过热降频问题

终极指南:如何用smcFanControl解决Mac过热降频问题 【免费下载链接】smcFanControl Control the fans of every Intel Mac to make it run cooler 项目地址: https://gitcode.com/gh_mirrors/smc/smcFanControl 你是否曾在运行视频编辑或游戏时,发…

2026/6/29 4:22:51阅读更多 →
基于HSV颜色空间和形态学特征的火灾与烟雾智能检测系统(全网首发)

基于HSV颜色空间和形态学特征的火灾与烟雾智能检测系统(全网首发)

摘要:随着城市化进程的加快,火灾事故频发给人民生命财产安全带来严重威胁。传统的火灾探测器基于温度或烟雾浓度传感器,存在响应时间慢、易受环境干扰等缺陷。本文提出了一种基于计算机视觉的火灾与烟雾智能检测系统,该系统采用HS…

2026/6/29 5:22:58阅读更多 →
基于 MATLAB 的实时火灾检测系统设计与实现

基于 MATLAB 的实时火灾检测系统设计与实现

随着智能安防技术的发展,火灾检测系统已经不再局限于传统烟雾传感器和温度传感器。相比传统方式,基于图像处理的火灾检测系统能够直接从视频画面中识别火焰区域,具有响应速度快、可视化效果强、部署灵活等优势。本文设计并实现了一套基于 MAT…

2026/6/29 5:22:58阅读更多 →
【计算机毕业设计案例】基于 SpringBoot 的建材租赁客户管理系统的设计与实现 建材租赁出入库与结算管理系统的设计与实现(程序+文档+讲解+定制)

【计算机毕业设计案例】基于 SpringBoot 的建材租赁客户管理系统的设计与实现 建材租赁出入库与结算管理系统的设计与实现(程序+文档+讲解+定制)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am…

2026/6/29 5:22:58阅读更多 →
告别合并!Windows 11任务栏图标拆分终极指南

告别合并!Windows 11任务栏图标拆分终极指南

1. Windows 11任务栏合并问题解析 每次打开多个浏览器窗口时,所有标签都挤在同一个任务栏图标里,找起来特别费劲——这可能是Windows 11用户最常遇到的困扰之一。微软在Windows 11中强制取消了任务栏图标拆分功能,这让习惯传统布局的用户感到…

2026/6/29 5:22:58阅读更多 →
GitHub中文界面插件:3分钟告别英文困扰的终极解决方案

GitHub中文界面插件:3分钟告别英文困扰的终极解决方案

GitHub中文界面插件:3分钟告别英文困扰的终极解决方案 【免费下载链接】github-chinese GitHub 汉化插件,GitHub 中文化界面。 (GitHub Translation To Chinese) 项目地址: https://gitcode.com/gh_mirrors/gi/github-chinese 你是否曾经因为GitH…

2026/6/29 5:22:58阅读更多 →
【Win11】Edge浏览器Alt+Tab多窗口混乱?一招设置回归清爽多任务视图

【Win11】Edge浏览器Alt+Tab多窗口混乱?一招设置回归清爽多任务视图

1. 为什么Edge浏览器会让AltTab变得一团糟? 如果你经常在Windows 11上使用Edge浏览器,同时打开十几个标签页,然后按下AltTab想快速切换应用时,可能会被满屏的Edge窗口图标吓到。这不是你的错觉,而是微软在Windows 11中…

2026/6/29 5:17:57阅读更多 →
AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

6个月前的2025年12月,Boris Cherny 公开宣布自己卸载了 IDE。一时间,Vibe Coding 成了全行业最热的话题。6个月后,当我们回过头来拉一份真实账本,发现事情远没有"一句话生成一个App"那么浪漫。本文从产品经理和研发两个…

2026/6/29 3:27:55阅读更多 →
审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

引言:审计结束三个月了,审计员的权限还没关某城商行每年按照监管要求开展至少一次数据安全审计。审计期间,内审部门需要抽样检查各类业务数据——交易流水、客户信息、员工操作日志、权限配置记录。这些数据分布在不同系统中,审计…

2026/6/29 2:19:08阅读更多 →
如何在3秒内从普通图片生成专业级法线贴图:DeepBump的终极指南

如何在3秒内从普通图片生成专业级法线贴图:DeepBump的终极指南

如何在3秒内从普通图片生成专业级法线贴图:DeepBump的终极指南 【免费下载链接】DeepBump Normal & height maps generation from single pictures 项目地址: https://gitcode.com/gh_mirrors/de/DeepBump 还在为3D建模中的纹理制作而烦恼吗?…

2026/6/29 0:01:47阅读更多 →
OCAuxiliaryTools:终极OpenCore配置工具,让黑苹果安装从未如此简单!

OCAuxiliaryTools:终极OpenCore配置工具,让黑苹果安装从未如此简单!

OCAuxiliaryTools:终极OpenCore配置工具,让黑苹果安装从未如此简单! 【免费下载链接】OCAuxiliaryTools Cross-platform GUI management tools for OpenCore(OCAT) 项目地址: https://gitcode.com/gh_mirrors/oc/OCA…

2026/6/29 0:01:47阅读更多 →
终极Windows 11精简指南:使用tiny11builder快速创建纯净系统镜像

终极Windows 11精简指南:使用tiny11builder快速创建纯净系统镜像

终极Windows 11精简指南:使用tiny11builder快速创建纯净系统镜像 【免费下载链接】tiny11builder Scripts to build a trimmed-down Windows 11 image. 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny11builder 你是否厌倦了Windows 11系统自带的20…

2026/6/29 0:01:47阅读更多 →