第四章 可执行文件的生成、加载与运行 ⭐⭐

4.1 翻译程序 ⭐⭐

4.1.1 翻译程序概述

高级语言程序需要经过多次转换才能变成可执行的机器代码。

源代码 → 目标代码
   ↓
翻译程序:编译、汇编、链接

4.1.2 四种翻译程序

类型 英文 输入 输出 功能
预处理器 Preprocessor .c .i 处理宏指令、文件包含
编译器 Compiler .i .s 翻译成汇编语言
汇编器 Assembler .s .o 翻译成机器语言
链接器 Linker 多个.o 可执行文件 合并、重定位、符号解析

4.1.3 预处理阶段

处理内容: - #include:文件包含,将头文件内容插入源程序 - #define:宏定义替换 - #if/#else/#endif:条件编译

#include <stdio.h>  // 被stdio.h的内容替换
#define MAX 100     // 后续所有MAX被替换为100

4.1.4 编译阶段

输入:预处理后的源程序(.i) 输出:汇编语言程序(.s)

编译过程: 1. 词法分析:分解成单词 2. 语法分析:生成语法树 3. 语义分析:类型检查 4. 优化:删除冗余运算 5. 代码生成:生成目标机器的汇编代码

4.1.5 汇编阶段

输入:汇编语言程序(.s) 输出:可重定位目标文件(.o)

汇编器的功能: - 将汇编指令翻译成机器指令 - 生成符号表(定义和引用) - 生成重定位信息

4.1.6 链接阶段 ⭐

输入:多个可重定位目标文件(.o) 输出:可执行目标文件

链接器的功能: 1. 符号解析:将所有符号引用与符号定义配对 2. 重定位:合并节,调整地址 3. 地址分配:为每个符号分配最终运行地址


4.2 ELF文件格式 ⭐⭐

4.2.1 ELF概述

ELF(Executable and Linkable Format):可执行和可链接格式

Linux/Unix系统标准的可执行文件格式。

4.2.2 ELF文件结构

┌─────────────────┐
│     ELF头       │  文件类型、入口地址、程序头偏移
├─────────────────┤
│   程序头表       │  段信息(运行时需要)
├─────────────────┤
│     .text        │  代码段(机器指令)
├─────────────────┤
│     .data        │  已初始化数据段
├─────────────────┤
│     .bss         │  未初始化数据段
├─────────────────┤
│    .symtab       │  符号表
├─────────────────┤
│    .rel          │  重定位信息
├─────────────────┤
│    .strtab       │  字符串表
├─────────────────┤
│   节头表         │  节信息(链接时需要)
└─────────────────┘

4.2.3 ELF各节详解

节名 作用 内容
ELF头 文件基本信息 类型(ET_EXEC)、入口地址、程序头偏移
.text 代码段 机器指令,只读
.data 已初始化数据段 全局变量、静态变量初值
.bss 未初始化数据段 未初始化的全局/静态变量(不占文件空间)
.rodata 只读数据段 常量数据
.symtab 符号表 所有符号(函数名、变量名)
.rel/.rela 重定位表 需要重定位的符号信息
.strtab 字符串表 符号名字符串
.shstrtab 节名字符串表 节名字符串

4.2.4 ELF文件类型

类型 说明
ET_REL 1 可重定位目标文件(.o)
ET_EXEC 2 可执行文件
ET_DYN 3 共享对象文件(.so)
ET_CORE 4 Core dump文件

4.3 符号解析与重定位 ⭐⭐

4.3.1 符号类型

类型 说明 示例
全局符号定义 在本模块定义,可被其他模块引用 int global_var;
全局符号引用 引用其他模块定义的符号 extern int ext_var;
本地符号 仅在模块内部使用 static int static_var;
函数名 函数的起始地址 void func(){}

4.3.2 符号表结构

符号名 地址 类型 绑定 所在节
main 0x0 函数 全局 .text
global_var 0x100 数据 全局 .data
buf 0x200 数据 全局 .bss
temp - 未定义 外部 -

4.3.3 符号解析过程 ⭐

问题:当多个目标文件链接时,如何找到符号的定义位置?

解析过程

文件A (.o)          文件B (.o)
┌──────────┐       ┌──────────┐
│ call foo │  ←──  │ foo定义  │
│ (引用)   │       │ (全局符号)│
└──────────┘       └──────────┘
    ↓                     ↑
    └─────── 链接器找到foo的定义 ───────┘

步骤: 1. 链接器扫描所有输入文件 2. 建立全局符号表(所有定义) 3. 对每个未解析的引用,在表中查找匹配的定义 4. 若找不到,报"undefined symbol"错误

4.3.4 重定位 ⭐⭐

为什么需要重定位? - 每个.o文件是独立编译的,地址从0开始 - 链接时需要合并所有.o文件,重新计算地址

重定位过程

1. 合并所有节的同类节
   .text节A + .text节B → 新的.text节

2. 分配运行地址
   .text节 → 地址0x08048000开始

3. 调整符号地址
   foo原在A的0x10 → 新地址0x08048010

4. 修改引用
   call指令中的foo地址 → 更新为新地址

4.3.5 重定位类型

类型 说明 示例
R_X86_64_PC32 相对PC的32位地址 call指令
R_X86_64_32 绝对32位地址 全局变量引用
R_X86_64_PLT32 PLT相关调用 函数调用

4.3.6 重定位例题 ⭐

假设: - main.o中调用函数foo,foo定义在foo.o - main.o编译时,假设foo在地址0x00(重定位条目) - 链接后,foo被分配到运行地址0x400500

链接器执行重定位

原指令:e8 00 00 00 00    (call foo, 目标地址=PC+4+0)
重定位后:e8 xx xx xx xx  (目标地址=foo的实际地址)

4.4 可执行文件的加载与运行 ⭐

4.4.1 加载过程

当在shell中输入命令时:

$ ./hello
    ↓
Shell调用execve("hello", argv, envp)
    ↓
内核创建新进程
    ↓
读取ELF文件头,验证格式
    ↓
读取程序头表,映射段到进程地址空间
    ↓
设置初始PC值(入口地址)
    ↓
开始执行

4.4.2 进程地址空间布局

高地 ┌────────────────┐
     │     栈          │ ← SP寄存器
     ├────────────────┤
     │      ↓         │
     │   (未使用)      │
     │      ↑         │
     ├────────────────┤
     │      ↓         │
     │     堆          │ ← malloc/new
     ├────────────────┤
     │   .data .bss   │ ← 全局变量
     ├────────────────┤
     │     .text       │ ← 代码
     └────────────────┘
低地    0x08048000

4.4.3 execve系统调用

// 原型
int execve(const char *filename, char *const argv[], char *const envp[]);

// 示例
execve("./hello", argv, envp);

execve工作步骤: 1. 检查文件是否为可执行格式(ELF) 2. 读取ELF头信息 3. 创建新的代码、数据段映射 4. 设置栈(参数、环境变量) 5. 设置PC指向入口地址


4.5 库与动态链接

4.5.1 静态库

特点: - 编译时链接 - 所有代码加入最终可执行文件 - 文件体积大,更新需重新编译

4.5.2 动态链接库(.so)

特点: - 运行时链接 - 多个进程共享同一份库代码 - 节省内存和磁盘空间 - 更新库无需重新编译程序

4.5.3 动态链接过程

加载时链接(运行时加载):
1. 加载器读取动态段信息
2. 加载所需的共享库
3. 进行符号解析和重定位

运行时链接(延迟绑定):
1. 首次调用函数时
2. 通过PLT/GOT跳转
3. 解析符号地址
4. 更新GOT,下次直接调用

章节练习题

一、选择题

  1. 将汇编语言程序翻译成机器语言程序的是( ) A. 预处理器 B. 编译器 C. 汇编器 D. 链接器

  2. ELF文件中,不占磁盘空间的是( ) A. .text B. .data C. .bss D. .rodata

  3. 符号解析的主要工作是( ) A. 合并节 B. 将符号引用与定义配对 C. 调整地址 D. 分配运行地址

  4. 可执行文件的入口地址保存在ELF的哪部分?( ) A. .text B. 程序头表 C. ELF头 D. 符号表

  5. 动态链接库的文件扩展名通常是( ) A. .o B. .a C. .so D. .exe

二、填空题

  1. 翻译程序包括预处理器、编译器、______和链接器。

  2. ELF是______的缩写。

  3. .bss节用于存放______的全局/静态变量。

  4. 重定位类型R_X86_64_PC32表示______地址。

  5. Shell执行程序时调用的是______系统调用。

三、简答题

  1. 简述从源代码到可执行文件的转换过程。

  2. 说明ELF文件中.text、.data、.bss三个节的作用。

  3. 什么是符号解析?为什么需要符号解析?

  4. 什么是重定位?链接器执行重定位的步骤是什么?

  5. 比较静态链接和动态链接的优缺点。

四、分析题

假设有两个源文件main.c和foo.c:

main.c:

extern int result;
int main() {
    result = foo(10);
    return 0;
}

foo.c:

int result;
int foo(int x) {
    return x * 2;
}

请回答: 1. 在foo.o中,result是什么类型的符号?foo呢? 2. 在main.o中,result和foo是什么类型的符号? 3. 链接过程中如何解析这些符号?


参考答案

选择题

  1. C 2. C 3. B 4. C 5. C

填空题

  1. 汇编器
  2. Executable and Linkable Format
  3. 未初始化
  4. 相对PC的32位
  5. execve

分析题答案

  1. 在foo.o中:
  2. result全局符号定义(定义并初始化)
  3. foo全局符号定义(函数定义)

  4. 在main.o中:

  5. result全局符号引用(外部引用)
  6. foo全局符号引用(外部引用)

  7. 符号解析过程:

  8. 链接器首先扫描所有目标文件
  9. 建立全局符号表,包含所有定义
  10. 对main.o中的每个未定义符号,在表中查找定义
  11. 找到foo的定义(在foo.o中),建立关联
  12. 链接器同时确保只有一个result定义