简单系统调用过程分析

前言

我写了一段C代码:

1
2
3
4
int main(){
printf("hello world!\n");
return 0;
}

在终端执行:

1
strace -o printf.log ./print_helloworld

终端中输出:

1
hello world!

printf.log中的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
execve("./print_helloworld", ["./print_helloworld"], 0x7ffddeb59340 /* 37 vars */) = 0
brk(NULL) = 0x55b56c23f000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffd179dbd20) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f16cdaba000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=34783, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 34783, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f16cdab1000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 48, 848) = 48
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0I\17\357\204\3$\f\221\2039x\324\224\323\236S"..., 68, 896) = 68
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=2220400, ...}, AT_EMPTY_PATH) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2264656, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f16cd888000
mprotect(0x7f16cd8b0000, 2023424, PROT_NONE) = 0
mmap(0x7f16cd8b0000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7f16cd8b0000
mmap(0x7f16cda45000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x7f16cda45000
mmap(0x7f16cda9e000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x215000) = 0x7f16cda9e000
mmap(0x7f16cdaa4000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f16cdaa4000
close(3) = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f16cd885000
arch_prctl(ARCH_SET_FS, 0x7f16cd885740) = 0
set_tid_address(0x7f16cd885a10) = 19390
set_robust_list(0x7f16cd885a20, 24) = 0
rseq(0x7f16cd8860e0, 0x20, 0, 0x53053053) = 0
mprotect(0x7f16cda9e000, 16384, PROT_READ) = 0
mprotect(0x55b552fa8000, 4096, PROT_READ) = 0
mprotect(0x7f16cdaf4000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7f16cdab1000, 34783) = 0
newfstatat(1, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x5), ...}, AT_EMPTY_PATH) = 0
getrandom("\x26\xbc\x99\x15\xfc\x24\x0d\x5b", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0x55b56c23f000
brk(0x55b56c260000) = 0x55b56c260000
write(1, "hello world!\n", 13) = 13
exit_group(0) = ?
+++ exited with 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)这个案例中涉及一些系统调用

  1. arch_prctl

    用于设置架构特定线程状态的系统调用。它允许在 x86 和 x86-64 架构上执行特定的操作,如设置和获取 FS 和 GS 寄存器的基地址,以及启用或禁用 CPUID 指令。

  2. mmap

  3. execve

    允许进程用一个全新的程序替换当前运行的程序。这个过程中,旧程序的栈、数据和堆段会被新程序所替换。execve函数不会创建新进程,而是在原有进程的基础上执行另一个程序,进程ID保持不变。

  4. access

  5. openat

用于相对于指定目录文件描述符打开文件,是 open 函数的扩展版本,提供了更灵活的文件打开方式,支持相对路径操作,避免了某些竞态条件。

  1. newfstatat
  2. set_tid_address
  3. set_robust_list
  4. rseq
  5. brk
  6. write
  7. close
  8. exit_group

具体分析

1. 程序启动 (execve)

1
execve("./print_helloworld", ["./print_helloworld"], 0x7ffddeb59340 /* 37 vars */) = 0
  • execve:这是程序启动时的系统调用,用于执行可执行文件。execve 会替换当前进程的映像为指定的程序,这里是 ./print_helloworld
  • 它传递了参数 ["./print_helloworld"],表示没有命令行参数传入。
  • 0x7ffddeb59340 是进程环境变量的位置(堆栈中的内存位置)。

2. 分配内存 (brkmmap)

1
brk(NULL) = 0x55b56c23f000
  • brkbrk 系统调用是为了调整程序的堆内存边界,分配内存。这里它返回了一个地址,表明堆的开始位置。
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. 加载动态链接库 (openatread)

1
2
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
  • openat:打开共享库缓存文件 /etc/ld.so.cache,它包含了动态库路径的信息。O_RDONLY 表示只读模式,O_CLOEXEC 表示当执行 exec 系统调用时会自动关闭该文件。
  • read:从文件中读取数据,这里是读取到 ELF 格式的共享库文件的部分内容。

4. 继续读取共享库信息 (pread64)

1
2
pread64(3, "\6\0\0\0\4\0\0\0@", 784, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 48, 848) = 48
  • pread64:从文件描述符中读取共享库的特定偏移位置的数据。这部分通常用于解析 ELF 文件头,提取程序头信息、节头等数据。

5. 加载 libc 库

1
2
3
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=2220400, ...}, AT_EMPTY_PATH) = 0
pread64(3, "\6\0\0\0\4\0\0\0@", 784, 64) = 784
mmap(NULL, 2264656, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f16cd888000
  • 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_prctlset_tid_address)

1
2
arch_prctl(ARCH_SET_FS, 0x7f16cd885740) = 0
set_tid_address(0x7f16cd885a10) = 19390
  • arch_prctl:设置与体系结构相关的特定寄存器。这通常用于线程的控制。
  • set_tid_address:设置当前线程的 ID 地址。

9. 进程的内存保护和资源限制 (mprotect, prlimit64)

1
2
mprotect(0x7f16cda9e000, 16384, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 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 表示程序正常退出。

总结

整个过程可以按以下步骤总结:

  1. 程序启动execve 启动 ./print_helloworld 程序。
  2. 内存分配:程序通过 brkmmap 分配内存。
  3. 加载共享库:通过 openatread 读取共享库,加载 libc.so.6 库,映射到进程内存中。
  4. 设置堆栈和线程:通过 arch_prctlset_tid_address 设置线程和堆栈信息。
  5. 保护内存:通过 mprotect 保护程序的代码和数据区域,防止非法访问。
  6. 执行代码:通过 mmap 映射并执行共享库代码。
  7. 输出结果:通过 write 输出字符串 hello world!\n 到终端。
  8. 程序退出:程序调用 exit_group 正常退出。

strace 输出展示了系统调用的详细信息,揭示了程序如何通过操作系统接口加载库、分配内存、执行代码和输出结果的过程。

(10 条消息) posix是什么都不知道,还好意思说你懂Linux? - 知乎

(11 条消息) 系统调用与内存管理(sbrk、brk、mmap、munmap) - 知乎

关于init进程

可执行文件目录:

1
2
3
4
dky@DESKTOP-57A415L:/$ ls -la init 
-rwxrwxrwx 1 root root 2260248 Nov 10 2024 init
dky@DESKTOP-57A415L:/$ file init
init: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

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
2
3
4
dky@DESKTOP-57A415L:~$ which gcc
/usr/bin/gcc
dky@DESKTOP-57A415L:~$ readlink -e /usr/bin/gcc
/usr/bin/x86_64-linux-gnu-gcc-11

或者

1
2
3
4
5
6
dky@DESKTOP-57A415L:~$ which gcc
/usr/bin/gcc
dky@DESKTOP-57A415L:~$ file /usr/bin/gcc
/usr/bin/gcc: symbolic link to gcc-11
dky@DESKTOP-57A415L:~$ file /usr/bin/gcc-11
/usr/bin/gcc-11: symbolic link to x86_64-linux-gnu-gcc-11

查看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
2
3
4
5
6
7
8
9
10
11
12
srun -p 64c512g -n 16 --pty /bin/bash
mkdir -p ${HOME}/01.application/12.glibc && cd ${HOME}/01.application/12.glibc
wget http://ftp.gnu.org/gnu/glibc/glibc-2.29.tar.gz
tar -zxvf glibc-2.29.tar.gz && cd glibc-2.29
mkdir build && cd build
../configure --prefix=${HOME}/01.application/12.glibc/ --disable-sanity-checks
make -j16
make install
cd ${HOME}/01.application/12.glibc/
rm -rf glibc-2.29
export PATH=${HOME}/01.application/12.glibc/bin:$PATH
ldd --version

参考:https://docs.hpc.sjtu.edu.cn/app/compilers_and_languages/glibc.html

glibc编译

1
2
3
4
5
6
7
8
9
cd /home/dky/print
rm -rf glibc-build
mkdir glibc-build
cd glibc-build

../glibc-2.35/configure \
--prefix=/home/dky/print/glibc-prefix \
--disable-werror

--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
2
ln -sf /home/dky/print/glibc-build/compile_commands.json \
/home/dky/print/glibc-2.35/compile_commands.json

(或在 VSCode clangd 参数里加:--compile-commands-dir=/home/dky/print/glibc-build

查看glibc版本

1
ldd --version

运行结果

1
2
3
4
5
6
dky@DESKTOP-57A415L:~/print/glibc-2.42$ ldd --version
ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

GLIBC 的动态库是可以执行运行的, 运行将显示版本号以及版权信息,可以看到版本为:2.35

1
/lib/x86_64-linux-gnu/libc.so.6

运行结果

1
2
3
4
5
6
7
8
9
10
dky@DESKTOP-57A415L:~/print/glibc-2.42$ /lib/x86_64-linux-gnu/libc.so.6 
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.8) stable release version 2.35.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 11.4.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

然后到Index of /pub/gnu/glibc网站上搜索2.35版本进行下载。找到以下版本进行下载

1
glibc-2.35.tar.gz	2022-02-03 01:35	34M	 

解压并进入对应文件夹

1
2
tar -zxvf glibc-2.35.tar.gz
cd glibc-2.35

验证下载的glibc就是系统所用的glibc代码

根据echo 'main(){}'| gcc -E -v -查看得到头文件搜索路径,可以得到以下几个头文件搜索路径

1
2
3
4
5
6
7
8
9
10
dky@DESKTOP-57A415L:~/print$ echo 'main(){}'| gcc -E -v -
...
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/11/include
/usr/local/include
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.
...

可以看到搜索路径都在/usr/*路径下,因此使用find搜索一个典型的头文件stdio.h

1
2
3
4
dky@DESKTOP-57A415L:~$ find /usr -name stdio.h
/usr/include/stdio.h
/usr/include/c++/11/tr1/stdio.h
/usr/include/x86_64-linux-gnu/bits/stdio.h

一共搜索出来3个stdio.h,但是可以确定的是对于C代码来说,使用的stdio.h就是/usr/include/stdio.h

在glibc-2.35中搜索stdio.h

1
2
3
4
5
6
7
dky@DESKTOP-57A415L:~/print/glibc-2.35$ find -name stdio.h
./include/stdio.h // 猜测提供给glibc内部使用

./include/bits/stdio.h //只包含libio/bits/stdio.h
./libio/bits/stdio.h // 与/usr/include/x86_64-linux-gnu/bits/stdio.h 文件相同

./libio/stdio.h // 与 /usr/include/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
2
tar -Jxvf linux-6.18.1.tar.xz
cd linux-6.18.1

为本机WSL编译内核:

查看本机信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
dky@DESKTOP-57A415L:~/linux_kernel/linux-6.18.1$ uname -m 
x86_64
dky@DESKTOP-57A415L:~/linux_kernel/linux-6.18.1$ uname -r
5.15.167.4-microsoft-standard-WSL2
dky@DESKTOP-57A415L:~/linux_kernel/linux-6.18.1$ cat /proc/version
Linux version 5.15.167.4-microsoft-standard-WSL2 (root@f9c826d3017f) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP Tue Nov 5 00:21:55 UTC 2024
dky@DESKTOP-57A415L:~/linux_kernel/linux-6.18.1$ dpkg --print-architecture 2>/dev/null || true
amd64
dky@DESKTOP-57A415L:~/linux_kernel/linux-6.18.1$ lscpu | egrep 'Architecture|Model name|Vendor ID' || true
Architecture: x86_64
Vendor ID: GenuineIntel
Model name: 12th Gen Intel(R) Core(TM) i5-12490F
dky@DESKTOP-57A415L:~/linux_kernel/linux-6.18.1$

可以看到机器架构是x86_64,

清除交叉编译的环境污染:

重置ARCH CROSS_COMPILE CC是因为我的wsl环境默认配置的交叉编译环境为multi_v7_defconfig

1
2
3
4
5
# 清掉交叉编译残留(只影响当前终端)
unset ARCH CROSS_COMPILE CC HOSTCC LLVM

# 清理源码树(把之前生成的 .config、临时文件等都清掉)
make mrproper

执行make O=../build-x86_64 ARCH=x86_64

1
2
3
4
# out-of-tree 构建(不污染源码目录)
mkdir -p ../build-x86_64
# 生成 x86_64 默认配置
make O=../build-x86_64 ARCH=x86_64 x86_64_defconfig

具体输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dky@DESKTOP-57A415L:~/linux_kernel/linux-6.18.1$ make O=../build-x86_64 ARCH=x86_64 x86_64_defconfig
make[1]: Entering directory '/home/dky/linux_kernel/build-x86_64'
GEN Makefile
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/lexer.lex.o
HOSTCC scripts/kconfig/menu.o
HOSTCC scripts/kconfig/parser.tab.o
HOSTCC scripts/kconfig/preprocess.o
HOSTCC scripts/kconfig/symbol.o
HOSTCC scripts/kconfig/util.o
HOSTLD scripts/kconfig/conf
#
# configuration written to .config
#
make[1]: Leaving directory '/home/dky/linux_kernel/build-x86_64'

编译:

1
2
# 编译
make O=../build-x86_64 ARCH=x86_64 -j"$(nproc)"

“内核源码 + compile_commands.json + VSCode clangd”的最省事配置步骤

下面给你一套**“最省事、最不容易踩坑”**的流程:内核源码(source tree)+ 单独构建目录(O=)+ 生成 compile_commands.json + VSCode(WSL) + clangd。

你的环境已经确认是 WSL2 + x86_64,所以全程按 x86_64 来。


0. 装好必备工具(WSL 里)

1
2
sudo apt update
sudo apt install -y clangd python3 bear build-essential flex bison libssl-dev libelf-dev pahole

1. 推荐的目录布局(source 与 build 分开)

假设你源码在:
~/linux_kernel/linux-6.18.1

我们用一个独立输出目录:
~/linux_kernel/build-x86_64

1
2
cd ~/linux_kernel/linux-6.18.1
mkdir -p ../build-x86_64

2. 先把内核“准备好”让 clangd 能解析(生成必要头文件)

1
2
3
4
5
6
7
cd ~/linux_kernel/linux-6.18.1
unset ARCH CROSS_COMPILE CC HOSTCC LLVM

make O=../build-x86_64 ARCH=x86_64 x86_64_defconfig

# 这一步会生成 include/generated/* 等关键文件(clangd 解析需要)
make O=../build-x86_64 ARCH=x86_64 prepare modules_prepare

3. 生成 compile_commands.json(两种方式,选一种)

方案 A(最推荐):用内核自带脚本生成

内核一般自带脚本(你 6.x 基本都有):

1
2
cd ~/linux_kernel/linux-6.18.1
ls scripts/clang-tools/gen_compile_commands.py

如果存在,就这样生成(会在当前目录生成 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
2
cd ~/linux_kernel/linux-6.18.1
bear -- make O=../build-x86_64 ARCH=x86_64 -j"$(nproc)"

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
2
3
4
5
6
7
8
9
10
11
{
"clangd.arguments": [
"--background-index",
"--compile-commands-dir=/home/dky/linux_kernel/build-x86_64",
"--header-insertion=never",
"--clang-tidy=false",
"-j=8"
],

"C_Cpp.intelliSenseEngine": "disabled"
}

把路径按你真实用户名/目录改一下。
-j=8 也可以换成你的 CPU 线程数。


5. 常见坑位(你以后遇到基本就这几个)

  1. 又出现 multi_v7_defconfig / ARM
    说明你某次终端里残留了 ARCH=arm / CROSS_COMPILE=...
    解决:
1
unset ARCH CROSS_COMPILE CC HOSTCC LLVM
  1. clangd 报大量 “找不到 generated headers / autoconf.h”
    通常是你没跑 prepare modules_prepare
1
make O=../build-x86_64 ARCH=x86_64 prepare modules_prepare
  1. 你换了 .config 之后(比如 menuconfig),clangd 解析又乱了
    重新跑一遍:
1
2
3
make O=../build-x86_64 ARCH=x86_64 olddefconfig
make O=../build-x86_64 ARCH=x86_64 prepare modules_prepare
python3 scripts/clang-tools/gen_compile_commands.py -d ../build-x86_64

如果你告诉我:你最终把 compile_commands.json 放在源码目录还是 build 目录,我可以给你一份完全匹配你路径的 .vscode/settings.json(直接复制粘贴即可)。

compile_commands.json放在build目录

好,那就按“compile_commands.jsonbuild 目录”的最省事做法来(你基本只要照抄路径就行)。

1) 生成与更新(放到 build 目录)

在源码目录执行:

1
2
3
4
5
6
7
8
9
10
cd ~/linux_kernel/linux-6.18.1
unset ARCH CROSS_COMPILE CC HOSTCC LLVM

# 生成/更新配置与 generated 头文件(clangd 必需)
make O=../build-x86_64 ARCH=x86_64 x86_64_defconfig
make O=../build-x86_64 ARCH=x86_64 prepare modules_prepare

# 生成 compile_commands.json(输出到 build 目录)
python3 scripts/clang-tools/gen_compile_commands.py -d ../build-x86_64
mv -f compile_commands.json ../build-x86_64/

验证一下:

1
ls -l ../build-x86_64/compile_commands.json

以后改过 .config 或拉新代码后,只要重复 prepare modules_prepare + 重新生成 compile_commands.json 就行。


2) VSCode(WSL)配置 clangd(直接可用)

源码根目录创建/修改:

1
2
3
4
5
6
7
8
9
10
11
12
.vscode/settings.json
{
"clangd.arguments": [
"--background-index",
"--compile-commands-dir=/home/dky/linux_kernel/build-x86_64",
"--header-insertion=never",
"--clang-tidy=false",
"-j=8"
],

"C_Cpp.intelliSenseEngine": "disabled"
}

把这一路径按你真实路径改成对应的绝对路径即可(你现在的结构看起来就是 /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

参考:The Buildroot user manual

官网上下载: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
2
tar -xjf buildroot-2020.02.12.tar.bz2
cd buildroot-2020.02.12

配置:

选 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/ 里有 bzImagerootfs.ext2,(纯串口模式):

1
2
3
4
5
6
qemu-system-x86_64 \
-kernel output/images/bzImage \
-drive file=output/images/rootfs.ext2,format=raw \
-append "root=/dev/sda console=ttyS0" \
-nographic

遇到问题:

“真错误点”在 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
2
3
4
5
6
7
8
# 1) 下载补丁到 Buildroot 的 m4 包目录(Buildroot 会自动应用该目录下的 *.patch)
mkdir -p package/m4
curl -L 'https://download.automotivelinux.org/AGL/release/lamprey/12.1.17/qemuarm/deploy/sources/arm-agl-linux-gnueabi/m4-1.4.18-r0/0001-c-stack-stop-using-SIGSTKSZ.patch' \
-o package/m4/0001-c-stack-stop-using-SIGSTKSZ.patch

# 2) 清掉失败的 host-m4,重新编
make host-m4-dirclean
make

日志里那些 _FORTIFY_SOURCE redefined 都只是 warning,不影响;真正导致退出的是上面这个 SIGSTKSZ 的预处理错误。