简单系统调用过程分析glibc扩展

〇,前言

libc,正是用户程序与Linux内核之间最关键的中间层之一。所谓“中间层”,是指位于高层应用与底层系统之间的一层抽象。它的核心作用是:向上为应用程序提供简洁、易用的接口,向下屏蔽复杂的底层细节

什么是libc?

libcC 标准库的实现,是用户空间中最基础、最核心的库之一。常见的实现包括 GNU 的 glibc、轻量级的 musl libc、以及嵌入式中常用的 uClibc 等。libc(C 标准库)本质上是应用程序与 Linux 内核之间的中间层。它通过封装系统调用、提供通用功能和抽象硬件差异,实现了应用程序与内核的解耦。

它提供了我们日常开发中耳熟能详的函数,例如:

  • 文件操作:fopen, fclose, fprintf
  • 字符串处理:strcpy, strlen, strcat
  • 内存管理:malloc, free, calloc
  • 输入输出:printf, scanf, puts 这些函数并非内核原生提供,而是由 libc 封装并实现的。可以说,几乎每一个C语言程序都依赖于libc

Linux内核与libc的关系

与用户空间的 libc 不同,Linux内核是操作系统的核心,运行在特权模式下,直接管理硬件资源,包括CPU调度、内存分配、文件系统、网络协议栈等。

内核通过 系统调用(System Calls) 向用户程序暴露功能接口,例如:

  • open():打开文件
  • read() / write():读写文件
  • fork() / exec():创建进程
  • mmap():内存映射

这些系统调用是用户程序与内核交互的唯一合法通道。但它们接口原始、使用复杂、性能开销大。

为什么需要中间层?——libc存在的意义

直接使用系统调用编写程序不仅繁琐,而且效率低下。libc 作为中间层,解决了以下几个关键问题:

  • 简化开发: 开发者无需直接调用 write(2) 来输出文本,只需使用 printf(),即可格式化输出,无需关心底层字节流处理。

  • 提高性能:缓冲机制: libc 在标准I/O(如 stdio)中引入了缓冲机制,将多次小的写操作合并为一次系统调用,显著减少上下文切换开销。

  • 增强可移植性: libc 抽象了不同操作系统和硬件平台的差异。同一份C代码,只要目标平台有兼容的libc实现,就可以编译运行。

  • 统一错误处理: libc 封装了系统调用的返回值和错误码(通过 errno),提供一致的错误报告机制。

程序执行的调用链

一个典型的C程序从用户代码到硬件的执行路径如下:

1
2
3
4
5
6
7
应用程序(如 ls, cat, 用户编写的C程序)

libc(glibc等,提供 printf, fopen, malloc 等)

Linux 内核(通过系统调用如 write, open, brk)

硬件设备(磁盘、网卡、内存等)

可以看到,libc 是用户程序通往内核的“必经之路”。

一,简单系统调用过程分析

前言

我写了一段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
  • 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. 加载动态链接库 (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) - 知乎

二,glibc扩展分析

案例1:使用 printf(通过libc缓冲)

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
for (int i = 0; i < 10; i++) {
printf("Hello");
}
return 0;
}

使用 strace 跟踪系统调用:

1
2
strace -e write ./a.out
write(1, "HelloHelloHelloHelloHelloHelloHe"..., 50) = 50

结果:只发生了一次 write 系统调用!

案例2:直接调用 write系统调用(绕过libc缓冲)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>

int main() {
for (int i = 0; i < 10; i++) {
write(1, "Hello", 5);
}
return 0;
}

strace -e write ./a.out
write(1, "Hello", 5) = 5
write(1, "Hello", 5) = 5
...
// 共10次 write 调用

结果:发生了10次系统调用!

为什么 printf 只触发一次系统调用?——libc的缓冲机制

关键在于 libc标准I/O缓冲策略printf 属于 stdio 库,其输出默认被缓冲,直到满足刷新条件才真正调用 write

标准I/O的三种缓冲模式:

模式 触发条件 示例
行缓冲(Line Buffering) 当输出为终端,比如直接运行程序将结果打印到终端,那么这个时候就是行缓冲。 行缓冲默认的大小为1024,但是当遇到回车换行符(\n)会强制刷新 printf(“Hello\n”)
全缓冲(Full Buffering) 输出到文件或管道时启用。缓冲区满(通常4KB)时刷新 fprintf(file, “…”)
无缓冲No Buffering) 立即输出,每次调用都触发系统调用。需要显式关闭缓冲 setbuf(stdout, NULL) stderr 默认无缓冲

注意:当输出目标是终端(TTY)时,stdout 默认为行缓冲。因此,如果 printf("Hello"); 没有换行符,数据会暂存于缓冲区,直到程序结束或缓冲区满才刷新。

实验:手动控制缓冲行为

1. 禁用缓冲(无缓冲模式)

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
setbuf(stdout, NULL); // 禁用缓冲
for (int i = 0; i < 10; i++) {
printf("Hello");
}
return 0;
}

strace -e write ./a.out
// 输出10次 write 调用

2. 手动刷新缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
for (int i = 0; i < 10; i++) {
printf("Hello");
if (i % 2 == 0) {
fflush(stdout); // 每两次输出后强制刷新
}
}
return 0;
}

strace -e write ./a.out
write(1, "Hello", 5) = 5
write(1, "HelloHello", 10) = 10
...

可以看出,fflush() 强制将缓冲区内容写入内核。

libc 与 内核的本质区别

特性 libc(用户空间库) Linux 内核
运行空间 用户空间 内核空间
权限 普通权限 特权模式(Ring 0)
功能 提供标准C函数、封装系统调用 管理硬件、调度资源、提供系统调用
性能开销 低(函数调用) 高(上下文切换)
可替换性 可更换(glibc vs musl) 不可替换(核心)
缓冲机制 有(stdio缓冲) 无(直接操作硬件)

总结:libc 是Linux系统的“隐形英雄”

  • libc 是C程序与操作系统之间的关键桥梁
  • 它通过封装系统调用、提供通用函数、实现缓冲机制,极大提升了程序的可移植性、易用性和性能
  • 理解 libc 的工作原理,尤其是其缓冲机制,有助于编写更高效、更可控的程序。
  • 在性能敏感或嵌入式场景中,选择合适的 libc 实现(如 musl)也能带来显著优势。

一句话总结:libc 不是内核,却比内核更贴近开发者;它不直接操控硬件,却是程序高效运行的幕后功臣。

三,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/dk/print
rm -rf glibc-build
mkdir glibc-build
cd glibc-build

../glibc-2.35/configure \
--prefix=/home/dk/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/dk/print/glibc-build/compile_commands.json \
/home/dk/print/glibc-2.35/compile_commands.json

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

四,查看glibc版本

1
ldd --version

运行结果

1
2
3
4
5
6
dk@DESKTOP:~/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
dk@DESKTOP:~/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
dk@DESKTOP:~/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
dk@DESKTOP:~$ 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
dk@DESKTOP:~/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
dk@DESKTOP:~/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
dk@DESKTOP:~/print/glibc-2.35$ cmp /home/dk/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系统调用,搞了半天也没搞明白,放弃!