程序链接

什么是程序链接?它要解决程序在编译成二进制过程中的什么问题?

我们可以通过以下示例来认识链接过程:

1
ld xx.o yy.o -o prog

比如给定简单的main程序:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello world\n");
return 0;
}

ld要解决当前main程序和printf拼接的问题。

标准程序和非标准程序:

  • 标准程序:程序不依赖其他.o文件,能够自闭环执行
  • 非标准程序:程序调用了其他.o文件,需要有一个链接过程

非标准程序 -> 标准程序:在编译阶段提供一个placeholder指令,在链接阶段通过观察多个.o文件后补全。

在未链接前main程序汇编如下:cc -c test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@localhost test]# objdump -S test.o

test.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 call e <main+0xe>
e: b8 00 00 00 00 mov $0x0,%eax
13: 5d pop %rbp
14: c3 ret

显然,该函数没有给出printf的代码,无法自闭环,也就不是标准程序。

ELF:Executable,Linkable,Format。所有链接过程都是基于ELF这样的数据结构进行。

Header编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;

Section header table(SHT),有e_shoff、e_shentsize、e_shnum

因为SHT位于ELF文件末尾,故总程序大小为:e_shoff + e_shentsize * e_shnum

现在已知:

  • header区间:[0, e_ehsize)

  • Section header table区间:[e_shoff, e_shoff + e_shentsize * e_shnum)

剩余的[e_ehsize, e_shoff)区间是什么内容?—— Sections,即Section header所描述的具体内容。比如:.text、.rodata等

image-20240610184504549

Section header编码

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;

main程序的Section header信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[root@localhost test]# readelf -S test.o
There are 14 section headers, starting at offset 0x248:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000015 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000188
0000000000000030 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 00000055
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000055
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 00000055
000000000000000c 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00000061
0000000000000013 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 00000074
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.pr[...] NOTE 0000000000000000 00000078
0000000000000030 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 000000a8
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 000001b8
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 000000e0
0000000000000090 0000000000000018 12 4 8
[12] .strtab STRTAB 0000000000000000 00000170
0000000000000012 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 000001d0
0000000000000074 0000000000000000 0 0 1

.symtab:字符串表(其实不是表,是一个所有字符串的merge,所以sh_entsize为0),保存了所有程序的字符串,比如“.text”字符串即存于.symtab内

.symtab:符号表,具有sh_entsize值,按照严谨的表项编排

符号表.symtab编码

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;

main程序的符号表内容:

1
2
3
4
5
6
7
8
9
10
[root@localhost test]# readelf -s test.o

Symbol table '.symtab' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata
4: 0000000000000000 21 FUNC GLOBAL DEFAULT 1 main
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts

当我们在解释“i = j”表达式时,分别需解释两层映射:

  • Environment映射:字符串到地址的映射,找到j的地址
  • State映射:地址到值的映射,解析j的值(基于j的数据类型,如int–>4Byte)。这一步由编译器来做,得到movq、movw等具有操作数长度的指令

Environment映射又可以分2层:

  • external映射:映射到.text段
  • internal映射:映射到stack

链接器关注external映射部分(参考:龙书、虎书、The c programing language)。

给定符号表,如何找到符号的值:

1
2
3
4
5
6
7
8
9
# 起始地址:
Start:
Elf64_Ehdr[Elf64_Sym.st_shndx].sh_offset + Elf64_Sym.st_value # 这里st_value为符号的相对偏移
# 结束地址:
End:
Start + Elf64_Sym.st_size # 这里st_size为符号的值长度(字节、字、双字等)

# 基于上述结果得到Symbol内容
ELF[Start, End)

符号表内的st_info编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct
{
Elf64_Half si_boundto; /* Direct bindings, symbol bound to */
Elf64_Half si_flags; /* Per symbol flags */
} Elf64_Syminfo;

/* Legal values for ST_BIND subfield of st_info (symbol binding). */
#define STB_LOCAL 0 /* Local symbol */
#define STB_GLOBAL 1 /* Global symbol */
#define STB_WEAK 2 /* Weak symbol */
...

/* Legal values for ST_TYPE subfield of st_info (symbol type). */
#define STT_NOTYPE 0 /* Symbol type is unspecified */
#define STT_OBJECT 1 /* Symbol is a data object */
#define STT_FUNC 2 /* Symbol is a code object */
...

STB_WEAK:对应到__attribute((weak))修饰的变量或函数等,表示某个变量或函数符号在链接和运行时没有找到对应符号实现时默认使用当前被修饰的weak实现。

符号表基于si_boundto、si_flags、st_shndx描述每个变量或函数的各种符号定义,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int a;
int b = 1;
extern c = 0;
extern d = 1;
__attribute((weak)) e;
__attribute((weak)) f = 1;

void g() {
a = 1;
b = 1;
c = 1;
d = 1;
e = 1;
f = 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost test]# readelf -s sym.o

Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS sym.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 a
4: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 b
5: 0000000000000004 4 OBJECT GLOBAL DEFAULT 4 c
6: 0000000000000004 4 OBJECT GLOBAL DEFAULT 3 d
7: 0000000000000008 4 OBJECT WEAK DEFAULT 4 e
8: 0000000000000008 4 OBJECT WEAK DEFAULT 3 f
9: 0000000000000000 67 FUNC GLOBAL DEFAULT 1 g

可重定位.rela编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;

/* How to extract and insert information held in the r_info field. */
#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
#define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))

/* AMD x86-64 relocations. */
#define R_X86_64_64 1 /* Direct 64 bit */
#define R_X86_64_PC32 2 /* PC relative 32 bit signed */
#define R_X86_64_GOT32 3 /* 32 bit GOT entry */
#define R_X86_64_PLT32 4 /* 32 bit PLT address */

以下面程序为例:

1
2
3
4
5
6
7
8
9
extern void undef_func();
extern int undef_array[2];

void main()
{
undef_func();
undef_array[0] = 1;
undef_array[1] = 2;
}

反汇编,call位置跳转的地址未知,暂时先写为0x0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@localhost test]# objdump -S rel.o

rel.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: b8 00 00 00 00 mov $0x0,%eax
9: e8 00 00 00 00 call e <main+0xe>
e: c7 05 00 00 00 00 01 movl $0x1,0x0(%rip) # 18 <main+0x18>
15: 00 00 00
18: c7 05 00 00 00 00 02 movl $0x2,0x0(%rip) # 22 <main+0x22>
1f: 00 00 00
22: 90 nop
23: 5d pop %rbp
24: c3 ret

rela节内容:

1
2
3
4
5
6
7
8
9
10
11
[root@localhost test]# readelf -r rel.o

Relocation section '.rela.text' at offset 0x198 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000a 000400000004 R_X86_64_PLT32 0000000000000000 undef_func - 4
000000000010 000500000002 R_X86_64_PC32 0000000000000000 undef_array - 8
00000000001a 000500000002 R_X86_64_PC32 0000000000000000 undef_array - 4

Relocation section '.rela.eh_frame' at offset 0x1e0 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0

结合被引用的c文件:

1
2
void undef_func() {}
int undef_array[2] = {-1, -2};

链接:

1
ld --entry=main rel.o  reled.o -i relocated.o

最终call位置被写入地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[root@localhost test]# objdump -S relocated.o

relocated.o: file format elf64-x86-64


Disassembly of section .text:

0000000000401000 <main>:
401000: 55 push %rbp
401001: 48 89 e5 mov %rsp,%rbp
401004: b8 00 00 00 00 mov $0x0,%eax
401009: e8 17 00 00 00 call 401025 <undef_func>
40100e: c7 05 e8 2f 00 00 01 movl $0x1,0x2fe8(%rip) # 404000 <undef_array>
401015: 00 00 00
401018: c7 05 e2 2f 00 00 02 movl $0x2,0x2fe2(%rip) # 404004 <undef_array+0x4>
40101f: 00 00 00
401022: 90 nop
401023: 5d pop %rbp
401024: c3 ret

0000000000401025 <undef_func>:
401025: 55 push %rbp
401026: 48 89 e5 mov %rsp,%rbp
401029: 90 nop
40102a: 5d pop %rbp
40102b: c3 ret

以上过程为相对寻址,基于%rip指令寄存器(总是指向下一条指令)。

动态链接

动态链接是为了解决一部分代码被多个程序引用时如何共享实现节省内存的问题。

动态链接是在运行时所做的动作,无法直接确定被链接程序所在的位置,且.text在运行时无法被修改,所以不能像本地链接那样直接修改指令操作数,为此需要有个中间媒介(即GOT、PLT)辅助。

动态链接分两种:

  • 变量的动态链接
  • 函数的动态链接

变量动态链接过程基于GOT:

给定例子:

1
2
3
4
5
6
extern int a;

void func()
{
int local_a = a;
}

如果使用常规编译,得到

1
2
3
4
5
6
7
8
9
0000000000000000 <func>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # b <func+0xb>
b: 8b 00 mov (%rax),%eax
d: 89 45 fc mov %eax,-0x4(%rbp)
10: 90 nop
11: 5d pop %rbp
12: c3 ret

如果使用-fPIC编译,得到

1
2
3
4
5
6
7
8
9
00000000000010f5 <func>:
10f5: 55 push %rbp
10f6: 48 89 e5 mov %rsp,%rbp
10f9: 48 8b 05 e8 2e 00 00 mov 0x2ee8(%rip),%rax # 3fe8 <a@Base>
1100: 8b 00 mov (%rax),%eax
1102: 89 45 fc mov %eax,-0x4(%rbp)
1105: 90 nop
1106: 5d pop %rbp
1107: c3 ret

此时变量寻址过程依赖了GOT表,位于.data段,表项大小为8字节,即一个地址的大小:

image-20240610231929156

GOT表中的值由动态链接器写入。

函数动态链接过程基于GOT和PLT

PLT实现在.text段中,每个表项大小为16字节,由若干指令组成

示例:

1
2
3
4
5
6
#include <stdlib.h>

void main()
{
int *a = malloc(64 * sizeof(int));
}

反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Disassembly of section .plt:

0000000000401020 <malloc@plt-0x10>:
401020: ff 35 e2 2f 00 00 push 0x2fe2(%rip) # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>
401026: ff 25 e4 2f 00 00 jmp *0x2fe4(%rip) # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>
40102c: 0f 1f 40 00 nopl 0x0(%rax)

0000000000401030 <malloc@plt>:
401030: ff 25 e2 2f 00 00 jmp *0x2fe2(%rip) # 404018 <malloc@GLIBC_2.2.5>
401036: 68 00 00 00 00 push $0x0
40103b: e9 e0 ff ff ff jmp 401020 <_init+0x20>
...

0000000000401122 <main>:
401122: 55 push %rbp
401123: 48 89 e5 mov %rsp,%rbp
401126: 48 83 ec 10 sub $0x10,%rsp
40112a: bf 00 01 00 00 mov $0x100,%edi
40112f: e8 fc fe ff ff call 401030 <malloc@plt>
401134: 48 89 45 f8 mov %rax,-0x8(%rbp)
401138: 90 nop
401139: c9 leave
40113a: c3 ret
...

Disassembly of section .got.plt:

0000000000404000 <_GLOBAL_OFFSET_TABLE_>: # 这部分本身不是指令,objdump -D dump出来的将此处认为是指令,所以解析有误
404000: 10 3e adc %bh,(%rsi)
404002: 40 00 00 rex add %al,(%rax)
...
404015: 00 00 add %al,(%rax)
404017: 00 36 add %dh,(%rsi) # 404018: 36 10 40 00
404019: 10 40 00 adc %al,0x0(%rax)
40401c: 00 00 add %al,(%rax)

函数的动态链接使用了延迟绑定技术:

首次执行过程:

1
2
3
4
40112f:       e8 fc fe ff ff          call   401030 <malloc@plt>
401030: ff 25 e2 2f 00 00 jmp *0x2fe2(%rip) # 404018 <malloc@GLIBC_2.2.5> 跳转到GOT表中(0x401026+0x2fe2=0x404018)尝试取malloc的缓存地址。首次执行从磁盘读入为0x401036,也就跳回了<malloc@plt>中的下一条指令
401036: 68 00 00 00 00 push $0x0
40103b: e9 e0 ff ff ff jmp 401020 <_init+0x20> # 跳转到动态链接器代码段

动态链接器填充GOT表中的malloc地址后,再次执行时:

1
2
40112f:       e8 fc fe ff ff          call   401030 <malloc@plt>
401030: ff 25 e2 2f 00 00 jmp *0x2fe2(%rip) # 404018 <malloc@GLIBC_2.2.5> 跳转到GOT表中(0x401026+0x2fe2=0x404018)尝试取malloc的缓存地址。再次执行malloc缓存地址获取成功,直接跳转到malloc代码段执行,不再调用动态链接器代码

完整过程如下:

image-20240611002011827

GOT为什么放在.data段?—— 可运行时修改

什么是PIC(位置无关代码)?—— 此段代码利用了GOT和PLT的相对偏移在文件和在内存均一致的特性,不依赖运行时的地址(也就是给什么地址都能保持代码原有的逻辑)。

评论