编译链接总结

本文是读<程序员的自我修养>的笔记和总结

编译过程

程序的编译可以分解为四个步骤, 预处理->编译->汇编->链接
下面以main.c文件为例分析:

预编译

main.c -> main.i

  • 展开宏定义, 替换#define
  • 处理预编译指令, #if, #endif, #ifdef等
  • 递归处理#include, 将引入的文件插入到对应位置
  • 删除注释
  • 添加行号, 文件名标识
  • 保留#pragma指令

编译

main.i -> main.s
词法分析: 源代码经过扫描器的处理, 运用类似有限状态机的算法将代码的字符分割成不同的记号
语法分析: 根据不同的记号, 构建语法树
语义分析: 分析语法树的静态语义是否正确, 主要是类型匹配和转换
中间语言生成:将语法树转换成中间代码, 优化一些中间结果. 中间代码使得编译器分为前端和后端, 前端产生与机器无关的中间代码, 后端将中间代码转换成目标机器代码

汇编

main.s -> main.o
根据汇编指令和机器指令的翻译一一对应

链接

修正指令对符号地址的引用
主要包括:地址和空间分配, 符号决议, 重定位

目标文件分析

目标文件是指编译后未进行链接的中间文件, 格式和可执行文件几乎一样, 在Windows称为PE-COFF, linux下称为ELF

目标文件按照节(section)或者段(segment)形式存储, 目标文件的开头是一个文件头, 描述了整个文件的属性, 静态链接还是动态链接, 入口地址等, 还包含一个段表, 段表用来描述各个段的属性和地址偏移.
常见的段名有:

  • .text(代码段)
    程序指令编译后放在代码段
  • .data(数据段)
    保存初始化了的全局静态变量和局部静态变量, 有些编译器也会将const变量保存在.rodata段里
  • .bbs(为初始化数据段)
    存放未初始化的全局变量和局部静态变量
  • 其他段
    .plt存放动态链接的跳转表, .got存放全局入口表, .dynamic存放动态链接信息, .comment存放编译器版本信息等
    除此之外, 也可以自定义段名, 但不能使用"."为前缀, 使用__ attribute__(section("customName"))可自定义段名.

注意:
源代码编译后会把程序指令和程序数据分成两个段, 即.text和.data, 这样做的优点是:
1.数据和指令分别映射到两个虚拟内存区域, 读写权限就可以分开, 防止指令被意外改写
2.提高程序的局部性, 有利于CPU缓存命中率的提高
3.对于只读数据和只读指令, 可以进程共享资源, 节省空间

.bbs段记录了未初始化的变量, 只是预留了位置, 没有内容, 因此在文件中不占据空间, 但是在链接器装载后的虚拟地址中是要分配虚拟地址空间的. 其中区别在于, 未初始化的变量不会增加可执行文件的大小, 但是会增加程序运行时的空间.

符号

函数和变量称为符号, 函数名和变量名就是符号名.
链接器的过程就是把多个不同的目标文件拼装在一起, 如果目标文件之间有引用关系, 那么就需要解析外部符号, 链接器主要通过符号表和重定位表实现.

  • 符号表
    记录了目标文件中用到的所有符号, 对应每个符号的地址
  • 重定位表
    在引用外部文件的符号时, 目标文件会先预置为undefined, 之后重定位. 重定位信息都记录在重定位表里.

静态链接

多个源代码文件编译后生成多个.o目标文件, 静态链接就是将多个目标文件链接在一起最终形成一个可执行文件.

相似段合并

将所有输入的目标文件的相同段合并在一起, 比如多个.text段合并为一个大的.text段. 链接器会扫描所有的输入文件, 读取段首信息, 将目标文件中的所有符号的定义和引用收集到一个全局符号表里. 简单来说, 链接器合并了多个目标文件, 并建立了全局符号映射关系

符号解析和重定位

链接之前, 目标文件引用的外部符号都标记为undefined, 链接时, 通过全局符号表替换所有undefined符号. 除了符号的重定位, 还有虚拟地址VMA(Virtual Memory Address)的分配, 即链接后, 各个段分配了虚拟地址. 各个段内的符号根据偏移量计算符号地址

注意:
虚拟地址并不是从0地址开始分配, 不同操作系统有不同的分配规则, 如Linux下默认分配的起始地址是0x08048000

Common块

Common块是编译器用来处理弱符号的
强符号与弱符号
编译器默认函数和初始化了的全局变量是强符号, 未初始化的全局变量为弱符号, 也可以使用__ attribute__(weak)定义一个强符号为弱符号, 编译器对强弱符号有以下规则:

  • 强符号不允许被定义多次
  • 一个符号在某个文件中为强符号, 其他文件只能为弱符号, 编译器最终选择强符号的类型
  • 一个符号在多个文件中只有弱符号, 没有强符号, 编译器会选择占用空间最大的弱符号, 比如double和int, 选择double

注意: 强符号和弱符号的并集并不是全部的符号, 很多符号既不是强符号, 也不是弱符号

直接导致需要Common机制的原因是编译器和链接器允许不同类型的弱符号存在, 但最本质的原因还是链接器不支持符号类型, 即链接器无法判断各个符号的类型是否一致

链接过程控制

链接过程可以使用ld链接脚本控制, 自定义链接过程

你可能感兴趣的:(编译链接总结)