- UID
- 1029342
- 性别
- 男
|
不同的处理器指令对于地址的格式和方式都不一样。我们这里采用的是32位的x86处理器,介绍两种寻址方式。
X86基本重定位类型
| 宏定义
| 值
| 重定位修正方法
| R_386_32
| 1
| 绝对寻址修正S + A
| R_386_PC32
| 2
| 相对寻址修正S + A - P
| 注:
A:保存在被修正位置的值,对于32位cpu的话,采用
R_386_PC32寻址的话
它应该为0xFFFFFFFC即-4,它是代表地址的四个字节;而采用
R_386_32寻址,它应该为0.
P:被修正的位置。考虑以下程序
...
1023: 11 11 11
1026:e8
fc
ff ff ff
102b: 11 11 11
...
上述蓝色fc标记处即是被修正的位置,即0x1027.
S:符号的实际地址。也就是第一步中空间和地址分配时得到的符号虚拟地址。
举例来说吧!链接成的可执行文件中,假设main函数的虚拟地址为0x1000,swap函数的虚拟地址为0x2000;shared变量的虚拟地址为0x3000;
绝对地址修正:对shared变量的地址修正。
l
S:shared的实际地址为0x3000;
l
A:被修正位置的值,即0.
所以最后这个重定位修正地址为:0x3000,不变!
相对寻址修正:对符号“swap”进行修正。
l
S:符号swap的实际地址,即0x2000;
l
A:被修正位置的值,即0xFFFFFFFC(-4);
l
P:被修正位置,及0x1027
最后的重定位修正地址为:S + A -P = 0x2000 +(-4)- 0x1027 = 0xFD5.即修正后的程序为:
...
1023: 11 11 11
1026:e8
d5 0f 00 00
102b: 11 11 11
...
发现熟悉的规则了吗?下一条指令(PC)的地址为0x102b,加上这个修正值正好等于0x2000,
0x102b + 0xFD5 = 0x2000,刚好是swap函数的地址。
以上内容没有涉及到c标准库,仅仅是自己实现的两个c语言程序之间的链接状况,也就是“程序里面的printf怎么处理”没有说明。这里,我们就要提及“静态库”的概念。其实一个静态库可以简单地看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。与静态库链接的过程是这样的:ld链接器自动查找全局符号表,找到那些为决议的符号,然后查出它们所在的目标文件,将这些目标文件从静态库中“解压”出来,最终将它们链接在一起成为一个可执行文件。也就是说只有少数几个库和目标文件被链接入了最终的可执行文件,而非所有的库一股脑地被链接进了可执行文件。
装载部分:
首先,小议一下动态链接。动态链接其实有分为装载时链接和运行时链接,在这里,我们只考虑装载时链接而不考虑运行时链接。
为什么要动态链接呢?
主要原因有两个:第一,考虑内存和磁盘空间。静态链接极大地浪费内存空间。因为在静态链接的情况下,假设有两个程序共享一个模块,那么在静态链接后输出的两个可执行文件中各有一个共享模块的副本。如果同时运行这两个可执行文件,那么这个共享模块将在磁盘和内存中都有两个副本,对磁盘和内存造成极大地浪费;第二,程序的更新。一旦程序中的一个模块被修改,那么整个程序都要重新链接、发布给用户。如果这个程序相当的大,那么后果就会更加严重!
动态链接做了什么?
务必知道,动态链接是相对于共享对象而言的。动态链接器将程序所需要的所有共享库装载到进程的地址空间,并且将程序汇总所有为决议的符号绑定到相应的动态链接库(共享库)中,并进行重定位工作。
下面开始说说装载。装载的方式主要有两种:覆盖装入和页映射。因为虚拟存储器的出现,覆盖装入已经被淘汰了。而页映射是虚拟存储机制的一部分,伴随着虚拟存储器的发明而诞生。具体的页映射可以参考《深入理解计算机系统》的第十章“虚拟存储器”。
以Linux内核装载ELF为例简述一下装载过程。当我们在Linux系统的bash下输入一个命令执行某个ELF程序时,在用户层面,bash进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()来执行指定的ELF文件,原先的bash进程继续返回等待刚才启动时新进程结束,然后继续等待用户输入命令。这里需注意,随着一个新进程的出现,操作系统会为它创建一个独立的虚拟地址空间。
【创建虚拟地址空间】我们知道一个虚拟空间由一组映射函数将虚拟空间的各个页映射到相应的物理空间,那么创建一个虚拟空间实际上并不是创建空间而是创建映射函数所需要的数据结构。举例来说,在x86的Linux下创建虚拟地址空间实际上只是分配一个页目录(页表)就可以了,甚至不设置页映射关系,这些映射关系等到后面程序发生“缺页”时在进行设置。
在进入execve()系统调用之后,Linux内核就开始进行真正的装载工作。在内核中,execve()系统调用相应的入口是sys_execve(),作用:参数的检查复制;调用do_execve(),流程:查找被执行的文件,读取文件的前128个字节以判断文件的格式是elf还是其它;调用search_binary_handle(),流程:通过判断文件头部的魔数确定文件的格式,并且调用相应的装载处理程序。ELF可执行文件的装载处理过程叫load_elf_binary(),它的主要步骤如下:
1,检查ELF可执行文件格式的有效性,比如魔数、程序头表中段的数量。
2,寻找动态链接的“.interp”段,找到动态链接器的路径,以便于后面动态链接时会用上。
3,读取可执行文件的程序头,并且创建虚拟空间与可执行文件的映射关系。
【读取可执行文件的程序头,并且创建虚拟空间与可执行文件的映射关系】创建虚拟空间时的页映射关系函数是虚拟空间到物理内存的映射关系,而这一步所做的事虚拟空间与可执行文件的映射关系。我们知道,当程序发生缺页是,操作系统会为物理内存分配一个物理页,然后将该缺页从磁盘中读取到内存,在设置缺页的虚拟页与物理页之间的映射关系,这样程序才可以得以正常运行。但是明显的一点是,当操作系统捕获到缺页错误时,他应当知道程序当前需要的页在可执行文件中的哪一个位置。而这就是虚拟存储与可执行文件之间的映射关系。实际上,这种映射关系仅仅是保存在操作系统内部的一个数据结构。当发生缺页错误是,CPU将控制权交给操作系统,操作系统利用专门的缺页处理例程来查询这个数据结构(映射关系),然后找到所需页所在的虚拟内存区域,以及在可执行文件的偏移,然后把该页加载进物理内存,同时将该虚拟页与物理页之间建立映射关系,最后把控制权还给进程,进程从刚才缺页位置重新开始执行。
4,初始化ELF进程环境。
5,将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,它就是ELF文件的文件头中e_entry所指的地址;对于动态链接的ELF可执行文件,程序入口点就是动态链接器。
【将CPU指令寄存器设置成可执行文件的入口,启动运行】对动态链接来讲,此时就启动了动态链接器。
当load_elf_binary()执行完毕,返回至do_execve()在返回至sys_execve()时,系统调用的返回地址已经被改写成了被装载的ELF程序的入口地址了。所以,当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到ELF程序的入口地址。此时,ELF可执行文件装载完成。接下来就是动态链接器对程序进行动态链接了。
动态链接基本分为三步:先是启动动态链接器本身,然后装载所有需要的共享对象,最后重定位和初始化。
1,动态链接器自举
就我们所知道的,对普通的共享对象文件来说,它的重定位工作是由动态链接器来完成;它也可以依赖于其他共享对象,其中被依赖的共享对象由动态链接器负责链接和装载。那么,对于动态链接器本身呢,它也是一个共享对象,它的重定位工作由谁完成?它是否可以依赖于其他的共享对象文件?
动态链接器有其自身的特殊性:首先,动态链接器本身不可以依赖其他任何共享对象(人为控制);其次动态链接器本身所需要的全局和静态变量的重定位工作由它自身完成(自举代码)。
|
|