最近在看《程序员的自我修养-链接、装载与库》一书。以前在一些其他书中看过一些这方面的资料,主要是《深入理解计算机系统》,《编程之道卓越一、二》这三本,对这方面有一个大概的认知,但一直没有一个完整的理解,最近通过看这本书,解决了很多细节上的疑惑。
当然东西还是要自己动手实验一下理解的更深。今天通过一个汇编语言的例子,初步试验了看到的一些知识。书上用的c语言和GUN的objdump工具,我拿Windows下的MASM和dumpbin来实验一下。
汇编源码如下, 是从《Windows汇编语言程序设计》上摘的一段MASM代码, 编译工具用的是MASM6.15, 是一个打印5的factoria值的简单例子:
.386
.model flat, stdcall
option casemap:none
includelib msvcrt.lib
printf PROTO c :dword, :vararg
.data
Fact dword ?
N equ 5
szFormat byte 'factorial(%d) = %d', 0ah, 0
.code
start:
mov ecx, N
mov eax, 1
e10:
imul eax, ecx
loop e10
mov Fact, eax
invoke printf, offset szFormat, N, Fact
ret
end start
编译(为了生成obj文件,不采取一步编译成exe的方法)
ml /c /coff factoria.asm
然后生成exe
link /subsystem:console factoria.obj
运行:
factoria
输出
factoria(5) = 120
先把用到的信息生成到文本文件中。命令如下
dumpbin /all factoria.obj > obj_all.txt
dumpbin /disasm factoria.obj > obj_disasm.txt
dumpbin /all factoria.exe > exe_all.txt
dumpbin /disasm factoria.exe > exe_disasm.txt
dumpbin有很多选项,可通过/? 查看, 除了上面两个,常用的应该还有/header /relocation
先来看obj_disasm中
_start:
00000000: B9 05 00 00 00 mov ecx,5
00000005: B8 01 00 00 00 mov eax,1
0000000A: 0F AF C1 imul eax,ecx
0000000D: E2 FB loop 0000000A
0000000F: A3 00 00 00 00 mov [_start],eax
00000014: FF 35 00 00 00 00 push dword ptr [_start]
0000001A: 6A 05 push 5
0000001C: 68 00 00 00 00 push offset _start
00000021: E8 00 00 00 00 call 00000026
00000026: 83 C4 0C add esp,0Ch
00000029: C3 ret
对照原始的汇编语句
我们发现了中间四个00 00 00 00,分别应该是源码中的Fact(两处), szFormat和prinf的地方 这就是所谓的需要重定位的地方,因为在编译成obj的时候还不知道这些地址将来是什么,所以先写0
需要注意的是,反汇编的时候,对于这些00 00 00 00 的解释可能会有误,上面的例子就把这些统一认为了_start。这点要注意。
再来看一下exe里这些地方变成什么了,打开exe_disasm:
00401000: B9 05 00 00 00 mov ecx,5
00401005: B8 01 00 00 00 mov eax,1
0040100A: 0F AF C1 imul eax,ecx
0040100D: E2 FB loop 0040100A
0040100F: A3 00 30 40 00 mov [00403000],eax
00401014: FF 35 00 30 40 00 push dword ptr ds:[00403000h]
0040101A: 6A 05 push 5
0040101C: 68 04 30 40 00 push 403004h
00401021: E8 04 00 00 00 call 0040102A
00401026: 83 C4 0C add esp,0Ch
00401029: C3 ret
0040102A: FF 25 00 20 40 00 jmp dword ptr ds:[00402000h]
我们看到几个00 00 00 00 的地方变成了00 30 40 00,04 30 40 00之类的地址。
下面就是分析过程了,exe中的地址是链接器写进去的,要写这些信息,应该要知道如下:
1. 哪些地方需要重定位?
2. 这些地方需要换成什么样的地址?
先看第一个问题,哪些地方需要重定位?
这个是在汇编器汇编的时候记录到了obj文件中的重定位表中。看obj_all.txt中
SECTION HEADER #1
.text name
这是txt段,有源码中的代码转来,下面有如下信息
RELOCATIONS #1
Symbol Symbol
Offset Type Applied To Index Name
-------- ---------------- ----------------- -------- ------
00000010 DIR32 00000000 D Fact
00000016 DIR32 00000000 D Fact
0000001D DIR32 00000000 C szFormat
00000022 REL32 00000000 B _printf
这就是是text段中需要重定位的地方列表,第一列是偏移地址,也就是这些地方需要修改,将来利用这些地址来定位。对应上面的反汇编,可以看到,正好是那四个 00000000 的地址。
知道哪里需要修改了,那怎么知道改成什么样的地址呢, 这需要用到符号表, 先看上面重定位表倒数第二列, Index 是对应的符号表的序号, 分别是BCD三项。再看obj_all.txt最后的符号表
COFF SYMBOL TABLE
000 00000000 DEBUG notype Filename | .file
factoria.asm
002 002A2263 ABS notype Static | @comp.id
003 00000000 SECT1 notype Static | .text
Section length 2A, #relocs 4, #linenums 0, checksum 0
005 00000000 SECT2 notype Static | .data
Section length 18, #relocs 0, #linenums 0, checksum 0
007 00000000 SECT3 notype Static | .debug$S
Section length 51, #relocs 0, #linenums 0, checksum 0
009 00000000 SECT4 notype Static | .drectve
Section length 24, #relocs 0, #linenums 0, checksum 0
00B 00000000 UNDEF notype () External | _printf
00C 00000004 SECT2 notype Static | szFormat
00D 00000000 SECT2 notype Static | Fact
00E 00000000 SECT1 notype External | _start
我们看到了BCD三个对应的符号表中的项, _printf, szFormat, Fact。这里面除了序号还有几列是下面要用到的,第二列偏移地址和第三列所述段。
下面是链接器的工作了。链接器首先要通过obj文件的段表读取各个段的信息,当然这里面一定有一个属性就是大小。然后定一个初始的虚拟地址, 就可以计算出各个段的起始虚拟地址了。
打开exe_all.txt可以看到
OPTIONAL HEADER VALUES
400000 image base
和
SECTION HEADER #1
.text name
30 virtual size
1000 virtual address
和
SECTION HEADER #3
.data name
18 virtual size
3000 virtual address
定了这些地址,那么符号表中的符号对应的虚拟地址也就定了,分别是段的这个地址加上其记录在obj符号表中的偏移地址。再遍历obj中的重定位表,就可以把这个地址替换了。
例如obj_dasm中第一个00 00 00 00,是Fact的地址,Fact属于数据段,数据段的虚拟地址是image base + data 段的virtual address, 400000 + 3000 = 403000
Fact在符号表中记录的其偏移地址是00000000, 那其最终的虚拟地址就是0x00403000, 在exe_dasm中看到正是这个地址。同样szFormat和Fact一样属于data段,但其偏移地址是00000004,紧跟在Fact之后,在exe_dasm的0040101C行,看到了这个地址。
总结一下这个过程:
汇编器完成部分:
1. 找出代码中需要重定位的地方,列在重定位表中,记录其偏移地址, 和符号表索引
2. 生成符号表, 记录其所属段、偏移地址。
3. 段表中记录段的属性、大小。
链接器完成部分:
4. 从段表的大小等属性加上虚拟基址计算出段的起始须知。
5. 遍历符号表,其所属段的基址加上其偏移地址(见2.)计算出其虚拟地址。
6. 遍历重定位表(见1.),通过其符号表索引找到符号表,用其虚拟地址替换掉需要替换的地方, 这个偏移地址是不会变的。
中间有一个printf的地址涉及到动态链接库,下次再动手实验。