程序装载

程序装载

建议先读静态链接文章,然后再看本文。

Linux内核装载ELF

  • bash输入./xx,bash进程调用fork系统调用创建一个新的进程
  • 新进程调用execve调用指定的ELF文件
    • clibc对execve进行了变体包装提供了execl,execlp,execle,execvexecvp等形式,见到后知道都是exec系列,底层都是调用execve系统调用。
    • execve首先检查要执行的文件检查前128字节,看是不是ELF文件,比如#!/usr/bin的就不是ELF而是普通shell文件,文件头4个字节就是魔数来,前面ELF魔数是del e l f对吧,java的是cafe,shell这种就是靠#!两个字符即可识别格式是文本,并且解析后面的部分最终确定咋执行。判断出ELF格式,就用load_elf_binary()来加载elf
    • 寻找动态链接的.interp端,设置动态链接的路径
    • ELF的程序头表描述,对ELF文件进行映射。
    • 初始化ELF进程环境。
    • 将系统调用的返回地址修改为ELF文件的入口点,如果静态链接的ELF入口点就是嗯剪头中e_entry所指的地址,动态链接的入口点是动态连接器。
  • 系统调用返回,返回值是入口点的地址,新程序就从这个地址开始执行,ELF装载完成。

gpt的回答

在 Linux 系统中,ELF (Executable and Linkable Format) 文件被装载到内存并开始执行的过程涉及到操作系统的多个组件,主要包括内核和动态链接器(如 ld-linux.so)。以下是该过程的简化概述:

1 启动执行:

当你在命令行输入程序名称并按下回车时,shell 会调用 exec() 系统调用来执行程序。
exec() 系统调用告诉内核加载和执行指定的程序。

2文件读取:

内核首先验证 ELF 文件的魔数(文件的前几个字节),确保其是一个有效的 ELF 文件。
然后,内核读取 ELF 文件的头部信息,以确定程序的段(如 .text、.data、.bss 等)以及它们在虚拟内存中的映射位置。

3 创建进程映像:

内核为新进程创建一个进程映像,分配必要的虚拟内存,并设置程序计数器(PC)指向入口点。
对于 ELF 文件中的每个程序段(segment),内核将它们映射到进程的虚拟地址空间。这可能涉及拷贝数据到内存,或者建立内存映射以便于惰性加载。

4 处理动态链接(如果适用):

如果 ELF 文件是动态可执行文件,它将包含对动态链接器(如 ld-linux.so)的引用。
内核将加载动态链接器,并且实际的程序入口点实际上是动态链接器的入口点。
动态链接器读取程序的动态段(.dynamic),解析所有需要的共享库依赖,并加载它们进入内存。
动态链接器还进行符号解析,即确定程序中引用的函数和变量在哪个加载的共享库中,并将这些引用链接到正确的地址。

5 转移控制:

一旦动态链接器完成了其工作,它会将控制权转交给程序的实际入口点(通常是 _start),该入口点之后会调用 main() 函数。

6 程序执行:

现在程序已经完全装载到内存中,所有必要的链接已经完成,程序开始执行。
运行时环境:

在程序开始运行之前,C 运行时环境(CRT)会进行一些初始化工作,包括设置标准 I/O 流、调用全局构造函数等,然后才会调用 main()。

7 程序终止:

当 main() 函数返回时,C 运行时会进行清理工作,比如调用全局析构函数,并最终调用 exit() 系统调用来结束进程。

入口是_start

gcc编译的ELF默认的入口是_start的位置,由该函数负责处理入参环境变量等初始化操作,并最后启动main函数。_start符号来自libc中的crt1.o文件,(其实是他又引用的别的文件,最终是一段asm汇编代码)。

我们也可以写一个只有_start函数的文件,这样就需要排除c标准库来进行编译,与此同时我们也不能使用标准库给我们提供的头文件了,代码如下mini.c.

注意这是x86平台的linux,能找到/glibc-2.35/sysdeps/unix/sysv/linux/x86_64/64/arch-syscall.h:#define __NR_write 1,平台系统调用序号并不是1,需要自己去查,当然其他平台寄存器也不是这么写。

// 裸机程序示例,使用 _start 作为程序入口
// 注意:这是在 Linux x86_64 架构下的示例
// 编译命令: gcc -static -nostdlib -nostartfiles -o mini mini.c

// 先从libc的sys/syscall.h找到以下定义拿过来,因为不能直接include
#define SYS_write 1 // 这是write系统调用的代号
#define SYS_exit 60 // 这是exit系统调用的代号

// 定义 _start 函数,这是执行时的程序入口点
void _start() {
    // 要写入的消息
    const char message[] = "Hello, World!\n";
    // 消息长度
    unsigned long length = sizeof(message) - 1;

    // 使用内联汇编进行系统调用
    // syscall(SYS_write, STDOUT_FILENO, message, length)
    __asm__("movq $1, %%rax\n\t"          // 系统调用号 SYS_write
            "movq $1, %%rdi\n\t"          // 文件描述符 STDOUT_FILENO
            "movq %0, %%rsi\n\t"          // 消息缓冲区的地址
            "movq %1, %%rdx\n\t"          // 消息的长度
            "syscall\n\t"
            :
            : "r"(message), "r"(length)
            : "%rax", "%rdi", "%rsi", "%rdx");

    // 使用内联汇编执行退出系统调用
    // syscall(SYS_exit, 0)
    __asm__("movq $60, %%rax\n\t"         // 系统调用号 SYS_exit
            "xor %%rdi, %%rdi\n\t"        // Exit status 0
            "syscall"
            :
            :
            : "%rax", "%rdi");
}

编译链接就可以运行了,建议使用静态链接

$ gcc -static -nostdlib -nostartfiles -o mini mini.c
$ ./mini
Hello, World!

$ nm mini
0000000000404000 R __bss_start
0000000000404000 R _edata
0000000000404000 R _end
0000000000401000 T _start

 

静态链接

准备一个hello.c文件

#include<stdio.h>
int main() {
    printf("Hello World\n");
    return 0;
}

1 编译链接的主要过程

第一步 预处理

预处理过程就是把所有的#开头的不管是头文件引用还是宏定义给干掉,替换成原始代码。

$ gcc -E -o hello.i hello.c # -E指定只进行预处理

得到的hello.i文件有17k这么大,都是#include<stdio.h>这一行引入的,因为有传染效应,该文件中的#也要被替换为源码。

第二步 编译

编译的过程就是将预处理后的hello.i文件,转换成汇编语言。

$ gcc -S -o hello.s hello.i # -S指定进行编译不进行汇编

# 或者
$ gcc -S -o hello.s hello.c

编译过程就是词法、语法、语意分析、还有各种优化,生成中间表达式或者叫中间文件,这也就是编译器前端的工作,之后由后端转为机器码/汇编代码。这里不展开,总之得到了汇编文件hello.s可以打开后,看到对应的汇编代码。

第三步 汇编

汇编是把汇编代码转换为机器码,机器码是给机器看的,所以之前的文件,人是可以看懂的,但是机器码就很难看懂了,机器码本身就是个二进制文件了。

$ gcc -c -o hello.o hello.c

hello.o文件是Object格式,一般叫做目标文件,该文件已经是二进制可执行文件的格式了,但是还不能直接运行,因为当前的目标文件,是一个可重定位目标文件,并没有达到最终的可执行的状态。还需要关键一步,也就是链接。

第四步 链接

链接就是本文接下来要讲述的过程,我们从上面拿到的hello.o文件开始。

2 读目标文件

我们说hello.o是个不可读的二进制文件,但是有一些系统内置的工具可以帮助我们来读取该文件。这里介绍两个readelfobjdump工具。

readelf顾名思义就是读取elf文件的工具,那为啥是elf文件呢,我们不是读取的目标文件吗。这是因为elf(Executable and Linkable Format)文件是一个二进制可执行文件的重要格式规范,目标文件就是改格式规范下的。

ELF文件主要由3部分组成:

  • ELF头部(ELF Header):位于文件开头,包含了描述整个文件的基本信息,如文件的类型(是可执行文件、可重定位文件还是共享对象文件)、目标机器类型(如x86、ARM等)、入口地址(如果文件是可执行文件)、ELF版本和各个段(Section)的位置信息等。ELF头部使得操作系统能够理解文件的基本结构和如何加载它。
  • 节头(Section Header):这一部分包含了一系列的条目,每个条目都描述文件中的一个节(section),例如.text、.data、.bss、.rodata、.symtab等。每个条目中包含了节的名称、类型、大小、地址、对齐约束等信息。节是文件的组成部分,用于存储程序的代码、数据、符号表、重定位信息等。
  • 节(Sections):实际的代码和数据都存储在这些节中。常见的节包括: .text:存放程序的执行代码。 .data:存放已初始化的全局变量和静态变量。 .bss:存放未初始化的全局变量和静态变量。 .rodata:存放只读数据,如字符串常量。 .symtab和.dynsym:存放符号表,用于名称解析和动态链接。 .rel.text、.rel.data等:存放重定位信息。 .dynamic:存放动态链接信息。 .note:存放注释信息。

实际排布上Section HeaderSection后面。

ELF header

hello.o的elf header部分如下,Magic部分是文件最初的几个字节,表示文件的类型是EFL类型;后面是一些其他元信息,注意在这部分中还显示声明了其他几个部分的地址和大小,比如program headers这里就是0字节,也就是没有这部分。

$ readelf -h hello.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          600 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         14
  Section header string table index: 13

section headers

因为没有program header部分,我们直接用-S查看section headers如下,主要记录了每个section的名字,类型,地址(这里都是0之后解释)等信息。

$ readelf -S  hello.o
There are 14 section headers, starting at offset 0x258:

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
       000000000000001e  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000198
       0000000000000030  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000005e
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000005e
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  0000005e
       000000000000000c  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  0000006a
       000000000000002c  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000096
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.pr[...] NOTE             0000000000000000  00000098
       0000000000000020  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000b8
       0000000000000038  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  000001c8
       0000000000000018  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  000000f0
       0000000000000090  0000000000000018          12     4     8
  [12] .strtab           STRTAB           0000000000000000  00000180
       0000000000000013  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  000001e0
       0000000000000074  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), l (large), p (processor specific)

section

为了更好的理解,我们使用objdump来配合查看section内部的具体内容,结果如下,我们注意到.rodata部分内容就是字符换Hello World\n\0印证之前说的这部分是存储数据的,.comment是存储了gcc编译器的信息不用管,.note.xx也是一些基础信息,eh_frame处理异常信息用先不管。

.text则是存储的"源代码"也就是机器码,直接对照下面最后部分的汇编代码。

 $ objdump -s -d hello.o

hello.o:     file format elf64-x86-64

Contents of section .text:
 0000 f30f1efa 554889e5 488d0500 00000048  ....UH..H......H
 0010 89c7e800 000000b8 00000000 5dc3      ............].  
Contents of section .rodata:
 0000 48656c6c 6f20576f 726c6400           Hello World.    
Contents of section .comment:
 0000 00474343 3a202855 62756e74 75203131  .GCC: (Ubuntu 11
 0010 2e342e30 2d317562 756e7475 317e3232  .4.0-1ubuntu1~22
 0020 2e303429 2031312e 342e3000           .04) 11.4.0.    
Contents of section .note.gnu.property:
 0000 04000000 10000000 05000000 474e5500  ............GNU.
 0010 020000c0 04000000 03000000 00000000  ................
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 1e000000 00450e10 8602430d  .........E....C.
 0030 06550c07 08000000                    .U......        

Disassembly of section .text:

0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # f <main+0xf>
   f:   48 89 c7                mov    %rax,%rdi
  12:   e8 00 00 00 00          call   17 <main+0x17>
  17:   b8 00 00 00 00          mov    $0x0,%eax
  1c:   5d                      pop    %rbp
  1d:   c3                      ret   

通过readelf -a可以查看所有信息,我们把上述没有展示的信息放到下面,主要是展开了section的entries,我们重点关注下.symtab也就是符号表的内容,这里有6项,其中第0项是个留空的不管。

$ readelf -a hello.o
...

Relocation section '.rela.text' at offset 0x198 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000b  000300000002 R_X86_64_PC32     0000000000000000 .rodata - 4
000000000013  000500000004 R_X86_64_PLT32    0000000000000000 puts - 4

Relocation section '.rela.eh_frame' at offset 0x1c8 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0
No processor specific unwind information to decode

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 hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     4: 0000000000000000    30 FUNC    GLOBAL DEFAULT    1 main
     5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

No version information found in this file.

Displaying notes found in: .note.gnu.property
  Owner                Data size        Description
  GNU                  0x00000010       NT_GNU_PROPERTY_TYPE_0

后面的几个size为0的分别为FILE类型,这是指向文件本身的Name就是文件名hello.cSECTION类型的.text.rodata声明了自己有这两个section。

main是当前文件中声明的函数,函数和变量都是符号,函数是FUNC类型,变量则是OBJECT类型,例如将代码修改如下,得到的符号表就会多个v1.

#include<stdio.h>

int v1 = 1;

int main() {
    printf("Hello World\n");
    return 0;
}

/*
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     4: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 v1
     5: 0000000000000000    30 FUNC    GLOBAL DEFAULT    1 main
     6: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
*/

最后解释下puts这个是编译器进行了优化将printf替换为puts了,使用的gcc版本如下

gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)

其他版本或者换成clang某个版本后没有该优化结果如下:

Symbol table '.symtab' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 .text
     3: 0000000000000000    13 OBJECT  LOCAL  DEFAULT    5 .L.str
     4: 0000000000000000    37 FUNC    GLOBAL DEFAULT    2 main
     5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

这里我们注意到printf类型是NOTYPE,未定义的类型,因为他的Ndx值是UND即未定义,换句话说printf函数不是当前文件定义的,是在stdio中定义的,需要在链接这一步给重定位了,clang的结果中.L.str就是字符串Hello World\n\013个字的存放大小。

可以看出不同编译器,不同版本,甚至不同os和cpu架构都可能会生成不同的elf。

理解汇编代码中的地址

我们上面通过objdump反编译出了汇编代码如下,我们详细分析一下每一部分的含义。

0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # f <main+0xf>
   f:   48 89 c7                mov    %rax,%rdi
  12:   e8 00 00 00 00          call   17 <main+0x17>
  17:   b8 00 00 00 00          mov    $0x0,%eax
  1c:   5d                      pop    %rbp
  1d:   c3                      ret   

首先是main函数其实地址为0,左侧一列是偏移量,可以看到总体便宜量也就是main函数大小是0x1d这么大。

前三行解释:

第一行endbr64是一个比较新的指令它与Intel的控制流强化技术有关,跳过不管。第二行的push %rbp是非常常见的函数第一行执行的指令,用于将基指针寄存器(Base Pointer,rbp)的内容压入栈中。rbp 寄存器通常用于在函数调用中保存栈的基地址。push %rbp 通常在函数的序言(prologue)部分出现,保存当前函数的栈基指针,为设置新的栈帧做准备。第三行和第二行是配合的mov %rsp, %rbp 是 x86_64 汇编中的一个常见指令,它的作用是将栈指针(Stack Pointer)寄存器 rsp 的当前值移动(复制)到基指针(Base Pointer)寄存器 rbp 中。这个操作通常是在函数调用的开头执行的,作为创建一个新的栈帧(Stack Frame)的一部分。

第4-5行:

lea 0x0(%rip),%raxlea用于加载有效地址,%rip 是 x86_64 架构中的指令指针(Instruction Pointer),又称程序计数器(Program Counter),0x0(%rip)就是从当前指令的下一条指令加上偏移量0的地址也就是下一行0x0f的地址,lea作用是让rax=0xf,RAX 是累加器寄存器(Accumulator Register)通常也用来存储函数返回值。mov %rax,%rdi则是将rdi目的索引寄存器(Destination Index Register)【RDI 用来传递第一个整数或指针参数给函数。如果函数有多个参数,后续的参数会按照特定的顺序使用 RSI、RDX、RCX、R8 和 R9 寄存器。】也复制为rax的值。这里第4-5行,看似不明所以,其实是为了准备helloworld字符串,如果把0x0(%rip)换成字符串的地址,就好理解了,即将字符串地址赋给rax,然后赋给rdi,rdi是作为接下来调用的函数的第一个入参的,也就是对应第6行call printf的第一个入参。

第6-9行:

call 17就是执行便宜量为17的地方的函数,其实是执行printf但是因为没有在当前文件中声明,需要依赖链接后确定最终地址,这里17就是第7行的地址,即和之前。第7行mov $0x0,%eax将立即数0复制给rax的低32位,rax前面介绍过是用来存储返回值的,这个0也就是最终的返回值,当然如果我们将代码改成return 1这里就变成了mov $0x1,%eax.pop %rbp函数出栈,ret出栈后返回调用者。

到这里我们大概理明白了这一小段汇编代码的作用,其中地址的部分有些奇怪,很多都是复制了当前的执行地址,而不是数据或函数,或者统一叫做符号的实际地址,而真正替换为实际的地址,需要用到链接过程了。

3 静态链接

hello.o静态链接为可执行文件hello

$ gcc -static -o hello hello.o

因为hello.c中使用了printf方法,该函数来自libc牵扯的东西非常多,所以静态链接后hello文件有几百k太大了不方便我们分析,所以这里我们重新创建两个文件a.cb.c,即按照《程序员的自我修养》的demo走一遍。

//a.c
extern int shared;
int main() {
     int a = 100;
     swap(&a, &shared);
}
//b.c
int shared = 1;

void swap(int* a, int* b) {
     // 不用额外空间进行swap的代码
     *a ^= *b ^= *a ^= *b;
}

通过gcc分别获得目标文件a.o和b.o,注意-fno-stack-protector是指定禁用栈保护,这样防止添加__stack_chk_fail这个符号。

$ gcc -fno-stack-protector -c b.c a.c

静态链接后,section的重定位情况

通过objdump查看两者汇编代码如下:

$ objdump -d a.o
a.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 83 ec 10             sub    $0x10,%rsp
   c:   c7 45 fc 64 00 00 00    movl   $0x64,-0x4(%rbp)
  13:   48 8d 45 fc             lea    -0x4(%rbp),%rax
  17:   48 8d 15 00 00 00 00    lea    0x0(%rip),%rdx        # 1e <main+0x1e>
  1e:   48 89 d6                mov    %rdx,%rsi
  21:   48 89 c7                mov    %rax,%rdi
  24:   b8 00 00 00 00          mov    $0x0,%eax
  29:   e8 00 00 00 00          call   2e <main+0x2e>
  2e:   b8 00 00 00 00          mov    $0x0,%eax
  33:   c9                      leave  
  34:   c3                      ret                    

在a中我们同样发现调用swap函数的地址是call 2e <main+0x2e>也就是下一条指令地址占位了,以及第一个参数rdi的赋值其实是给了0x64也就是100给了栈-4的地址(还记得栈向下生长吧,然后int占4byte),第二个参数rsi是00 00 00 00作为占位符了。

$ objdump -d b.o
b.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <swap>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
   c:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  10:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  14:   8b 10                   mov    (%rax),%edx
  16:   48 8b 45 f0             mov    -0x10(%rbp),%rax
  1a:   8b 00                   mov    (%rax),%eax
  1c:   31 c2                   xor    %eax,%edx
  1e:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  22:   89 10                   mov    %edx,(%rax)
  24:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  28:   8b 10                   mov    (%rax),%edx
  2a:   48 8b 45 f0             mov    -0x10(%rbp),%rax
  2e:   8b 00                   mov    (%rax),%eax
  30:   31 c2                   xor    %eax,%edx
  32:   48 8b 45 f0             mov    -0x10(%rbp),%rax
  36:   89 10                   mov    %edx,(%rax)
  38:   48 8b 45 f0             mov    -0x10(%rbp),%rax
  3c:   8b 10                   mov    (%rax),%edx
  3e:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  42:   8b 00                   mov    (%rax),%eax
  44:   31 c2                   xor    %eax,%edx
  46:   48 8b 45 f8             mov    -0x8(%rbp),%rax
  4a:   89 10                   mov    %edx,(%rax)
  4c:   90                      nop
  4d:   5d                      pop    %rbp
  4e:   c3                      ret 

手动链接得到二进制文件ab

$ ld -static -o ab a.o b.o -e main

我们来对比a.o b.oab的各个section

$ objdump -h a.o 
a.o:     file format elf64-x86-64
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000035  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000075  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000075  2**0
                  ALLOC
  3 .comment      0000002c  0000000000000000  0000000000000000  00000075  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000a1  2**0
                  CONTENTS, READONLY
  5 .note.gnu.property 00000020  0000000000000000  0000000000000000  000000a8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .eh_frame     00000038  0000000000000000  0000000000000000  000000c8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
$ objdump -h b.o
b.o:     file format elf64-x86-64
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000004f  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  00000090  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000094  2**0
                  ALLOC
  3 .comment      0000002c  0000000000000000  0000000000000000  00000094  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000c0  2**0
                  CONTENTS, READONLY
  5 .note.gnu.property 00000020  0000000000000000  0000000000000000  000000c0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .eh_frame     00000038  0000000000000000  0000000000000000  000000e0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
$ objdump -h ab
ab:     file format elf64-x86-64
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .note.gnu.property 00000020  00000000004001c8  00000000004001c8  000001c8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .text         00000084  0000000000401000  0000000000401000  00001000  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .eh_frame     00000058  0000000000402000  0000000000402000  00002000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .data         00000004  0000000000404000  0000000000404000  00003000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  4 .comment      0000002b  0000000000000000  0000000000000000  00003004  2**0
                  CONTENTS, READONLY

发现ab是把a和b的各个section进行了整合,去掉了size为0的。

静态链接后,符号表的重定位情况

# a.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 a.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    53 FUNC    GLOBAL DEFAULT    1 main
     4: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND shared
     5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap

# b.o
Symbol table '.symtab' contains 5 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS b.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 shared
     4: 0000000000000000    79 FUNC    GLOBAL DEFAULT    1 swap

# ab
Symbol table '.symtab' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.c
     2: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS b.c
     3: 0000000000401035    79 FUNC    GLOBAL DEFAULT    2 swap
     4: 0000000000404000     4 OBJECT  GLOBAL DEFAULT    4 shared
     5: 0000000000404004     0 NOTYPE  GLOBAL DEFAULT    4 __bss_start
     6: 0000000000401000    53 FUNC    GLOBAL DEFAULT    2 main
     7: 0000000000404004     0 NOTYPE  GLOBAL DEFAULT    4 _edata
     8: 0000000000404008     0 NOTYPE  GLOBAL DEFAULT    4 _end

a中的两个UND未定义的符号shared和swap在b中找到了,所以最后ab的符号表中没有UND的符号了(第0个跳过不用管)

这里额外补充知识:

  • 符号的不同bind,local是当前文件能用的,global是全局的。在全局范围声明的符号(函数、变量)默认是global的例如shared变量和swap函数,而如果添加了static关键词修饰的就是local的,栈上的不是符号,所以int a不存在于符号表中。
  • 除了a和b中的符号,ab还额外增加了__bss_start、_edata、_end这三个符号,我们暂时不用管。

静态链接后,汇编代码的情况

这是ab的汇编代码,我们可以对比前面列出的a.o和b.o的汇编,找下不同:

  • lea 0x2fe2(%rip),%rdx # 404000 <shared> 这一行给rdx->rsi最终赋值的不再是0x0而是0x2fe2,后面注释表示这就是shared的符号地址。
$ objdump -d ab
ab:     file format elf64-x86-64
Disassembly of section .text:
0000000000401000 <main>:
  401000:       f3 0f 1e fa             endbr64 
  401004:       55                      push   %rbp
  401005:       48 89 e5                mov    %rsp,%rbp
  401008:       48 83 ec 10             sub    $0x10,%rsp
  40100c:       c7 45 fc 64 00 00 00    movl   $0x64,-0x4(%rbp)
  401013:       48 8d 45 fc             lea    -0x4(%rbp),%rax
  401017:       48 8d 15 e2 2f 00 00    lea    0x2fe2(%rip),%rdx        # 404000 <shared>
  40101e:       48 89 d6                mov    %rdx,%rsi
  401021:       48 89 c7                mov    %rax,%rdi
  401024:       b8 00 00 00 00          mov    $0x0,%eax
  401029:       e8 07 00 00 00          call   401035 <swap>
  40102e:       b8 00 00 00 00          mov    $0x0,%eax
  401033:       c9                      leave  
  401034:       c3                      ret    

0000000000401035 <swap>:
  401035:       f3 0f 1e fa             endbr64 
  401039:       55                      push   %rbp
  40103a:       48 89 e5                mov    %rsp,%rbp
  40103d:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
  401041:       48 89 75 f0             mov    %rsi,-0x10(%rbp)
  401045:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  401049:       8b 10                   mov    (%rax),%edx
  40104b:       48 8b 45 f0             mov    -0x10(%rbp),%rax
  40104f:       8b 00                   mov    (%rax),%eax
  401051:       31 c2                   xor    %eax,%edx
  401053:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  401057:       89 10                   mov    %edx,(%rax)
  401059:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  40105d:       8b 10                   mov    (%rax),%edx
  40105f:       48 8b 45 f0             mov    -0x10(%rbp),%rax
  401063:       8b 00                   mov    (%rax),%eax
  401065:       31 c2                   xor    %eax,%edx
  401067:       48 8b 45 f0             mov    -0x10(%rbp),%rax
  40106b:       89 10                   mov    %edx,(%rax)
  40106d:       48 8b 45 f0             mov    -0x10(%rbp),%rax
  401071:       8b 10                   mov    (%rax),%edx
  401073:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  401077:       8b 00                   mov    (%rax),%eax
  401079:       31 c2                   xor    %eax,%edx
  40107b:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  40107f:       89 10                   mov    %edx,(%rax)
  401081:       90                      nop
  401082:       5d                      pop    %rbp
  401083:       c3                      ret 

4 其他符号

在上面静态链接过程中我们看到了诸如_end这样的符号,虽然我们的目标文件中是没有这个符号定义的,但是静态链接之后就自动多出了这个符号。这是由链接器自动植入的,通常我们不需要理会他,_end的作用就是表示.data.bss全局数据段的结尾地址,而_edata标识.data的结尾地址,__bss_start就更能从名字看出他的意思了。这三个符号我们不需要理会,对于用户来说没有特别的用途。

不过我们注意到上面进行链接的时候指定了-e main即入口函数为main,如果不做指定的话,入口函数是_start,如下,但是_start_end不同,_start是个入口函数的地址,并且这个函数是由crt(例如glibc)提供的,如果不指定这些目标文件会获得如下报错,生成的ab文件中_start符号没有定义,无法正常使用。

$ ld -static -o ab a.o b.o 
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000

crt1.o(包含 _start)、crti.o(包含一些初始化代码)、crtn.o(包含终结代码)等,以及 libc.a 或 libc.so(C 库),所以手动链接要包含_start符号是一个很长的指令串(多个crtx.o文件),所以建议直接用gcc指令自动链接这些glibc相关的目标文件。

$ gcc -statc -o ab a.o b.o

不过这样生成的ab文件符号多达上千个,因为将libc中各种符号都引入了,比如brk``printf等等还有一堆_开头的,对于学习静态链接过程来说没有帮助,我们就不展开说了。只需知道_start是程序的真正入口,该函数下会进行一系列的初始化准备工作,并最终调用main方法,通过ld -e main ...创建的二进制文件,是一种nostd的形式,是无法./ab来运行的~

感兴趣的话可以在代码中打印下这些符号的地址看看:

extern _end char[];
int main() {
     printf("_end %X\n", _end);
}

5 段(Segment)与区(Section)

可能会在多个ELF场景下看到段和区的描述,他们往往很相似。我们上面介绍的都是目标文件的区section,而程序段segment和区是相似的东西,区的划分非常多,段为了更好的管理,和区不再一一映射,而是一个段会包含一个或多个区,段就是区最终映射到内存的形式。

区在elf的section header中通过-S查看,段则是存在program header通过-l如下,目标文件a.o b.o因为不是可执行的文件没有该部分,只有ab有,注意这里没有堆、栈这种段,这些是在程序运行时动态管理的内存。

$ readelf -l a.o
There are no program headers in this file.

$ readelf -l b.o
There are no program headers in this file.

$ readelf -l ab
Elf file type is EXEC (Executable file)
Entry point 0x401000
There are 7 program headers, starting at offset 64
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000001e8 0x00000000000001e8  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x0000000000000084 0x0000000000000084  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
                 0x0000000000000058 0x0000000000000058  R      0x1000
  LOAD           0x0000000000003000 0x0000000000404000 0x0000000000404000
                 0x0000000000000004 0x0000000000000004  RW     0x1000
  NOTE           0x00000000000001c8 0x00000000004001c8 0x00000000004001c8
                 0x0000000000000020 0x0000000000000020  R      0x8
  GNU_PROPERTY   0x00000000000001c8 0x00000000004001c8 0x00000000004001c8
                 0x0000000000000020 0x0000000000000020  R      0x8
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10

 Section to Segment mapping:
  Segment Sections...
   00     .note.gnu.property 
   01     .text 
   02     .eh_frame 
   03     .data 
   04     .note.gnu.property 
   05     .note.gnu.property 
   06   

 

posted @ 2024-03-03 20:27  CharyGao  阅读(69)  评论(0)    收藏  举报