程序的内存空间

总结:典型的内存区域划分(从低地址到高地址)

以下是内存布局的经典划分(不同系统可能略有差异,但核心逻辑一致):

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 中的 arg1 arg2,供程序根据参数调整行为);
    • 环境变量(如系统路径 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

操作系统会:

  1. 加载你的程序hello)到内存。
  2. 发现它依赖 libc.so.6(因为用了 printf)。
  3. 去系统里找 libc.so.6(通常在 /lib/x86_64-linux-gnu//usr/lib)。
  4. libc.so.6 也加载到内存(如果还没加载)。
  5. printf 的地址“绑定”到你的程序里 → 这叫 动态链接(Dynamic Linking)
  6. 然后才真正开始运行 main() 函数。

🔔 这个过程发生在程序启动时,所以叫 运行时链接(Run-time Linking)


🎯 五、回到你的问题:objdump -t-T

现在我们再看这个命令:

objdump -t libmylib.so   # 显示 .symtab(调试符号)→ 被 strip 就没了
objdump -T libmylib.so   # 显示 .dynsym(动态符号)→ 运行时必需,不会被删
  • .symtab:像“厨房内部的详细备忘录”,记录了所有细节(包括私有调料配方),发布时可以删掉
  • .dynsym:像“公开的调料目录”,告诉别人“我提供了 saltsoy_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 文件是不完整的,它依赖链接器做两件事:

  1. 符号解析(Symbol Resolution) 找到所有 U(未定义)符号的定义,比如 printflibc.so 里。

  2. 重定位(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 的“对外接口清单”。
  • 只包含需要导出或导入的符号
  • 即使 .symtabstrip 掉,.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,支持运行时动态链接

🛠️ 下一步建议

如果你想更深入:

  1. 写一个简单的 .c 文件,编译成 .o,用 objdump -d 看汇编
  2. 写一个 .so,用 objdump -T 看它导出了什么
  3. ldd your_program 看它依赖了哪些 .so
  4. gdb 加载程序,info sharedlibrary 看运行时加载了哪些库

如果你愿意,我可以带你一步步做一个 .so 并分析它!😊

动态库实例

太棒了!我们来一步一步完成这个完整的实践:从写代码、编译动态库、链接程序,到用 objdumplddgdb 深入分析底层细节。


🧱 第一步:准备代码

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 main
  • main.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.6printf 所需)。
  • 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(返回值)。
  • disassemblemul 的汇编代码。

🧩 第六步:关键概念总结

工具用途
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 验证了函数调用过程


🚀 下一步挑战(可选)

  1. mul__attribute__((visibility("hidden"))),再看 objdump -T 是否还显示它?
  2. dlopendlsym 在运行时加载 libmylib.so 调用 mul
  3. readelf -d main 看主程序的 .dynamic 段,找 NEEDED libmylib.so

如果你愿意,我可以继续带你做这些!😊