musl.libc 里面:先调用 _start
函数进行几个内联汇编的指令,然后运行 _start_c
,在进行一些预备操作后,使用 exit(main(…))
(所以 main 函数才需要返回一个值)
popen
pclose
:pipe stream to or from a process。用在在一个程序中启动另一个程序,并读取返回的值
环境变量的传递:
int main(argc, char *argv[], char *envp[]);
environ
,然后尝试读取,如:#include <stdio.h>
extern char **exviron;
extern void ******************************end;
int main() {
for (char **env = environ; *env; env++) {
printf("%s\n", *env);
}
end = NULL;
}
虽然 mmap
可以向操作系统申请任意大的内存(即使超过物理内存上限也可以),但是操作系统不支持分配一小段内存,需要应用程序每次想操作系统多要一点内存,并自己在内存上实现一个数据结构
脱离 workload 做优化就是耍流氓
需要管理的对象:认为对象空间越大,生存周期也应该越长才合理;否则例如如果取到了一个很大的对象(分配了一个很大的空间),然后就初始化一遍就结束了,显然不太合理。结论是:malloc()
几乎只需要管理较小的对象就好了。
在 glibc 中,malloc()
对小块内存分配,使用 brk
扩展堆,而对大块分配使用 mmap
;同时维护了多个空闲块链表,被释放的内存可能不立即归还系统,而是放入空闲链表等待复用。如下是 linux 中每个进程的虚拟地址空间布局
+---------------------+
| text segment | 代码
+---------------------+
| data segment | 全局变量
+---------------------+
| heap | malloc/sbrk/brk 扩展的区域
| ↑ grows up |
+---------------------+
| mmap 区域 | mmap 分配的大块内存、库等
+---------------------+
| stack | 局部变量、函数调用栈
| ↓ grows down |
+---------------------+
当然会引发一系列可能出现的问题,如内存泄漏、悬挂指针、越界访问、二次释放等
什么是可执行文件:一个操作系统中的对象(文件)、一个字节序列、一个描述了状态机初始状态的数据结构
ELF:executable and linkable format(linkable 指可以链接外部函数、变量等)
可执行文件需要包含:动态链接库、基本信息(版本、体系结构……)、内存布局、其他……这当然不是唯一的选择,可以有其他的实现
+----------------------+
| ELF Header | <-- 文件起始处 (0x00)
+----------------------+
| Program Header Table | <-- 可选(用于可执行文件 / 共享库)
+----------------------+
| Section Header Table | <-- 可选(用于目标文件 / 静态链接)
+----------------------+
| Sections (段数据) |
| .text, .data, etc. |
+----------------------+
.text
段、.data
段等的内存映射关系.text
(代码)、.data
(数据)、.bss
(未初始化变量)、.symtab
(符号表)、.strtab
(字符串表)等.text
:汇编/机器码指令.data
:已初始化数据.rodata
:只读数据(如字符串字面量).bss
:未初始化数据(不实际存储,但会占地址空间).symtab
, .strtab
:符号表和字符串表显然 ELF 不是人类友好的
core dump 是指操作系统在某个进程发生严重错误(通常是崩溃)时,将该进程当时的内存内容、寄存器状态、调用栈等信息保存到一个文件中的行为(瞬间快照)。包括:
保存下来的 core 文件也是一个 ELF 文件,这样就可以用 gdb 等工具还原程序崩溃前的状态进行分析。Windows 的休眠恢复就用了这个原理。
在 ELF 之前,使用 a.out
(assembler output) 格式,但功能太少了(不支持动态链接、调试信息、内存对齐……)
重定位:在需要链接外部函数/变量时,将对应位置留空,之后在链接的时候补齐即可实现重定位的功能。链接的时候,先把同名 section (.text
.data
等) 拼接,然后计算需要的地址等
加载 ELF 文件(execve
)是内核实现的一部分
#!
:操作系统在运行可执行文件时,会先检查前两个字符是否是 #!
,如果是,则通过修改 execve
的参数的方式运行对应的程序,如对 a.py
:
#!/usr/bin/env python3
print("Hello")
当使用 chmod +x
指令后,运行 ./a.py
时,会自动将 execve
的参数修改为 execve("/usr/bin/env", ["/usr/bin/env", "python3", "a.py"], …)
拆解应用程序,实现运行库和应用代码分离:应用之间的库共享,大型项目分解
用 file
查看文件性质,ldd
查看依赖的库
通过 mmap
将 ELF 搬运到内存中,也就包括动态链接库(.so
.dll
)
动态链接的可执行文件执行时,首先加载的不是文件本身或者 libc,而是链接器如 ld-linux-x86-64.so.2
,在 program header 中的前几排可以看到这样一行:
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
virtual memory:操作系统维护 memory mappings 的数据结构,采用延迟加载(非必要不分配内存)、写时复制 copy-on-write(fork
时,父子进程先只读共享全部地址空间,当 page fault 的时候,写者复制一份来写,减少内存消耗)。如下是写时复制的说明图:
flowchart TD A["父进程 fork()"] --> B["父子进程共享内存页面
(页表指向相同物理页)
页面标记为只读"] B --> C[父进程或子进程尝试写内存] C --> D{该页面为只读?} D -- 是 --> E["触发页错误 (Page Fault)"] E --> F[内核分配一块新的物理页] F --> G[将旧页面内容复制到新页面中] G --> H[将写入进程的页表指向新页面] H --> I[设置新页面为可写] I --> J[写入成功,COW 完成] D -- 否 --> J style A fill:#f9f,stroke:#333,stroke-width:1px style C fill:#ff9,stroke:#333,stroke-width:1px style F fill:#bbf,stroke:#333,stroke-width:1px style G fill:#bbf,stroke:#333,stroke-width:1px style J fill:#9f9,stroke:#333,stroke-width:1px
graph TD subgraph Parent Process P1[页表项A ➝] --> PageA P2[页表项B ➝] --> PageB end subgraph Child Process C1[页表项A' ➝] --> PageA C2[页表项B' ➝] --> PageB end subgraph Physical Memory PageA[物理页 A
只读] PageB[物理页 B
只读] end style PageA fill:#ffe,stroke:#c00,stroke-width:2px style PageB fill:#ffe,stroke:#c00,stroke-width:2px style P1 fill:#bbf style P2 fill:#bbf style C1 fill:#bfb style C2 fill:#bfb
memory deduplication:操作系统在后台扫描内存,如果有两个重复的 read-only pages,就合并;发现 cold pages,就压缩/swap 到硬盘
动态链接:编译时,动态链接库调用 = 查表;链接时,收集所有符号,“生成”符号信息和相关代码。实现流程:
.h
)了解库中有哪些函数。.so
文件的符号表生成可执行文件,但不会复制库代码,只记录“需要链接哪些库”。ld.so
动态链接器会:.dynamic
和 .interp
段,识别需要加载哪些共享库。.so
文件)到内存。动态加载:程序运行过程中,有程序自己决定是否加载一个动态库并使用其中的函数,接口 dlopen
, dlsym
, dlclose
,实现流程:
dlopen("libxxx.so", RTLD_LAZY)
加载库到进程地址空间中。dlsym(handle, "function_name")
获取函数指针。dlclose(handle)
卸载库(可选)。对比:
特性 | 动态链接 | 动态加载 |
---|---|---|
加载时机 | 程序启动时 | 程序运行时 |
加载方式 | 系统自动加载 | 程序主动调用 dlopen 等 |
使用方式 | 直接调用函数 | 先获取函数指针再调用 |
使用场景 | 正常依赖、系统库等 | 插件系统、可选功能、热更新等 |
是否需要链接时指定 | 需要 (-lxxx ) | 不需要,在代码中动态指定 .so 名称 |
代码示例:
# hello.c
#include <stdio.h>
void hello() { printf("Hello, dynamic link!\n"); }
# main.c
void hello(); // 声明
int main() {
hello(); // 链接时由动态链接器找到真正的 hello 实现
return 0;
}
# 编译
gcc -fPIC -shared -o libhello.so hello.c
gcc -o main main.c -L. -lhello
export LD_LIBRARY_PATH=.
./main
// main.c
#include <stdio.h>
#include <dlfcn.h>
int main() {
void* handle = dlopen("./libhello.so", RTLD_LAZY);
if (!handle) { perror("dlopen"); return 1; }
void (*hello)() = dlsym(handle, "hello");
if (!hello) { perror("dlsym"); return 1; }
hello();
dlclose(handle);
return 0;
}
LD_PRELOAD
机制:在程序运行前预加载指定的共享库,并用库中的函数覆盖原本程序或其它库中的实现。工作原理:
ld.so
读取 ELF 文件头中的 .interp
段,找到动态链接器(如 /lib64/ld-linux-x86-64.so.2
)。LD_PRELOAD
。LD_PRELOAD
中指定的 .so
文件:可以用于如下场景:
应用场景 | 说明 |
---|---|
✅ 系统调用拦截 | hook open , read , write , connect 等函数,实现审计、沙箱、调试 |
✅ 内存调试工具 | 替换 malloc /free 实现,记录调用栈,检测内存泄漏(如 Valgrind) |
✅ 性能分析 | 记录每次函数调用时间,例如 malloc /free 的时间开销 |
✅ 热修复 bug | 修复某些已部署程序的逻辑 bug,无需重编译(灰度更新、补丁) |
✅ 库替代/兼容层 | 实现替代 libc、libGL、libpthread 等实现,用于模拟、兼容 |
举个例子:可以写一个 mymalloc.c
来修改 malloc
的使用,从而在不修改原有程序的基础上查看 malloc 情况
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
#include <string.h>
void* malloc(size_t size) {
static void* (*real_malloc)(size_t) = NULL;
if (!real_malloc) {
real_malloc = dlsym(RTLD_NEXT, "malloc");
}
void* p = real_malloc(size);
// 用 write 打印,避免 printf 引发递归
char buf[100];
int len = snprintf(buf, sizeof(buf), "malloc(%zu) = %p\n", size, p);
write(STDOUT_FILENO, buf, len);
return p;
}
然后编译
gcc -fPIC -shared -o libmymalloc.so mymalloc.c -ldl
运行 ls
命令:
LD_PRELOAD=./libmymalloc.so ls
需要注意的是,避免在预加载的动态库中调用被修改了的函数,不然会导致递归调用和栈溢出,出现 segment fault
等错误。比方说上面的例子中使用的是 write
而不能是 printf
,因为 printf
会调用 malloc
,最终出现类似于 [1] 82377 segmentation fault (core dumped) LD_PRELOAD=./libmymalloc.so ls
的错误
[BIOS/UEFI]
↓
[Bootloader (GRUB)] → 加载 vmlinuz + initramfs
↓
[Kernel]
↓
[initramfs] → 执行 /init → 挂载真实根
↓
[switch_root]
↓
[init (systemd)] → 启动服务、网络、图形界面等
↓
[用户登录 / 图形界面]
如下展示 linux 启动过程:
flowchart TD PowerOn[上电] Firmware[BIOS / UEFI] Bootloader["Bootloader (GRUB, systemd-boot)"] KernelAndInitramfs["加载内核 (vmlinuz) + initramfs"] JumpToKernel[跳转执行内核] PowerOn --> Firmware Firmware --> Bootloader Bootloader --> KernelAndInitramfs KernelAndInitramfs --> JumpToKernel
flowchart TD KernelStart[开始执行内核] InitDrivers[初始化驱动和子系统] MountInitramfs["挂载 initramfs 为根 /"] ExecuteInit["执行 initramfs 中的 /init 脚本"] KernelStart --> InitDrivers InitDrivers --> MountInitramfs MountInitramfs --> ExecuteInit
flowchart TD InitScript["/init 脚本"] LoadModules[加载必要内核模块] DetectRoot[探测并挂载真实根文件系统] SwitchRoot["switch_root/pivot_root 切换根"] ExecuteInit2["执行真实根中的 init(PID 1)"] InitScript --> LoadModules LoadModules --> DetectRoot DetectRoot --> SwitchRoot SwitchRoot --> ExecuteInit2
flowchart TD InitPID1["init (PID 1)"] MountVirtualFS["挂载 /proc, /sys, /dev"] StartUdev["启动 udev 管理设备"] StartServices[启动服务(网络、sshd、图形等)] ReachTarget["达到默认目标(multi-user.target, graphical.target)"] UserLogin[用户登录终端或图形界面] InitPID1 --> MountVirtualFS MountVirtualFS --> StartUdev StartUdev --> StartServices StartServices --> ReachTarget ReachTarget --> UserLogin