深入解析C语言代码到机器码的过程
从大的方面来说,可以划分为两个阶段:
- 第一个阶段:由编译(Compile)、汇编(Assemble)以及链接(Link)三个阶段组成,生成了一个可执行文件(Executable Program)。
- 第二个阶段:通过装载器(Loader)把可执行文件装载(Load)到内存中,然后CPU 从内存中读取指令和数据,来开始真正执行程序。
第一个阶段:编译、汇编和链接
- 编译(Compile):在这个阶段,使用C语言编译器(如GCC)将C源代码文件(.c文件)编译成汇编代码文件(.s文件)。编译器会对C代码进行词法分析、语法分析和语义分析,然后生成中间代码表示程序的逻辑结构。
- 汇编(Assemble):在这个阶段,使用汇编器(如GNU汇编器)将汇编代码文件(.s文件)转换为机器码指令文件(.o文件)。汇编器会将汇编代码中的每条指令翻译为对应的机器码指令。
- 链接(Link):在这个阶段,使用链接器(如GNU链接器)将多个机器码指令文件(.o文件)以及所需的库文件链接在一起,生成最终的可执行文件(Executable Program)。链接器会解析函数和全局变量的引用,并将它们的定义与相应的引用进行关联,创建可执行文件。
第二个阶段:装载和执行
- 装载(Load):在这个阶段,操作系统的装载器(Loader)负责将可执行文件加载到内存中的适当位置。装载器会分配内存空间,将可执行文件的指令、数据和其他资源复制到相应的内存地址。
- 执行:一旦可执行文件被成功加载到内存中,CPU从内存中读取指令和数据,并按照指令的顺序开始执行程序。CPU会根据指令进行算术运算、逻辑判断、内存访问等操作,最终实现程序的功能。
深入理解ELF格式:在Linux系统中的重要作用
什么是ELF?
-
ELF(Executable and Linkable Format,可执行与可链接格式)
-
在Linux系统中,使用ELF来存储和组织数据
ELF的文件结构
ELF主要文件结构:
.text Section
:代码段或者指令段(Code Section),用来保存程序的代码和指令;.data Section
:数据段(Data Section),用来保存程序里面设置好的初始化数据信息;.rel.text Secion
,:重定位表(Relocation Table)。重定位表里,保留的是当前的文件里面,哪些跳转地址其实是我们不知道的。.symtab Section
:符号表(Symbol Table)。符号表保留了我们所说的当前文件里面定义的函数名称和对应地址的地址簿。
ELF格式在编译过程中的关键作用
- 编译阶段(Compile):编译器生成的目标文件通常使用ELF格式来存储编译后的代码和数据。
- 汇编阶段(Assemble):ELF格式在这个阶段中用于存储汇编后的机器指令和数据。
- 链接阶段(Link):链接阶段是ELF格式的主要应用领域。在链接阶段,链接器读取多个目标文件和库文件,根据符号引用关系进行符号解析和重定位,最终生成可执行文件。ELF格式提供了段表、符号表和重定位表等结构来描述文件的各个部分和符号之间的关系,使得链接器能够准确地处理符号引用和重定位操作。
- 装载阶段(Load):ELF格式在这个阶段中帮助操作系统(Operation System)理解可执行文件的布局和重定位需求。
ELF运行示例
C代码
以下两个文件 add_lib.c
和 link_example.c
协同工作,实现了一个加法功能。
// 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);
}
汇编
以下是 add_lib.c
和 link_example.c
生成的目标文件(Object File):add_lib.o
和link_example .o
。
使用gcc编译:
$ 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函数的汇编代码
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函数的汇编代码
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
链接
gcc -c add_lib.s
gcc -c link_example.s
可执行代码
gcc -o executable add_lib.o link_example.o
$ ./executable
c = 15 # 运行结果为15
- 注意:
main
函数里调用add
的跳转地址,不再是下一条指令的地址了,而是add
函数的入口地址了
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> # 直接在main函数中调用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]
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:
...
链接器会扫描所有输入的目标文件,然后把所有符号表里的信息收集起来,构成一个全局的符号表。然后再根据重定位表,把所有不确定要跳转地址的代码,根据符号表里面存储的地址,进行一次修正。最后,把所有的目标文件的对应段进行一次合并,变成了最终的可执行代码。
Windows OS:PE
- Windows 的可执行文件格式是一种叫作 PE(Portable Executable Format)。
- Linux 下的装载器只能解析 ELF 格式而不能解析 PE 格式。
如何在Window系统和Linux系统下进行格式兼容?
- Linux 下著名的开源项目 Wine,支持兼容 PE 格式的装载器,使得我们能直接在 Linux 下运行 Windows 程序
- Windows 里面也提供了 WSL,也就是 Windows Subsystem for Linux,可以解析和加载 ELF 格式的文件
- 虽然存在各种工具可以实现可执行文件格式兼容,但是程序还依赖各种操作系统本身提供的动态链接库,系统调用等,仍然需要针对特定平台进行适配和测试。换句话说,格式兼容只是第一步。
参考文献
- 徐文浩. ELF和静态链接:为什么程序无法同时在Linux和Windows下运行?极客时间. 2019