前言:
我们都知道我们所写的程序是被编译为一条条的CPU指令去执行的,但是在linux系统下能够运行的程序在windows环境下却运行不起来,但是我们使用的CPU明明是一样的,这又是为什么呢?
一、程序的执行:编译、链接和装载
程序示例1:
// add_lib.c
int add(int a, int b)
{
return a+b;
}
// 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 -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: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <add>:
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
a: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]
d: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
10: 01 d0 add eax,edx
12: 5d pop rbp
13: c3 ret
link_example.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 48 83 ec 10 sub rsp,0x10
8: c7 45 fc 0a 00 00 00 mov DWORD PTR [rbp-0x4],0xa
f: c7 45 f8 05 00 00 00 mov DWORD PTR [rbp-0x8],0x5
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
2d: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]
30: 89 c6 mov esi,eax
32: 48 8d 3d 00 00 00 00 lea rdi,[rip+0x0] # 39 <main+0x39>
39: b8 00 00 00 00 mov eax,0x0
3e: e8 00 00 00 00 call 43 <main+0x43>
43: b8 00 00 00 00 mov eax,0x0
48: c9 leave
49: c3 ret
分析:实际上以上编译结果是无法运行的,下面来分析一下
- 编译结果的两个文件左侧指令地址行都从0开始显然是重复的,这必然导致call指令无法寻址,它也不知道应该跳转去哪个文件。
- 实际上以上编译结果的两个文件并不是可执行文件,只是目标文件,只有通过链接器将多个目标文件以及调用库链接起来,才能成为一个可执行文件。
gcc -o 命令进行链接:
$ gcc -o link-example add_lib.o link_example.o
$ ./link_example
c = 15
链接结果:
link_example: file format elf64-x86-64
Disassembly of section .init:
...
Disassembly of section .plt:
...
Disassembly of section .plt.got:
...
Disassembly of section .text:
... 6b0: 55 push rbp
6b1: 48 89 e5 mov rbp,rsp
6b4: 89 7d fc mov DWORD PTR [rbp-0x4],edi
6b7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
6ba: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]
6bd: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
6c0: 01 d0 add eax,edx
6c2: 5d pop rbp
6c3: c3 ret
00000000000006c4 <main>:
6c4: 55 push rbp
6c5: 48 89 e5 mov rbp,rsp
6c8: 48 83 ec 10 sub rsp,0x10
6cc: c7 45 fc 0a 00 00 00 mov DWORD PTR [rbp-0x4],0xa
6d3: c7 45 f8 05 00 00 00 mov DWORD PTR [rbp-0x8],0x5
6da: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8]
6dd: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
6e0: 89 d6 mov esi,edx
6e2: 89 c7 mov edi,eax
6e4: b8 00 00 00 00 mov eax,0x0
6e9: e8 c2 ff ff ff call 6b0 <add>
6ee: 89 45 f4 mov DWORD PTR [rbp-0xc],eax
6f1: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]
6f4: 89 c6 mov esi,eax
6f6: 48 8d 3d 97 00 00 00 lea rdi,[rip+0x97] # 794 <_IO_stdin_used+0x4>
6fd: b8 00 00 00 00 mov eax,0x0
702: e8 59 fe ff ff call 560 <printf@plt>
707: b8 00 00 00 00 mov eax,0x0
70c: c9 leave
70d: c3 ret
70e: 66 90 xchg ax,ax
...
Disassembly of section .fini:
...
分析:
- 可以发现通过链接生成的可执行文件和之前生成的目标文件在格式上并无区别,这中文件格式称为 ELF(Execuatable and Linkable File Format)
- 文件中包含函数(如main和add),此外还包含一些定义的全局可访问的变量名称,这些变量被存储在被称为符号表的位置,符号表用于关联名称和地址;
- 在该文件中main函数调用add函数也不再是跳转下一指令地址,而是直接跳转到add函数入口。
由c语言生成汇编码再到机器码执行的过程分两部分:
- 编译、汇编、链接生成可执行文件
- 通过装载器将可执行文件装载到内存,而CPU是从内存中读取指令并运行的。
二、ELF文件结构
- ELF文件分为多个部分,称为多个section,并且有一个基本的文件头用来保存文件的基本属性,包括是否为可执行文件、操作系统、CPU型号等
- .text Section 保存代码段和指令段
- .data Section 保存程序里设置好的初始化信息
- .real.text Section 保存程序里面我们暂时位置的跳转地址,比如链接之前的目标文件中add函数的跳转地址
- .symtab Section 符号表,保存当前文件里面我们未知的函数名称和地址的对应关系
三、ELF文件链接过程
链接时,扫描所有目标文件,收集所有符号表,将所有符号表合并成一个符号表;
根据重定向表所有不确定的地址进行修正;
将代码段进行合并,生成一个可执行文件;
总结:
讲了这么多,为什么linux环境下的可执行文件和windows环境下的不同呢?这是因为两种环境下运行的可执行文件的文件格式是不一样的,而想要在linux环境运行windows环境的可执行文件,需要通过一种能够兼容ELF格式文件的装载器。
扩展:这里可以联系java的类装载机制,class被加载到方法区才能运行,其实jvm内的方法区就是一个大的.text Section,全局的。