CSAPP_Chapter7
本文章是观看学习B站up九曲阑干大佬所记的笔记,下附大佬的B站主页链接:
九曲阑干的个人空间-九曲阑干个人主页-哔哩哔哩视频 (bilibili.com)
链接 —— Linking
引入
概念
链接是将各种代码和数据收集并组合成一个文件的过程,最终得到的文件可以被加载到内存执行
链接的实现
- 早期:手动完成
- 现在:链接器完成
应用
大型应用程序开发过程中,将各个功能实现分解为更小更容易管理的模块
当修改其中的一个模块时,只需要重新编译修改后的模块,无需重新编译其他模块
学习原因
1. 构建大型程序时
可能遇到缺少库文件或者版本不兼容导致的链接错误
- 需要我们理解链接器是如何使用库文件来解析引用
2. 避免一些难以发现的编程错误
3. 理解编程语言中的作用域规则
全局变量和局部变量的区别
static属性的实际意义
4. 理解重要的系统概念
例如程序的加载和运行、虚拟内存、内存映射……
5. 更好地利用共享库
随着共享库和动态链接在现代操作系统中变得越来越重要,链接也逐渐变得复杂
具体示例
![[CSAPPJQLG7-1.png]]
![[CSAPPJQLG7-2.png]]
输入该命令可得到可执行程序
prog
-Og
表示代码优化等级(告诉编译器生成的机器代码要符合原始C代码的结构,可以方便调试,实际使用中为了程序的性能会选择-O1
或-O2
的优化选项)-o prog
选项用来指定可执行文件的名字是prog (若不指明,默认可执行文件为a.out)
生成可执行程序的过程
预处理
main.c
-> main.i
![[CSAPPJQLG7-3.png]]
cpp
指的是c预处理器cpp
或gcc
这两条指令都可以使用-E
用来限制gcc只进行处理,不做编译、汇编、链接处理main.i
是一个ASCII码的中间文件
编译
main.i
-> main.s
![[CSAPPJQLG7-4.png]]
c
指C编译器-S
表示只对文件进行编译,不做汇编和链接处理
汇编
main.s
-> main.o
![[CSAPPJQLG7-5.png]]
- 使用汇编器
as
将main.s
翻译成一个可重定位目标文件文件main.o sum.o
获得过程与main.o
类似
链接
使用链接器ld构建可执行文件
链接所需要的文件如下:
![[CSAPPJQLG7-6.png]]
crt
:c runtime
- 链接就是将这些文件打包成一个可执行文件
手动连接的命令如图
![[CSAPPJQLG7-7.png]]
ld
表示链接器-static
表示采用静态链接的方式
运行
- 在shell中输入
./prog
- shell调用操作系统中的加载器(loader)函数来实现
- 加载器将可执行文件prog的代码和数据复制到内存中,并将cup的控制权转移到
prog
的程序开头,随后程序开始运行
总结
链接就是将可重定位目标文件和必要的系统文件组合起来,生成一个可执行目标文件的操作
可重定位目标文件 —— Relocatable Object Files
代码引入
main.c
:
![[CSAPPJQLG7-8.png]]
- 使用gcc将代码文件翻译成可重定位目标文件
-c
表示编译和汇编但是不链接- 使用
wc
(Word Count)命令查看main.o
的大小,-c
表示查看有多少个字节
![[CSAPPJQLG7-9.png]]
可执行目标文件
组成部分
- ELF header
- Sections
- Section header table
![[CSAPPJQLG7-10.png]]
ELF header
- readlf是linux系统提供的工具
-h
选项表示只显示header信息
![[CSAPPJQLG7-11.png]]
开头16字节
![[CSAPPJQLG7-12.png]]
开始4字节
- 被称为ELF的魔术(ELF Magic)
- 魔术:用来确认文件类型(操作系统加载可执行文件时会确认魔术是否正确,如果不正确则拒绝加载)
第5字节
- 表示ELF文件类型
- 0x1 表示32位
- 0x1 表示64位
第6字节
- 表示字节序
- 0x1表示小端法
- 0x2表示大端法
第7字节
- 表示ELF的版本号,通常为1
后9个字节
- ELF标准中没有定义,用0填充
Type
1 | Type: REL(Relocatable file) |
- 表示文件为可重定位目标文件
- 还有两种类型:可执行文件、共享文件
Size of this header
1 | Size of this header: 64(bytes) |
- 根据此信息可以推测中section在elf文件中的起始位置为0x40
Section header table 的信息
1 | Start of section header: 1064(bytes into file ) |
- section headers的起始地址为1064
- 该表一共包含13个表项
- 每个表项共包含64个字节
- 可计算出Section header table的大小为$13*64=832$ 个字节
ELF文件大小
![[CSAPPJQLG7-13.png]]
section header table
-S
表示打印整个表的信息
![[CSAPPJQLG7-14.png]]- 共有13个表
Offset
表示每个表的起始位置,Size
表示表的大小, 根据这两个信息就可以确定每个表在整个ELF中的位置
![[CSAPPJQLG7-15.png]]
.text
- 该部分存放的是已经编译好的机器代码
- 查看已编译好的机器代码需要使用反汇编工具objdump, 命令如下
![[CSAPPJQLG7-16.png]]1
0000 554889e5 4883ec10 ……
- 左边为指令地址(0000), 右边是这部分具体的机器指令(554889e5 4883ec10 ……)
- 每个字节所对应的汇编代码也可以通过反汇编得到
![[CSAPPJQLG7-17.png]]
.data
.data section
用来存放已初始化的全局变量和静态变量的值
.bss
作用
.bss section
中存储未初始化的全局变量和静态变量- 被初始化为0的全局和静态变量也被存放在bss中
.data & .bss
- 区分方法,将bss理解为Better Save Space
- 局部变量既不在data中,也不再bss中
观察到的问题
![[CSAPPJQLG7-18.png]]
.bss
的起始地址与.rodata
一致- 此处
.bss
的大小为4字节, 但程序中全局变量value和静态变量b的大小总共应为8字节, 大小不匹配
原因
.bss
实际上并不占用空间,仅是占位符- 区分已初始化和未初始化的变量是为了节省空间, 程序运行时会在内存中分配这些变量并把其初始值设为0
.rodata
rodata
(read only)用来存放只读数据(printf语句格式串,switch语句跳转表……)
其余sections
![[CSAPPJQLG7-19.png]]
符号与符号表
引入
链接的本质就是把不同的文件粘合在一起,为了是这些文件能相互粘合,这些目标文件之间必须有固定的规则才行,我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能完成
.symtab
每一个ELF文件都有一个符号表,该符号表包含该模块定义和引用的符号信息
![[CSAPPJQLG7-20.png]]
![[CSAPPJQLG7-21.png]]
列表说明
- Value(hex):表示相对于所在section起始位置的偏移量
- Size:所占字节数
- Type:
- Bind:
- Vis:
- Ndx:表示的是section的索引值,可以通过查看section header table来确定
- Name:符号名
函数
main & func
- main和func是两个函数,其
Type
为FUNC
- 两函数全局可见,其
Bind
(binding)字段也是全局 - 所在位置为
.text
,其Ndx
为1
printf
- 在源文件中只是被引用,定义不再
main.c
中,所以其Ndx为UND(undefined)
全局变量
count & value
Type
为Object
,表示符号为数据对象Ndx
值不同(count经过初始化,在.data
中,value没有被初始化,在.common
中)Bind
均为GLOBAL
Common
COMMON
:存放未初始化的全局变量.bss
:存放未初始化的静态变量,初始化为0的全局或静态变量
局部静态变量
a & b
Type
为Object
,表示符号为数据对象Ndx
值不同(a经过初始化,在.data
中,b被初始化为0,在.bss
中)Bind
均为LOCAL
Name
变为了a.2254和b.2255 ,这种处理方式被称为名称修饰,防止静态变量的名字冲突
符号名无显式表项
- 这些符号的符号名就是他们section的名称
- 例如
Ndx
为1但无符号名的符号,其符号名实际上就是.text
局部变量
- 局部变量x并没有出现在符号表中
- 局部变量在运行时被中被管理,链接器对此类符号并不感兴趣,所以局部变量的信息不会出现在符号表中
Symbol
在链接器的上下文中,有三种不同的符号
Global Symbols(全局符号)
- 被该模块定义同时能被其他模块引用的全局符号
Externals Symbols(外部符号)
- 被其他模块定义同时能被该模块引用的全局符号
Local Symbols(局部符号)
- 只能被该模块定义和引用的局部符号
static
- 区别局部符号和全局符号的关键是static属性
- 带有static属性的函数以及变量是不能被其他模块引用的
- 对于c语言来说,static属性的功能就是隐藏模块内部的变量以及函数声明,任何带有static属性声明的全局变量或者函数都是模块私有的,也就是说任何不带static属性声明的全局变量或者函数都是公共的
符号解析和静态库(Static Library)
符号解析
符号解析会面临两个问题
- 找不到符号的定义
- 找到了符号的多个定义
找不到符号定义
下面是代码示例:
![[CSAPPJQLG7-22.png]]
- 图中代码仅对函数foo进行了声明,并且调用了foo
- 该代码文件可以被编译和汇编,因为当编译器遇到一个不是在当前模块定义的符号时,它会假设该符号是在其他某个模块中定义的,并且可以在符号表中看到汇编器为它生成了相应的符号
- 但在链接生成可执行文件时,链接器在其他输入模块中找不到这个被引用符号的定义,链接器就会输出错误信息终止链接操作
![[CSAPPJQLG7-23.png]]
Strong symbols(强符号) & Weak symbols(弱符号)
在编译时,编译器向汇编器输出每个全局符号,或是强符号或是弱符号,接下来汇编器把这个强弱信息隐含地编码在符号表中
- 强符号:函数和已初始化的全局变量
- 弱符号:未初始化的全局变量
static
以下内容来自GPT-4o
在C语言中,static
关键字用于限制变量或函数的作用域。在不同的上下文中,static
的作用有所不同,具体来说:
1. 局部变量
当在函数内部使用 static
修饰变量时,这个变量的作用域仅限于该函数,但其生命周期延续整个程序运行期间。每次调用该函数时,static
变量不会重新分配内存,而是保留上次调用的值。
1 | void func() { |
在这种情况下,local_var
不是强符号也不是弱符号,因为它是局部变量,不会出现在符号表中。
2. 全局变量
当在文件作用域(即函数外部)使用 static
修饰变量时,这个变量的作用域仅限于该文件(编译单元)。这意味着其他文件无法直接访问该变量。此时,static
修饰的全局变量不会被当作强符号或弱符号导出给链接器。
1 | static int file_var = 0; // 文件静态变量 |
在这种情况下,file_var
是一个内部链接的符号,不会被导出到其他编译单元。因此,它既不是强符号也不是弱符号。
3. 函数
当使用 static
修饰函数时,这个函数的作用域仅限于定义它的文件(编译单元)。这使得该函数在其他文件中不可见,类似于文件静态变量的情况。
1 | static void file_func() { |
同样,file_func
也是一个内部链接的符号,不会被导出,因此它既不是强符号也不是弱符号。
总结
static
修饰的变量或函数都具有内部链接属性,限制其作用域在定义它的文件(编译单元)内。这意味着它们不会被导出为强符号或弱符号供链接器使用。以下是 static
的几个关键点:
- 局部静态变量:仅限于函数内部,不出现在符号表中。
- 文件静态变量:仅限于文件内部,不导出为强符号或弱符号。
- 文件静态函数:仅限于文件内部,不导出为强符号或弱符号。
因此,被 static
修饰的变量或函数不会被链接器处理为强符号或弱符号。
链接器的符号解析规则(如何解析多重定义的符号)
多个同名的强符号一起出现
链接器不允许有多个同名的强符号一起出现,每个强符号只能被定义一次,否则链接器会报错
- 多个相同函数名的函数
![[CSAPPJQLG7-24.png]] - 具有相同的已初始化的全局变量名
![[CSAPPJQLG7-25.png]]
一个强符号和多个同名弱符号一起出现
如果有一个强符号和多个弱符号,选择强符号,对弱符号的引用会被解析成强符号
![[CSAPPJQLG7-26.png]]
- bar3.c中的x并未被初始化,属于弱符号;foo3.c中的x被初始化,属于强符号
- 编译器会选择在foo3.c中定义的强符号,此时编译器可以生成可执行文件,不会报错或警告
多个弱符号一起出现
如果有多个弱符号,任意选择一个
为了避免任意选择一个所带来的难以发现且危险的错误,可以在编译时添加-fno-common
的编译选项来覆盖这条规则,即gcc -fno-common
(现在已经成为了默认选项)
该选项会告诉编译器,当遇到多重定义的全局符号是,触发一个错误
(-Werror
选项会把所有的警告都变为错误)
![[CSAPPJQLG7-27.png]]
当同名若符号类型不同时,会出现很难被改成的错误:
![[CSAPPJQLG7-28.png]]
![[CSAPPJQLG7-29.png]]
静态库
概念
- 在linux系统中,静态库以一种称为archive的特殊文件格式(.a)存放在磁盘上
- archive文件是一组可重定位目标文件的集合
静态库 (.a 存档文件)
- 将一些相关联的可重定位目标文件连接成一个带索引的文件(称为
档案 archive) - 改进链接器,使它可以在一个或多个档案文件中寻找符号,以解析
未被解析的外部引用 - 如果某个档案文件能够解析某个引用,就将它链接到可执行文件中
构造静态库
![[CSAPPJQLG7-30.png]]
![[CSAPPJQLG7-31.png]]
静态库的使用
![[CSAPPJQLG7-32.png]]
![[CSAPPJQLG7-33.png]]
具体的链接过程
- 当链接器运行时,它确定
main.o
中引用了addvec.o
中定义的addvec
符号 - 所以链接器从
libvector.a
中复制addvec.o
到可执行文件 - 因为程序中没有用到
multvec.o
中定义的符号,所以链接器就不会将这个模块复制到可执行文件 - 除此之外,链接器还会从libc.a中复制printf.o模块以及其他C runtime所需的模块
补充部分
一下内容源自WHU提供的翻译后的CMU的ppt
![[CSAPPJQLG7-34.png]]
![[CSAPPJQLG7-35.png]]
![[CSAPPJQLG7-36.png]]
静态库的解析过程 —— Static Linking
符号解析阶段
链接器从左到右按照命令行中出现的顺序来扫描可重定位文件和静态库文件
e.g.linux> gcc -static -o prog main.o ./libvector.a
对上述指令,链接器先处理main.o,在处理libvector.a,最后处理libc.a (编译器驱动程序总是会把libc.a传给链接器,所以命令中无需显示的引用libc.a)
扫描过程中,链接器一共维护了三个集合
- 集合E:在链接器扫描的过程中发现了可重定位目标文件就会放到这个集合中,在链接即将完成时,这个集合中的文件最终会被合并起来形成可执行文件
- 集合U:链接器会把引用了但是尚未定义的符号放在这个集合里
- 集合D:它用来存放输入文件中已定义的符号
链接开始时,这三个集合均为空
对于命令行上的每一个输入文件f,链接器都会判断其为目标文件还是静态库文件
如果f为目标文件,那么链接器把f添加到集合E中,同时修改集合U和D来反应f中的符号定义和引用
e.g.
对于main.o文件,将其添加到集合E中,并且把尚未定义的符号addvec
和printf
加入集合U中,把已定义的符号x y z main
放入集合D中
![[CSAPPJQLG7-37.png]]
如果f为静态库文件,那么链接器就尝试在这个静态库文件中寻找集合U中未解析的符号
e.g.
当链接器发现成员addvector.o
中存在未定义的符号addvec
的定义,此时就把addvector.o
添加到集合E中,然后将集合U中的符号addvec
删除。
如果addvector.o
中还定义了其他的符号,则还要添加到集合D中,所以addcnt
也要被添加到集合D中
![[CSAPPJQLG7-38.png]]
- 对于静态库文件中的所有成员目标文件,都要依次进行上述处理过程,直到集合U和集合D不再发生变化
- 此时,任何不包含在集合E中的成员目标文件都被简单的丢弃
e.g.
最后链接器扫描libc.a
文件,printf.o
被加入集合E,集合U中的printf被删除
![[CSAPPJQLG7-39.png]]
完成对所有输入文件的扫描后:
- 如果集合U为空,链接器会合并集合E中的文件来生成可执行文件
- 如果集合U是非空的,那就说明程序中使用了未定义的符号,此时链接器输出一个错误并终止
存在的问题
命令行上的目标文件和库文件的输入顺序非常重要
e.g.
交换libvector.a
和main.o
,当链接器处理libvector.a
时,集合U是空的,所以没有libvector.a
中的成员会被添加到集合E中
接下来再处理main.o
,addvec
则会被添加到集合U中,所以对addvec
的引用不会被解析
最终链接器会产生错误信息并终止
![[CSAPPJQLG7-40.png]]
通常情况下,关于库的一般使用准则就是将它们放在命令行的结尾:
- 如果各个库的成员是相互独立的(不同库的成员没有相互引用):这些库可以按照任意的顺序可以按照任意顺序放置到命令行的结尾处
- 如果库不是独立的,就必须对他们进行排序,以保证符号能被正常解析
e.g.
![[CSAPPJQLG7-41.png]]
- case1:
liby.a
必须出现在libx.a
和libz.a
之后 - case2:库之间直接存在相互引用,那么库文件可以重复出现(另一种解决方法就是将
libx.a
和liby.a
合并成一个静态库文件
静态库的明显缺点
- 需要定期维护和更新
也就是说,如果有个开发人员需要使用这个静态库,他就必须以某种方式获取到该库的更新情况,然后将更新后的静态库与他们编写的程序重新进行连接,这样程序才能使用到最新版的静态库 - 几乎每个C程序都要使用标准的I/O函数
在运行时,这些函数的代码会被复制到每个进程的代码段中,对于一个运行了成百上千个进程的系统,这种方式对于内存资源来说是一种极大的浪费
![[CSAPPJQLG7-42.png]]
重定位 —— Relocation
引入
当链接器把代码中的符号引用和对应的符号定义关联起来之后,链接器就可以确定将哪些目标文件进行合并了,同时,链接器也获得了这些目标文件的代码节和数据节的大小信息,接下来就开始进行重定位的操作
重定位的过程
该过程中,链接器合并输入模块,并为每个符号分配运行时地址,具体过程分为两步:
- Relocating sections and symbol definitions(重定位节和符号定义)
- Reloacting symbol references within sections(重定位符号引用)
代码示例
![[CSAPPJQLG7-43.png]]
第一步
链接器把main.o
和sum.o
中所有相同类型的section合并为一个新的section
![CSAPPJQLG7-44.png]]
例如图中新合成的.text
就是可执行文件的text section
合成前,main.o
和sum.o
中的.text
都是从0开始的
书中假定合成后的.text
起始地址是0x4004d0,原因是在64位的linux系统中,ELF可执行文件默认从地址0x400000处开始分配,由于ELF的header和text section之前还有一些其他的信息,所以假定text section从4004d0处开始
这一步完成之后程序中每条指令和全局变量都有了唯一的运行时内存地址
第二步
例如main
中调用了函数sum
,但如图所示,call的目的地址是0
![[CSAPPJQLG7-45.png]]
显然这并非是sum
真正的地址,所以在这一步中链接器需要修改对符号sum
的引用,使其指向正确的运行地址
执行这一步,链接器需要依赖于可重定位条目的数据结构
Relocation Entries(重定位条目)
概念
当汇编器在生成可重定位目标文件时,并不知道数据和代码最终放在内存的什么位置,也不知道该模块所引用的外部定义的函数以及全局变量的位置
所以,当汇编器遇到最终位置不确定的符号引用时,它就会产生一个重定位条目
功能
用来告诉链接器在合成可执行文件时应该如何修改这个引用
位置
- 对于代码的重定位条目放在
.rel.text
中 - 对于已初始化数据的重定位条目放在
.rel.data
中
结构体定义
![[CSAPPJQLG7-46.png]]
- offset:表示被修改的引用的节偏移量
- type:重定位类型。链接器根据type修改新的引用
- symbol:表示被修改的引用是哪一个符号
- addend:是一个常数,一些类型的重定位要使用它对被应用的值做偏移调整
重定位类型(type)
ELF中定义了32中不同的重定位类型,重点有以下两种
- R_X86_64_PC32(PC 相对地址)
- R_X86_64_32(绝对地址)
链接器如何使用重定位条目进行重定位
本例中,汇编器产生了两个重定位条目
![[CSAPPJQLG7-47.png]]
重定位相对引用 (以sum为例)
![[CSAPPJQLG7-48.png]]
0xe8
表示指令call的操作码- 重定位前,操作码后的内容被汇编器填充为0
![[CSAPPJQLG7-49.png]]
- 链接器根据sum的重定位条目来确定被填充部分的具体内容
- 链接器根据重定位条目计算出引用的运行时地址:通过函数main的起始地址(在重定位的第一步可以得到,假设是0x4004d0)与重定位条目中的偏移量字段相加(得到结果0x4004df)
- 更新这个符号引用,使得它在运行时指向sum函数,其计算方法:用sum的起始地址(假设为0x4004e8)减去计算得到的运行时地址,然后加上addend字段(默认为-4)做一下修正
![[CSAPPJQLG7-50.png]]
经过计算,在最终的可执行程序中,call指令的形式如上图
- call指令的地址在
0x4004de
处 - CPU执行call指令时,PC(正在执行指令的下一条指令的地址)的值为0x4004e3
- call的执行分为两步:
- CPU将PC的值压入栈中(函数调用后要接着执行下面的add指令)
- 修改PC的值,修改方式为:PC当前值加上偏移量(根据上述计算,偏移量为0x5)
- PC的值加上0x5刚好为0x4004e8,就是函数sum的第一条指令
重定位绝对引用(以array为例)
![[CSAPPJQLG7-51.png]]
- offset字段告诉编译器要从偏移量0xa(相对于main函数地址的偏移量)开始修改
- addend字段默认为0
假设链接器已经确认array所在的data section位于0x601018处
![[CSAPPJQLG7-52.png]] ![[CSAPPJQLG7-53.png]]
完成重定位
![[CSAPPJQLG7-54.png]]
当程序执行加载的时候,加载器会把这些section中的字节直接复制到内存里,不用执行任何修改就可以执行
可执行目标文件(Executable Object Files)
文件格式
![[CSAPPJQLG7-55.png]]
- 左侧为可执行文件的格式
- 右侧为可重定位目标文件的格式
- ELF header:描述文件的总体格式,其中有一项是程序的入口,也就是程序运行时要执行的第一条指令的地址
.init
节定义了一个名为_init的函数,代码会调用这个函数进行初始化.text
.rodata
.data
中与可重定位目标文件的节是类似的,不过它们已经被重定位到最终的运行时内存地址上,因此,可执行文件中不再需要.rel
文件
程序运行时,可执行文件代码段和数据段会被加载到内存执行,还有一部分不会被加载到内存(例如符号表和调试信息等)
![[CSAPPJQLG7-56.png]]
程序头部表(Program Header Table)
- 程序头部表中描述了代码段,数据段与内存的映射关系
![[CSAPPJQLG7-57.png]]
代码段与内存的映射关系
- flag标志每段的权限(rwx)
- off表示这个段在可执行文件中的偏移量
- vaddr 和 paddr表示这个段开始位置的内存地址
- filesz表示代码段的在目标文件中的大小
- memsz表示在内存中所占的大小(与filesz一致)
- 这些内容包括ELF header、 程序头部表已经
.init
.text
.rodata
节的内容
数据段与内存的映射关系
- flag标志每段的权限(rwx)
- off表示这个段在可执行文件中的偏移量
- vaddr 和 paddr表示这个段开始位置的内存地址
- filesz表示此段在目标文件中的大小
- memsz表示此段在内存中所占的大小(多出来8个字节:用来存放
.bss
的数据。.bss
不占用可执行文件的空间,但是在运行时会把其中的数据初始化为0) - 这些内容包括ELF header、 程序头部表已经
.init
.text
.rodata
节的内容
段加载到内存时的一种优化
- 对于任何一个段,链接器必须选择一个起始地址vaddr,使得 $vaddr \mod\ align = off \mod\ align$
- off表示段在可执行文件中相对于起始位置的偏移量
- align是程序表中指定的对齐量
- 这种对齐要求属于优化的一种,可以使程序在执行时,可执行文件中的段能更高效的传送到内存中
如何加载运行
- 运行prog,在shell中输入
./prog
- 所有的linux程序都可以通过调用
execve
来调用加载器 - 加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后跳转到程序的入口来运行该程序
- 将程序从磁盘复制到内存并运行的过程叫做加载
![[CSAPPJQLG7-58.png]]
- 每一个Linux程序都有一个运行时内存镜像
- 在linux系统中代码段总是从0x4000000开始,然后是数据段
- 运行时堆在数据段之后,堆的生长方向是从低地址到高地址
- 堆后面的区域为共享模块保留,这个区域将堆和栈隔开
- 用户栈的起始地址是2^48 - 1,这是最大的合法用户地址,栈的生长方向从高地址到低地址
- 再往上,地址从2^48 - 1开始,是为操作系统的代码和数据保留的,这部分内存空间对用户代码不可见
- 实际上,由于数据段有地址对齐的要求,所以代码段和数据段之间有间隙
- 防止程序受到攻击,在分配栈,共享库已经堆的运行地址时,采用栈随机化的策略,每次程序运行时这些区域的地址位置会改变,但相对位置是不变的
- 当加载器运行时,他会为程序创建如图的内存镜像,根据头部程序表的内容,加载器将可执行文件的section复制到内存相应的位置,接下来加载器跳转到程序的入口处,也就是_start函数的地址
- (_start函数在系统目标文件
ctrl.o
中定义,对于所有的c程序都是一样的) - start函数调用libc_start_main函数,(位于
libc.so
,作用是初始化执行环境) - 然后,调用用户层的main函数,开始执行可执行程序
prog
中的main函数 prog
执行完毕后,函数main的返回值还是由libc.so
中的这个函数来处理,并且在需要的时候把控制权交还给操作系统
动态链接共享库
共享库
由来
为了解决静态库的缺陷,操作系统提供了一种共享库的技术(Shared Libraries)
概念
- 共享库是一种特殊的可重定位目标文件,在linux系统中通常用.so的后缀来表示(windows系统中.dll文件就属于共享库)
- 共享库在运行或加载时,可以被加载到任意的内存地址,还能和一个在内存中的程序链接起来,这个过程称为动态链接,具体是由动态链接器执行
如何构造共享库
![[CSAPPJQLG7-59.png]]
构造共享库
1 | linux>gcc -shared -fpic -o libvector.so addvec.c mulvec.c |
-shared
选项是指示编译器创建一个共享的目标文件-fpic
选项是告诉编译器生成位置无关的代码(这样共享库才能被加载到任意的内存位置)libvector.so
就是最终得到的共享库
利用共享库构造可执行程序prog2
1 | linux>gcc -c prog2 main.c ./libvector.so |
动态链接(Dynamic Linking)
过程
![[CSAPPJQLG7-60.png]]
- 与利用静态库构造可执行程序相比,似乎只是改变了库的文件后缀
- 但是与静态库不同的是,libvector.so中的代码文件和数据并没有真的被复制到可执行文件prog2中,这个操作只是复制了符号表和一些重定位信息
- 当可执行程序prog2被加载运行时,加载器会发现可执行程序prog2中存在一个名为.interp的section,这个section中包含了动态链接器的路径名。实际上,这个动态链接器本身也是一个共享目标文件(ld-linux.so)
- 接下来,加载器会将这个动态链接器加载到内存中运行,然后由动态链接器执行重定位代码和数据的工作
下面是重定位代码和数据的过程
- 重定位libc.so的代码和数据到某个内存段
- 重定位libvector.so中的代码和数据到另一个内存段
- 重定位prog2中由libc.so和libvector.so定义的符号引用
![[CSAPPJQLG7-61.png]]
上述操作执行后,动态链接器把控制权交给应用程序prog2,从这个时刻开始,共享库的位置就固定了,并且在程序执行的过程中都不会改变
运行时加载和链接共享库
![[CSAPPJQLG7-62.png]]
- linux系统为动态链接器提供了一个简单的接口,允许应用程序在运行时加载和链接共享库
- 使用函数dlopen可以动态地加载共享库libvector.so
- RTLD_LAZY指示链接器将符号解析的操作推迟,直到共享库代码执行时再进行符号解析
- 调用函数dlsym,该函数有两个参数
- 函数dlopen已经打开的共享库的句柄(handle)
- 符号名(addvec),如果符号存在,返回符号的地址,否则返回NULL
- 若不再使用libvector.so,则可以调用函数dlclose来卸载该共享库
强大的功能
- 分发软件(Distributing software)
Windows应用的开发者常常利用共享库来进行软件版本的更新,一般会发布一个共享库的新版本,然后用户下载这些新版本的共享库来替代当前的版本,下一次运行程序时,应用程序将自动链接和加载新的共享库 - 构建高性能的Web服务器(Building high-performance Web Server)
当需要生成个性化的Web页面已经账户余额时,现代高性能的web服务器可以使用动态链接的方法来生成动态内容
(具体思路:将每个生成动态内容的函数打包在共享库中,当一个来自浏览器的请求到达时,Web服务器动态的加载和链接适当的函数,然后直接调用它,由于函数会一直缓存在服务器的地址空间中,所以只需要一个简单的函数调用就可以处理随后的请求了)
在添加或更新已存在函数时,不需要停止正在运行的服务器就可以实现