- UID
- 824598
|
链接和加载--对arm的位置无关性有帮助
网上资料整理得到
地址绑定:从历史的角度
最早的计算机完全是用机器语言进行编程的。程序员需要在纸质表格上写
下符号化程序,然后手工将其汇编为机器码,通过开关、纸带或卡片将其输入到
计算机中.。如果程序员使用符号化的地址,
那他就得手工完成符号到地址的绑定。如果后来发现需要添加或删除一条指令,
那么整个程序都必须手工检查一遍并将所有被添加或删除的指令影响的地址都
进行修改
这个问题就在于名字和地址绑定的过早了。程序员使用符号
化名字编写程序,并通过汇编器完成符号化名字到机器地址的绑定。
如果程序被改变了,那么程序员重新将它汇编,汇编器更新符号化名字到机器地址的绑定。
地址分配的工作推给计算机。
在有操作系统之前,一个程序可以支配机器所有的内存,由于知道计算机中
所有的地址都是可用的,因此它能以固定的内存地址来汇编和链接。但是有了操
作系统以后,程序就必须和操作系统甚至其它程序共享计算机的内存。这意味着
在操作系统将程序加载到内存之前是无法确定程序运行的确切地址的,并将最终
的地址绑定从链接时推延到了加载时。现在链接器和加载器已经将这个工作划分
开了,链接器对每一个程序的部分地址进行绑定并分配相对地址,加载器完成最
后的重定位步骤并赋予的实际地址。
随着硬件重定位(MMU)和虚拟内存的出现,由于每一个程序可以再次拥有整个地
址空间,因此链接器和加载器变得不那么复杂了。由于硬件(而不是软件)重定
位可以对任何加载时重定位进行处理,程序可以按照被加载到固定地址(虚拟地址)
的方式来链接。
链接过程 :
把每个输入文件的一系列的段(segments),连续存放在输出文件中的代码或数据块
两遍链接:
1.首先对输入文件进行扫描,得到各个段的大小,并收集对所有符号的定义和引用。
创建(一个列出输入文件中定义的所有段的)段表和(包含所有导出、导入符号的)符号表
2.利用第一遍扫描得到的数据,链接器可以为符号分配数字地址,决定各个
段在输出地址空间中的大小和位置,并确定每一部分在输出文件中的布局。
3.第二遍扫描会利用第一遍扫描中收集的信息来控制实际的链接过程。它会
读取并重定位目标代码,为符号引用替换数字地址,调整代码和数据的内存地址
以反映重定位的段地址,并将重定位后的代码写入到输出文件中。
通常还会再向输出文件中写入文件头部信息,重定位的段和符号表信息
;------------------------------------------------------------------------------------------------------------------------
重定位和代码修改 :
链接器和加载器的核心动作是重定位和代码修改
当编译器或汇编器产生一个目标代码文件时, 它使用文件中定义的未重定位代码
地址和数据地址来生成代码,对于其它地方定义的数据或代码通常就是0。作为链接
过程的一部分,链接器会修改目标代码以反映实际分配的地址。
例如,考虑如下这段将变量 a中的内容通过寄存器 eax 移动到变量 b 的 x86 代码片段。
mov a,%eax
mov %eax,b
如果 a 定义在同一文件的位置 0x1234,而 b 是从其它地方导入的,那么生
成的代码将会是:
A1 34 12 00 00 mov a,%eax ;现在变量a位置 = 0x1234
A3 00 00 00 00 mov %eax,b ;现在变量b位置 = 0
每条指令包含了一个字节的操作码和其后 4 个字节的地址。第一个指令有
对地址 1234 的引用(由于 x86 使用从右向左的字节序,因此这里是逆序),
而第二个指令由于 b 的位置是未知的因此引用位置为 0。
现在想象链接器将这段代码进行连接,a 所属段被重定位到了 0x10000,b
最终位于地址 0x9A12。则链接器会将代码修改为:
A1 34 12 01 00 mov a,%eax ;现在变量a位置 = 0x11234
A3 12 9A 00 00 mov %eax,b ;现在变量b位置 = 0x9A12
也就是说,链接器将第一条指令中的地址加上0x10000,现在它所标识的 a
的重定位地址就是 0x11234,并且也补上了b 的地址。
虽然这些调整影响的是指令,但是(目标文件中数据部分任何相关的)指针也必须修改。
;--------------------------------------------------------------------------------------------------------
链接:一个真实的例子
实例篇——静态地址重定位时一个编译链接加载的简单示例
代码段:
static int gVar;
extern put_record(int iNum);
int proc_a( int arg ){
...
gVar = 7;
put_record(gVar);
...
}
1. gVar是一个模块内局部变量(因为static关键字),编译器为其在proc_a同一模块内分配内存空间。
假设给其分配到相对位置0x36.并在模块内符号表里记录符号的内存地址。
2. 外部应用和定义组成了一张External reference table.编译器在链接时会对ERT中的每一个表项,
在全局范围内查找其绝对位置(通过查找别的模块的符号表),将变量的内存地址写入符号表中。
3. 对于在当前模块外的put_record()函数入口地址,编译器会在编译生成的中间代码中(.lib中),
临时的将变量符号名填入需要用到变量的地方,等待在链接时替换为其绝对地址。
通过编译过程,生成的中间代码如下:
相对地址 代码
0000 ....
0008 entry proc_a
0036 [space for gVar variable]
220 load R1,=7
0224 store R1,0036
0228 push 0036
0232 call 'put_record'
0400 [external definition table]
0404 put_record 0232
....
0600 [optional symbol table]
0604 gVar 0036
0630 proc_a 0008
....
0799 [last location in the module]
链接时将各个可重定位模块绑定到一起。首先用户声明的前后顺序,将各个模块连接起来。
在一个个模块绑定的时候,会根据模块被连接到的绝对基地址,对模块内被引用了的地址进行重新运算。
在将各个模块连接到一起后,链接程序搜索整个模块中的外部地址引用表[external definition table],
将相应位置的临时符号(如这里的'put_record')替换为该符号的实际地址。
在链接过程以后,external definition table失去了作用,被删去,而符号表被汇集成一个全局符号表,
可以有选择的保存在程序的某些位置(通常是尾部)。
上面的模块经过链接后,得到下面的代码:
相对地址 生成的代码
0000
....
1008 extry proc_a
....
036 [space for gVar variable]
....
1220 load R1,=7
1224 store R1,1036
1228 push 1036
1232 call 2334
1399 (end of proc_a)
(other modules)
2334 entry put_record
....
2670 (optional symbol table)
....
gVar 1036
proc_a 1008
put_record 2334
2999 (last location in the module)
通过上面的过程,便形成了一个可以被加载器加载的二进制文件(.out,.bin等等【不清楚.exe算不算,所以没列出来】)
。当然,二进制文件也可以通过无损压缩,形成容量小的多的镜像文件(如.srec)在实际加载程序时(如用户要求操作系
统加载文件系统中的.out文件,或者用户双击了一个.exe文件),操作系统的加载程序会将二进制代码拷贝到内存中,
并在运行前做最后一次地址重定位。假设程序被加载到从内存地址4000开始放置。则程序中所有要访问到的程序、
数据位置都要在加载前调整。
调整后的最终内存中程序如下?
0000 (别的程序,这个地址多半被映射到ROM上去了)
4000 (other module)
5008 extry proc_a
....
5036 [space for gVar variable]
....
5220 load R1,=7
5224 store R1,5036
5228 push 5036
5232 call 6334
5399 (end of proc_a)
(other modules)
6334 entry put_record
....
6670 (optional symbol table)
....
gVar 5036
proc_a 5008
put_record 6334
6999 (last location in the module)
最后来小结一下:
1.符号表的作用是为了debug的时候能让debugger把代码和当前内存位置对上号。
当然如果没有符号表代码尺寸会减小很多。所以final release里面肯定不能带符号表。
2.外部引用符号表主要是在编译阶段使用,在链接确定外部符号地址后,这个表项就可以删除了。
;-------------------------------------------------------------------------------------
加载域(VMA)和运行域(LMA)
;--------------------------------------------------
源程序m.c
extern void a(char *);
int main(int ac, char **av)
{
static char string[] = "Hello, world!\n";
a(string);
}
;--------------------------------------------------
源程序a.c
#i nclude <unistd.h>
#i nclude <string.h>
void a(char *s)
{
write(1, s, strlen(s));
}
;----------------------------------------------------
图1-4 m.o的目标代码
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000010 00000000 00000000 00000020 2**3
1 .data 00000010 00000010 00000010 00000030 2**3
Disassembly of section .text:
00000000 <_main>:
0: 55 pushl %ebp
1: 89 e5 movl %esp,%ebp
3: 68 10 00 00 00 pushl $0x10
4: 32 .data
8: e8 f3 ff ff ff call 0
9: DISP32 _a
d: c9 leave
e: c3 ret
10: .data ;size=16
"Hello, world!\n";
;--------------------------------------------------
图1-5 a.c的目标代码
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000001c 00000000 00000000 00000020 2**2
CONTENTS, ALLOC, LOAD, RELOC, CODE
1 .data 00000000 0000001c 0000001c 0000003c 2**2
CONTENTS, ALLOC, LOAD, DATA
Disassembly of section .text:
00000000 <_a>:
0: 55 pushl %ebp
1: 89 e5 movl %esp,%ebp
3: 53 pushl %ebx
4: 8b 5d 08 movl 0x8(%ebp),%ebx ;Strlen的参数入栈
7: 53 pushl %ebx
8: e8 f3 ff ff ff call 0
9: DISP32 _strlen
d: 50 pushl %eax ;write的参数入栈
e: 53 pushl %ebx
f: 6a 01 pushl $0x1
11: e8 ea ff ff ff call 0
12: DISP32 _write
16: 8d 65 fc leal -4(%ebp),%esp
19: 5b popl %ebx
1a: c9 leave
1b: c3 ret
1c: .data ;size = 0;
;--------------------------------------------------
链接器将这两个目标文件,产生一个可执行程序
图1-6 可执行程序的部分代码
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000fe0 00001020 00001020 00000020 2**3
1 .data 00001000 00002000 00002000 00001000 2**3
2 .bss 00000000 00003000 00003000 00000000 2**3
Disassembly of section .text:
00001020 <start-c>:
...
1092: e8 0d 00 00 00 call 10a4 <_main>
...
000010a4 <_main>:
10a4: 55 pushl %ebp
10a5: 89 e5 movl %esp,%ebp
10a7: 68 24 20 00 00 pushl $0x2024
10ac: e8 03 00 00 00 call 10b4 <_a>
10b1: c9 leave
10b2: c3 ret
...
000010b4 <_a>:
10b4: 55 pushl %ebp
10b5: 89 e5 movl %esp,%ebp
10b7: 53 pushl %ebx
10b8: 8b 5d 08 movl 0x8(%ebp),%ebx
10bb: 53 pushl %ebx
10bc: e8 37 00 00 00 call 10f8 <_strlen>
10c1: 50 pushl %eax
10c2: 53 pushl %ebx
10c3: 6a 01 pushl $0x1
10c5: e8 a2 00 00 00 call 116c <_write>
10ca: 8d 65 fc leal -4(%ebp),%esp
10cd: 5b popl %ebx
10ce: c9 leave
10cf: c3 ret
...
000010f8 <_strlen>:
...
0000116c <_write>:
...
合并后的文本段包含名为 start-c 的库启动代码,由 m.o 重定位到 0x10a4
的代码,重定位到 0x10b4 的 a.o,以及被重定位到文本段更高地址从 C 库中链
接来的例程。数据段,没有显示在这里,按照和文本段相同的顺序包含了合并后
的数据段。由于_main 的代码被重定位到地址 0x10a4,所以这个代码要被修改到
start-c 代码的 call 指令中。在 main 例程内部,对字符串 string 的引用被重
定位到 0x2024,这是 string 在数据段最终的位置,并且 call 指令中地址修改
为 0x10b4,这是_a 最终确定的地址。在_a 内部,对_strlen 和_write 的 call
指令也要修改为这两个例程的最终地址。 |
|