关于 ELF 文件格式的笔记(二)

关于 ELF 文件格式的笔记(二)

我在 关于 ELF 格式文件的笔记(一) 中讲了基本的 C 编译过程和基本的 ELF 文件格式,本篇文章是 ELF 相关文章的第二篇,没有看过的同学,推荐先看第一篇。

我在第一篇的文章中说过在 ELF 文件中最最最最重要的就是节 (Section),本篇内容就是讲如何通过各种 Section 中的信息来调用本地的方法。(非 .so 库中的方法)

在开始之前先列出一下本篇文章会用到的 Section:

  • .text :可谓是最重要的部分,存放 CPU 执行的机器码。
  • .rela.text :记录 .text Section 中的重定向地址,简单来说就是 .text 中的某些使用到的地址是不可用的,需要借助 .rela.text 重新计算地址。当别的 Section 也需要重新计算地址时也会有一个对应的 .rela.xxxSection,例如 .rela.plt
  • .symtab:符号表,描述了我们定义的方法和全局变量等等,我们可以通过符号表中的信息定位到这些符号所对应的地址。
  • .shstrtab / .strtab :字符表,描述程序中用到的字符,比如代码中用到的方法名,变量名等等。它的记录方法很简单就是一个字符串相对于该表的偏移量,每一个字符串结束都用 \0 表示。
  • .data:存放已经初始化后的全局变量。
  • .bss:存放没有初始化的全局变量。
  • .rodata:存放常量。

虽然你可能看了上面的 Section 的描述现在还有很多的疑问,开始时我也和你一样,当你读完本篇文章后,你就会对上面的 Section 有全新的认识。

准备工作

Tips:我是基于 Linux x86_64 的环境测试的。

写一段测试的代码:

C 复制代码
int add5(int num) {
    return num + 5;
}

int add10(int num) {
    int n1 = add5(num);
    return add5(n1);
}

char* getStr() {
    return "Hello, World!";
}

int var = 3;

void setVar(int n) {
    var = n;
}

int getVar() {
    return var;
}

上面的代码应该不需要解释吧,哈哈😄。

通过 gcc -c [source file] 编译上面的代码,最后会生成一个 .o 的文件。

通过 readelf -a [ELF file] 命令来查看上面的 .o 文件的信息:

text 复制代码
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:          1136 (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:
  [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
       0000000000000073  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000308
       0000000000000078  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  000000b4
       0000000000000004  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000b8
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  000000b8
       000000000000000e  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000c6
       000000000000002e  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000f4
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.propert NOTE             0000000000000000  000000f8
       0000000000000020  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  00000118
       00000000000000b8  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000380
       0000000000000078  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  000001d0
       00000000000000f0  0000000000000018          12     4     8
  [12] .strtab           STRTAB           0000000000000000  000002c0
       0000000000000045  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  000003f8
       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),
  l (large), p (processor specific)

There are no section groups in this file.

There are no program headers in this file.

Relocation section '.rela.text' at offset 0x308 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000028  000400000004 R_X86_64_PLT32    0000000000000000 _Z4add5i - 4
000000000035  000400000004 R_X86_64_PLT32    0000000000000000 _Z4add5i - 4
000000000046  000300000002 R_X86_64_PC32     0000000000000000 .rodata - 4
00000000005c  000700000002 R_X86_64_PC32     0000000000000000 var - 4
00000000006d  000700000002 R_X86_64_PC32     0000000000000000 var - 4

Relocation section '.rela.eh_frame' at offset 0x380 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0
000000000040  000200000002 R_X86_64_PC32     0000000000000000 .text + 13
000000000060  000200000002 R_X86_64_PC32     0000000000000000 .text + 3b
000000000080  000200000002 R_X86_64_PC32     0000000000000000 .text + 4c
0000000000a0  000200000002 R_X86_64_PC32     0000000000000000 .text + 63

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.

Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS My_Math.cpp
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     4: 0000000000000000    19 FUNC    GLOBAL DEFAULT    1 _Z4add5i
     5: 0000000000000013    40 FUNC    GLOBAL DEFAULT    1 _Z5add10i
     6: 000000000000003b    17 FUNC    GLOBAL DEFAULT    1 _Z6getStrv
     7: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 var
     8: 000000000000004c    23 FUNC    GLOBAL DEFAULT    1 _Z6setVari
     9: 0000000000000063    16 FUNC    GLOBAL DEFAULT    1 _Z6getVarv

No version information found in this file.

Displaying notes found at file offset 0x000000f8 with length 0x00000020:
  Owner                 Data size	Description
  GNU                  0x00000010	Unknown note type: (0x00000005)

通过 objdump --disassemble --section=.text [ELF file] 命令来反编译 .text 节机器码,反编译后的源码是汇编(不要害怕,本篇文章不会涉及到汇编的指令使用):

text 复制代码
My_Math.o:	file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_Z4add5i>:
       0: f3 0f 1e fa                  	endbr64
       4: 55                           	pushq	%rbp
       5: 48 89 e5                     	movq	%rsp, %rbp
       8: 89 7d fc                     	movl	%edi, -4(%rbp)
       b: 8b 45 fc                     	movl	-4(%rbp), %eax
       e: 83 c0 05                     	addl	$5, %eax
      11: 5d                           	popq	%rbp
      12: c3                           	retq

0000000000000013 <_Z5add10i>:
      13: f3 0f 1e fa                  	endbr64
      17: 55                           	pushq	%rbp
      18: 48 89 e5                     	movq	%rsp, %rbp
      1b: 48 83 ec 18                  	subq	$24, %rsp
      1f: 89 7d ec                     	movl	%edi, -20(%rbp)
      22: 8b 45 ec                     	movl	-20(%rbp), %eax
      25: 89 c7                        	movl	%eax, %edi
      27: e8 00 00 00 00               	callq	0x2c <_Z5add10i+0x19>
      2c: 89 45 fc                     	movl	%eax, -4(%rbp)
      2f: 8b 45 fc                     	movl	-4(%rbp), %eax
      32: 89 c7                        	movl	%eax, %edi
      34: e8 00 00 00 00               	callq	0x39 <_Z5add10i+0x26>
      39: c9                           	leave
      3a: c3                           	retq

000000000000003b <_Z6getStrv>:
      3b: f3 0f 1e fa                  	endbr64
      3f: 55                           	pushq	%rbp
      40: 48 89 e5                     	movq	%rsp, %rbp
      43: 48 8d 05 00 00 00 00         	leaq	(%rip), %rax  # 4a <_Z6getStrv+0xf>
      4a: 5d                           	popq	%rbp
      4b: c3                           	retq

000000000000004c <_Z6setVari>:
      4c: f3 0f 1e fa                  	endbr64
      50: 55                           	pushq	%rbp
      51: 48 89 e5                     	movq	%rsp, %rbp
      54: 89 7d fc                     	movl	%edi, -4(%rbp)
      57: 8b 45 fc                     	movl	-4(%rbp), %eax
      5a: 89 05 00 00 00 00            	movl	%eax, (%rip)  # 60 <_Z6setVari+0x14>
      60: 90                           	nop
      61: 5d                           	popq	%rbp
      62: c3                           	retq

0000000000000063 <_Z6getVarv>:
      63: f3 0f 1e fa                  	endbr64
      67: 55                           	pushq	%rbp
      68: 48 89 e5                     	movq	%rsp, %rbp
      6b: 8b 05 00 00 00 00            	movl	(%rip), %eax  # 71 <_Z6getVarv+0xe>
      71: 5d                           	popq	%rbp
      72: c3                           	retq

这里看到我们自己定义的方法全部都加上了一个前缀和后缀,这个不重要跳过。

我这里假如 .text.data.rodata 加载到内存中的排列如下:

这里没有考虑其他 Section,图中也忽略了段 (Segment),也没有考虑内存对齐,加载的地址也不可能从 0x00 开始,大家都忽略上面的问题,这个图只是为了让你理解 ELF 文件中的一些原理。

如何手动执行特定方法

通常我们的可执行文件都是都是有一个入口地址(通常是我们的 main() 函数),假如我们现在就是要绕过入口地址去执行一个特定的函数,比如我们定义的 add5()add10() 方法。

我们前面提到符号表中展示了定义的方法和全局变量等等,我们就是从它入手:

text 复制代码
Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     // ...
     4: 0000000000000000    19 FUNC    GLOBAL DEFAULT    1 _Z4add5i
     5: 0000000000000013    40 FUNC    GLOBAL DEFAULT    1 _Z5add10i
     // ...

我这里来描述一下他们的意思:

  • Name:就是符号的名字,它是存放在 .shstrtab 中的,需要通过一个 offset 去查找的,上面为了方便阅读,readelf 直接打印出来了。
  • Ndx:表示该符号属于的 Section 编号,上面的为 1,正好就是 .text
  • Type:符号对应的类型,我们的都是 FUNC
  • Size:符号对应实现的大小。
  • Value:对应符号在 Section 中的位置,它和 Ndx 一起就能够找到对应符号的位置。

看看我们的 add5()add10() 对应的就是 .text Section,位置分别为 00000000000000000000000000000013

我们再看看反编译后的汇编代码:

text 复制代码
// ...
0000000000000000 <_Z4add5i>:
       0: f3 0f 1e fa                  	endbr64
       4: 55                           	pushq	%rbp
       5: 48 89 e5                     	movq	%rsp, %rbp
       8: 89 7d fc                     	movl	%edi, -4(%rbp)
       b: 8b 45 fc                     	movl	-4(%rbp), %eax
       e: 83 c0 05                     	addl	$5, %eax
      11: 5d                           	popq	%rbp
      12: c3                           	retq

0000000000000013 <_Z5add10i>:
      13: f3 0f 1e fa                  	endbr64
      17: 55                           	pushq	%rbp
      18: 48 89 e5                     	movq	%rsp, %rbp
      1b: 48 83 ec 18                  	subq	$24, %rsp
      1f: 89 7d ec                     	movl	%edi, -20(%rbp)
      22: 8b 45 ec                     	movl	-20(%rbp), %eax
      25: 89 c7                        	movl	%eax, %edi
      27: e8 00 00 00 00               	callq	0x2c <_Z5add10i+0x19>
      2c: 89 45 fc                     	movl	%eax, -4(%rbp)
      2f: 8b 45 fc                     	movl	-4(%rbp), %eax
      32: 89 c7                        	movl	%eax, %edi
      34: e8 00 00 00 00               	callq	0x39 <_Z5add10i+0x26>
      39: c9                           	leave
      3a: c3                           	retq
      
// ...

哈哈巧了不是,add5() 正好在 0000000000000000add10() 正好在 0000000000000013

按照我们上面加载到内存中的地址就可以得出 add5() 方法在内存中的地址是 0x00 + 0x0000000000000000add10() 方法在内存中的地址是 0x00 + 0x0000000000000013。得到他们的地址和长度后我们就可以在内存中执行了,实际上 add5() 是可以正常执行的,但是 add10() 不能正常执行,原因我们继续看下一节。

重定向 .text 中的方法调用地址

add5() 方法只是做了简单的加运输,而没有做响应的方法调用跳转,所以在上面的代码中是可以正常运行的,但是 add10() 方法中调用了两次 add5() 方法来实现操作,但是它调用 add5() 方法的地址是错的:

text 复制代码
// ...
0000000000000000 <_Z4add5i>:
       0: f3 0f 1e fa                  	endbr64
       4: 55                           	pushq	%rbp
       5: 48 89 e5                     	movq	%rsp, %rbp
       8: 89 7d fc                     	movl	%edi, -4(%rbp)
       b: 8b 45 fc                     	movl	-4(%rbp), %eax
       e: 83 c0 05                     	addl	$5, %eax
      11: 5d                           	popq	%rbp
      12: c3                           	retq

0000000000000013 <_Z5add10i>:
      13: f3 0f 1e fa                  	endbr64
      17: 55                           	pushq	%rbp
      18: 48 89 e5                     	movq	%rsp, %rbp
      1b: 48 83 ec 18                  	subq	$24, %rsp
      1f: 89 7d ec                     	movl	%edi, -20(%rbp)
      22: 8b 45 ec                     	movl	-20(%rbp), %eax
      25: 89 c7                        	movl	%eax, %edi
      27: e8 00 00 00 00               	callq	0x2c <_Z5add10i+0x19>
      2c: 89 45 fc                     	movl	%eax, -4(%rbp)
      2f: 8b 45 fc                     	movl	-4(%rbp), %eax
      32: 89 c7                        	movl	%eax, %edi
      34: e8 00 00 00 00               	callq	0x39 <_Z5add10i+0x26>
      39: c9                           	leave
      3a: c3                           	retq
      
// ...

我们注意 0x270x34 位置,他们的值都是 e8 00 00 00 00,其中 e8 就表示 callq 调用方法的指令,后面四个字节表示方法的相对当前位置的距离( x86_64 是这样的),所以 0x27 那里的地址就应该是 -0x2c,而 0x34 那里的地址是 -0x39,他们表示都指向 add5() 方法,具体参考以下图(图中的地址和我的例子不一样,注意下):

因为在计算机中表示数据要用补码 (不懂的同学去别的地方查一下),-0x2c 的补码就是 0xffffffd4-0x39 的补码就是 ffffffc7,在这里的数据表示是小端(little endian,不懂的同学去网上查一下),所以 0x27 位置就应该是 e8 d4 ff ff ff,而 0x34 位置就应该是 e8 c7 ff ff ff。当我们把值替换成我们计算后的值后 add10() 方法就可以正常运行了。

emmm,因为我们是有源码,知道调用的是哪个方法,但是 CPU 它不知道啊,所以就引出了这一节要讲的东西,我们需要根据 .rela.text Section 中的数据来重新计算这个地址。

text 复制代码
Relocation section '.rela.text' at offset 0x308 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000028  000400000004 R_X86_64_PLT32    0000000000000000 _Z4add5i - 4
000000000035  000400000004 R_X86_64_PLT32    0000000000000000 _Z4add5i - 4
// ...
  • Offset:需要修改的地址的位置,我们的 callq 指令刚好是 0x270x34,但是指令还要占一个字节,所以对应的位置就是 0x280x35
  • Info:它是一个符合结构,高 32 位表示对应的字符表中的位置,我们这里也就是 _Z4add5i ,低 32 位表示类型,我们这里也就是 R_X86_64_PLT32
  • Type:表示重定位的类型。
  • Sysm. Value:不同的类型有不同的表示,大部分情况下都是和符号表中 Value 对应的,也就是在目标 Section 中的位置,我们的 add5() 方法在 .text 中的位置就是 0x00
  • Name + Addend:符合的名字和对应的偏移量,这里的偏移量是 -4,后续计算地址时会用到。

不同的重定位类型有不同的计算方式,参照这个。我们的这个类型是 R_X86_64_PLT32 ,它的计算公式就是 L + A - P

  • L:对应的符号所在的内存地址,在我们这里就是 add5() 方法 0x00(内存基准地址) + 0x00(add5() 方法在 .text 中的相对偏移)。
  • A:也就是上面表中提到的 Addend,我们这里是 -4
  • P:执行的指令的偏移量,也就是 Offset 的值加上内存地址,所以我们这里就是 0x00 (内存基准地址) + 0x28 (或者 0x35)。

所以 0x28 的位置的地址通过以上方法后计算后得到的结果是 0x00 + 0x00 - 0x04 - 0x28(0x35) = -0x2c(-0x39),哈哈哈哈,和我们自己计算的结果是一样的,对应的补码就是 0xffffffd4(ffffffc7)。

重定向 .text 中的常量和全局变量调用地址

我们再来看看剩下的三个方法,他们涉及到常量和全局变量的调用。

通过 readelf -x .data [elf file] 查看变量表中的数据:

text 复制代码
Hex dump of section '.data':
  0x00000000 03000000  

比较短,一共就 4 Bytes。

通过 readelf -x .rodata [elf file] 查看常量池中的数据:

text 复制代码
Hex dump of section '.rodata':
  0x00000000 48656c6c 6f2c2057 6f726c64 2100     Hello, World!.

一共 14 Bytes,也就是一个字符串 Hello, World!

我们接着看看符号表中的信息:

text 复制代码
Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     // ...
     7: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 var
     // ...

这里的 var 就是我们定义的全局变量,Ndx 为 3 表示对应的 Section.dataValue0x00 表示符号在 .data 中的偏移值为 0

这里看看对应的 .text 反编译后的汇编代码:

text 复制代码
// ...
000000000000003b <_Z6getStrv>:
      3b: f3 0f 1e fa                  	endbr64
      3f: 55                           	pushq	%rbp
      40: 48 89 e5                     	movq	%rsp, %rbp
      43: 48 8d 05 00 00 00 00         	leaq	(%rip), %rax  # 4a <_Z6getStrv+0xf>
      4a: 5d                           	popq	%rbp
      4b: c3                           	retq

000000000000004c <_Z6setVari>:
      4c: f3 0f 1e fa                  	endbr64
      50: 55                           	pushq	%rbp
      51: 48 89 e5                     	movq	%rsp, %rbp
      54: 89 7d fc                     	movl	%edi, -4(%rbp)
      57: 8b 45 fc                     	movl	-4(%rbp), %eax
      5a: 89 05 00 00 00 00            	movl	%eax, (%rip)  # 60 <_Z6setVari+0x14>
      60: 90                           	nop
      61: 5d                           	popq	%rbp
      62: c3                           	retq

0000000000000063 <_Z6getVarv>:
      63: f3 0f 1e fa                  	endbr64
      67: 55                           	pushq	%rbp
      68: 48 89 e5                     	movq	%rsp, %rbp
      6b: 8b 05 00 00 00 00            	movl	(%rip), %eax  # 71 <_Z6getVarv+0xe>
      71: 5d                           	popq	%rbp
      72: c3                           	retq

我们再来看看 .rela.text 中的数据:

.text 复制代码
Relocation section '.rela.text' at offset 0x308 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
// ...
000000000046  000300000002 R_X86_64_PC32     0000000000000000 .rodata - 4
00000000005c  000700000002 R_X86_64_PC32     0000000000000000 var - 4
00000000006d  000700000002 R_X86_64_PC32     0000000000000000 var - 4

我们看到重定向常量和全局变量都是 R_X86_64_PC32 类型,它的重定向计算的公式是 S + A - P,其中这个 SR_X86_64_PLT32 中的 L 是一样的。

getStr() 方法中的重定向

  • S: 符号所在的内存地址( .rodata ):0x00(基准地址) + 0x74.text 占用的偏移)+ 0x04 (.data 占用的偏移) + 0x00 (对应常量值在 .rodata 中的偏移)
  • A-0x04
  • P0x00(基准地址) + 0x46

所以最后计算出来的结果是 0x2e,所以 0x43 地方的值就应该由 48 8d 05 00 00 00 00 修改为 48 8d 05 2e 00 00 00

setVar() 方法中的重定向

  • S: 0x00 (基准地址) + 0x74 (.text 占用偏移) + 0x00 ( var 全局变量在 .data 中的偏移)
  • A-0x04
  • P: 0x00(基准地址) + 0x5c

计算出来的结果是 0x14,所以 0x5a 地方的值就因该由 89 05 00 00 00 00 修改为 89 05 14 00 00 00

getVar() 方法中的重定向

  • S: 0x00 (基准地址) + 0x74 (.text 占用偏移) + 0x00 ( var 全局变量在 .data 中的偏移)
  • A-0x04
  • P: 0x00(基准地址) + 0x6d

计算出来的结果是 0x03,所以 0x6b 地方的值就因该由 8b 05 00 00 00 00 修改为 8b 05 03 00 00 00

总结

到这里相信你应该知道 ELF 文件中是如何调用内部的方法,常量和全体变量了,如果没有看懂就多看几遍,我自己都不知道看了多少遍,下一篇文章介绍如何调用 .so 中的方法。

参考文章

blog.cloudflare.com/how-to-exec...

相关推荐
内核程序员kevin2 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
朝九晚五ฺ6 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream6 小时前
Linux的桌面
linux
xiaozhiwise7 小时前
Makefile 之 自动化变量
linux
长亭外的少年7 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
意疏9 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu9 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器
一只爱撸猫的程序猿9 小时前
一个简单的Linux 服务器性能优化案例
linux·mysql·nginx
建群新人小猿10 小时前
会员等级经验问题
android·开发语言·前端·javascript·php