:程序是怎样被链接和加载的?

个真实的例子
我们通过一个简小的链接实例来结束对链接过程的介绍。图 3 所示为一对 C 语言源代码
文件,m.c 中的主程序调用了一个名为 a 的例程,而调用了库例程 strlen 和 write 的 a 例程
bbs.theithome.com
在 a.c 中。
---------------------------------------------------------------------------------------------
图 1-3 源程序
源程序 m.c
extern void a(char *);
int main(int ac, char **av)
{
static char string[] = "Hello, world!\n";
a(string);
}
源程序 a.c
#include <unistd.h>
#include <string.h>
void a(char *s)
{
write(1, s, strlen(s));
}
---------------------------------------------------------------------------------------------
如图 4 所示,主程序 m.c 在我的 Pentium 机器上用 gcc 编译成一个典型 a.out 目标代码
格式长度为 165 字节的目标文件。该目标文件包含一个固定长度的头部,16 个字节的“文本
”段,包含只读的程序代码,16 个字节的数据段,包含字符串。其后是两个重定位项,其
中一个标明 pushl 指令将字符串 string 的地址放置在栈上为调用例程 a 作准备,另一个标明
call 指令将控制转移到例程 a。符号表分别导出和导入了_main 与_a 的定义,以及调试器需
要的其它一系列符号(每一个全局符号都会以下划线做为前缀,第 5 章中将会讲述原因)。
注意由于和字符串 string 在同一个文件中,pushl 指令引用了 string 的临时地址 0x10,而
由于_a 的地址是未知的所以 call 指令引用的地址为 0x0。
---------------------------------------------------------------------------------------------
图 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
bbs.theithome.com
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
...
---------------------------------------------------------------------------------------------
如图 5 所示,子程序文件 a.c 编译成一个长度为 160 字节的目标文件,包括头部, 28
字节的文本段,无数据段。两个重定位项标记了对 strlen 和 write 的 call 指令,符号表中
导出_a 并导入了_strlen 和_write。
---------------------------------------------------------------------------------------------
图 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
7: 53 pushl %ebx
8: e8 f3 ff ff ff call 0
9: DISP32 _strlen
d: 50 pushl %eax
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
---------------------------------------------------------------------------------------------
bbs.theithome.com
为了产生一个可执行程序,链接器将这两个目标文件,以及一个标准的 C 程序启动初始
化例程,和必要的 C 库例程整合到一起,产生一个部分如图 6 所示的可执行文件。
---------------------------------------------------------------------------------------------
图 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
bbs.theithome.com
10cf: c3 ret
...
000010f8 <_strlen>:
...
0000116c <_write>:
...
---------------------------------------------------------------------------------------------
链接器将每个输入文件中相应的段合并在一起,故只存在一个合并后的文本段,一个
合并后的数据段和一个 bss 段(两个输入文件不会使用的,被初始化为 0 的数据段)。由于
每个段都会被填充为 4K 对齐以满足 x86 的页尺寸,因此文本段为 4K(减去文件中 20 字节长
度的 a.out 头部,逻辑上它并不属于该段),数据段和 bss 段每个同样也是 4K 字节。
合并后的文本段包含名为 start-c 的库启动代码,由 m.o 重定位到 0x10a4 的代码,重
定位到 0x10b4 的 a.o,以及被重定位到文本段更高地址从 C 库中链接来的例程。数据段,没
有显示在这里,按照和文本段相同的顺序包含了合并后的数据段。由于_main 的代码被重定
位到地址 0x10a4,所以这个代码要被修改到 start-c 代码的 call 指令中。在 main 例程内部,
对字符串 string 的引用被重定位到 0x2024,这是 string 在数据段最终的位置,并且 call
指令中地址修改为 0x10b4,这是_a 最终确定的地址。在_a 内部,对_strlen 和_write 的 cal
l 指令也要修改为这两个例程的最终地址。
可执行程序中仍然有很多其它的 C 库例程,没有显示在这里,它们由启动代码和_write
(在稍后例子中的出错处理例程)直接或间接的调用。由于可执行程序的文件格式不是可以重
链接的,且操作系统从已知的固定位置加载它,因此它不包含重定位数据。它带有一个有助
于调试器(debugger)工作的符号表,尽管这个程序没有使用这个符号表并且可以将其删除
以节省空间。
在这个例子中,从库中链接的代码明显要多于程序本身的代码。这是很正常的,尤其
当程序使用大的图形库或窗口库,这就促进了共享库的出现,详见第 9 章和第 10 章。这个
链接好的程序大小为 8K,但若使用共享库链接则同样的程序大小仅为 264 字节。当然这是一
个像玩具一样的例子,但真实程序经常也会采用同样的方法节省空间。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值