简单系统调用过程分析
前言
我写了一段C代码:
1 | int main(){ |
在终端执行:
1 | strace -o printf.log ./print_helloworld |
终端中输出:
1 | hello world! |
printf.log中的内容为:
1 | execve("./print_helloworld", ["./print_helloworld"], 0x7ffddeb59340 /* 37 vars */) = 0 |
涉及到的系统调用
(1)涉及到strace 命令
通过man strace可以看到他是一个跟踪系统调用和信号传递的工具
It intercepts and records the system calls which are called by a process and the signals which are received by a process. The name of each system call, its arguments and its return value are printed on standard error or to the file specified with the -o option.
(2)这个案例中涉及一些系统调用
arch_prctl
用于设置架构特定线程状态的系统调用。它允许在 x86 和 x86-64 架构上执行特定的操作,如设置和获取 FS 和 GS 寄存器的基地址,以及启用或禁用 CPUID 指令。
mmap
execve
允许进程用一个全新的程序替换当前运行的程序。这个过程中,旧程序的栈、数据和堆段会被新程序所替换。execve函数不会创建新进程,而是在原有进程的基础上执行另一个程序,进程ID保持不变。
access
openat
用于相对于指定目录文件描述符打开文件,是 open 函数的扩展版本,提供了更灵活的文件打开方式,支持相对路径操作,避免了某些竞态条件。
- newfstatat
- set_tid_address
- set_robust_list
- rseq
- brk
- write
- close
- exit_group
具体分析
1. 程序启动 (execve)
1 | execve("./print_helloworld", ["./print_helloworld"], 0x7ffddeb59340 /* 37 vars */) = 0 |
- execve:这是程序启动时的系统调用,用于执行可执行文件。
execve会替换当前进程的映像为指定的程序,这里是./print_helloworld。 - 它传递了参数
["./print_helloworld"],表示没有命令行参数传入。 0x7ffddeb59340是进程环境变量的位置(堆栈中的内存位置)。
2. 分配内存 (brk 和 mmap)
1 | brk(NULL) = 0x55b56c23f000 |
- brk:
brk系统调用是为了调整程序的堆内存边界,分配内存。这里它返回了一个地址,表明堆的开始位置。
1 | mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f16cdaba000 |
- mmap:这个调用分配了一块 8192 字节的内存区域。
PROT_READ|PROT_WRITE表示这块内存可读可写,MAP_PRIVATE|MAP_ANONYMOUS表示这是匿名内存(没有文件映射)。它返回了内存区域的起始地址。
3. 加载动态链接库 (openat 和 read)
1 | openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 |
- openat:打开共享库缓存文件
/etc/ld.so.cache,它包含了动态库路径的信息。O_RDONLY表示只读模式,O_CLOEXEC表示当执行 exec 系统调用时会自动关闭该文件。 - read:从文件中读取数据,这里是读取到 ELF 格式的共享库文件的部分内容。
4. 继续读取共享库信息 (pread64)
1 | pread64(3, "\6\0\0\0\4\0\0\0@", 784, 64) = 784 |
- pread64:从文件描述符中读取共享库的特定偏移位置的数据。这部分通常用于解析 ELF 文件头,提取程序头信息、节头等数据。
5. 加载 libc 库
1 | newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=2220400, ...}, AT_EMPTY_PATH) = 0 |
- mmap:加载共享库
libc.so.6的内容到内存中,大小为 2264656 字节。该内存映射使用了PROT_READ(只读)权限,以防止写入修改库内容。
6. 保护库的只读区域 (mprotect)
1 | mprotect(0x7f16cd8b0000, 2023424, PROT_NONE) = 0 |
- mprotect:将库的一部分内存设置为不可访问(
PROT_NONE),以保护代码区不被修改。
7. 内存映射(执行区域)
1 | mmap(0x7f16cd8b0000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7f16cd8b0000 |
- mmap:将库的代码部分映射到进程的地址空间,且此区域可执行(
PROT_EXEC)。这使得程序能够执行共享库中的代码。
8. 设置堆栈信息 (arch_prctl 和 set_tid_address)
1 | arch_prctl(ARCH_SET_FS, 0x7f16cd885740) = 0 |
- arch_prctl:设置与体系结构相关的特定寄存器。这通常用于线程的控制。
- set_tid_address:设置当前线程的 ID 地址。
9. 进程的内存保护和资源限制 (mprotect, prlimit64)
1 | mprotect(0x7f16cda9e000, 16384, PROT_READ) = 0 |
- mprotect:对堆栈的内存进行保护,使其只读。
- prlimit64:检查进程的栈内存限制,确保栈内存足够。
10. 写入标准输出 (write)
1 | write(1, "hello world!\n", 13) = 13 |
- write:调用
write系统调用将数据输出到文件描述符 1(即标准输出)。它将hello world!\n输出到终端。
11. 程序退出 (exit_group)
1 | exit_group(0) = ? |
- exit_group:这是进程退出时的系统调用,
0表示程序正常退出。
总结
整个过程可以按以下步骤总结:
- 程序启动:
execve启动./print_helloworld程序。 - 内存分配:程序通过
brk和mmap分配内存。 - 加载共享库:通过
openat和read读取共享库,加载libc.so.6库,映射到进程内存中。 - 设置堆栈和线程:通过
arch_prctl和set_tid_address设置线程和堆栈信息。 - 保护内存:通过
mprotect保护程序的代码和数据区域,防止非法访问。 - 执行代码:通过
mmap映射并执行共享库代码。 - 输出结果:通过
write输出字符串hello world!\n到终端。 - 程序退出:程序调用
exit_group正常退出。
strace 输出展示了系统调用的详细信息,揭示了程序如何通过操作系统接口加载库、分配内存、执行代码和输出结果的过程。
(10 条消息) posix是什么都不知道,还好意思说你懂Linux? - 知乎
(11 条消息) 系统调用与内存管理(sbrk、brk、mmap、munmap) - 知乎
关于init进程
可执行文件目录:
1 | dky@DESKTOP-57A415L:/$ ls -la init |
init是 Linux 系统启动的第一个用户态进程(PID 通常为 1),负责初始化系统环境、启动后续的服务进程。
init对应的systemd源代码仓库
1 | https://github.com/systemd/systemd.git |
经过确认,wsl的init对应与WSL环境专用的初始化程序,其对应的代码仓库为:
1 | https://github.com/microsoft/WSL.git |
具体路径位于:https://github.com/microsoft/WSL/blob/master/src/linux/init/main.cpp,发现竟然是用cpp写的,还挺神奇的
查看gcc命令具体位置
1 | dky@DESKTOP-57A415L:~$ which gcc |
或者
1 | dky@DESKTOP-57A415L:~$ which gcc |
查看gcc头文件库文件搜索路径
1 | gcc -print-search-dirs |
1 | echo 'main(){}'| gcc -E -v - |
1 | gcc -v -E -x c /dev/null |
1 | gcc -Wl,--verbose 2>&1 | grep -E 'SEARCH_DIR|library search' |
1 | gcc -Wl,--verbose 2>&1 |
glibc安装
glibc下载地址:https://ftp.gnu.org/gnu/glibc/?C=M;O=A
glibc是GNU发布的libc库,即c运行库。 glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。 glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。
glibc涉及的系统组件多,无法在超算平台上整体部署高版本glibc,如果需要高版本glibc,可通过源码自行安装。
1 | srun -p 64c512g -n 16 --pty /bin/bash |
参考:https://docs.hpc.sjtu.edu.cn/app/compilers_and_languages/glibc.html
glibc编译
1 | cd /home/dky/print |
--disable-werror 很重要,否则很多 warning 会被当成 error 卡住
--prefix 只是“未来如果 install 会装到哪”,不执行 make install 就不会动系统。
–prefix需要配置,否则默认为–prefix=/usr/local,如果你把它装进 /usr/local 可能把系统搞坏”,执行configure时会直接拦住。
只编译(不安装)
1 | make -j"$(nproc)" |
给 clangd 用:生成 compile_commands.json
1 | bear -- make -j"$(nproc)" -k |
然后把它链接到源码根目录:
1 | ln -sf /home/dky/print/glibc-build/compile_commands.json \ |
(或在 VSCode clangd 参数里加:--compile-commands-dir=/home/dky/print/glibc-build)
查看glibc版本
1 | ldd --version |
运行结果
1 | dky@DESKTOP-57A415L:~/print/glibc-2.42$ ldd --version |
GLIBC 的动态库是可以执行运行的, 运行将显示版本号以及版权信息,可以看到版本为:2.35
1 | /lib/x86_64-linux-gnu/libc.so.6 |
运行结果
1 | dky@DESKTOP-57A415L:~/print/glibc-2.42$ /lib/x86_64-linux-gnu/libc.so.6 |
然后到Index of /pub/gnu/glibc网站上搜索2.35版本进行下载。找到以下版本进行下载
1 | glibc-2.35.tar.gz 2022-02-03 01:35 34M |
解压并进入对应文件夹
1 | tar -zxvf glibc-2.35.tar.gz |
验证下载的glibc就是系统所用的glibc代码
根据echo 'main(){}'| gcc -E -v -查看得到头文件搜索路径,可以得到以下几个头文件搜索路径
1 | dky@DESKTOP-57A415L:~/print$ echo 'main(){}'| gcc -E -v - |
可以看到搜索路径都在/usr/*路径下,因此使用find搜索一个典型的头文件stdio.h
1 | dky@DESKTOP-57A415L:~$ find /usr -name stdio.h |
一共搜索出来3个stdio.h,但是可以确定的是对于C代码来说,使用的stdio.h就是/usr/include/stdio.h
在glibc-2.35中搜索stdio.h
1 | dky@DESKTOP-57A415L:~/print/glibc-2.35$ find -name stdio.h |
一共有4个stdio.h
首先认为/usr/include/stdio.h就是./include/stdio.h,但是很遗憾并不是,猜测这个stdio.h是提供给glibc内部接口使用的,看了下具体代码,全部是extern的一些print声明。
接下来看第二个和第三个文件,发现第二个./include/bits/stdio.h里面只有一行代码#include <libio/bits/stdio.h>,即说明第二个文件和第三个文件是类同的,可以认为是同一个文件。又因为这俩文件都包含了bits目录,所以很自然的将./libio/bits/stdio.h 与 /usr/include/x86_64-linux-gnu/bits/stdio.h对比,发现他俩的内容是一摸一样的。
1 | dky@DESKTOP-57A415L:~/print/glibc-2.35$ cmp ./libio/bits/stdio.h /usr/include/x86_64-linux-gnu/bits/stdio.h |
最后只剩下./libio/stdio.h 了,将./libio/stdio.h 与 /usr/include/stdio.h使用cmp对比发现两者一模一样。当然也可以在vscode中点击【将已选择项目进行比较】来直观验证
1 | dky@DESKTOP-57A415L:~/print/glibc-2.35$ cmp /home/dky/print/glibc-2.35/libio/stdio.h /usr/include/stdio.h |
结论就是:glibc-2.3/libio/stdio.h对应 /usr/include/stdio.h
想追踪printf怎么调用到C库的write,最后又如何调到write系统调用,搞了半天也没搞明白,放弃!
编译内核
参考:https://mazhen.tech/p/%E4%BB%8E%E6%BA%90%E7%A0%81%E6%9E%84%E5%BB%BA-linux-%E5%86%85%E6%A0%B8/
官网上下载:https://www.kernel.org/
点击最新版本下载:linux-6.18.1.tar.xz
1 | tar -Jxvf linux-6.18.1.tar.xz |
为本机WSL编译内核:
查看本机信息:
1 | dky@DESKTOP-57A415L:~/linux_kernel/linux-6.18.1$ uname -m |
可以看到机器架构是x86_64,
清除交叉编译的环境污染:
重置ARCH CROSS_COMPILE CC是因为我的wsl环境默认配置的交叉编译环境为multi_v7_defconfig
1 | # 清掉交叉编译残留(只影响当前终端) |
执行make O=../build-x86_64 ARCH=x86_64
1 | # out-of-tree 构建(不污染源码目录) |
具体输出:
1 | dky@DESKTOP-57A415L:~/linux_kernel/linux-6.18.1$ make O=../build-x86_64 ARCH=x86_64 x86_64_defconfig |
编译:
1 | # 编译 |
“内核源码 + compile_commands.json + VSCode clangd”的最省事配置步骤
下面给你一套**“最省事、最不容易踩坑”**的流程:内核源码(source tree)+ 单独构建目录(O=)+ 生成 compile_commands.json + VSCode(WSL) + clangd。
你的环境已经确认是 WSL2 + x86_64,所以全程按 x86_64 来。
0. 装好必备工具(WSL 里)
1 | sudo apt update |
1. 推荐的目录布局(source 与 build 分开)
假设你源码在:
~/linux_kernel/linux-6.18.1
我们用一个独立输出目录:
~/linux_kernel/build-x86_64
1 | cd ~/linux_kernel/linux-6.18.1 |
2. 先把内核“准备好”让 clangd 能解析(生成必要头文件)
1 | cd ~/linux_kernel/linux-6.18.1 |
3. 生成 compile_commands.json(两种方式,选一种)
方案 A(最推荐):用内核自带脚本生成
内核一般自带脚本(你 6.x 基本都有):
1 | cd ~/linux_kernel/linux-6.18.1 |
如果存在,就这样生成(会在当前目录生成 compile_commands.json):
1 | python3 scripts/clang-tools/gen_compile_commands.py -d ../build-x86_64 |
如果你希望把
compile_commands.json放到 build 目录,也可以生成后移动过去:
1 | mv compile_commands.json ../build-x86_64/ |
方案 B(通吃):用 bear 抓编译命令
如果你更信任 “实际编译过程”,用 bear:
1 | cd ~/linux_kernel/linux-6.18.1 |
bear 会在你当前目录生成 compile_commands.json(同样可以 mv 到 build 目录)。
4. VSCode(Remote - WSL)里配置 clangd:一劳永逸
4.1 安装/启用扩展
- 安装 clangd 扩展(LLVM 官方那个)
- 如果你装了 Microsoft 的 C/C++ 扩展:建议禁用它的 IntelliSense(否则经常抢解析)
4.2 在工程根目录写 .vscode/settings.json
假设你把 compile_commands.json 放在 ../build-x86_64/(推荐):
1 | { |
把路径按你真实用户名/目录改一下。
-j=8也可以换成你的 CPU 线程数。
5. 常见坑位(你以后遇到基本就这几个)
- 又出现 multi_v7_defconfig / ARM
说明你某次终端里残留了ARCH=arm/CROSS_COMPILE=...。
解决:
1 | unset ARCH CROSS_COMPILE CC HOSTCC LLVM |
- clangd 报大量 “找不到 generated headers / autoconf.h”
通常是你没跑prepare modules_prepare:
1 | make O=../build-x86_64 ARCH=x86_64 prepare modules_prepare |
- 你换了
.config之后(比如 menuconfig),clangd 解析又乱了
重新跑一遍:
1 | make O=../build-x86_64 ARCH=x86_64 olddefconfig |
如果你告诉我:你最终把 compile_commands.json 放在源码目录还是 build 目录,我可以给你一份完全匹配你路径的 .vscode/settings.json(直接复制粘贴即可)。
compile_commands.json放在build目录
好,那就按“compile_commands.json 在 build 目录”的最省事做法来(你基本只要照抄路径就行)。
1) 生成与更新(放到 build 目录)
在源码目录执行:
1 | cd ~/linux_kernel/linux-6.18.1 |
验证一下:
1 | ls -l ../build-x86_64/compile_commands.json |
以后改过
.config或拉新代码后,只要重复prepare modules_prepare+ 重新生成compile_commands.json就行。
2) VSCode(WSL)配置 clangd(直接可用)
在源码根目录创建/修改:
1 | .vscode/settings.json |
把这一路径按你真实路径改成对应的绝对路径即可(你现在的结构看起来就是 /home/dky/linux_kernel/build-x86_64)。
3) 一眼判断 clangd 是否吃到 compile_commands
在 VSCode 里打开 View → Output → clangd,你应该能看到它加载 compile commands 的日志。
如果你想命令行快速验证路径是否对(可选):
1 | realpath ~/linux_kernel/build-x86_64/compile_commands.json |
4) 最常见的“能跳一部分、但很多红线”
一般是 build 目录里缺生成头文件。补一遍就好:
1 | make O=~/linux_kernel/build-x86_64 ARCH=x86_64 prepare modules_prepare |
如果你愿意贴一下 realpath ~/linux_kernel/build-x86_64 的输出(就一行),我可以把 settings.json 里的路径替你写成完全准确的一份。
编译buildroot
官网上下载:https://buildroot.org/downloads/
选择一个版本,每个文件名的结构是:buildroot-版本号.压缩格式(.sign)。rc 是候选版本(release candidate),后面的 r2、r3 是修订版,tar.bz2 和 tar.gz 是压缩格式,还有.sign 是签名文件。
因此排除掉rc版本,排除掉带sign的验证文件,这里选一个2020年2月份发布的最后一个稳定版本。点击下载:buildroot-2020.02.12.tar.bz2
1 | tar -xjf buildroot-2020.02.12.tar.bz2 |
配置:
选 QEMU 的x86_64相关 defconfig,可以执行:make list-defconfigs 会输出可用的 defconfig 列表,随便选一个,比如:qemu_x86_64_defconfig
1 | make qemu_x86_64_defconfig |
编译:
1 | make V=1 |
编译产物
1 | ls -lh output/images |
运行:
用 QEMU 启动,如果 output/images/ 里有 bzImage 和 rootfs.ext2,(纯串口模式):
1 | qemu-system-x86_64 \ |
遇到问题:
“真错误点”在 host-m4-1.4.18 这一步,不是配错了。日志里关键报错是:
- 在
c-stack.c里做预处理判断:SIGSTKSZ < 16384 - 但你当前系统的 glibc 把
SIGSTKSZ变成了运行时sysconf(...)(不再是编译期常量),所以预处理器直接炸:missing binary operator before token "("【pasted】
这正是 glibc 2.34+ 的典型兼容性坑:当 _GNU_SOURCE 等启用时 SIGSTKSZ 变成 sysconf(_SC_SIGSTKSZ),老版本 m4/gnulib 会在 #if 里用它从而编译失败。GitHub+1
✅ 推荐最快修复(在 2020.02.12 里打一个补丁)
Buildroot 社区已经给 m4 回补过修复补丁(“c-stack: stop using SIGSTKSZ”)。Buildroot Lists+1
在 buildroot-2020.02.12 目录执行:
1 | # 1) 下载补丁到 Buildroot 的 m4 包目录(Buildroot 会自动应用该目录下的 *.patch) |
日志里那些
_FORTIFY_SOURCE redefined都只是 warning,不影响;真正导致退出的是上面这个SIGSTKSZ的预处理错误。