编译与链接 - 01
目录编译过程静态链接目标文件有什么目标文件链接视图装载视图符号表弱符号和强符号静态链接空间分配重定位符号解析静态库链接可执行文件装载页覆盖进程虚拟内存空间分布Linux 装载 ELF动态链接地址无关代码编译过程预处理 - 编译 - 汇编 - 链接静态链接静态链接其实就是把所有要编译处理的 .o 文件加上库文件链接到一起去。把同一类的信息放到同个段位置中并对所有的函数制定符号表能够识别函数的基本信息最后把所有的地址重定位。目标文件有什么目标文件ELF 文件类型说明实例可重定位文件Relocatable File包含代码和数据用于链接生成可执行文件或共享库静态库也属于此类.oLinux、.objWindows可执行文件Executable File可直接运行的程序加载后形成进程/bin/bashLinux、.exeWindows共享目标文件Shared Object File可被链接器用于静态链接也可被动态链接器在运行时加载.soLinux如 /lib/libc-2.6.1.so、.dllWindows核心转储文件Core Dump File进程异常终止时系统保存的内存快照用于调试coreLinux目标文件是源代码经过编译后生成的中间文件如.o文件或最终可执行文件如a.out其内容包含机器指令和数据链接和调试所需的信息如符号表、调试信息、字符串等。这些信息按照属性被组织成不同的段在本书中统一称为段。部分说明1. 文件头 readelf -h .out位于文件最开头描述整个文件的全局属性- 是否可执行- 链接方式静态/动态- 入口地址若为可执行文件- 目标硬件架构、操作系统等- 段表Section Table的偏移和数量2. 段表 readelf -S .out一个数组每个元素描述一个段的详细信息- 段名如 .text, .data, .bss- 在文件中的偏移位置- 段大小- 权限可读、可写、可执行- 类型如 SHT_PROGBITS、SHT_NOBITS作用链接器和调试器通过段表定位各段内容。3. 各个段 objdump -s -j [段] [exe]文件主体内容按功能划分存储区域常见段如下链接视图段名内容是否占用文件空间说明.text编译后的机器指令代码如 func1, main 函数的汇编指令是通常只读、可执行。对应源码中的函数体。.data已初始化的全局变量和已初始化的局部静态变量如 int global_init_var 84;、static int static_var 85;是存储实际的初始值如 84、85占用文件空间。.bss未初始化的全局变量和未初始化的局部静态变量如 int global_uninit_var;、static int static_var2;否不占文件空间仅预留大小信息不存储数据默认值为 0。程序加载时由操作系统分配内存并清零。其他段.symtab符号表、.strtab字符串表、.debug调试信息等是供链接器、调试器使用运行时通常不需要。装载视图readelf -l a.out它不在乎下面具体是什么段Section它只关心如何把一大块连续的、具有相同访问权限读、写、执行的文件内容高效地映射到内存的某个地址区间。__attribute__((section(name)) XXX 把变量或者函数放到指定的段名中符号表表示函数的位置和属性。C 与 C 的符号表生成的符号不太一样所以 C 想要调用 C 语言函数需要 extern C。弱符号和强符号强符号定义好的全局变量、函数。弱符号未被定义的全局变量、函数或者使用__attribute__((weak))显式声明的符号。1. 强符号修饰的变量和函数有且只有一个。2. 弱符号本质上是一个占位的修饰功能当我们定义了弱符号那么就可以在本文件中使用它并且不会有报错的风险。一旦出现强符号的同款变量、函数则会覆盖它此时使用的便是强符号的内容。3. 当没有强符号修饰但存在多个相同的弱符号时则需要看弱符号中最强的修饰。链接的先后决定强弱关系。静态链接空间分配多个文件链接时编译器做的事情其实是把相似功能的段合并计算大小设计偏移量。加载到内存中时先确定当前虚拟内存的基地址加上文件的偏移就能加载到虚拟内存的对应位置了。重定位一个文件使用了不是它本身定义的函数时一定是需要被链接的。当然当前文件中的地址是不存在的表现出来的形式是无意义的地址。重定位表/段重定位表是 ELF 文件中专门用于记录哪些指令/数据需要被修改、如何修改、修改成什么值的结构。编译时就自动生成出来了类似的重定位段 .rel-xxx 段。objdump -r a.o OFFSET TYPE VALUE 0000001c R_386_32 Shared typedef struct { Elf32_Addr r_offset; // 重定位位置偏移在段内的字节偏移 Elf32_Word r_info; // 重定位类型 符号索引 } Elf32_Rel;符号解析1. 链接的目的不仅是合并目标文件更重要的是解决符号引用与定义之间的关联。2. 链接器必须遍历所有输入目标文件的符号表构建一个全局符号表将所有未定义符号UND与对应的定义匹配。3.readelf -s查看符号表。静态库链接静态库是一组目标文件.o的集合经过打包和索引后形成一个单一文件。1. 链接器按目标文件为单位进行链接。2. 符号依赖链的自动解析。3. 每个函数单独一个目标文件。可执行文件装载页覆盖1. 虚拟内存是 Linux 虚构的它是给程序一个独立地址的错觉物理内存是真实的它存放着一切的数据用于执行程序。2. 虚拟内存 物理内存为了运行的顺利内存和虚拟内存就要拆分成页的逻辑分别对应物理内存页和虚拟内存页一个页的大小固定不变。3. 切换页由操作系统执行当程序运行到未加载的页时触发缺页中断由操作系统自身执行页交换覆盖原先的页继续执行。进程虚拟内存空间分布readelf -S XXX查看段表 Section 满足开发与链接需求结构精细。readelf -l XXX查看段 Segment 满足运行时内存效率只看属性。类似的段表会被合并到同一个段中方便加载到内存中大小地址属性等。ELF 用 Section 编织代码用 Segment 加载内存。Linux 装载 ELF用户输入命令 ↓ bash → fork() → 子进程调用 execve() ↓ 内核sys_execve() → do_execve() ↓ 读取前 128 字节 → 判断魔数 → 匹配 load_elf_binary() ↓ load_elf_binary() 执行 1. 验证 ELF 格式 2. 解析 .interp 段 → 获取动态链接器 3. 映射代码/数据段到内存 4. 初始化寄存器环境如 EDX 5. 设置 EIP e_entry或动态链接器入口 ↓ sys_execve() 返回 → CPU 自动跳转至新入口 ↓ 新程序执行静态 vs 动态链接静态链接程序入口 用户程序的_start。动态链接程序入口 动态链接器由它负责加载共享库、重定位符号再跳转到用户程序。动态链接动态链接将链接过程推迟到程序运行时模块以独立文件形式存在运行时按需加载和链接。核心机制程序主模块如Program1.o与共享库如Lib.so分离。程序启动时动态链接器如 Linux 的ld-linux.so加载主程序识别依赖的共享库加载共享库到内存进行符号解析和地址重定位将控制权交给程序入口。每次程序启动需进行符号解析、重定位带来约 5% 的性能损失。优化方案采用延迟绑定Lazy Binding仅在首次调用函数时才解析符号大幅减少启动开销。地址无关代码动态链接方式方案描述优点缺点是否适用静态共享库操作系统为每个共享库预留固定地址空间简单直接地址冲突、升级困难、空间受限、无法扩展已淘汰装载时重定位装载时修改指令中的绝对地址实现简单兼容旧系统指令被修改 → 无法共享失去动态链接节省内存优势可用但低效GCC -shared 默认方式*地址无关代码编译时避免绝对地址使用相对寻址 GOT指令可共享、支持多进程、灵活、现代标准有少量性能开销GOT 间接访问推荐方案GCC -fPIC唯一实现动态链接的方式地址无关代码方式讨论类型描述实现方式是否可共享1. 模块内部调用/跳转如 foo() → bar()同一共享对象内的函数调用使用相对地址调用call rel32偏移量基于当前指令地址✅ 可共享无需修改2. 模块内部数据访问如访问 static int a;访问本模块定义的全局/静态变量通过 PC 相对寻址1. call __i686.get_pc_thunk.cx 获取当前 PC2. add $offset, %ecx 得到变量地址✅ 可共享偏移量编译时确定3. 模块外部数据访问如访问 extern int b;访问其他模块的全局变量使用 GOTGlobal Offset Table1. 通过 PC 相对寻址找到 GOT 地址2. 从 GOT 中读取变量真实地址3. 间接访问mov (%eax), %edx✅ 可共享GOT 在数据段每个进程独立4. 模块外部调用/跳转如 ext()调用其他模块的函数使用 GOT 间接调用1. 获取 PC → 找到 GOT2. 从 GOT 取函数地址 → call *(%eax)✅ 可共享GOT 由动态链接器填充1. 动态链接最重要的其实就是相对地址。2. 对于同一个共享库内部的函数调用和静态全局变量访问现代 PIC 直接使用基于 RIP 的相对寻址。因为它们在同一个加载单元内相对位置是固定的编译时就可以计算出来。3. 对于外部调用的情况使用到了 GOT 动态表每一个 ELF 中都存在这个表这个表中存放着目标文件加载到内存中时的绝对地址只需要找到 GOT 中对应的对象就能找到实际内存中那个对象了。4. 静态变量static地址确定可直接用相对寻址但全局变量extern可能跨模块必须按外部符号处理。主程序将所有被共享的全局变量设置一个副本这个副本用于提供地址。其他的共享文件中对应的对象通过 GOT 寻找GOT 指向的就是副本位置。

相关新闻