计算系统安全速成之链接:理解程序的构建过程【7】

文章目录

  • 说明
  • [一 链接:理解程序的构建过程](#一 链接:理解程序的构建过程)
    • [1.1 编译与链接流程](#1.1 编译与链接流程)
      • [1.1.1 链接器的优势](#1.1.1 链接器的优势)
      • [1.1.2 链接器的功能](#1.1.2 链接器的功能)
    • [1.2 ELF(可执行与可链接格式)【重点】](#1.2 ELF(可执行与可链接格式)【重点】)
      • [1.2.1 ELF 文件结构](#1.2.1 ELF 文件结构)
      • [1.2.2 链接器符号分类](#1.2.2 链接器符号分类)
      • [1.2.3 符号解析详解](#1.2.3 符号解析详解)
      • [1.2.4 符号识别练习](#1.2.4 符号识别练习)
    • [1.3 局部静态变量](#1.3 局部静态变量)
      • [1.3.1 符号重复定义的处理规则](#1.3.1 符号重复定义的处理规则)
      • [1.3.2 链接器规则](#1.3.2 链接器规则)
    • [1.4 重定位过程](#1.4 重定位过程)
  • [二 函数库的封装方式](#二 函数库的封装方式)
    • [2.1 静态库(传统解决方案)](#2.1 静态库(传统解决方案))
      • [2.1.1 创建静态库](#2.1.1 创建静态库)
      • [2.1.2 静态库的链接过程](#2.1.2 静态库的链接过程)
    • [2.2 共享库:现代解决方案](#2.2 共享库:现代解决方案)
      • [2.2.1 动态库依赖查询](#2.2.1 动态库依赖查询)
      • [2.2.2 共享库构建示例](#2.2.2 共享库构建示例)
      • [2.2.3 加载时动态链接流程](#2.2.3 加载时动态链接流程)
  • [三 总结](#三 总结)

说明

  • 庄老师的课堂,如春风拂面,启迪心智。然学生愚钝,于课上未能尽领其妙,心中常怀惭愧。
  • 幸有课件为引,得以于课后静心求索。勤能补拙,笨鸟先飞,一番沉浸钻研,方窥见知识殿堂之幽深与壮美,竟觉趣味盎然。
  • 今将此间心得与笔记整理成篇,公之于众,权作抛砖引玉。诚盼诸位学友不吝赐教,一同切磋琢磨,于学海中结伴同行。
  • 资料地址:computing-system-security

一 链接:理解程序的构建过程

  • main.c 文件包含主函数和对 sum 函数的调用
  • sum.c 文件实现 sum 函数
  • 全局数组 array[2] = {1, 2}
  • main 函数调用 sum(array, 2) 并返回结果

1.1 编译与链接流程

  • 使用命令:gcc -Og -o prog main.c sum.c
  • 流程:
    • 源文件 → 预处理器(cpp)→ 编译器(cc1)→ 汇编器(as)→ 可重定位目标文件(.o)
    • 链接器(ld)将多个 .o 文件合并为可执行文件 prog
  • 输入:两个可重定位目标文件(main.o, sum.o
  • 输出:完全链接的可执行文件(包含所有函数代码和数据)

1.1.1 链接器的优势

  1. 模块化:程序可以拆分为多个小的源文件,支持构建通用函数库(如数学库、标准C库),提高代码组织性和可维护性

  1. 时间效率:支持独立编译,修改一个源文件后只需重新编译该文件并重新链接,不需要重新编译其他未修改的文件,支持多文件并发编译。

  1. 空间效率:库函数集中管理。
    • 静态链接:可执行文件仅包含实际使用的库代码
    • 动态链接:可执行文件不包含库代码,运行时共享同一份库代码,节省内存空间

1.1.2 链接器的功能

一:符号解析(Symbol Resolution)

  • 程序中定义和引用符号(全局变量、函数)
  • 符号表由汇编器生成,存储在目标文件中
  • 每个符号条目包括名称、大小、位置
  • 链接器将每个符号引用关联到唯一的符号定义

二:重定位(Relocation)

  • 合并各个 .o 文件的代码段和数据段
  • 将符号从相对地址重定位到最终的绝对内存地址
  • 更新所有对这些符号的引用以反映新地址

1.2 ELF(可执行与可链接格式)【重点】

  • 支持三种文件类型:可重定位文件(.o)、可执行文件(a.out)、共享对象文件(.so)统称为 ELF 二进制文件
  • 可重定位目标文件(.o):包含可与其他 .o 文件合并形成可执行文件的代码和数据。
  • 每个 .c 文件生成一个 .o 文件
  • 可执行目标文件(a.out):可直接加载到内存并执行的格式
  • 共享对象文件(.so):特殊类型的可重定位文件,可在加载或运行时动态加载和链接。Windows 中称为 DLL(动态链接库)

1.2.1 ELF 文件结构

  • ELF 头部:描述字长、字节序、文件类型、机器类型等。
  • 段头部表(Program Header Table):用于可执行文件,描述虚拟内存中的段(section)及其属性。

主要节区(Sections)

  • .text:程序代码。
  • .rodata:只读数据(如字符串常量、跳转表)。
  • .data:已初始化的全局变量。
  • .bss:未初始化的全局变量,符号起始块,占用符号表条目但不占实际空间。

其他节区

  • .symtab:符号表
  • .rel.text.text 节的重定位信息
  • .rel.data.data 节的重定位信息
  • .debug:调试信息(使用 gcc -g 时生成)

1.2.2 链接器符号分类

  • 全局符号(Global Symbols):当前模块定义、可被其他模块引用,如非静态的 C 函数和全局变量。
  • 外部符号(External Symbols):当前模块引用但由其他模块定义的全局符号。
  • 局部符号(Local Symbols):仅在当前模块内定义和使用,如使用 static 关键字定义的函数和变量
  • 注意:局部链接器符号 ≠ 局部程序变量(如栈上变量)

1.2.3 符号解析详解

  • main.c 定义 main,引用 sumarray
  • sum.c 定义 sum,使用局部变量 i, s
  • 链接器处理的是全局符号,不关心局部变量

1.2.4 符号识别练习

c 复制代码
int incr = 1;
static int foo(int a) {
  int b = a + incr;
  return b;
}

int main(int argc,
         char* argv[]) {
  printf("%d\n", foo(5));
  return 0;
}
  • 文件 symbols.c 中哪些名称会进入符号表?
    • incr(已初始化全局变量)✅
    • foo(静态函数)✅
    • main(主函数)✅
    • printf(外部函数)✅(作为外部符号)
    • "%d\n"(字符串常量)❌(属于 .rodata,不单独列为符号)
    • argc, argv, a, b ❌(局部变量,不在符号表中)

1.3 局部静态变量

  • 非静态局部变量:栈上分配。
  • 静态局部变量:.data.bss 段中分配。
  • 编译器处理方式:为每个静态局部变量分配独立空间,在符号表中创建唯一命名的局部符号(如 x, x.1721, x.1724)。

1.3.1 符号重复定义的处理规则

  • 强符号:函数、已初始化的全局变量
  • 弱符号 :未初始化的全局变量、用 extern 声明的变量

1.3.2 链接器规则

规则一:不允许多个强符号。同名强符号只能有一个定义,否则导致链接错误。

规则二:强弱共存时选择强符号。若存在一个强符号和多个弱符号,采用强符号,所有引用指向强符号定义。

规则三:多个弱符号任选其一。若全是弱符号,链接器任意选择一个,可通过 gcc -fno-common 禁用此行为。

  • 尽量避免使用全局变量,如果必须使用,优先使用 static 限制作用域,定义时尽量初始化(避免成为弱符号),引用外部全局变量时使用 extern。声明为弱符号,若最终未定义,链接时报错。

1.4 重定位过程

  • 可重定位文件到可执行文件:合并 .text.data 段,分配最终内存地址,更新所有符号引用。
  • 重定位入口(Relocation Entries),来自 main.o 的反汇编输出,包含待修正的地址和修正方式。
  • R_X86_64_32 array:修正 array 地址
  • R_X86_64_PC32 sum-0x4:PC 相对寻址修正 sum 调用

  • 重定位后的文本段,地址映射
    • main 函数起始地址:0x4004d0
    • sum 函数起始地址:0x4004e8
    • 调用指令使用 PC 相对寻址:callq 4004e8 = 0x4004e3 + 0x5
  • 数据地址修正:mov $0x601018,%edi%edi = &arrayarray 被重定位到 0x601018

  • 可执行文件加载

内存布局

  • 用户空间起始于 0x400000
  • 只读代码段.init, .text, .rodata
  • 读写数据段.data, .bss
  • 运行时堆 :由 malloc 创建,向上增长
  • 共享库映射区
  • 用户栈 :由内核创建,向下增长(%rsp 指向栈顶)
  • 未使用区域

内核虚拟内存

  • 对用户代码不可见
  • 包含页表、内核代码等

加载来源

  • 从可执行文件加载 .text, .data等节。
  • .bss 节清零初始化。
  • brk 指针标记堆的当前边界

二 函数库的封装方式

2.1 静态库(传统解决方案)

  • 静态库为 .a 归档文件,将相关的可重定位目标文件合并为一个带索引的单一文件(归档)。
  • 增强链接器功能:自动在归档中查找并解析未定义的外部符号
  • 若某归档成员能解析引用,则将其链接进可执行文件

2.1.1 创建静态库

  • 使用归档工具 ar 增量更新库。
  • 修改函数后重新编译对应 .o 文件并替换进归档。
  • 示例命令:ar rs libc.a atoi.o printf.o ... random.o

2.1.2 静态库的链接过程

  • 自定义向量库 libvector.a
  • 包含 addvec.cmultvec.c 实现
  • 主程序 main2.c 调用 addvec
  • 头文件 vector.h 声明接口

链接流程

  • 编译生成 main2.oaddvec.o, multvec.o
  • 使用 ar 打包成 libvector.a
  • 链接时使用 -L. 指定路径,-lvector 引入库
  • 静态链接命令:gcc -static -o prog2c main2.o -L. -lvector
  • 生成完全链接的可执行文件(大小 861,232 字节)

2.2 共享库:现代解决方案

静态库的缺点

  • 存储冗余:每个可执行文件都复制 libc 等库代码
  • 运行时内存冗余
  • 系统库的小幅修复需重新链接所有应用
  • 举例:glibc 的 CVE-2015-7547 安全漏洞需大规模重编译

  • 动态链接库(Shared Libraries)/动态链接库(DLL)、.so 文件:代码和数据在加载或运行时动态加载并链接
  • 加载时链接(Load-time Linking):可执行文件首次加载运行时完成,Linux 中由动态链接器 ld-linux.so 自动处理,标准 C 库 libc.so 通常采用此方式。
  • 运行时链接(Run-time Linking):程序启动后进行动态链接,Linux 中通过 dlopen() 接口实现。
  • 应用场景:软件分发,高性能 Web 服务器,运行时库拦截(interpositioning),多进程共享库例程(虚拟内存支持)。

2.2.1 动态库依赖查询

  • .interp 段:指定使用的动态链接器(如 ld-linux.so)。
  • .dynamic 段:列出所需动态库名称等信息。
  • 使用 ldd 命令查看可执行文件依赖的共享库。
  • 示例输出显示依赖 linux-vdso.so.1, libc.so.6, ld-linux-x86-64.so.2

2.2.2 共享库构建示例

  • 编译选项 -fpic 生成位置无关代码,使用 gcc -shared 构建共享库。
  • 示例命令:gcc -Og -c addvec.c multvec.c -fpicgcc -shared -o libvector.so addvec.o multvec.o

2.2.3 加载时动态链接流程

三 总结

  • 链接是一种技术,它允许程序由多个目标文件构建而成。
  • 链接可在程序生命周期的不同阶段发生生命周期:
    • 编译时(程序被编译时)
    • 加载时(程序被加载到内存时)
    • 运行时(程序正在执行期间)
相关推荐
我在人间贩卖青春5 天前
汇编之伪指令
汇编·伪指令
我在人间贩卖青春5 天前
汇编之伪操作
汇编·伪操作
济6175 天前
FreeRTOS基础--堆栈概念与汇编指令实战解析
汇编·嵌入式·freertos
myloveasuka5 天前
汇编TEST指令
汇编
我在人间贩卖青春5 天前
汇编编程驱动LED
汇编·点亮led
我在人间贩卖青春5 天前
汇编和C编程相互调用
汇编·混合编程
myloveasuka6 天前
寻址方式笔记
汇编·笔记·计算机组成原理
请输入蚊子6 天前
《操作系统真象还原》 第六章 完善内核
linux·汇编·操作系统·bochs·操作系统真像还原
myloveasuka6 天前
指令格式举例
汇编·笔记·计算机组成原理
我在人间贩卖青春7 天前
汇编之分支跳转指令
汇编·arm·分支跳转