程序的内存空间
总结:典型的内存区域划分(从低地址到高地址)
以下是内存布局的经典划分(不同系统可能略有差异,但核心逻辑一致):
1. 代码区(Text Segment)存放 “执行指令”
- 位置:通常在内存低地址区域。
- 核心作用:存储程序编译后的二进制机器指令(CPU 可直接执行的操作码)
- 特性:
- 只读(防止程序意外修改指令);
- 可共享(多个进程运行同一程序时,可共享同一份代码区,节省内存);
- 大小在编译时就已确定(指令数量固定)。
2. 数据区(Data Segment)存放 “长期存在的变量”
数据区用于存储全局变量和静态变量(生命周期与程序一致,随程序启动而分配,随程序退出而释放),又细分为:
- 初始化数据区(Initialized Data):
- 存放 “已显式赋值” 的全局变量和静态变量(如
int global = 10;或static int s = 20;)。 - 作用:程序启动时直接加载这些变量的初始值,无需运行时再初始化,提高效率。
- 存放 “已显式赋值” 的全局变量和静态变量(如
- 未初始化数据区(BSS Segment):
- 存放 “未显式赋值” 的全局变量和静态变量(如
int global;或static int s;)。 - 作用:操作系统会在程序启动时自动将该区域的变量初始化为 0(或 NULL),避免 “垃圾值” 导致程序异常,同时节省编译后的文件大小(无需存储初始值)。
- 存放 “未显式赋值” 的全局变量和静态变量(如
3. 堆(Heap)存放 “动态创建的数据”
- 位置:位于数据区上方,从低地址向高地址动态增长。
- 核心作用:用于存储程序运行时 “动态分配” 的内存(大小不确定或需要长期存在的数据)。例如:
- 程序运行中根据用户输入创建的数组(如
int[] arr = new int[n];,n由用户输入决定); - 生命周期超过函数调用的对象(如 C++ 中
new创建的对象,需要手动delete才释放)。
- 程序运行中根据用户输入创建的数组(如
- 特性:
- 大小不固定,可随动态分配扩展(受限于系统总内存);
- 由程序员手动管理(C/C++)或垃圾回收机制(Java/Python)自动管理;
- 内存空间不连续,分配时需要查找空闲块,效率低于栈。
- 为什么需要:
- 栈的大小固定且较小,无法满足 “动态大小”“长期存在” 的数据需求;
- 堆的大小灵活(受限于系统总内存),支持按需分配,适配复杂场景(如动态数据结构、大型对象)。
4. 栈(Stack)存放 “临时数据”
- 位置:位于内存高地址区域,从高地址向低地址动态增长(与堆的增长方向相反)。
- 核心作用:为函数调用提供 “临时存储空间”,主要存储:
- 函数的局部变量(如
int a = 5;,仅在函数执行时存在); - 函数参数(传给函数的输入值,如
func(10, 20)中的10和20); - 返回地址(函数执行完后,CPU 需回到的原代码位置,确保程序流程连续)。
- 栈帧信息(维护函数调用的上下文)。
- 函数的局部变量(如
- 特性:
- 由编译器自动分配和释放(函数进入时压栈,退出时弹栈);
- 内存连续,分配效率极高(仅需移动栈指针);
- 大小固定(通常几 MB,超出会导致栈溢出)。
- 为什么需要:
- 函数调用是程序执行的基本单元,临时数据需快速创建和销毁,栈的 “自动管理”(编译器控制压栈 / 弹栈)和 “连续内存” 特性可实现极高效率(纳秒级操作);
- 避免临时数据占用堆的资源(堆分配效率低)。
5. 命令行参数与环境变量区
- 位置:通常在栈的顶部(更高地址处)。
- 核心作用:存储程序启动时的外部输入,包括:
- 命令行参数(如
python script.py arg1 arg2中的arg1arg2,供程序根据参数调整行为); - 环境变量(如系统路径
PATH、用户目录HOME等,让程序能获取系统配置)。
- 命令行参数(如
- 为什么需要:程序并非孤立运行,需与用户、系统交互,该区域是 “外部信息传入程序” 的通道。
核心区域的关系与特点
- 地址增长方向:堆从低地址向高地址 “向上长”,栈从高地址向低地址 “向下长”,两者之间有大量空闲内存,避免冲突。
- 生命周期:
- 代码区、数据区:随程序启动存在,直到程序退出。
- 堆:手动分配后存在,直到手动释放(或 GC 回收)。
- 栈:仅在函数调用期间存在,函数退出后自动释放。
- 管理方式:栈由编译器自动管理,堆由程序员(或 GC)手动管理,数据区和代码区由操作系统在程序加载时初始化。
程序的构造
🌟 一、先打个比方:程序就像一家餐厅
想象你要开一家餐厅:
- 你写的代码 → 就像你的菜谱(怎么做宫保鸡丁、鱼香肉丝)。
- 编译器(如
gcc)→ 就像你的厨师长,他把菜谱翻译成厨房能执行的步骤。 - 可执行程序(比如
a.out)→ 就是你做好的一桌菜,顾客可以直接吃。 - 动态库(
.so文件)→ 就像你厨房里公用的调料台,比如盐、酱油、味精。很多菜都会用到它,但你不需要每道菜都自带一瓶酱油。
🧱 二、程序是怎么“造”出来的?(编译过程)
一个 C/C++ 程序从代码到运行,要经过几个步骤:
1. 源代码(.c 或 .cpp 文件)
// hello.c
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}2. 预处理(Preprocessing)
- 处理
#include,#define,#ifdef等。 - 把
#include <stdio.h>替换成真正的头文件内容。 - 输出:一个“展开后”的大文件。
3. 编译(Compilation)
- 把 C 代码翻译成汇编语言,再变成机器码(CPU 能听懂的 01 指令)。
- 输出:
.o文件(目标文件,Object File),比如hello.o。
✅
.o文件还不能运行,因为它可能引用了别的函数(比如printf),但还不知道这些函数在哪。
4. 链接(Linking)
这是关键一步!
链接器(Linker)要做
- 把多个
.o文件“拼”在一起。 - 找到所有函数的地址,比如
printf到底在哪? - 生成最终的可执行文件(如
a.out)。
🔗 三、链接有两种方式:静态链接 Vs 动态链接
✅ 1. 静态链接(Static Linking)
- 把所有用到的代码(包括
printf这种库函数)全部复制一份,塞进你的可执行文件里。 - 生成的程序很大,但独立,不依赖外部文件。
- 像:每道菜都自带一瓶酱油。
🔧 命令示例:
gcc -static hello.c -o hello_static✅ 2. 动态链接(Dynamic Linking)——重点来了
- 不把库函数复制进你的程序。
- 只记录:“我需要
printf,它在系统的一个叫libc.so的文件里”。 - 程序运行时,操作系统帮它找到并加载这些库。
📦 动态库(Shared Library / Dynamic Library)
- 文件名通常是:
libxxx.so(Linux),xxx.dll(Windows),xxx.dylib(macOS)。 - 比如:
libc.so.6→ C 标准库(包含printf,malloc等)libpthread.so→ 多线程库libm.so→ 数学库(sin,cos)
🎯 动态库的本质: 它是一个独立的文件,里面装了很多函数,多个程序可以同时使用它,节省内存和磁盘空间。
🔧 默认情况下,gcc 就是动态链接:
gcc hello.c -o hello # 默认使用 libc.so,不打包进去⏱️ 四、运行时发生了什么?(运行时链接)
当你运行一个动态链接的程序时:
./hello操作系统会:
- 加载你的程序(
hello)到内存。 - 发现它依赖
libc.so.6(因为用了printf)。 - 去系统里找
libc.so.6(通常在/lib/x86_64-linux-gnu/或/usr/lib)。 - 把
libc.so.6也加载到内存(如果还没加载)。 - 把
printf的地址“绑定”到你的程序里 → 这叫 动态链接(Dynamic Linking)。 - 然后才真正开始运行
main()函数。
🔔 这个过程发生在程序启动时,所以叫 运行时链接(Run-time Linking)。
🎯 五、回到你的问题:objdump -t 和 -T
现在我们再看这个命令:
objdump -t libmylib.so # 显示 .symtab(调试符号)→ 被 strip 就没了
objdump -T libmylib.so # 显示 .dynsym(动态符号)→ 运行时必需,不会被删.symtab:像“厨房内部的详细备忘录”,记录了所有细节(包括私有调料配方),发布时可以删掉。.dynsym:像“公开的调料目录”,告诉别人“我提供了salt和soy_sauce函数”,必须保留,否则别人没法用。
所以:
-t没符号 → 说明这个库是“发布版”,去掉了调试信息。-T有符号 → 说明它确实导出了函数,别人能调用。
✅ 总结:一句话讲清楚
| 概念 | 通俗解释 |
|---|---|
动态库(.so) | 就是一个“公共函数仓库”,多个程序可以共用,节省空间。 |
| 动态链接 | 程序运行时,操作系统帮它“临时拼上”所需的库。 |
| 静态链接 | 把所有需要的代码“提前打包”进程序,程序变大但独立。 |
| 运行时链接 | 动态链接发生在程序启动那一刻,系统加载库并绑定函数地址。 |
🚀 举个真实例子
# 你写了一个程序,用了数学函数
gcc main.c -lm -o mymath-lm表示:链接数学库libm.so。mymath程序本身不包含sin()、cos()的代码。- 运行时,系统加载
libm.so,把sin()的地址告诉mymath。 - 多个程序(如游戏、科学计算)都可以同时用同一个
libm.so。
如果你还有不清楚的地方,比如:
- 怎么自己写一个
.so? dlopen是什么?- 为什么有时候提示 “xxx.so: cannot open shared object file”?
欢迎继续问我!😊
目标文件
太棒了!你已经理解了编译和链接,现在我们深入一步,来真正“看懂” .o 和 .so 文件的内部结构。
我们可以把它们想象成“机器世界的文档”,它们有严格的格式,操作系统和链接器靠这个格式来理解程序。
🧱 一、.o 文件是什么?—— ELF 格式的目标文件
当你编译一个 .c 文件时:
gcc -c hello.c -o hello.o生成的 hello.o 是一个 ELF 文件(Executable and Linkable Format),中文叫 可执行与可链接格式。
✅ ELF 是 Linux 下最核心的二进制文件格式,它用于:
- 目标文件(
.o)- 可执行文件(
a.out)- 共享库(
.so)
🔍 1. .o 文件的结构(从逻辑上看)
一个 .o 文件就像一本“技术手册”,包含多个“章节”(Section),每个章节有不同的用途:
| 节(Section) | 作用 | 例子 |
|---|---|---|
.text | 存放编译后的机器指令(你的函数代码) | main, my_function |
.data | 存放已初始化的全局/静态变量 | int global = 10; |
.bss | 存放未初始化的全局/静态变量(只记录大小,不占空间) | int buffer[1024]; |
.rodata | 存放只读数据(如字符串常量) | "Hello, World!\n" |
.symtab | 符号表:记录所有函数和变量的名字、地址、类型 | main 是函数,global 是变量 |
.strtab | 字符串表:存放符号名的字符串 | 避免重复存储 "main", "printf" |
.rel.text | 重定位表:告诉链接器“这里需要填一个地址” | 调用 printf 时,地址还不知道 |
.debug_* | 调试信息(行号、变量名等) | 供 GDB 调试使用 |
💡 想象
.o是一本“未装订的书”:每章都写好了,但章节之间的引用(比如“见第 5 章”)还没填具体页码。
🔧 2. 如何“看” .o 文件的内容?
✅ objdump:查看代码和符号
# 查看汇编代码(.text 段)
objdump -d hello.o
# 查看所有符号(.symtab)
objdump -t hello.o
# 查看段头信息
objdump -h hello.o✅ readelf:查看 ELF 结构
# 查看段表(Sections)
readelf -S hello.o
# 查看符号表(更详细)
readelf -s hello.o
# 查看重定位信息
readelf -r hello.o✅ nm:快速查看符号
nm hello.o输出示例:
0000000000000000 T main
0000000000000004 D global_var
U printf
T:在.text段的全局函数D:在.data段的全局变量U:未定义符号(需要链接时解决)
🎯 3. .o 的核心作用:等待“拼装”
.o 文件是不完整的,它依赖链接器做两件事:
-
符号解析(Symbol Resolution) 找到所有
U(未定义)符号的定义,比如printf在libc.so里。 -
重定位(Relocation) 把代码中的“占位符”替换成真实的内存地址。
🔗 链接器把多个
.o文件“缝合”起来,生成最终的可执行文件或.so。
📦 二、.so 文件是什么?—— ELF 格式的共享库
动态库 .so 也是一个 ELF 文件,但它是一种特殊的 ELF:共享目标文件(Shared Object)。
gcc -shared -fPIC -o libmylib.so mylib.o生成的 libmylib.so 也是 ELF 格式,但它和 .o 有重要区别:
🔍 1. .so 文件的结构(与 .o 的对比)
| 特性 | .o 文件 | .so 文件 |
|---|---|---|
| 用途 | 等待链接 | 可被加载运行 |
| 是否可执行 | 否 | 是(但不能直接运行) |
| 段(Sections) | .text, .data, .symtab 等 | 类似,但有 .dynsym, .dynamic |
| 符号表 | .symtab(完整) | .symtab 可能被 strip,但 .dynsym 必须存在 |
| 重定位 | 需要链接器重定位 | 支持地址无关代码(PIC),可在任意地址加载 |
| 依赖信息 | 无 | 有 .dynamic 段,记录依赖了哪些库(如 libc.so.6) |
🔧 2. .so 的关键特性
✅ (1) 地址无关代码(PIC, Position Independent Code)
.so中的代码被编译成 不依赖固定地址 的形式。- 这样它就可以被加载到内存的任意位置,多个程序可以共享同一份代码。
- 编译时用
-fPIC或-fPIE生成。
✅ (2) 动态符号表(.dynsym)
- 这是
.so的“对外接口清单”。 - 只包含需要导出或导入的符号。
- 即使
.symtab被strip掉,.dynsym也必须保留,否则动态链接器无法工作。
readelf -s libmylib.so # 查看 .dynsym(动态符号)✅ (3) 动态段(.dynamic)
- 记录库的元信息:
- 依赖哪些库(
NEEDED) - 符号表地址(
.dynsym) - 字符串表地址(
.dynstr) - 初始化函数(
INIT) - 入口点(
ENTRY)
- 依赖哪些库(
readelf -d libmylib.so # 查看 .dynamic 段输出示例:
NEEDED libc.so.6
INIT 0x0000000000001000
INIT_ARRAY 0x0000000000200dc8
SONAME libmylib.so
SYMBOLIC 0x0000000000000000
✅ (4) 导出与隐藏符号
- 默认情况下,
.so中的全局符号是导出的。 - 但你可以用
__attribute__((visibility("hidden")))隐藏它们。 - 最佳实践:只导出必要的接口,减少符号冲突。
🔍 3. 如何“看” .so 文件?
# 查看动态符号(谁可以调用我?)
objdump -T libmylib.so
# 查看依赖了哪些库
objdump -p libmylib.so | grep NEEDED
# 查看段信息
readelf -S libmylib.so
# 查看动态段
readelf -d libmylib.so
# 查看导出的函数
nm -D libmylib.so # -D 表示只看动态符号🧩 三、.o 和 .so 的关系(图解)
hello.c → 编译 → hello.o → 链接 → a.out(可执行)
↑
mylib.c → 编译 → mylib.o → 链接 → libmylib.so(共享库)
.o是“原材料”.so是“打包好的模块”a.out是“最终产品”
🔗 链接器可以把
.o链接到.so,也可以把.so链接到a.out。
✅ 总结:一句话理解
| 文件 | 本质 | 关键特点 |
|---|---|---|
.o | 未链接的 ELF 文件 | 包含代码、数据、符号、重定位信息,等待链接器“拼装” |
.so | 可被加载的 ELF 共享库 | 包含 PIC 代码、.dynsym、.dynamic,支持运行时动态链接 |
🛠️ 下一步建议
如果你想更深入:
- 写一个简单的
.c文件,编译成.o,用objdump -d看汇编。 - 写一个
.so,用objdump -T看它导出了什么。 - 用
ldd your_program看它依赖了哪些.so。 - 用
gdb加载程序,info sharedlibrary看运行时加载了哪些库。
如果你愿意,我可以带你一步步做一个 .so 并分析它!😊
动态库实例
太棒了!我们来一步一步完成这个完整的实践:从写代码、编译动态库、链接程序,到用 objdump、ldd、gdb 深入分析底层细节。
🧱 第一步:准备代码
1. 创建 mylib.c(动态库源码)
// mylib.c
// 一个简单的乘法函数
int mul(int a, int b) {
return a * b;
}2. 创建 main.c(主程序)
// main.c
#include <stdio.h>
// 声明外部函数(告诉编译器:mul 在别处定义)
extern int mul(int, int);
int main() {
int result = mul(6, 7);
printf("6 * 7 = %d\n", result);
return 0;
}🔧 第二步:编译生成动态库 .so
1. 先编译成位置无关的目标文件(.o)
gcc -c -fPIC mylib.c -o mylib.o-c:只编译,不链接。-fPIC:生成位置无关代码(Position Independent Code),这是.so必需的,让它能在内存任意地址加载。
2. 链接生成动态库 .so
gcc -shared -o libmylib.so mylib.o-shared:告诉链接器生成一个共享库(.so)。- 输出文件:
libmylib.so
✅ 现在你有一个动态库
libmylib.so,它导出了mul函数。
🔗 第三步:编译主程序并链接动态库
gcc main.c -L. -lmylib -o mainmain.c:你的主程序。-L.:告诉链接器在当前目录查找库。-lmylib:链接名为libmylib.so的库(-l会自动补全lib和.so)。-o main:输出可执行文件main。
🔍 第四步:验证程序运行
./main输出:
6 * 7 = 42
✅ 成功!程序调用了动态库中的 mul 函数。
🔎 第五步:用工具深入分析
1. 用 objdump 分析 mylib.o(目标文件)
查看汇编代码(.text 段)
objdump -d mylib.o输出片段:
Disassembly of section .text:
0000000000000000 <mul>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d fc mov %edi,-0x4(%rbp) # a
7: 89 75 f8 mov %esi,-0x8(%rbp) # b
a: 8b 55 fc mov -0x4(%rbp),%edx
d: 8b 45 f8 mov -0x8(%rbp),%eax
10: 0f af d0 imul %eax,%edx
13: 89 d0 mov %edx,%eax
15: 5d pop %rbp
16: c3 retq
📌 解读:
mul函数的机器码从地址0x0开始。- 它使用栈帧(
%rbp)保存参数。 imul是乘法指令。- 地址是
0x0,因为.o文件还没链接,地址是“相对的”。
查看符号表
objdump -t mylib.o输出:
SYMBOL TABLE:
0000000000000000 l df *ABS* 00000000 mylib.c
0000000000000000 l d .text 00000000 .text
0000000000000000 l d .data 00000000 .data
0000000000000000 l d .bss 00000000 .bss
0000000000000000 g F .text 0000000000000017 mul
g:全局符号。F:函数。.text:在代码段。mul的大小是0x17(23 字节)。
2. 用 objdump 分析 libmylib.so(动态库)
查看动态符号表(.dynsym)
objdump -T libmylib.so输出:
DYNAMIC SYMBOL TABLE:
0000000000001129 g DF .text 0000000000000017 Base mul
📌 解读:
g:全局符号。D:动态符号(在.dynsym中)。F:函数。Base:在基础加载地址。mul的地址是0x1129(这是相对.so加载基址的偏移)。
✅ 这说明
mul函数被成功导出,可以被其他程序调用。
查看 .so 的段信息
readelf -S libmylib.so | grep -E "(text|data|dynsym|dynamic)"你会看到:
.text:代码段.data:数据段.dynsym:动态符号表.dynamic:动态链接元信息
查看 .so 依赖了哪些库
objdump -p libmylib.so | grep NEEDED输出:
NEEDED libc.so.6
📌 说明这个 .so 依赖 C 标准库(即使 mul 没用到,但运行时环境需要)。
3. 用 ldd 查看可执行文件依赖了哪些 .so
ldd main输出(类似):
linux-vdso.so.1 (0x00007fff…)
libmylib.so => ./libmylib.so (0x00007f…)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f…)
/lib64/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f…)
📌 解读:
main依赖libmylib.so(在当前目录)。- 依赖
libc.so.6(printf所需)。 ld-linux.so是动态链接器本身。
4. 用 gdb 调试,查看 mul 是否被正确调用
gdb ./main在 gdb 中执行:
(gdb) break main
(gdb) run
(gdb) step # 一步步走,直到调用 mul
(gdb) step
(gdb) print result
$1 = 42或者直接跳到 mul:
(gdb) break mul
Function "mul" not defined.⚠️ 可能报错,因为 libmylib.so 还没加载。
继续运行,让它加载:
(gdb) continue
# 程序会停在 main 的 printf 前
(gdb) break mul
Breakpoint 2 at 0x…: file mylib.c, line 3.
(gdb) continue
# 程序会在进入 mul 时暂停现在你可以:
step单步执行mul的汇编。info registers看%edi,%esi(参数)和%eax(返回值)。disassemble看mul的汇编代码。
🧩 第六步:关键概念总结
| 工具 | 用途 |
|---|---|
objdump -d | 查看机器码(汇编) |
objdump -t | 查看 .o 的完整符号表 |
objdump -T | 查看 .so 的动态符号(谁可以调用我) |
objdump -p | 查看 .so 的元信息(依赖、入口点) |
readelf -S | 查看 ELF 段结构 |
ldd | 查看可执行文件依赖了哪些 .so |
gdb | 调试,验证函数是否被调用 |
🎯 最终成果
你已经完成了:
✅ 写了一个动态库 libmylib.so
✅ 导出了 mul 函数
✅ 主程序成功调用它
✅ 用 objdump 看到了 .so 中的 mul 符号
✅ 用 ldd 确认了依赖关系
✅ 用 gdb 验证了函数调用过程
🚀 下一步挑战(可选)
- 给
mul加__attribute__((visibility("hidden"))),再看objdump -T是否还显示它? - 用
dlopen和dlsym在运行时加载libmylib.so调用mul。 - 用
readelf -d main看主程序的.dynamic段,找NEEDED libmylib.so。
如果你愿意,我可以继续带你做这些!😊