前言:
链接器的工作过程可以分成两步,一、空间与地址分配;二、符号解析与重定位,下面详细介绍下这两步的工作原理。
一、静态链接
1、空间与地址分配
静态链接的输入是一组目标文件(.obj,由编译器生成)和静态库(.lib,本质是目标文件的集合)。完成空间与地址分配后就可以确定所有符号的虚拟地址,核心工作如下:
- 扫描所有的输入目标文件,获取它们的各个段的长度、属性和位置。将多个目标文件中相同类型的段合并为一个大段,计算输出文件中各个段合并后的长度与位置,并建立映射关系。
- 将输入文件中的符号表中的所有符号定义及引用收集起来,统一放到一个全局符号表。
示例代码:
// util.cpp
#include "util.h"
int shared = 1;
int add(int a, int b) {
return a + b;
}
// util.h
#pragma once
int add(int a, int b);
// main.cpp
#include "util.h"
extern int shared;
int main()
{
int a = 10, b = 20;
shared = add(a, b);
return 0;
}
2、符号解析与重定位
使用第一步收集到的信息,读取输入文件中的段的数据、重定位信息,并进行符号解析与重定位、调整代码中的地址,这一步是链接的核心,特别是重定位的过程。
2.1、编译源文件
编译源文件
cl /Zi /Od main.cpp util.cpp # 生成符号信息并禁用优化
分别查看
util.obj
与main.obj
对应的反汇编代码
util.obj
段反汇编信息:
E:\VS_Workspace\Example>dumpbin /DISASM util.obj
Microsoft (R) COFF/PE Dumper Version 14.43.34808.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file util.obj
File Type: COFF OBJECT
?add@@YAHHH@Z (int __cdecl add(int,int)):
0000000000000000: 89 54 24 10 mov dword ptr [rsp+10h],edx
0000000000000004: 89 4C 24 08 mov dword ptr [rsp+8],ecx
0000000000000008: 8B 44 24 10 mov eax,dword ptr [rsp+10h]
000000000000000C: 8B 4C 24 08 mov ecx,dword ptr [rsp+8]
0000000000000010: 03 C8 add ecx,eax
0000000000000012: 8B C1 mov eax,ecx
0000000000000014: C3 ret
main.obj
反汇编信息:
E:\VS_Workspace\Example>dumpbin /DISASM main.obj
Microsoft (R) COFF/PE Dumper Version 14.43.34808.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file main.obj
File Type: COFF OBJECT
main:
0000000000000000: 48 83 EC 38 sub rsp,38h
0000000000000004: C7 44 24 24 0A 00 mov dword ptr [rsp+24h],0Ah
00 00
000000000000000C: C7 44 24 20 14 00 mov dword ptr [rsp+20h],14h
00 00
0000000000000014: 8B 54 24 20 mov edx,dword ptr [rsp+20h]
0000000000000018: 8B 4C 24 24 mov ecx,dword ptr [rsp+24h]
000000000000001C: E8 00 00 00 00 call ?add@@YAHHH@Z
0000000000000021: 89 05 00 00 00 00 mov dword ptr [?shared@@3HA],eax
0000000000000027: 33 C0 xor eax,eax
0000000000000029: 48 83 C4 38 add rsp,38h
000000000000002D: C3 ret
通过分析
main.obj
对应的反汇编代码,引用的add
及shared
地址都是一个临时的地址,重定位过程就是把这些临时地址修改成实际的虚拟地址。
2.2、重定位表
链接器怎么知道哪些指令需要调整?这些指令的那些部分需要被调整?如何进行调整?在重定位表里面有记录记录编译时未确定地址的符号,让链接器在链接时修正。
main.obj
重定位表(简化版):
E:\VS_Workspace\Example>dumpbin /relocations main.obj
Microsoft (R) COFF/PE Dumper Version 14.43.34808.0
Copyright (C) Microsoft Corporation. All rights reserved.
RELOCATIONS #3
Symbol Symbol
Offset Type Applied To Index Name
-------- ---------------- ----------------- -------- ------
0000001D REL32 00000000 9 ?add@@YAHHH@Z (int __cdecl add(int,int))
00000023 REL32 00000000 12 ?shared@@3HA (int shared)
- 重定位表字段解析
字段名 | 说明 |
---|---|
Symbol Name | 需要重定位的符号(如 shared 或 add )。 |
Offset | 该符号在当前 .obj 文件中的偏移地址(相对于所在节的起始位置)。 |
Type | 重定位类型,常见的有: • DIR32 :直接 32 位地址修正。• REL32 :相对 32 位地址修正(用于函数调用)。 |
Applied To | 该重定位条目所属的目标文件(如 main.obj )。 |
2.3、符号重定位
在
main.cpp
中,有两个外部符号需要重定位:shared
(全局变量) 与add
(函数调用),重定位的流程如下:
- 获取符号虚拟地址:根据全局符号表,确定
shared
和add
的最终虚拟地址,空间与地址分配后每个符号的虚拟地址已经确定(虚拟地址 = 基址 + 段偏移 + 符号偏移)。 - 修正重定位条目:
- 对于
DIR32
类型(如shared
):- 找到
main.obj
中Offset
指定的位置,填入shared
的绝对地址(如0x00403000
)。
- 找到
- 对于
REL32
类型(如add
):- 计算
call add
指令与add
函数地址的相对偏移。 - 将计算出的偏移值写入
REL32
指定的位置。
- 计算
- 对于
- 生成最终可执行文件:
- 所有重定位修正完成后,生成可执行文件。
2.4、链接生成可执行文件
把
main.obj
与util.obj
链接生成main.exe
,链接命令如下:
E:\VS_Workspace\Example>link /OUT:main.exe /DEBUG main.obj util.obj
- 链接后
main
函数反汇编代码:
从反汇编代码可以看到,之前未确定的地址都已重定位到符号的实际虚拟地址。