计算机组成原理 | 深入理解ELF格式和静态链接

深入解析C语言代码到机器码的过程

第二阶段
Loader
Cache
CPU
第一阶段
Compile
Assemble
Link
Load
读取指令和数据
CPU
内存
装载器
链接
汇编
编译
C代码
可执行文件
图:从C代码到机器码执行过程

从大的方面来说,可以划分为两个阶段:

  1. 第一个阶段:由编译(Compile)、汇编(Assemble)以及链接(Link)三个阶段组成,生成了一个可执行文件(Executable Program)。
  2. 第二个阶段:通过装载器(Loader)把可执行文件装载(Load)到内存中,然后CPU 从内存中读取指令和数据,来开始真正执行程序。

第一个阶段:编译、汇编和链接

  1. 编译(Compile):在这个阶段,使用C语言编译器(如GCC)将C源代码文件(.c文件)编译成汇编代码文件(.s文件)。编译器会对C代码进行词法分析、语法分析和语义分析,然后生成中间代码表示程序的逻辑结构。
  2. 汇编(Assemble):在这个阶段,使用汇编器(如GNU汇编器)将汇编代码文件(.s文件)转换为机器码指令文件(.o文件)。汇编器会将汇编代码中的每条指令翻译为对应的机器码指令。
  3. 链接(Link):在这个阶段,使用链接器(如GNU链接器)将多个机器码指令文件(.o文件)以及所需的库文件链接在一起,生成最终的可执行文件(Executable Program)。链接器会解析函数和全局变量的引用,并将它们的定义与相应的引用进行关联,创建可执行文件。

第二个阶段:装载和执行

  1. 装载(Load):在这个阶段,操作系统的装载器(Loader)负责将可执行文件加载到内存中的适当位置。装载器会分配内存空间,将可执行文件的指令、数据和其他资源复制到相应的内存地址。
  2. 执行:一旦可执行文件被成功加载到内存中,CPU从内存中读取指令和数据,并按照指令的顺序开始执行程序。CPU会根据指令进行算术运算、逻辑判断、内存访问等操作,最终实现程序的功能。

深入理解ELF格式:在Linux系统中的重要作用

什么是ELF?

  • ELF(Executable and Linkable Format,可执行与可链接格式)

  • 在Linux系统中,使用ELF来存储和组织数据

ELF的文件结构

ELF主要文件结构:

  1. .text Section:代码段或者指令段(Code Section),用来保存程序的代码和指令;
  2. .data Section:数据段(Data Section),用来保存程序里面设置好的初始化数据信息;
  3. .rel.text Secion,:重定位表(Relocation Table)。重定位表里,保留的是当前的文件里面,哪些跳转地址其实是我们不知道的。
  4. .symtab Section:符号表(Symbol Table)。符号表保留了我们所说的当前文件里面定义的函数名称和对应地址的地址簿。

计算机组成原理 | 深入理解ELF格式和静态链接_第1张图片

图:ELF文件关键结构

ELF格式在编译过程中的关键作用

  1. 编译阶段(Compile):编译器生成的目标文件通常使用ELF格式来存储编译后的代码和数据。
  2. 汇编阶段(Assemble):ELF格式在这个阶段中用于存储汇编后的机器指令和数据。
  3. 链接阶段(Link):链接阶段是ELF格式的主要应用领域。在链接阶段,链接器读取多个目标文件和库文件,根据符号引用关系进行符号解析和重定位,最终生成可执行文件。ELF格式提供了段表、符号表和重定位表等结构来描述文件的各个部分和符号之间的关系,使得链接器能够准确地处理符号引用和重定位操作。
  4. 装载阶段(Load):ELF格式在这个阶段中帮助操作系统(Operation System)理解可执行文件的布局和重定位需求。

ELF运行示例

C代码

以下两个文件 add_lib.clink_example.c 协同工作,实现了一个加法功能。

// add_lib.c
int add(int a, int b)
{
    return a+b;
}
// link_example.c

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

汇编

以下是 add_lib.clink_example.c生成的目标文件(Object File):add_lib.olink_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 
  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:
...

链接器会扫描所有输入的目标文件,然后把所有符号表里的信息收集起来,构成一个全局的符号表。然后再根据重定位表,把所有不确定要跳转地址的代码,根据符号表里面存储的地址,进行一次修正。最后,把所有的目标文件的对应段进行一次合并,变成了最终的可执行代码。

计算机组成原理 | 深入理解ELF格式和静态链接_第2张图片

图:可执行文件生成过程示意图

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

你可能感兴趣的:(#,计算机组成原理,计算机基础知识,ELF,计算机组成原理,Linux)