程序和进程的区别(P150)
- 程序是一个静态的概念,是一些预先编译好的指令和数据集合的一个文件
- 进程是一个动态的概念,它是程序运行时的一个过程
程序和进程有什么区别
程序(或者狭义上讲可执行文件)是一个静态的概念,它就是一些预先编译好的指令
和数据集合的一个文件;进程则是一个动态的概念,它是程序运行时的一个过程,很
多时候把动态库叫做运行时(Runtime)也有一定的含义。有人做过一个很有意思的
比喻,说把程序和进程的概念跟做菜相比较的话,那么程序就是菜谱,计算机的 CPU
就是人,相关的厨具则是计算机的其他硬件,整个炒菜的过程就是一个进程。计算机
按照程序的指示把输入数据加工成输出数据,就好像菜谱指导着人把原料做成美味可
口的菜肴。从这个比喻中我们还可以扩大到更大范围,比如一个程序能在两个 CPU上
执行等。
装载的方式(P152)
动态装载的两种方法
- 覆盖装入(Overlay)
- 页映射(Paging)
动态装入的基本原理:程序的局部性原理
程序执行时所需要的指令和数据必须在内存中才能够正常运行,最简单的办法就是将程序运行所需要的指令和数据全都装入内存中,这样程序就可以顺利运行,这就是最简单的静态装入的办法。但是很多情况下程序所需要的内存数量大于物理内存的数量,当内存的数量不够时,根本的解决办法就是添加内存。相对于磁盘米说,内存是品贵且稀有的,这种情况自计算机磁盘诞生以来一直如此。所以人们想尽各种办法,希望能够在不添加内存的情况下让更多的程序运行起来,尽可能有效地利用内存。后来研究发现,程序运行时是有局部性原理的。所以我们可以将程序最常用的部分驻留在内存中,而将一些不太常用的数搭存放在磁盘里面,这就是动态装入的基本原理
页映射(P156)
概括:
一张图足以
详情:
将内存和所有磁盘中的数据和指令按照“页”为单位划分为若干个页,所有的装载和操作的单位都是页。
进程的建立(P157-P159)
从操作系统的角度来看,一个进程最关键的特征是它拥有独立的虚拟地址空间,这使得它有别于其他进程。
创建进程的三件事情:
- 创建一个独立的虛拟地址空间
- 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。
- 将 CPU 的指令寄存器设置成可执行文件的入口地址,启动运行。
- 首先是创建虛拟地址空间。回忆第1章的页映射机制,我们知道一个虚拟空间由一组贞
映射函数将虚拟空间的各个页映射至相应的物理空间,那么创建一个虛拟空间实际上并不是创建空间而是创建映射函数所需要的相应的数据结构,在 i386 的Linux 下,创建虚拟地址空间实际上只是分配一个页目录 (Page Directory)就可以了,甚至不设置页映射关系,这些映射关系等到后面程序发生页错误的时候再进行设置。- 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。上面那一步的页映射关系两数是虛拟空间到物理内存的映射关系,这一步所做的是虛拟空间与可执行文件的映射关系。我们知道,当程序执行发生页错误时,操作系统将从物理内存中分配
一个物理页,然后将该“缺页,从磁盛中读取到内存中,再设置缺页的虛拟页和物理页的映射关系,这样程序才得以正常运行。但是很明显的一点是,当操作系统捕获到缺页错误时,它应知道程序当前所需要的页在可执行文件中的哪一个位置。这就是虛拟空间与可执行文件之间的映射关系。从某种角度来看,这一步是幣个装载过程中最重要的一步,也是传统意义上 “装载”的过程由于可执行文件在装载时实际上是被映射的虛拟空间,所以可执行文件很多时候又被
叫做映像文件 (lmage)。
页错误
segment VS section
- segment: 从装载的角度看,ELF 文件按照 segment 划分
- section:从链接的角度看,ELF 文件是按照 section 存储的
当我们站在操作系统装载可执行文件的角度看问题时,可以发现它实际上并不关心可执行文件各个段所包含的实际内容,操作系统只关心一些跟装载相关的问题,最主要的是段的权限(可读、可写、可执行)。
那么我们可以找到一个很简单的方案就是:对于相同权限的段,把它们合井到一起当作一个段进行映射。
从链接的角度看,ELF 文件是按"Section"存储的,事实也的确如此;从装载的角度看,ELF 文件又可以按照"Segment"划分.
“Segment"的概念实际上是从装载的角度重新划分了 ELF 的各个段。在将目标文件链接成可执行文件的时候,链接器会尽量把相同权限属性的段分配在同一空间。
在ELF中把这些属性相似的、又连在一起的段叫做一个"Segment”,而系统正是按照"Segment"而不是"Section"来映射可执行文件的。
总的来说,“Segment"和"Section"是从不同的角度来划分同一个ELF文件。这个在ELF中被称为不同的视图(View),从"Section"的角度来看ELF 文件就是链接视图(Linking View),从"Segment"的角度来看就是执行视图(Execution View)。当我们在谈到ELF 装载时,“段”专门指"Segment”;而在其他的情况下,“段”指的是"Section"。
P164
ELF 的可执行文件和共享库文件有一个专门的数据结构叫做程序头表(Program Header Table)用来保存 Segment 信息。因为 ELF 文件不需要被加载,所以它没有程序头表
注:对应 readelf -l 展示的信息
P166
进程的 maps 解释
动态链接
地址无关代码(PIC) P190 - P197
PIC(Position-independent Code)
基本思想:把与地址相关的代码放到数据段中
也就是把代码段里需要变化的部分抽出来放到原本就要变化的数据段中,使代码段成为无需变化的
将共享对象模块中的地址引用
按照是否为跨模块分为两类:模块内部引用和模块外部引用
按照不同的引用方式分为:指令引用和数据访问
-
模块内部
- 指令引用:都是相对地址调用,不需要重定位
- 数据访问:相对寻址
原理:任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,那么只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据(其实就是找到 .data 段的位置,然后在找到对应变量的位置)
-
模块外部
- 指令引用:与模块外部数据访问相同,只不过 GOT 中相应的项保存的是目标函数的地址
- 数据访问:建立全局偏移表,当代码需要引用全局变量时,通过全局偏移表中的条目进行间接引用
注:模块内部定义的全局变量也被当做模块外部的全局变量来处理,因为编译器编译期间无法确认是模块内的还是模块外的
很明显,外部模块的全局变量是和装载地址有关的,也就是“变化”的,按照 PIC 的基本思想,需要把这部分变化的挪到数据段中,而数据段中存放这类数据的叫做全局偏移表(GOT:Global Offset Table)
查找过程:当指令中需要访问变量b时,程序会先找到GOT,然后根据 GOT 中变量所对应的项找
到变量的目标地址。每个变量都对应一个4个字节的地址,链接器在装载模块的时候会查找
每个变量所在的地址,然后填充 GOT 中的各个项,以确保每个指针所指向的地址正确。由
于 GOT 本身是放在数据段的,所以它可以在模块装载时被修改,并且每个进程都可以有独
立的副本,相五不受影响。GOT 如何做到地址无关(P194总结):
关键点:
1、模块编译时就能确定下来内部变量相对当前指令的偏移,也就能确定 GOT 相对于当前指令的偏移
2、把代码中对外部变量的引用转换成了对 GOT 中指定条目的引用,而这个指定条目对应的引用会在动态链接时填充成对应变量真实的地址感觉就是把代码中使用的外部变量换成了一种特殊的内部变量(放在GOT),只不过这个特殊的内部变量指向外部变量的地址,而这个地址会在动态链接时更新成真实的地址,这个解题的思想也挺好,往已解决的问题上靠拢
各种地址引用方式汇总:
指令跳转、调用 | 数据访问 | |
---|---|---|
模块内部 | 相对跳转和调用 | 相对地址访问 |
模块外部 | 间接跳转和调用(GOT) | 间接访问(GOT) |
注意点:编译期间,实际上并不能确定 b 和函数 ext() 是模块外部的还是模块内部的,因为他们有可能被定义在同一个共享对象的其他目标文件中,而由于没法确定,编译器只能把他们当做模块外部的函数和变量来处理
延迟绑定(PLT)P200-P202
基本思想:当函数第一次被用到的时候才进行绑定(符号查找、重定位等),如果没用到就不进行绑定
ELF 将 GOT 拆分成了两个表,叫做 .got 和 .got.plt(与刚才讲的对外部指令和外部数据的引用呼应)
- .got:保存全局变量引用的地址
- .got.plt:保存对外部函数引用的地址
.got.plt 的前三项有特殊含义:
- 第一项保存 .dynamic 段的地址
- 第二项保存的是本模块的 ID
- 第三项保存的是_dl_runtime_resolve()的地址
第二项和第三项由动态连接器(who)在装载共享模块时(when)负责将它们初始化(what)
#readelf -d lib7z.so
Dynamic section at offset 0x8dc8 contains 26 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x000000000000000e (SONAME) Library soname: [lib7z.so]
0x000000000000001a (FINI_ARRAY) 0x9db0
0x000000000000001c (FINI_ARRAYSZ) 16 (bytes)
0x0000000000000004 (HASH) 0x228
0x000000006ffffef5 (GNU_HASH) 0x288
0x0000000000000005 (STRTAB) 0x498
0x0000000000000006 (SYMTAB) 0x2d0
0x000000000000000a (STRSZ) 198 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x9fa8
0x0000000000000002 (PLTRELSZ) 168 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x620
0x0000000000000007 (RELA) 0x5a8
0x0000000000000008 (RELASZ) 120 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW
0x000000006ffffffe (VERNEED) 0x588
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x55e
0x000000006ffffff9 (RELACOUNT) 5
0x0000000000000000 (NULL) 0x0
d_tag 类型 | d_un 的含义 |
---|---|
DT_SYMTAB | 动态链接符号表的地址,d_ptr 表示 “.dynsym” 的地址 |
DT_STRTAB | 动态链接字符串表地址,d_ptr 表示 “.dynstr” 的地址 |
DT_STRSZ | 动态链接字符串表大小,d_val 表示大小 |
DT_HASH | 动态链接哈希表地址,d_ptr 表示 “.hash” 的地址 |
DT_SONAME | 本共享对象的 “SO-NAME”,我们将在后面介绍 “SO-NAME” |
DT_RPATH | 动态链接共享对象搜索路径 |
DT_INIT | 初始化代码地址 |
DT_FINI | 结束代码地址 |
DT_NEED | 依赖的共享对象文件,d_ptr 表示所依赖的共享对象文件名 |
DT_REL | 动态链接重定位表地址 |
DT_RELA | 动态链接重定位表地址(基于增量) |
DT_RELENT | 动态重定位项入口数量 |
DT_RELAENT | 动态重定位项入口数量(基于增量) |
一些段含义及其内容汇总
- .got:保存全局变量引用的地址
- .got.plt:保存对外部函数引用的地址
.got.plt 的前三项有特殊含义:
- 第一项保存 .dynamic 段的地址
- 第二项保存的是本模块的 ID
- 第三项保存的是_dl_runtime_resolve()的地址
第二项和第三项由动态连接器(who)在装载共享模块时(when)负责将它们初始化(what)
- .interp 段存放 可执行文件需要的动态链接器的路径
- .dynamic 段(可以通过 readelf -d 指令查看本段内容)
- 依赖的共享对象
- 符号表位置
- 重定位表位置
- 初始化代码地址
- .symtab(Symbol Table)静态链接中保存关于该目标文件的符号的定义和引用
- .dynsym(Dynamic Symbol Table)动态符号表,保存动态链接模块间的符号导入导出关系
与 .symtab 不同的是, .dynsym 只保存与动态链接相关的符号
很多动态链接的模块同时拥有 .dynsym 和 .symtab 两个表,.symtab 保存了所有符号,包含 .dynsym 中的符号 - .strtab(String Table) 符号字符串表,静态链接符号表的辅助表,用于保存符号名的字符串表
- .dynstr(Dynamic String Table) 动态符号字符串表,动态符号表的辅助表,保存符号名的字符串表
- .hash 符号哈希表,辅助加快程序运行时符号查找过程的表
- .rel.text 静态链接中,目标文件的代码段的重定位表
- .rel.data 静态链接中,目标文件的数据段的重定位表
- .rel.dyn 动态链接文件中,对数据引用的修正,它所修正的位置位于 .got 以及数据段
- .rel.plt 动态链接文件中,对函数引用的修正,它所修正的位置位于 .got.plt
共享对象需重定位的主要原因是因为导入符号的存在
目标文件的重定位是在链接时完成的,共享对象的重定位是在装载时完成的
“got.plt〞的前三项是被系统占据的,从第四项开始才是真止存放导入两数地址的地方。而第四项刚好是 Ox000015c8+4*3=0x000015d4,即"gmon start",第五项是“printf",第六项是"sleep",第七项是"exa finalize"。
当动态链接器需要进行重定位时,它先查找“printr” 的地址,“printf"位于 libc-2.6.1.s0。
假设链接器在全局符号表里面找到“printf” 的地址为 Ox08801234,那么链接器就会将这个
地地址填入到“-got.plt〞中的偏移为 Ox000015d8 的位置中去,从而实现了地址的重定位,即
实现了动态链接最关键的一个生骤。
动态链接器的步骤和实现(P214 - P)
动态链接器自举–> 装载共享对象–>重定位和初始化
动态链接器的特殊性:
- 本身不能依赖于其他任何共享对象
本身所需要的全局和静态变量的重定位工作由它自己完成
动态链接器入口地址就是自举代码的入口
完成基本自举以后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表中,也就是全局符号表(GOT),然后链接器开始寻找可执行文件所依赖的共享对象,之前提到的 .dynamic 段中,有一种类型的入口是 DT_NEEDED,它所指出的是该可执行文件(或共享对象)所依赖的共享对象。由此,链接器可以列出可执行文件所需要的所有共享对象,并将这些共享对象的名字放入到一个装载集合中,然后链接器开始从集合中取一个所需要的共享对象的名字,找到对应的文件后打开该文件,读取相应的 ELF 文件头和 .dynamic 段,然后将它相应的代码段和数据段映射到进程空间。如果这个 ELF 共享对象还依赖于其他共享对象,那么将所依赖的共享对象的名字放到装载集合中。如此循环直到所有依赖的共享对象都被装载进来为止。
当然链接器可以有不同的装载顺序,如果我们把依赖关系看做一个图的话,那么这个装载过程就是一个图的遍历过程,链接器可能会使用深度优先或者广度优先或者其他的顺序来遍历整个图,这取决于链接器,比较常见的算法一般都是广度优先的。
全局符号介入(Global Symbol Interpose):一个共享对象里面的全局符号被另一个共享对象的同名全局符号覆盖的现象
Linux 动态链接器处理规则:当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略