目录
- 程序翻译和动静态编译
- 程序翻译的四步
- 动态链接和静态链接
- 优缺点对比
- 理解库
- 动静态库的制作和使用
- 动静态库的制作
- 动静态库的使用
- ELF文件
- 组成部分
- ELF的布局原理
- ELF布局是虚拟地址空间的预处理
- ELF的两个视图
- 为什么要有两个视图
- 链接与加载
- 静态链接加载
- 动态链接加载
- 动态库不需要拷贝的原因
- 程序调用的流程
- 加载过程
- 位置无关码和GOT
- 库间依赖注意:
程序翻译和动静态编译
程序翻译的四步
- 预处理:主要包括宏替换、头文件展开、去注释、条件编译
gcc –E hello.c –o hello.i
-E:让gcc在预处理后停止编译过程 - 编译:生成汇编语言,进行词法、语法、语义检测分析。
gcc –S hello.i –o hello.s
-S:只进行编译生成汇编代码 - 汇编:将汇编代码转化成生成二进制代码
gcc –c hello.s –o hello.o
-c:编译并汇编到目标文件 - 链接:生成可执行文件
动态链接和静态链接
静态链接:在编译阶段将程序依赖的库代码完整复制到最终的可执行文件中
动态链接:动态链接是在程序运行时才将依赖的库代码动态加载到内存中,编译阶段仅记录库的引用信息。
- 在Linux当中,以.so为后缀的是动态库,以.a为后缀的是静态库。
- 在Windows当中,以.dll为后缀的是动态库,以.lib为后缀的是静态库。
优缺点对比
静态链接
优点:不依赖库,运行时无需加载外部库,启动速度略快。
缺点:浪费空间(磁盘和内存,网络等),库更新后需要重新编译程序。
动态链接
优点:
- 动态库可以由多个可执行文件共享,节省内存
- 可执行文件体积小(仅包含自身代码和库引用)。
- 动态库更新后,无需重新编译程序(直接替换库文件即可)。
缺点: - 运行时依赖外部动态库,若库缺失或版本不匹配,程序无法运行
- 启动时需加载动态库并解析符号,运行效率略低于静态链接。
理解库
库的本质是预先编写、编译好的代码集合
标准库是按部分C++标准生成的代码集合,是由编译器厂商代码实现,C++标准只制定规范不具体实现代码
动静态库的本质:预先编写、编译好的二进制代码集合,由多个.o文件打包而成
作用:封装可复用的功能(如函数、变量、类等),供其他程序直接调用,避免重复开发。
动静态库的制作和使用
linux可以通过ldd 文件名来查看一个可执行程序所依赖的库文件。
动静态库的制作
静态库的制作:
使用ar归档工具将多个.o归档形成静态库
动态库的制作:
-fPIC选项实现编译位置无关码,偏移量编址,进程加载时进行链接可与GOT表结合,实现位置无关
动静态库的使用
动静态库同名文件同时存在时优先匹配动态库
上面是告诉编译器库位置,而运行时加载动态库需要系统也知道动态库位置,4种方法解决
ELF文件
ELF是通用的二进制文件格式,可执行程序、动态库(.so)、目标文件(.o)都是ELF文件。
组成部分
位于文件开头,记录文件类型(可执行文件 / 动态库 / 目标文件)、架构(x86/ARM 等)、入口地址(_start函数地址),以及节头部表和程序头部表的位置。
2. 节(Section)与节头部表
- 节是静态链接的最小单位(如.text存代码、.data存已初始化数据、.bss存未初始化数据、.symtab存符号表)。
- 节头部表记录所有节的位置、大小、类型,供编译器 / 链接器使用。
- 段(Segment)与程序头部表
- 段是动态加载的最小单位,由多个属性相似的节合并而成(如代码段含.text,数据段含.data)。
- 程序头部表记录段的加载地址、权限(读 / 写 / 执行),供操作系统加载时使用。
- 关键特性
- 同时支持 “链接视图”(节)和 “执行视图”(段)。
- 包含动态链接所需信息(如.dynamic节、动态符号表),支持动态库共享。
简单说:ELF 通过头部定位表,用节管理静态代码数据,用段指导动态加载,兼顾编译链接与运行加载需求。
链接是各个.o文件ELF格式中相同的部分进行合并,并分配统一的虚拟地址
ELF的布局原理
ELF文件的布局是根据虚拟地址空间设置的,虚拟地址空间的划分规则(地址范围、段权限、大小)决定了 ELF 文件中各段的偏移、大小和权限
ELF布局是虚拟地址空间的预处理
- ELF布局预先规划了程序加载到虚拟内存后的 “理想布局”,包括各部分(代码、数据、动态链接信息等)的地址、权限和相对位置
- 操作系统在加载 ELF 可执行文件时,不会从零开始创建全新的虚拟地址空间,而是以文件中预设的虚拟地址布局为基础(从ELF的管理结构中读取信息加载到mm_struct中),进行必要的调整和映射,最终形成进程的虚拟地址空间。这种 “基于预设、局部调整” 的模式,是高效加载程序的关键。
ELF的两个视图
ELF中设置节和段两个单位分别对应编译和运行两个视图。
- 链接视图(Linking View)
- 面向编译器、链接器(如gcc、ld),用于静态链接阶段。
- 以节(Section) 为基本单位,如.text(代码)、.data(已初始化数据)、.symtab(符号表)等。
- 通过节头部表记录所有节的位置、大小和属性,便于链接器进行代码拼接、符号解析和重定位。
- 执行视图(Execution View)
- 面向操作系统加载器、动态链接器(如execve、ld.so),用于程序运行阶段。
- 以段(Segment) 为基本单位,每个段由多个属性相似的节合并而成(如代码段包含.text和.rodata)。
- 通过程序头部表记录段的虚拟地址、内存权限(读 / 写 / 执行)和加载方式,确保操作系统能将文件正确加载到进程虚拟地址空间。
为什么要有两个视图
- 链接阶段(链接视图)需要 “精细拆分”编译器 / 链接器的工作是拼接代码、解析符号、处理重定位,需要将程序按功能拆分为最小单元(节 Section),比如:
- 代码要拆分为.text(指令)、.rodata(只读常量);
- 数据要拆分为.data(已初始化)、.bss(未初始化);
- 还要单独保留符号表(.symtab)、重定位信息(.rel.text)等辅助数据。这种 “按功能拆分” 的精细管理,才能让链接器高效完成模块合并和地址修正。
- 运行阶段(执行视图)需要 “高效合并”操作系统加载程序时,核心目标是快速将文件映射到内存并设置权限,此时需要将 “属性相似的节” 合并为更大的单元(段 Segment),比如:
- 把.text(可执行)和.rodata(只读)合并为 “代码段”,统一设置 “可读可执行” 权限;
- 把.data和.bss合并为 “数据段”,统一设置 “可读可写” 权限。这种 “按内存属性合并” 的方式,能减少内存管理的粒度(按段分配而非按节),大幅提升加载效率和运行性能。
简言之:链接视图为 “静态构建的精细性” 服务,执行视图为 “动态运行的高效性” 服务。
链接与加载
静态链接加载
静态链接的操作:
- 符号解析:匹配代码中引用的函数 / 变量,找到其在目标文件或库中的定义,解决 “引用的东西在哪” 的问题。
- 重定位:为代码和数据分配最终内存地址,将目标文件中的相对地址修正为绝对地址,解决 “放哪” 的问题。
- 合并段:把所有目标文件的代码段(.text)、数据段(.data)等同类段合并,生成符合系统格式的可执行文件。
从磁盘加载到内存:
初始化mm_struct,根据ELF文件中预设的虚拟地址和对应的物理地址填写页表。
将ELF header中的入口地址Entry address(_start函数地址)加载到CPU中开始执行代码。
动态链接加载
动态链接需要将链接延迟到程序加载时,因为编译时无法确定动态库加载到内存的具体地址
动态库不需要拷贝的原因
动态库被设计为独立的、可动态绑定的二进制模块,通过 “符号引用” 而非 “代码嵌入” 的方式与其他程序交互,配合位置无关代码实现高效复用。这种架构从根本上区别于静态库 “代码拷贝合并” 的模式
程序调用的流程
ELF中有一个入口地址向_start函数,cpu从该位置开始执行代码。
_start函数会执行一系列初始化操作:
1.设置堆栈:为程序创建⼀个初始的堆栈环境。
2.初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位 置,并清零未初始化的数据段。
3.动态链接:_start调用动态链接器,来解析和加载程序所依赖的 动态库(sharedlibraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调⽤和变量访问能够正确地映射到动态库中的实际地址。
4.调⽤ __libc_start_main :⼀旦动态链接完成, _start 函数会调⽤ __libc_start_main (这是glibc提供的⼀个函数)。 __libc_start_main 函数负责执⾏⼀些额外的初始化⼯作,⽐如设置信号处理函数、初始化线程库(如果使⽤了线程)等。
5.调⽤ main 函数:最后, __libc_start_main 函数会调⽤程序的 main 函数,此时程序的执⾏控制权才正式交给⽤⼾编写的代码。
6.处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回 值,并最终调⽤ _exit 函数来终⽌程序
加载过程
其它步骤和静态文件加载相同,而动态链接器打开动态库文件后将其映射到进程的共享区,共享区对应的vm_area_struct中有动态库文件的file指针,由此建立连接。
地址修正:
由于共享区的虚拟地址不能预先完全确定,所以在加载ELF文件中其它部分预设的地址可能与共享区位置存在冲突,这时需要将其它部分的地址修正动态库共享原理:动态库加载到内存中一次(物理地址相同),可以映射到不同进程的虚拟地址空间中实现共享
位置无关码和GOT
动态库中的地址都是相对动态库起始基准的地址,通过库的起始虚拟地址+⽅法偏移量即可定位库中的⽅法
全局偏移表(GOT)
- 在数据段中维护一张地址表(GOT),存储外部符号的实际内存地址
- 代码段通过固定偏移量访问 GOT 中的条目(偏移量在编译时确定,与加载地址无关)
- 动态加载时,由动态链接器更新 GOT 中的绝对地址,代码段无需修改
GOT 的核心作用是将代码段中对外部符号(函数 / 变量)的引用,转换为对一个可修改的地址表的引用,从而实现代码段的完全只读和位置无关。
程序链接表(PLT)
配合 GOT 处理外部函数调用,实现延迟绑定(首次调用时才解析地址): - 每个外部函数对应一个 PLT 条目(存于代码段)
- 首次调用时,PLT 触发动态链接器解析函数地址并更新 GOT
- 后续调用直接通过 GOT 跳转,避免重复解析
延迟绑定的流程:第一次调用时通过PLT查找然后在GOT表中填入地址,后续调用直接使用。避免加载
避免对不使用的方法进行重定位,提高效率
库间依赖注意:
• 不仅仅有可执⾏程序调⽤库
• 库也会调⽤其他库!!库之间是有依赖的,如何做到库和库之间互相调⽤也是与地址⽆关的呢??
• 库中也有.GOT,和可执⾏⼀样!这也就是为什么⼤家为什么都是ELF的格式