【Linux笔记】Linux库的制作与原理

一、库的认识

库(Library) 是一组预先编写好的代码、函数、类或模块的目标代码(.o)集合,提供其他程序在编译链接或运行时链接使用,而不需要从头编写这些功能。

一个源文件编译成可执行文件的过程:

源文件形成可执行文件的过程:

源码 (.c)  -->  编译  -->  目标文件 (.o)  -->  链接  -->  可执行文件

目标代码文件是: .o 文件里的东西——机器能看懂的指令,其中 .o 文件不能独立运行,因为里面可能有未解析的符号(比如调用了别的函数,地址还没填上)。

一个 库将多个相关的 .o 文件 进行打包:

一个库不是只有一个.o文件,而是把多个相关的目标文件打包在一起形成一个集合。

libmymath.a
├── mymath_add.o      (加法)
├── mymath_sub.o      (减法)
├── mymath_mul.o      (乘法)
└── mymath_div.o      (除法)

库的制作与使用流程图如下所示:

库的制作与使用流程图:

别人写好源码 --> 编译成 .o --> 打包成库(.a 或 .so)
                                    |
                                    v
					你的程序在 "编译时或运行时",去"借用"这些现成的机器码

在 Linux 环境下,库分为两种:

类型文件后缀链接方式
静态库.a(archive)编译时链接,代码拷贝进可执行文件
动态库.so(shared object)运行时链接,通过地址映射调用

在windows系统中,其中 静态库的文件后缀为 .lib ,动态库的文件后缀为 .so 。

二、静态库

2.1 什么是静态库

静态库的特征 : 在 Linux 下以 .a为文件后缀,在windows下以 .lib 为文件后缀。

静态库本质:在程序编译链接阶段,库中的目标代码会被完整拷贝到最终的可执行文件中,程序运行时不再需要该库文件。

静态库使用的本质就是将 .o 打包的文件原封不动的进行拷贝到需要链接的可执行文件中。

静态库命名格式:

静态库遵循固定的命名格式:

lib<库名>.a

- 开头为: lib 

- 中间为: 自定义的库名

- 末尾为: .a


例如:libcalc.a、libmyutil.a

2.2 制作静态库

以创建一个简单的计算器库,包含加法、减法、乘法、除法四个功能,演示制作静态库。

mymath/
|
├── include/          # .h 头文件目录
│   └── calc.h
|
├── lib/              # .c 源文件目录
│   ├── add.c
│   ├── sub.c
│   ├── mul.c
│   └── div.c
|
└── bin/              # 将 .o 输出到此目录.
  	├── add.o
    ├── div.o
    ├── mul.o
    └── sub.o

2.2.1 头文件包含

文件:include/calc.h:头文件声明了所有对外暴露的函数接口,使用者只需包含这个头文件即可。

#ifndef __CALC.H__
#define __CALC .H__

int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);

#endif

2.2.2 源文件编写

文件:lib/add.c

#include "calc.h"

int add(int a, int b) 
{
    return a + b;
}

文件:lib/sub.c

#include "calc.h"

int sub(int a, int b) 
{
    return a - b;
}

文件:lib/mul.c

#include "calc.h"

int mul(int a, int b) 
{
    return a * b;
}

文件:lib/div.c

#include "calc.h"
​
int div(int a, int b)
{
    if (b == 0)
    {
        perror("dev fail\n");
        return 0;
    }
    return a / b;
}

2.2.3 输出目标文件

将源文件编译为目标文件(.o)

gcc -c lib/add.c -I include -o bin/add.o
​
gcc -c lib/sub.c -I include -o bin/sub.o
​
gcc -c lib/mul.c -I include -o bin/mul.o
​
gcc -c lib/div.c -I include -o bin/div.o

-c:只编译生成目标文件(.o),不进行链接。

-I:指定头文件的搜索路径

-o bin/xxx.o:指定输出文件的路径和名称

2.2.4 目标文件打包

使用 ar 命令打包为静态库:

ar命令:

ar rcs libcalc.a bin/*.o

参数含义
rreplace,将目标文件插入归档文件中。如果归档中已存在同名文件,则替换它
ccreate,如果归档文件不存在,则创建它
s为归档文件创建索引,加速链接时的符号查找

2.3 使用静态库

目录结构如下所示:

.
├── main.c
└── mymath
    ├── bin
    │   ├── add.o
    │   ├── div.o
    │   ├── mul.o
    │   └── sub.o
    ├── include
    │   └── calc.h
    ├── lib
    │   ├── add.c
    │   ├── div.c
    │   ├── mul.c
    │   └── sub.c
    └── libcalc.a


编译指令:
    
gcc main.c -o main -L mymath -l calc -I mymath/include 

gcc 编译可执行文件的参数如下:

参数含义
-I指定 头文件搜索路径
-L指定 库文件的搜索路径
-l指定 要链接的库名,编译器会自动查找 libcalc.a 或 libcalc.so
-o指定 输出的可执行文件名

在C/C++中,如果要使用自己编写的库 或者 第三方外部库,都需要指明 -L 和 -l 参数。

2.4 静态库的优缺点

静态库的优点如下:

优点:

- 编译后的程序可以独立运行,无需额外依赖

- 部署简单,适合嵌入式或分发场景

- 链接时编译器可以进行更好的优化

静态库的缺点如下:

静态库的缺点:

- 可执行文件体积较大(所有用到的库代码都被拷贝进去)

- 库更新后,所有使用该库的程序都需要重新编译

- 多个程序使用同一个库时,会浪费内存和磁盘空间(每个程序都有一份拷贝)

三、动态库 (共享库)

3.1 什么是动态库

假设你的系统上有 100 个程序都调用了 printf,如果用静态库:内存中同时存在 100 份完全相同的代码,浪费 400KB。

程序A (含一份 printf 机器码)   占用 4KB
程序B (含一份 printf 机器码)   占用 4KB
程序C (含一份 printf 机器码)   占用 4KB
...
程序100 (含一份 printf 机器码) 占用 4KB

动态库的思路:内存中只放一份 printf 的代码,100 个进程共享它。

程序A ──┐
程序B ──┼──> 内存中唯一一份 libc.so (含 printf)
程序C ──┤
...  ──┘

动态库的本质:是一个编译好的、可被多个进程在运行时共同加载的代码文件。

- 文件后缀:Linux 下为 .so ,Windows 下为 .dll

- 命名规则:lib<名字>.so,例如 libc.so、libm.so、libpthread.so

- 运行逻辑:它不能独立运行,必须被某个可执行文件在运行时加载

3.2 制作动态库

以创建一个简单的demo_lib库,演示制作动态库。

代码结构如下所示:

.
└── demo_lib
	|
    ├── include
    │   └── mymath.h
    |
    └── src
    │    └── mymath.c
    ├── bin
    |
    └── main.c

3.2.1 头文件包含

mymath.h

#ifndef MYMATH_H
#define MYMATH_H

int my_add(int a, int b);
int my_sub(int a, int b);
int my_mul(int a, int b);

#endif

3.2.2 源文件实现

mymath.c

#include "mymath.h"

int my_add(int a, int b)
{
    return a + b;
}

int my_sub(int a, int b)
{
    return a - b;
}

int my_mul(int a, int b)
{
    return a * b;
}

3.2.3 测试代码实现

main.c

#include <stdio.h>
#include "mymath.h"

int main()
{
    printf("10 + 3 = %d\n", my_add(10, 3));
    printf("10 - 3 = %d\n", my_sub(10, 3));
    printf("10 * 3 = %d\n", my_mul(10, 3));
    return 0;
}

3.2.4 输出目标文件

gcc -fPIC -c src/mymath.c -o bin/mymath.o -I include

参数作用
-fPIC生成位置无关代码,动态库被加载到内存的地址不固定,代码必须用相对寻址,不能写死绝对地址
-c只编译,不链接,输出 .o 文件
-I指定头文件的搜索路径

注意:如果不加 -fPIC:这个 .o 只能用于静态库。若强行用它做动态库,在 x86_64 上会报错。

3.2.5 打包动态库

打包命令:

gcc -shared -o libmymath.so bin/mymath.o

参数作用
-shared输出共享库,而非可执行文件
-o libmymath.so指定输出文件名,必须以 lib 开头、.so 结尾

文件目录如下所示:

.
├── bin
│   └── mymath.o
|
├── include
│   └── mymath.h
|
├── libmymath.so
|
├── main.c
|
└── src
    └── mymath.c
    
    
hamber@VM-0-14-ubuntu:~/code/lesson23/demo_lib$ gcc main.c -o main -L. -lmymath -I include/

3.3 使用动态库

运行main出现错误:

运行可执行程序:./main


预期报错:

./main: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory


原因分析:

1. 编译时,链接器通过 -L. 找到了库,确认符号存在,在 main 中记录了"我需要 libmymath.so"。

2. 运行时,动态链接器 ld-linux-x86-64.so.2 按自己的规则搜索寻找动态库,但当前目录不在默认搜索路径中,故而找不到动态库。

用 ldd 查看动态库依赖:

输入指令:ldd main

运行结果如下:

linux-vdso.so.1 (0x00007ffd2f8b9000)
/$LIB/libonion.so => /lib/x86_64-linux-gnu/libonion.so (0x00007f0b321c3000)
libmymath.so => not found		<-- 找不到
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0b31fc8000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0b31fc2000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0b322d0000)

3.4 解决动态库运行时搜索路径

3.4.1 将自定义动态库拷贝到系统目录

执行cp指令:

sudo cp libmymath.so /lib64


ldd 验证:

linux-vdso.so.1 (0x00007ffd50754000)
/$LIB/libonion.so => /lib/x86_64-linux-gnu/libonion.so (0x00007ff479465000)
libmymath.so (0x00007ff479460000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff479265000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff47925f000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff479572000)

3.4.2 环境变量 LD_LIBRARY_PATH

添加临时环境变量:

export LD_LIBRARY_PATH=${$LD_LIBRARY_PATH}:/home/user/demo_lib


ldd 验证:

linux-vdso.so.1 (0x00007ffd50754000)
/$LIB/libonion.so => /lib/x86_64-linux-gnu/libonion.so (0x00007ff479465000)
libmymath.so (0x00007ff479460000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff479265000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff47925f000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff479572000)

3.4.3 编译时写入 rpath

编译时写入 rpath

gcc main.c -o main -L. -lmymath -I include/ -Wl,-rpath,'$ORIGIN' #通过环境变量指定路径 $ORIGIN

gcc main.c -o main -L. -lmymath -I. -Wl,-rpath,/home/user/demo_lib #指定绝对路径

ldd 验证:

linux-vdso.so.1 (0x00007ffd50754000)
/$LIB/libonion.so => /lib/x86_64-linux-gnu/libonion.so (0x00007ff479465000)
libmymath.so (0x00007ff479460000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff479265000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff47925f000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff479572000)

$ORIGIN 的含义: 可执行文件自身所在的目录。

demo_lib/
├── main              <-- $ORIGIN 就是 demo_lib/
└── libmymath.so      <-- 所以能找到同目录下的 .so

项目说明
生效范围仅该可执行文件
持久性永久(嵌入二进制中)
适用场景发布软件包、不想依赖系统配置
是否需要 root不需要

3.4.4 编写 .conf 配置文件

1. 在 /etc/ld.so.conf.d/ 目录下新建一个 .conf 文件

2. 在新建的.conf文件中填写,动态库的绝对路径

3. 执行命令 ldconfig

四、ELF文件

4.1 什么是ELF文件

ELF(Executable and Linkable Format)是一种用于二进制文件的标准格式,它是 Linux 等类 Unix 系统中最常见的目标文件格式,用于表示以下四类文件:

类型说明
可执行文件(Executable)可直接运行的程序,如 ls
可重定位文件(Relocatable)编译产生但未链接的目标文件,如 .o 文件
共享目标文件(Shared Object)动态链接库,如 .so 文件
核心转储文件(Core Dump)程序崩溃时的内存快照,由dump信号触发

4.2 ELF 文件的整体结构

+-------------------+
|   ELF Header      |   <-- 文件最开头,描述文件元信息(属性)
+-------------------+
| Program Header    |   <-- 描述"段"(Segment),运行时视图
|   Table           |
+-------------------+
|                   |
|   .text           |   <-- 代码段
|   .rodata         |   <-- 只读数据
|   .data           |   <-- 已初始化全局变量
|   .bss            |   <-- 未初始化全局变量
|   .symtab         |   <-- 符号表
|   .strtab         |   <-- 字符串表
|   ...             |
|                   |
+-------------------+
| Section Header    |   <-- 描述"节"(Section),链接时视图
|   Table           |
+-------------------+

核心要点:ELF 文件同时包含两种视图

视角表基本单位谁在用
链接视图Section Header Table节(.text、.data、.symtab、.rela.text…)链接器
执行视图Program Header Table段(segment)操作系统加载器

4.3 ELF Header ---ELF头

ELF Header:位于文件最开头,固定大小(64 位系统下为 64 字节),包含以下关键字段。

typedef struct 
{
    unsigned char e_ident[16];  // 魔数及标识信息
    uint16_t e_type;            // 文件类型(ET_EXEC, ET_DYN, ET_REL...)
    uint16_t e_machine;         // 目标架构(EM_X86_64, EM_ARM...)
    uint32_t e_version;         // ELF 版本
    uint64_t e_entry;           // 程序入口地址
    uint64_t e_phoff;           // ★ Program Header Table 的文件偏移
    uint64_t e_shoff;           // ★ Section Header Table 的文件偏移
    uint32_t e_flags;           // 处理器相关标志
    uint16_t e_ehsize;          // ELF Header 大小
    uint16_t e_phentsize;       // 每个 Program Header 条目大小
    uint16_t e_phnum;           // Program Header 条目数量
    uint16_t e_shentsize;       // 每个 Section Header 条目大小
    uint16_t e_shnum;           // Section Header 条目数量
    uint16_t e_shstrndx;        // 节名字符串表的索引
} Elf64_Ehdr;

4.4 Program Header Table ---程序头表

描述如何将文件映射到内存,是运行时的关键结构。

typedef struct 
{
    uint32_t p_type;    // 段类型(PT_LOAD, PT_DYNAMIC, PT_INTERP...)
    uint32_t p_flags;   // 权限标志(PF_R, PF_W, PF_X)
    uint64_t p_offset;  // 段在文件中的偏移
    uint64_t p_vaddr;   // 段在内存中的虚拟地址
    uint64_t p_paddr;   // 物理地址(通常忽略)
    uint64_t p_filesz;  // 段在文件中的大小
    uint64_t p_memsz;   // 段在内存中的大小
    uint64_t p_align;   // 对齐要求
} Elf64_Phdr;

常见的段类型:PT_LOAD、PT_INTERP 和 PT_DYNAMIC

类型含义
PT_LOAD映射进内存的段,携带地址、长度、权限、文件位置四要素。
PT_INTERP指定解释器路径,如 /lib64/ld-linux-x86-64.so.2(动态链接器)
PT_DYNAMIC动态链接信息(.dynamic 节)

4.5 Section Header Table ---节头表

描述文件中各个"节"的元信息,是链接时的关键结构:

typedef struct 
{
    uint32_t sh_name;       // 节名(在 .shstrtab 中的偏移)
    uint32_t sh_type;       // 节类型(SHT_PROGBITS, SHT_SYMTAB...)
    uint64_t sh_flags;      // 标志(SHF_WRITE, SHF_ALLOC, SHF_EXECINSTR)
    uint64_t sh_addr;       // 内存地址(若可加载)
    uint64_t sh_offset;     // 文件偏移
    uint64_t sh_size;       // 节大小
    uint32_t sh_link;       // 关联节索引
    uint32_t sh_info;       // 附加信息
    uint64_t sh_addralign;  // 对齐
    uint64_t sh_entsize;    // 条目大小(若为表结构)
} Elf64_Shdr;

常见的节(section):

  • .text:机器指令(可执行)

  • .rodata:只读数据(如字符串常量)

  • .data:已初始化的全局/静态变量

  • .bss:未初始化的全局/静态变量(不占文件空间)

  • .symtab:符号表

  • .strtab:字符串表

  • .dynamic:动态链接信息

  • .got / .plt:全局偏移表 / 过程链接表(用于动态链接)

4.6 符号表 .symtab

符号表(.symtab) 记录函数名、变量名等符号信息:

typedef struct 
{
    uint32_t st_name;   // 符号名(在 .strtab 中的偏移)
    unsigned char st_info;  // 绑定类型 + 符号类型
    unsigned char st_other; // 可见性
    uint16_t st_shndx;  // 所在节索引
    uint64_t st_value;  // 符号值(地址)
    uint64_t st_size;   // 符号大小
} Elf64_Sym;

五、分析ELF文件的命令

工具用途
readelf -a file全面显示 ELF 结构
readelf -h file仅显示 ELF Header
readelf -l file显示 Program Headers
readelf -S file显示 Section Headers
readelf -s file显示ELF文件的符号表(symbol table)
objdump -d file反汇编
objdump -t file显示符号表
nm file列出符号
ldd file查看动态库依赖

六、静态库链接

6.1 实例演示静态链接原理

6.1.1 hello.c 文件

#include <stdio.h>

void run();

int main()
{
    printf("hello Linux\n");
    run();
    return 0;
}

6.1.2 hello.o 的汇编代码

hello.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:	f3 0f 1e fa          	endbr64 
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	48 8d 3d 00 00 00 00 	lea    0x0(%rip),%rdi        # f <main+0xf>
   f:	e8 00 00 00 00       	callq  14 <main+0x14>        
  14:	b8 00 00 00 00       	mov    $0x0,%eax
  19:	e8 00 00 00 00       	callq  1e <main+0x1e>
  1e:	b8 00 00 00 00       	mov    $0x0,%eax
  23:	5d                   	pop    %rbp
  24:	c3                  	retq   

6.1.3 hello.o 的符号表

hamber@VM-0-14-ubuntu:~/code/lesson23/static_link$ readelf -s hello.o

Symbol table '.symtab' contains 14 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    10: 0000000000000000    37 FUNC    GLOBAL DEFAULT    1 main
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND run

6.1.4 code.c 文件

#include <stdio.h>

void run()
{
    printf("running ...\n");
}

6.1.5 code.o 的汇编代码

code.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <run>:
   0:	f3 0f 1e fa          	endbr64 
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	48 8d 3d 00 00 00 00 	lea    0x0(%rip),%rdi        # f <run+0xf>
   f:	e8 00 00 00 00       	callq  14 <run+0x14>
  14:	90                   	nop
  15:	5d                   	pop    %rbp
  16:	c3                   	retq   

6.1.6 code.o 的符号表

hamber@VM-0-14-ubuntu:~/code/lesson23/static_link$ readelf -s code.o

Symbol table '.symtab' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS code.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    10: 0000000000000000    23 FUNC    GLOBAL DEFAULT    1 run
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

6.1.7 main.exe 的符号表

hamber@VM-0-14-ubuntu:~/code/lesson23/static_link$ readelf -s main.exe 

Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     6: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)

Symbol table '.symtab' contains 67 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000318     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000000338     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000000358     0 SECTION LOCAL  DEFAULT    3 
     4: 000000000000037c     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000000003a0     0 SECTION LOCAL  DEFAULT    5 
     6: 00000000000003c8     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000000470     0 SECTION LOCAL  DEFAULT    7 
     8: 00000000000004f2     0 SECTION LOCAL  DEFAULT    8 
     9: 0000000000000500     0 SECTION LOCAL  DEFAULT    9 
    10: 0000000000000520     0 SECTION LOCAL  DEFAULT   10 
    11: 00000000000005e0     0 SECTION LOCAL  DEFAULT   11 
    12: 0000000000001000     0 SECTION LOCAL  DEFAULT   12 
    13: 0000000000001020     0 SECTION LOCAL  DEFAULT   13 
    14: 0000000000001040     0 SECTION LOCAL  DEFAULT   14 
    15: 0000000000001050     0 SECTION LOCAL  DEFAULT   15 
    16: 0000000000001060     0 SECTION LOCAL  DEFAULT   16 
    17: 0000000000001208     0 SECTION LOCAL  DEFAULT   17 
    18: 0000000000002000     0 SECTION LOCAL  DEFAULT   18 
    19: 000000000000201c     0 SECTION LOCAL  DEFAULT   19 
    20: 0000000000002068     0 SECTION LOCAL  DEFAULT   20 
    21: 0000000000003db8     0 SECTION LOCAL  DEFAULT   21 
    22: 0000000000003dc0     0 SECTION LOCAL  DEFAULT   22 
    23: 0000000000003dc8     0 SECTION LOCAL  DEFAULT   23 
    24: 0000000000003fb8     0 SECTION LOCAL  DEFAULT   24 
    25: 0000000000004000     0 SECTION LOCAL  DEFAULT   25 
    26: 0000000000004010     0 SECTION LOCAL  DEFAULT   26 
    27: 0000000000000000     0 SECTION LOCAL  DEFAULT   27 
    28: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    29: 0000000000001090     0 FUNC    LOCAL  DEFAULT   16 deregister_tm_clones
    30: 00000000000010c0     0 FUNC    LOCAL  DEFAULT   16 register_tm_clones
    31: 0000000000001100     0 FUNC    LOCAL  DEFAULT   16 __do_global_dtors_aux
    32: 0000000000004010     1 OBJECT  LOCAL  DEFAULT   26 completed.8061
    33: 0000000000003dc0     0 OBJECT  LOCAL  DEFAULT   22 __do_global_dtors_aux_fin
    34: 0000000000001140     0 FUNC    LOCAL  DEFAULT   16 frame_dummy
    35: 0000000000003db8     0 OBJECT  LOCAL  DEFAULT   21 __frame_dummy_init_array_
    36: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS code.c
    37: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
    38: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    39: 000000000000218c     0 OBJECT  LOCAL  DEFAULT   20 __FRAME_END__
    40: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
    41: 0000000000003dc0     0 NOTYPE  LOCAL  DEFAULT   21 __init_array_end
    42: 0000000000003dc8     0 OBJECT  LOCAL  DEFAULT   23 _DYNAMIC
    43: 0000000000003db8     0 NOTYPE  LOCAL  DEFAULT   21 __init_array_start
    44: 000000000000201c     0 NOTYPE  LOCAL  DEFAULT   19 __GNU_EH_FRAME_HDR
    45: 0000000000003fb8     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE_
    46: 0000000000001000     0 FUNC    LOCAL  DEFAULT   12 _init
    47: 0000000000001200     5 FUNC    GLOBAL DEFAULT   16 __libc_csu_fini
    48: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
    49: 0000000000004000     0 NOTYPE  WEAK   DEFAULT   25 data_start
    50: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@@GLIBC_2.2.5
    51: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    52: 0000000000001149    23 FUNC    GLOBAL DEFAULT   16 run
    53: 0000000000001208     0 FUNC    GLOBAL HIDDEN    17 _fini
    54: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    55: 0000000000004000     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    56: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    57: 0000000000004008     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
    58: 0000000000002000     4 OBJECT  GLOBAL DEFAULT   18 _IO_stdin_used
    59: 0000000000001190   101 FUNC    GLOBAL DEFAULT   16 __libc_csu_init
    60: 0000000000004018     0 NOTYPE  GLOBAL DEFAULT   26 _end
    61: 0000000000001060    47 FUNC    GLOBAL DEFAULT   16 _start
    62: 0000000000004010     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    63: 0000000000001160    37 FUNC    GLOBAL DEFAULT   16 main
    64: 0000000000004010     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
    65: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    66: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@@GLIBC_2.2

6.1.8 main.exe 的汇编代码

main.exe:     file format elf64-x86-64


Disassembly of section .init:

0000000000001000 <_init>:
    1000:	f3 0f 1e fa          	endbr64 
    1004:	48 83 ec 08          	sub    $0x8,%rsp
    1008:	48 8b 05 d9 2f 00 00 	mov    0x2fd9(%rip),%rax        # 3fe8 <__gmon_start__>
    100f:	48 85 c0             	test   %rax,%rax
    1012:	74 02                	je     1016 <_init+0x16>
    1014:	ff d0                	callq  *%rax
    1016:	48 83 c4 08          	add    $0x8,%rsp
    101a:	c3                   	retq   

Disassembly of section .plt:

0000000000001020 <.plt>:
    1020:	ff 35 9a 2f 00 00    	pushq  0x2f9a(%rip)        # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:	f2 ff 25 9b 2f 00 00 	bnd jmpq *0x2f9b(%rip)        # 3fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
    102d:	0f 1f 00             	nopl   (%rax)
    1030:	f3 0f 1e fa          	endbr64 
    1034:	68 00 00 00 00       	pushq  $0x0
    1039:	f2 e9 e1 ff ff ff    	bnd jmpq 1020 <.plt>
    103f:	90                   	nop

Disassembly of section .plt.got:

0000000000001040 <__cxa_finalize@plt>:
    1040:	f3 0f 1e fa          	endbr64 
    1044:	f2 ff 25 ad 2f 00 00 	bnd jmpq *0x2fad(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    104b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

Disassembly of section .plt.sec:

0000000000001050 <puts@plt>:
    1050:	f3 0f 1e fa          	endbr64 
    1054:	f2 ff 25 75 2f 00 00 	bnd jmpq *0x2f75(%rip)        # 3fd0 <puts@GLIBC_2.2.5>
    105b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

Disassembly of section .text:

0000000000001060 <_start>:
    1060:	f3 0f 1e fa          	endbr64 
    1064:	31 ed                	xor    %ebp,%ebp
    1066:	49 89 d1             	mov    %rdx,%r9
    1069:	5e                   	pop    %rsi
    106a:	48 89 e2             	mov    %rsp,%rdx
    106d:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
    1071:	50                   	push   %rax
    1072:	54                   	push   %rsp
    1073:	4c 8d 05 86 01 00 00 	lea    0x186(%rip),%r8        # 1200 <__libc_csu_fini>
    107a:	48 8d 0d 0f 01 00 00 	lea    0x10f(%rip),%rcx        # 1190 <__libc_csu_init>
    1081:	48 8d 3d d8 00 00 00 	lea    0xd8(%rip),%rdi        # 1160 <main>
    1088:	ff 15 52 2f 00 00    	callq  *0x2f52(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    108e:	f4                   	hlt    
    108f:	90                   	nop

0000000000001090 <deregister_tm_clones>:
    1090:	48 8d 3d 79 2f 00 00 	lea    0x2f79(%rip),%rdi        # 4010 <__TMC_END__>
    1097:	48 8d 05 72 2f 00 00 	lea    0x2f72(%rip),%rax        # 4010 <__TMC_END__>
    109e:	48 39 f8             	cmp    %rdi,%rax
    10a1:	74 15                	je     10b8 <deregister_tm_clones+0x28>
    10a3:	48 8b 05 2e 2f 00 00 	mov    0x2f2e(%rip),%rax        # 3fd8 <_ITM_deregisterTMCloneTable>
    10aa:	48 85 c0             	test   %rax,%rax
    10ad:	74 09                	je     10b8 <deregister_tm_clones+0x28>
    10af:	ff e0                	jmpq   *%rax
    10b1:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)
    10b8:	c3                   	retq   
    10b9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

00000000000010c0 <register_tm_clones>:
    10c0:	48 8d 3d 49 2f 00 00 	lea    0x2f49(%rip),%rdi        # 4010 <__TMC_END__>
    10c7:	48 8d 35 42 2f 00 00 	lea    0x2f42(%rip),%rsi        # 4010 <__TMC_END__>
    10ce:	48 29 fe             	sub    %rdi,%rsi
    10d1:	48 89 f0             	mov    %rsi,%rax
    10d4:	48 c1 ee 3f          	shr    $0x3f,%rsi
    10d8:	48 c1 f8 03          	sar    $0x3,%rax
    10dc:	48 01 c6             	add    %rax,%rsi
    10df:	48 d1 fe             	sar    %rsi
    10e2:	74 14                	je     10f8 <register_tm_clones+0x38>
    10e4:	48 8b 05 05 2f 00 00 	mov    0x2f05(%rip),%rax        # 3ff0 <_ITM_registerTMCloneTable>
    10eb:	48 85 c0             	test   %rax,%rax
    10ee:	74 08                	je     10f8 <register_tm_clones+0x38>
    10f0:	ff e0                	jmpq   *%rax
    10f2:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)
    10f8:	c3                   	retq   
    10f9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

0000000000001100 <__do_global_dtors_aux>:
    1100:	f3 0f 1e fa          	endbr64 
    1104:	80 3d 05 2f 00 00 00 	cmpb   $0x0,0x2f05(%rip)        # 4010 <__TMC_END__>
    110b:	75 2b                	jne    1138 <__do_global_dtors_aux+0x38>
    110d:	55                   	push   %rbp
    110e:	48 83 3d e2 2e 00 00 	cmpq   $0x0,0x2ee2(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    1115:	00 
    1116:	48 89 e5             	mov    %rsp,%rbp
    1119:	74 0c                	je     1127 <__do_global_dtors_aux+0x27>
    111b:	48 8b 3d e6 2e 00 00 	mov    0x2ee6(%rip),%rdi        # 4008 <__dso_handle>
    1122:	e8 19 ff ff ff       	callq  1040 <__cxa_finalize@plt>
    1127:	e8 64 ff ff ff       	callq  1090 <deregister_tm_clones>
    112c:	c6 05 dd 2e 00 00 01 	movb   $0x1,0x2edd(%rip)        # 4010 <__TMC_END__>
    1133:	5d                   	pop    %rbp
    1134:	c3                   	retq   
    1135:	0f 1f 00             	nopl   (%rax)
    1138:	c3                   	retq   
    1139:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

0000000000001140 <frame_dummy>:
    1140:	f3 0f 1e fa          	endbr64 
    1144:	e9 77 ff ff ff       	jmpq   10c0 <register_tm_clones>

0000000000001149 <run>:
    1149:	f3 0f 1e fa          	endbr64 
    114d:	55                   	push   %rbp
    114e:	48 89 e5             	mov    %rsp,%rbp
    1151:	48 8d 3d ac 0e 00 00 	lea    0xeac(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    1158:	e8 f3 fe ff ff       	callq  1050 <puts@plt>
    115d:	90                   	nop
    115e:	5d                   	pop    %rbp
    115f:	c3                   	retq   

0000000000001160 <main>:
    1160:	f3 0f 1e fa          	endbr64 
    1164:	55                   	push   %rbp
    1165:	48 89 e5             	mov    %rsp,%rbp
    1168:	48 8d 3d a1 0e 00 00 	lea    0xea1(%rip),%rdi        # 2010 <_IO_stdin_used+0x10>
    116f:	e8 dc fe ff ff       	callq  1050 <puts@plt>
    1174:	b8 00 00 00 00       	mov    $0x0,%eax
    1179:	e8 cb ff ff ff       	callq  1149 <run>
    117e:	b8 00 00 00 00       	mov    $0x0,%eax
    1183:	5d                   	pop    %rbp
    1184:	c3                   	retq   
    1185:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    118c:	00 00 00 
    118f:	90                   	nop

0000000000001190 <__libc_csu_init>:
    1190:	f3 0f 1e fa          	endbr64 
    1194:	41 57                	push   %r15
    1196:	4c 8d 3d 1b 2c 00 00 	lea    0x2c1b(%rip),%r15        # 3db8 <__frame_dummy_init_array_entry>
    119d:	41 56                	push   %r14
    119f:	49 89 d6             	mov    %rdx,%r14
    11a2:	41 55                	push   %r13
    11a4:	49 89 f5             	mov    %rsi,%r13
    11a7:	41 54                	push   %r12
    11a9:	41 89 fc             	mov    %edi,%r12d
    11ac:	55                   	push   %rbp
    11ad:	48 8d 2d 0c 2c 00 00 	lea    0x2c0c(%rip),%rbp        # 3dc0 <__do_global_dtors_aux_fini_array_entry>
    11b4:	53                   	push   %rbx
    11b5:	4c 29 fd             	sub    %r15,%rbp
    11b8:	48 83 ec 08          	sub    $0x8,%rsp
    11bc:	e8 3f fe ff ff       	callq  1000 <_init>
    11c1:	48 c1 fd 03          	sar    $0x3,%rbp
    11c5:	74 1f                	je     11e6 <__libc_csu_init+0x56>
    11c7:	31 db                	xor    %ebx,%ebx
    11c9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)
    11d0:	4c 89 f2             	mov    %r14,%rdx
    11d3:	4c 89 ee             	mov    %r13,%rsi
    11d6:	44 89 e7             	mov    %r12d,%edi
    11d9:	41 ff 14 df          	callq  *(%r15,%rbx,8)
    11dd:	48 83 c3 01          	add    $0x1,%rbx
    11e1:	48 39 dd             	cmp    %rbx,%rbp
    11e4:	75 ea                	jne    11d0 <__libc_csu_init+0x40>
    11e6:	48 83 c4 08          	add    $0x8,%rsp
    11ea:	5b                   	pop    %rbx
    11eb:	5d                   	pop    %rbp
    11ec:	41 5c                	pop    %r12
    11ee:	41 5d                	pop    %r13
    11f0:	41 5e                	pop    %r14
    11f2:	41 5f                	pop    %r15
    11f4:	c3                   	retq   
    11f5:	66 66 2e 0f 1f 84 00 	data16 nopw %cs:0x0(%rax,%rax,1)
    11fc:	00 00 00 00 

0000000000001200 <__libc_csu_fini>:
    1200:	f3 0f 1e fa          	endbr64 
    1204:	c3                   	retq   

Disassembly of section .fini:

0000000000001208 <_fini>:
    1208:	f3 0f 1e fa          	endbr64 
    120c:	48 83 ec 08          	sub    $0x8,%rsp
    1210:	48 83 c4 08          	add    $0x8,%rsp
    1214:	c3                   	retq   

6.2 静态链接原理

6.2.1 链接前的状态

A. 编译器填写占位符

编译器将 hello.c 翻译为 hello.o 时,面临一个根本问题:它不知道其他目标文件中的函数和变量最终会被放在内存的哪个位置。

由此可知:

  1. 编译器只能在指令中留下"占位符",等待链接器来填补。

  2. 编译器进行对每个文件的编译是孤立得。

链接器核心问题:把所有 UND(未定义)符号与某个目标文件中已定义的符号匹配起来。

0000000000000000 <main>:
   0:  f3 0f 1e fa          endbr64
   4:  55                   push   %rbp
   5:  48 89 e5             mov    %rsp,%rbp
   8:  48 8d 3d 00 00 00 00 lea    0x0(%rip),%rdi      # 空洞1:字符串地址
   f:  e8 00 00 00 00       callq  14                  # 空洞2:调用 puts
  14:  b8 00 00 00 00       mov    $0x0,%eax
  19:  e8 00 00 00 00       callq  1e                  # 空洞3:调用 run()
  1e:  b8 00 00 00 00       mov    $0x0,%eax
  23:  5d                   pop    %rbp
  24:  c3                   retq

三个"空洞"分别是:

位置指令含义为什么是 0
偏移 0x08lea 0x0(%rip),%rdi加载字符串 "hello Linux\n" 的地址字符串在 .rodata 节中,链接前不知道最终地址
偏移 0x0Fcallq 14调用 printf(被优化为 puts)puts 在 libc 中,链接前不知道地址
偏移 0x19callq 1e调用 run()run 定义在 code.o 中,链接前不知道地址

B. 符号表记录依赖

hello.o 的符号表清晰地表达了这种依赖关系:

Num:    Value          Size Type    Bind   Vis      Ndx Name
 10: 0000000000000000    37 FUNC    GLOBAL DEFAULT    1 main      ← 我定义了 main
 12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts      ← 我需要 puts(未定义)
 13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND run       ← 我需要 run(未定义)

  • Ndx = 1:main 定义在本文件的第 1 个节(.text)中。

  • Ndx = UND:puts 和 run 在本文件中未定义,必须由链接器从其他目标文件或库中找到。

6.2.2 链接器的工作流程

执行链接命令 gcc -o main.exe hello.o code.o,链接器依次完成以下步骤:

hello.o  ──┐
           ├──→ [1. 符号解析] ──→ [2. 节合并] ──→ [3. 地址分配] ──→ [4. 重定位] ──→ main.exe
code.o   ──┘

A.步骤 1:符号解析

链接器扫描所有输入文件的符号表,建立一张全局符号映射表:

符号名          定义位置           状态
─────────────────────────────────────────
main           hello.o (.text)    已定义
run            code.o  (.text)    已定义
puts           libc.so / libc.a   动态链接时解析(本例中为动态)
_GLOBAL_OFFSET_TABLE_             链接器自行创建
__libc_start_main  libc           动态链接时解析

关键规则:

  • 强符号(函数、已初始化全局变量)在多个目标文件中只能定义一次,否则报 multiple definition 错误。

  • 弱符号(未初始化全局变量)允许多处定义,链接器选一个。

  • 如果某个 UND 符号在所有输入中都找不到定义,链接器报 undefined reference 错误。

在本例中:

hello.o 需要 run → code.o 提供了 run,匹配成功。

hello.o 和 code.o 都需要 puts → 本例中 puts 并未被静态链接进 main.exe,而是标记为动态符号(UND),运行时由动态链接器解析。

B. 步骤 2:节合并

链接器把所有输入文件的同名节拼接在一起:

合并前:                          合并后(main.exe):
┌─────────────────┐              ┌─────────────────────────────┐
│ hello.o: .text  │──┐           │  .text                      │
│  				  │  │           │  ┌───────────────────────┐  │
└─────────────────┘  │           │  │ _start    (crt1.o)    │  │
┌─────────────────┐  ├────────→  │  │ ......    	 		│  │
│ code.o: .text   │  │           │  │ run       (code.o)    │  │ ← 0x1149
│ 				  │──┘           │  │ main      (hello.o)   │  │ ← 0x1160
└─────────────────┘              │  │ __libc_csu_init       │  │
                                 │  │ ...                   │  │
                                 │  └───────────────────────┘  │
┌─────────────────┐              ┌─────────────────────────────┐
│ hello.o: .rodata│──┐           │  .rodata                    │
│ "hello Linux\n" │  ├────────→  │  "hello Linux\n"            │
└─────────────────┘  │           │  "running ...\n"            │
┌─────────────────┐  │           │  ...                        │
│ code.o: .rodata │──┘           └─────────────────────────────┘
│ "running ...\n" │
└─────────────────┘

C. 步骤 3:地址分配

将相应的 .o 文件链接合并后,链接器为每个节分配最终的虚拟地址,在 main.exe 符号表可以读出:

节号   地址        内容
──────────────────────────────
 12   0x1000      .init
 13   0x1020      .plt
 14   0x1040      .plt.got / .plt.sec
 15   0x1050      (可能是 .plt.sec 的延续)
 16   0x1060      .text        ← 代码段起始
 17   0x1208      .fini
 18   0x2000      .rodata      ← 只读数据
 19   0x201c      .eh_frame_hdr
 20   0x2068      .eh_frame
 21   0x3db8      .init_array
 22   0x3dc0      .fini_array
 23   0x3dc8      .dynamic
 24   0x3fb8      .got
 25   0x4000      .data
 26   0x4010      .bss

由 mian.exe 读出关键符号的地址

_start   = 0x1060    (程序入口)
run      = 0x1149    (code.o 的 run 函数)
main     = 0x1160    (hello.o 的 main 函数)
puts     = UND       (动态链接,运行时才确定)

D. 步骤 4:重定位

这是链接器最本质的工作:修改指令中的占位符,填入正确的地址或偏移。

编译器在生成 .o 文件时,不仅留下了空洞,还在 重定位表(.rela.text) 中记录了"哪个位置需要修补、修补什么符号、用什么方式修补"。

具体例子:在 hello.o 中 callq run

编译后(hello.o):
    callq  00 00 00 00      ← 要调用 run(),但不知道地址,先填 0
    重定位表记录:位置=这里,符号=run,方式=相对偏移

链接时:
    链接器查重定位表 → run 最终在 0x1149
    算出相对偏移 = -0x35
    把 00 00 00 00 替换为 cb ff ff ff

链接后(main.exe):
    callq  cb ff ff ff      ← 现在能正确跳到 run() 了

七、 ELF文件加载与可执行程序加载运行

可程序程序加载到内存并运行的流程图:

image-20260805140228571

task_struct 结构体的流程图:

image-20260805142528348

7.1 程序加载过程中各个角色

1. 可执行程序(磁盘):ELF 文件,代码段内容按页对齐存放。

2. 物理内存:代码页最终落脚的地方。图中把物理地址也画成 0x1060,这是教学简化;实际上物理地址与虚拟地址不同,靠页表翻译。

3. task_struct:进程描述符,其中 mm 指针指向该进程的内存描述结构。

4. mm_struct:描述整个地址空间,持有页表基址(mm->pgd)和 VMA 链表头(mm->mmap)。

5. 页表:虚拟地址 → 物理地址的翻译表。

6. CPU 的 CR3:存放当前进程页表的物理基址。

7.2 从全局理解可执行程序到 CPU 执行

当 shell 执行 ./main.exe 时,实际发生的是 fork() + execve() 系统调用。

内核中处理 ELF 的函数是 load_elf_binary(),它的核心工作可以概括为:

execve("./main.exe")
        │
        ▼
[1] 读取并校验 ELF Header(魔数、e_type、e_machine)
        │
        ▼
[2] task_struct 创建新的 mm_struct + 页表
        │
        ▼
[3] 遍历 Program Header Table,对每个 PT_LOAD 段
    创建 vm_area_struct(VMA),建立"虚拟地址 → 文件偏移"的映射
        │
        ▼
[4] 处理 BSS(p_memsz > p_filesz 的部分清零)
        │
        ▼
[5] 建立用户栈,压入 argc / argv / envp / 辅助向量
        │
        ▼
[6] 设置 EIP = e_entry(0x1060),CR3 = 新页表基址
        │
        ▼
[7] 返回用户态,CPU 从 _start 开始执行

7.3 入口点与"加载"

ELF Header 中的 e_entry = 0x1060,正是可执行文件中符号表中 _start 的地址。

内核在最后一步把它写入 RIP(EIP 是 32 位时代的名字,64 位下即 RIP)。

7.3.1 程序入口位置

回顾之前的main.exe中的符号表:

_start            = 0x1060    ← e_entry,进程第一条指令
main              = 0x1160
__libc_csu_init   = 0x1190
__libc_csu_fini   = 0x1200
_GLOBAL_OFFSET_TABLE_ = 0x3fb8

7.3.2 PT_LOAD 映射到 进程地址空间

进程地址空间与 VMA:

task_struct ──mm──→ mm_struct ──mmap──→ vm_area_struct 链表
                     │  (mm->pgd → 页表)       │
                     │                        ├─ vm_start / vm_end:区域范围
                     │                        ├─ vm_flags:权限位
                     │                        ├─ vm_mm:指回 mm_struct
                     │                        ├─ vm_next / vm_prev:链表
                     │                        └─ vm_file / vm_pgoff:文件映射来源
                     └←── 每个 VMA 的 vm_mm 指回mm_struct
                     
                     
task_struct:进程描述符(含 pid、files、mm 等)。

mm_struct:整个用户地址空间的"总账本",mmap 字段指向 VMA 链表头。

vm_area_struct(VMA):描述一段连续的、属性相同的虚拟内存区域(栈、堆、代码段等)

PT_LOAD 是 ELF 程序头表 中的一种段类型,其作用是:"将这个段中的内容在进程启动加载(映射)到进程的虚拟内存中,并进行填写VMA的相关字段"。

PT_LOAD 字段                     	 内核结构

[p_vaddr, p_vaddr+p_memsz]  →   VMA 的 [vm_start, vm_end](页对齐后)

p_flags                     →   vm_flags(VM_READ/VM_WRITE/VM_EXEC)→ 将来 PTE 的权限位

p_offset                    →   vm_pgoff(缺页时知道去文件哪个位置读页)

p_memsz - p_filesz          →   匿名零页映射(BSS 的 0 从这来)

注意两点:

1. 此时还没有把文件内容真正读进内存, 只是建立了 VMA(虚拟内存区域)和页表映射关系,真正读磁盘是在第一次访问某页触发缺页异常时才发生的(按需加载)。

2. 这一步结束后,main.exe 的代码和数据在虚拟地址空间里"各就各位"了,但还不能执行——因为如果有动态库依赖,那些符号的地址还没解析。

7.3.2 CPU执行第一条指令

CPU 取指 EIP = 0x1060
   → MMU 用 CR3 找到页表,逐级翻译页表
   → 发现页面还没读入
   → 触发缺页异常 #PF
   → 内核 do_page_fault → 找到 0x1060 所属的 VMA(代码段,vm_file = main.exe)
   → 从磁盘读取对应页 → 分配物理页 → 填写页面
   → 返回用户态,重新执行 0x1060 处的指令

7.4 总结完整流程

可执行程序编译与运行的完整流程:

1. 编译   hello.c/code.c → .o        留下占位符 + 重定位表

2. 链接   .o → main.exe              链接器填空:分配虚拟地址(main=0x1160 等)

3. 加载   main.exe → 进程             内核按 PT_LOAD 建 VMA,EIP = e_entry = 0x1060

4. 运行   _start → __libc_start_main → main    缺页异常按需把代码/数据页从磁盘搬进物理内存

ELF文件中的section 和 segment形成并加载的完整流程:

[编译时]  gcc -c hello.c -o hello.o
          产物:.o 文件
          状态:只有节(.text/.data/...),地址未定,没有段,没有程序头表

[链接时]  ld
          工作: 1. 给所有节分配最终虚拟地址
                2. 按"权限分组 + 页对齐"把节打包成段
                3. 把打包结果写进程序头表(PT_LOAD 等表目)
          产物:main.exe ← 段在这一刻诞生,并固化在文件里

[加载时]  execve → load_elf_binary()
          工作:读程序头表,为每个 PT_LOAD 建 VMA

[运行时]  CPU 首次访问某页
          工作:缺页异常按 VMA 填页表

八、动态库加载与链接

8.1 动态链接的流程

[编译期]  gcc -c hello.c                → hello.o        (引用 printf,地址未定)

[链接期]  ld hello.o -lc                → hello          (只记依赖,不复制 libc)
                                             ↓
                                        记录 DT_NEEDED=libc.so.6
                                        为 printf 建 PLT/GOT 桩
                                        PT_INTERP=/lib64/ld-linux-x86-64.so.2

[加载期]  execve → 内核发现 PT_INTERP → 先映射 ld.so,把控制权交给它

[运行期]  ld.so:
            1. 读 hello 的 .dynamic,发现需要 libc.so.6
            2. mmap libc 的所有 PT_LOAD 段到进程地址空间
            3. 解析符号:在 libc 的 .dynsym 里找到 printf 的地址
            4. 把地址填进 main 的 GOT
            5. 跳转到 main 的 _star → __libc_start_main → main

[首次调用] call printf → PLT → GOT → 若尚未解析则走 resolver 现场解析

知识点补充:

  • "ld.so 是文件" 说的是它的物理存在形式;"ld.so 是动态链接器"说的是它被加载执行后扮演的角色。

  • ld.so 和普通 .so 有一个本质区别:它有真正的入口点,可以被直接"执行",普通的.so如 libc.so.6是被动的:等着ld.so来加载它、解析它的符号。

ld.so 文件(磁盘上)
    │
    │  内核通过 PT_INTERP 发现它
    │  内核 mmap 它的 PT_LOAD 段
    │  内核把 %rip 跳到它的入口点
    │
    ▼
ld.so 代码(内存中,正在执行)= 动态链接器
    │
    │  ld.so 开始工作...
    │
    ▼
跳转到 main 的 _start → 你的程序开始运行

8.2 动态库链接的细节分析 --printf 的运转

8.2.1 代码"引用"printf

假设你在 hello.c 中写了:

// hello.c
#include <stdio.h>
int main() 
{
    printf("hello %d\n", 42);
    return 0;
}

编译 hello.c 为可执行文件 hello:

gcc -o hello hello.c

编译器看到 printf,发现它没有在当前编译单元中定义(而是定义在动态库libc 中):

  1. 在 .dynsym(动态符号表)中记录:printf 是一个未定义符号(UND)

  2. 在 .rela.plt(重定位表)中记录:在 GOT 里有个槽位,需要填 printf 的地址"

  3. 在代码中生成一条 PLT 跳转指令,而不是直接调用 printf

8.2.2 引入GOT(全局偏移表)

GOT的本质:是一张指针数组,存放在可写的.data数据段(.got 或 .got.plt)中,每个槽位(4字节/ 8 字节)存放一个地址。

GOT的作用:

用途说明
存放外部函数的真实地址如 GOT[n] = printf 的地址
存放外部全局变量的地址如 GOT[m] = errno 的地址
存放动态链接器的辅助信息如 GOT[0] 指向 _DYNAMIC 段

实例说明:

.got 段
┌──────────┬────────────────────────────────────────┐
│ 地址      │ 内容                                   │
├──────────┼────────────────────────────────────────┤
│ 0x3fb8   │ [0]  _DYNAMIC          			    │ 
│ 0x3fc0   │ [1]                                    │
│ 0x3fc8   │ [2]                                    │
│ 0x3fd0   │ [3] printf 的地址 ← 关键槽位             │
│ ...      │ ...                                    │
└──────────┴────────────────────────────────────────┘

8.2.3 引入PLT (过程链接表)

PLT 的本质:PLT 是一组跳转桩(stub),存放在只读的代码段(.plt)中。每个外部函数对应一个桩。

PLT 的作用:PLT 桩是"跳板",你的代码不直接调用 printf,而是调用 printf 的 PLT 桩,桩再从 GOT 中读出 printf 的真实地址并跳过去。

实例说明:

0000000000001050 <printf@plt>:
    1050:   endbr64 
    1054:   bnd jmpq *0x2f75(%rip)   # 3fd0 <printf@GLIBC_2.2.5>
    105b:   nopl   0x0(%rax,%rax,1)
    
从 GOT[0x3fd0] 读出地址,跳过去

为什么需要PLT表呢?

因为第一次调用时 GOT 还是空的(或存的是初始值)。

PLT 桩除了跳转之外,还承担了"如果 GOT 没填好,就去调用解析函数"的后备逻辑(在延迟绑定模式下)。

8.2.4 延迟绑定模式

第一次调用

main                    PLT 桩                  GOT                   ld.so
────                    ──────                  ───                   ─────
call printf@plt  ──▶  jmp *GOT[0x3fd0]
                        │
                        │ GOT[0x3fd0] 里存的
                        │ 不是 printf 地址,
                        │ 而是下一条指令的地址
                        ▼
                      push 符号索引
                      jmp _dl_runtime_resolve  ──────────────────▶
                                                                    查找 libc.so
                                                                    找到 printf 地址
                                                                    写入 GOT[0x3fd0]
                                                                    跳转到 printf
                        ◀─────────────────────────────────────────
                      执行 printf

第二次调用

main                    PLT 桩                  GOT
────                    ──────                  ───
call printf@plt  ──▶  jmp *GOT[0x3fd0]
                        │
                        │ GOT[0x3fd0] 已经是
                        │ printf 的真实地址
                        ▼
                      直接跳到 printf(不再经过解析)

8.2.5 验证一:.dynsym 中 printf 是未定义符号(UND)

验证:在 .dynsym(动态符号表)中记录:printf 是一个未定义符号(UND)

hamber@VM-0-14-ubuntu:~/code/lesson23/demo_printf$ readelf -s hello                           

Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)   # printf(UND)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     6: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)

逐字段解读 printf 那一行(Num = 2)

字段值含义
Num2符号表中的第 2 项(第 0 项是空哨兵)
Value0000000000000000地址为 0——编译器不知道 printf 在哪,留空
Size0函数体大小未知(定义不在这里)
TypeFUNC它是一个函数
BindGLOBAL全局绑定,必须被解析,找不到就报错
VisDEFAULT默认可见性
NdxUNDUndefined——未定义。这是核心证据
Nameprintf@GLIBC_2.2.5 (2)符号名 + 版本约束(要求 glibc >= 2.2.5)

8.2.6 验证二:.rela.plt 中有一条重定位项指向 GOT 槽位

在 .rela.plt(重定位表)中记录:在GOT 里有个槽位,需要填 printf 的地址"

hamber@VM-0-14-ubuntu:~/code/lesson23/demo_printf$ readelf -r hello  

Relocation section '.rela.plt' at offset 0x5e8 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003fd0  000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0


在地址 0x3fd0 处(GOT 表的一个槽位),请 ld.so 在运行时把 printf 的真实地址填进去

逐字段解读

字段值含义
Offset0x3fd0要回填的内存地址——即 GOT 中 printf 槽位的位置
Info0x000200000007编码了两层信息(见下方拆解)
TypeR_X86_64_JUMP_SLOT重定位类型,语义是"把函数地址写入此槽位"
Sym. Value0x0符号当前值为 0(未定义,所以是 0)
Sym. Nameprintf@GLIBC_2.2.5要解析的符号
Addend0无额外偏移

8.2.7 验证三:代码中生成的是 PLT 跳转,不是直接调用 printf

在代码中生成一条 PLT 跳转指令,而不是直接调用 printf

hamber@VM-0-14-ubuntu:~/code/lesson23/demo_printf$ objdump -d hello | grep -A 20 '<main>'

0000000000001149 <main>:
    1149:	f3 0f 1e fa          	endbr64 
    114d:	55                   	push   %rbp
    114e:	48 89 e5             	mov    %rsp,%rbp
    1151:	be 2a 00 00 00       	mov    $0x2a,%esi
    1156:	48 8d 3d a7 0e 00 00 	lea    0xea7(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    115d:	b8 00 00 00 00       	mov    $0x0,%eax
    1162:	e8 e9 fe ff ff       	callq  1050 <printf@plt>
    1167:	b8 00 00 00 00       	mov    $0x0,%eax
    116c:	5d                   	pop    %rbp
    116d:	c3                   	retq   
    116e:	66 90                	xchg   %ax,%ax

关键行是 callq 1050 <printf@plt>:

  • 目标是 0x1050,这是 PLT 桩的地址,不是 printf 函数体

  • 编译器没有(也不可能)直接写 call printf,因为编译时不知道 printf 的地址

查看PLT桩

hamber@VM-0-14-ubuntu:~/code/lesson23/demo_printf$ objdump -d hello | grep -A 10 '<printf@plt>'
    
0000000000001050 <printf@plt>:
    1050:	f3 0f 1e fa          	endbr64 
    1054:	f2 ff 25 75 2f 00 00 	bnd jmpq *0x2f75(%rip)        # 3fd0 <printf@GLIBC_2.2.5>
    105b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

关键行是 bnd jmpq *0x2f75(%rip):

  • * 表示间接跳转:读取内存中的值作为跳转目标

  • 0x2f75(%rip) = 0x1054 + 0x2f75 + 7(指令长度) = 0x3fd0

  • objdump 已经标注了:目标就是 # 3fd0 <printf@GLIBC_2.2.5>

8.2.8 完整流程总结

时间线(从上到下)


【用户态】你在终端敲下 ./hello 回车

    │
    ▼

【内核态】shell 调用 execve("./hello", ...)
    │
    ├─ 1. 内核打开 ./hello,读取 ELF 头
    ├─ 2. 内核发现 PT_INTERP 段:
    │      ".interp: /lib64/ld-linux-x86-64.so.2"
    │      → 说明这是一个动态链接程序
    │
    ├─ 3. 内核 mmap 加载 hello 的 PT_LOAD 段到虚拟地址空间
    │      (.text, .rodata, .data 等)
    │
    ├─ 4. 内核 mmap 加载 ld.so 本身到虚拟地址空间
    │      (ld.so 也是 ELF,也要被加载)
    │
    ├─ 5. 内核把控制权交给 ld.so 的入口点
    │      (不是交给 hello 的 main!)
    │
    ▼

【ld.so 开始工作】(此时 main 还没执行)
    │
    ├─ 6. 读取 hello 的 .dynamic 段
    │      → 发现 DT_NEEDED: libc.so.6
    │      → 知道需要加载 libc.so
    │
    ├─ 7. 打开 libc.so.6 文件
    │      open("/lib/x86_64-linux-gnu/libc.so.6")
    │
    ├─ 8. 读取 libc.so.6 的 ELF 头,了解其 LOAD 段
    │
    ├─ 9. mmap 把 libc.so.6 的各段映射到虚拟地址空间
    │      ├─ 代码段:mmap(..., PROT_READ|PROT_EXEC, ...)
    │      └─ 数据段:mmap(..., PROT_READ|PROT_WRITE, ...)
    │      此时 libc.so 在虚拟地址空间中"存在"了, 但大部分页面还没读入物理内存(按需加载)
    │      
    │
    ├─ 10. 符号解析 + 重定位
    │       ├─ 读取 libc.so 的 .dynsym、.gnu.hash
    │       ├─ 查找 "printf" → 得到偏移 0x4f4e0
    │       ├─ 计算真实地址 = libc基址 + 0x4f4e0
    │       └─ 写入 GOT[0x3fd0]
    │       (BIND_NOW 模式下,这一步在启动时全部完成)
    │
    ├─ 11. 调用所有 .so 的初始化函数(.init / .init_array)
    │       libc 的初始化会设置 stdout、stderr 等
    │
    ├─ 12. 跳转到 hello 的入口点 _start
    │
    ▼

【hello 的 _start】
    │
    ├─ 13. _start → __libc_start_main → main()
    │
    ▼

【main() 终于开始执行】
    │
    ├─ call printf@plt → PLT → GOT[0x3fd0] → printf
    │  (此时 GOT 早已填好,直接跳转)
    │
    ▼

输出:hello 42

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值