代码拆分

源代码

add_lib.c

1
2
3
4
5
// add_lib.c
int add(int a, int b)
{
return a+b;
}
1
2
3
4
5
6
7
8
9
10
11
// link_example.c

#include <stdio.h>

int main()
{
int a = 10;
int b = 5;
int c = add(a, b);
printf("c=%d\n", c);
}

gcc + objdump

1
2
3
$ gcc -g -c add_lib.c link_example.c
$ objdump -d -M intel -S add_lib.o
$ objdump -d -M intel -S link_example.o

add_lib.o

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
add_lib.o:     文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <add>:
// add_lib.c
int add(int a, int b)
{
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 89 7d fc mov DWORD PTR [rbp-0x4],edi
7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
return a+b;
a: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
d: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]
10: 01 d0 add eax,edx
}
12: 5d pop rbp
13: c3 ret
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
link_example.o:     文件格式 elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
// link_example.c
#include <stdio.h>

int main()
{
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 48 83 ec 10 sub rsp,0x10
int a = 10;
8: c7 45 fc 0a 00 00 00 mov DWORD PTR [rbp-0x4],0xa
int b = 5;
f: c7 45 f8 05 00 00 00 mov DWORD PTR [rbp-0x8],0x5
int c = add(a, b);
16: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8]
19: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
1c: 89 d6 mov esi,edx
1e: 89 c7 mov edi,eax
20: b8 00 00 00 00 mov eax,0x0
25: e8 00 00 00 00 call 2a <main+0x2a>
2a: 89 45 f4 mov DWORD PTR [rbp-0xc],eax
printf("c=%d\n", c);
2d: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]
30: 89 c6 mov esi,eax
32: bf 00 00 00 00 mov edi,0x0
37: b8 00 00 00 00 mov eax,0x0
3c: e8 00 00 00 00 call 41 <main+0x41>
}
41: c9 leave
42: c3 ret
1
2
3
4
5
6
7
$ ll link_example.o
-rw-r--r--. 1 root root 3408 4月 2 21:24 link_example.o

$ chmod u+x link_example.o

$ ./link_example.o
-bash: ./link_example.o: 无法执行二进制文件
  1. add_lib.olink_example.o,通过objdump后,两个程序的地址都是从0开始的
  2. add_lib.olink_example.o并不是一个可执行文件,而只是目标文件(Object File)
    • 只有通过链接器(Linker)把多个目标文件以及调用的各种函数库链接起来,才能得到一个可执行文件
    • gcc的-o参数,可以生成对应的可执行文件

生成可执行文件

1
2
3
4
$ gcc -o link-example add_lib.o link_example.o

$ ./link-example
c = 15

C代码 -> 汇编代码 -> 机器码

  1. 编译(Compile) -> 汇编(Assemble) -> 链接(Link)
    • 生成可执行文件
  2. 通过装载器(Loader)把可执行文件装载(Load)到内存中,CPU从内存中读取指令数据,来开始真正执行程序

ELF格式 + 链接

程序最终通过装载器变成指令数据,但生成的可执行代码不仅仅是一条条的指令

1
$ objdump -d -M intel -S link-example
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
link-example:     文件格式 elf64-x86-64
Disassembly of section .init:
......
Disassembly of section .plt:
......
Disassembly of section .text:
......
000000000040052d <add>:
// add_lib.c
int add(int a, int b)
{
40052d: 55 push rbp
40052e: 48 89 e5 mov rbp,rsp
400531: 89 7d fc mov DWORD PTR [rbp-0x4],edi
400534: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
return a+b;
400537: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
40053a: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]
40053d: 01 d0 add eax,edx
}
40053f: 5d pop rbp
400540: c3 ret

0000000000400541 <main>:
// link_example.c
#include <stdio.h>
int main()
{
400541: 55 push rbp
400542: 48 89 e5 mov rbp,rsp
400545: 48 83 ec 10 sub rsp,0x10
int a = 10;
400549: c7 45 fc 0a 00 00 00 mov DWORD PTR [rbp-0x4],0xa
int b = 5;
400550: c7 45 f8 05 00 00 00 mov DWORD PTR [rbp-0x8],0x5
int c = add(a, b);
400557: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8]
40055a: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
40055d: 89 d6 mov esi,edx
40055f: 89 c7 mov edi,eax
400561: b8 00 00 00 00 mov eax,0x0
400566: e8 c2 ff ff ff call 40052d <add>
40056b: 89 45 f4 mov DWORD PTR [rbp-0xc],eax
printf("c=%d\n", c);
40056e: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]
400571: 89 c6 mov esi,eax
400573: bf 20 06 40 00 mov edi,0x400620
400578: b8 00 00 00 00 mov eax,0x0
40057d: e8 8e fe ff ff call 400410 <printf@plt>
}
......
Disassembly of section .fini:
......
  1. 可执行代码目标代码类似,在Linux下,可执行文件目标文件所使用的都是ELF格式
    • ELF:Execuatable and Linkable File Format可执行可链接文件格式
  2. main函数里调用add的跳转地址,不再是下一条指令的地址,而是add函数的入口地址,这是ELF格式链接器的功劳

ELF格式

  1. ELF有一个基本的文件头,用来表示这个文件的基本属性(是否是可执行文件、对应的CPU、操作系统等)
  2. ELF文件格式把各种信息,分成一个个的Section保存起来
    • .text section
      • 代码段或指令段(Code Section),用来保存程序的代码和指令
    • .data section
      • 数据段(Data Section),用来保存程序里面设置好的初始化数据信息
    • .rel.text section
      • 重定位表(Relocation Table)
      • link_example.o里面的main函数调用了add和printf这两个函数
        • 但在链接发生之前,并不知道该跳转到哪里,这些信息会存储在重定位表里(链接的时候进行修正
    • .symtab Section
      • 符号表(Symbol Table),保存了当前文件里面定义的函数名称和对应地址的地址簿

链接

  1. 链接器会扫描所有输入的目标文件,然后把所有符号表里的信息收集起来,构成一个全局的符号表
  2. 然后再根据重定位表,把所有不确定要跳转到哪个地址的代码,根据全局符号表里面存储的地址,进行一次修正
  3. 最后,把所有的目标文件的对应段进行一次合并,变成了最终的可执行代码
  4. 因此,可执行文件里面的函数调用的地址都是正确的
    • 装载器不再需要考虑地址跳转的问题,只需要解析ELF文件,把对应的指令和数据,加载到内存里面供CPU执行即可

Linux + Windows

  1. Linux和Windows的可执行文件的格式是不一样的
    • Windows:PE(Portable Executable Format)
    • Linux下的装载器只能解析ELF格式,而不能解析PE格式
  2. Wine:兼容PE格式的装载器
  3. WSL(Windows Subsystem for Linux):可以解析和加载ELF格式的文件

参考资料

深入浅出计算机组成原理