美文网首页
linux系统的ELF文件解析(二)

linux系统的ELF文件解析(二)

作者: 拉丁吴 | 来源:发表于2024-04-09 14:49 被阅读0次

    前言

    上一篇文章我们介绍了ELF文件的整体结构,可以说操作系统就是ELF Header,SHT,PHT来获得可执行程序的操作说明书的。但是对于ELF文件内部的具体细节仍然值得我们深入研究一下。

    ELF的段

    代码段:程序的入口

    我们从ELF-Header中可以看到 Entry point address的信息,指向的就是程序的代码段的起始地址,CPU一般会跳转到这里开启程序的执行。

    通过objdump工具,我们可以将可执行文件的所有段以16进制的方式打印出来。

    $ objdump -s main.o
    
    ...
    ...
    Contents of section .text:
     1060 f30f1efa 31ed4989 d15e4889 e24883e4  ....1.I..^H..H..
     1070 f0505445 31c031c9 488d3de4 000000ff  .PTE1.1.H.=.....
     1080 15532f00 00f4662e 0f1f8400 00000000  .S/...f.........
     1090 488d3d79 2f000048 8d05722f 00004839  H.=y/..H..r/..H9
     10a0 f8741548 8b05362f 00004885 c07409ff  .t.H..6/..H..t..
     10b0 e00f1f80 00000000 c30f1f80 00000000  ................
     10c0 488d3d49 2f000048 8d35422f 00004829  H.=I/..H.5B/..H)
     10d0 fe4889f0 48c1ee3f 48c1f803 4801c648  .H..H..?H...H..H
     10e0 d1fe7414 488b0505 2f000048 85c07408  ..t.H.../..H..t.
     10f0 ffe0660f 1f440000 c30f1f80 00000000  ..f..D..........
     1100 f30f1efa 803d052f 00000075 2b554883  .....=./...u+UH.
     1110 3de22e00 00004889 e5740c48 8b3de62e  =.....H..t.H.=..
     1120 0000e819 ffffffe8 64ffffff c605dd2e  ........d.......
     1130 0000015d c30f1f00 c30f1f80 00000000  ...]............
     1140 f30f1efa e977ffff fff30f1e fa554889  .....w.......UH.
     1150 e5488d05 ac0e0000 4889c7e8 f0feffff  .H......H.......
     1160 905dc3f3 0f1efa55 4889e5b8 00000000  .].....UH.......
     1170 e8d4ffff ffb80000 00005dc3           ..........].    
    ...
    ...
    

    这种16进制表示得代码很难读,因此我们一般会使用反汇编的方式把它转换为汇编代码,更易读。

    objdump -d main.o // 获取二进制文件中可执行段的反汇编代码
    objdump -D main.o // 获取所有段的反汇编代码
    
    
    Disassembly of section .text:
    
    0000000000001060 <_start>:
        1060:   f3 0f 1e fa             endbr64 
        1064:   31 ed                   xor    %ebp,%ebp
        1066:   49 89 d1                mov    %rdx,%r9
        1069:   5e                      pop    %rsi
        106a:   48 89 e2                mov    %rsp,%rdx
        106d:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
        1071:   50                      push   %rax
        1072:   54                      push   %rsp
        1073:   45 31 c0                xor    %r8d,%r8d
        1076:   31 c9                   xor    %ecx,%ecx
        1078:   48 8d 3d e4 00 00 00    lea    0xe4(%rip),%rdi        # 1163 <main>
        107f:   ff 15 53 2f 00 00       call   *0x2f53(%rip)        # 3fd8 <__libc_start_main@GLIBC_2.34>
        1085:   f4                      hlt    
        1086:   66 2e 0f 1f 84 00 00    cs nopw 0x0(%rax,%rax,1)
        108d:   00 00 00 
    
    0000000000001090 <deregister_tm_clones>:
        1090:   48 8d 3d 79 2f 00 00    lea    0x2f79(%rip),%rdi        # 4010 <__TMC_END__>
        1097:   48 8d 05 72 2f 00 00    lea    0x2f72(%rip),%rax        # 4010 <__TMC_END__>
        109e:   48 39 f8                cmp    %rdi,%rax
        10a1:   74 15                   je     10b8 <deregister_tm_clones+0x28>
        10a3:   48 8b 05 36 2f 00 00    mov    0x2f36(%rip),%rax        # 3fe0 <_ITM_deregisterTMCloneTable@Base>
        10aa:   48 85 c0                test   %rax,%rax
        10ad:   74 09                   je     10b8 <deregister_tm_clones+0x28>
        10af:   ff e0                   jmp    *%rax
        10b1:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
        10b8:   c3                      ret    
        10b9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    
    00000000000010c0 <register_tm_clones>:
        10c0:   48 8d 3d 49 2f 00 00    lea    0x2f49(%rip),%rdi        # 4010 <__TMC_END__>
        10c7:   48 8d 35 42 2f 00 00    lea    0x2f42(%rip),%rsi        # 4010 <__TMC_END__>
        10ce:   48 29 fe                sub    %rdi,%rsi
        10d1:   48 89 f0                mov    %rsi,%rax
        10d4:   48 c1 ee 3f             shr    $0x3f,%rsi
        10d8:   48 c1 f8 03             sar    $0x3,%rax
        10dc:   48 01 c6                add    %rax,%rsi
        10df:   48 d1 fe                sar    %rsi
        10e2:   74 14                   je     10f8 <register_tm_clones+0x38>
        10e4:   48 8b 05 05 2f 00 00    mov    0x2f05(%rip),%rax        # 3ff0 <_ITM_registerTMCloneTable@Base>
        10eb:   48 85 c0                test   %rax,%rax
        10ee:   74 08                   je     10f8 <register_tm_clones+0x38>
        10f0:   ff e0                   jmp    *%rax
        10f2:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
        10f8:   c3                      ret    
        10f9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    
    0000000000001100 <__do_global_dtors_aux>:
        1100:   f3 0f 1e fa             endbr64 
        1104:   80 3d 05 2f 00 00 00    cmpb   $0x0,0x2f05(%rip)        # 4010 <__TMC_END__>
        110b:   75 2b                   jne    1138 <__do_global_dtors_aux+0x38>
        110d:   55                      push   %rbp
        110e:   48 83 3d e2 2e 00 00    cmpq   $0x0,0x2ee2(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
        1115:   00 
        1116:   48 89 e5                mov    %rsp,%rbp
        1119:   74 0c                   je     1127 <__do_global_dtors_aux+0x27>
        111b:   48 8b 3d e6 2e 00 00    mov    0x2ee6(%rip),%rdi        # 4008 <__dso_handle>
        1122:   e8 19 ff ff ff          call   1040 <__cxa_finalize@plt>
        1127:   e8 64 ff ff ff          call   1090 <deregister_tm_clones>
        112c:   c6 05 dd 2e 00 00 01    movb   $0x1,0x2edd(%rip)        # 4010 <__TMC_END__>
        1133:   5d                      pop    %rbp
        1134:   c3                      ret    
        1135:   0f 1f 00                nopl   (%rax)
        1138:   c3                      ret    
        1139:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    
    0000000000001140 <frame_dummy>:
        1140:   f3 0f 1e fa             endbr64 
        1144:   e9 77 ff ff ff          jmp    10c0 <register_tm_clones>
    
    0000000000001149 <sayWords>:
        1149:   f3 0f 1e fa             endbr64 
        114d:   55                      push   %rbp
        114e:   48 89 e5                mov    %rsp,%rbp
        1151:   48 8d 05 ac 0e 00 00    lea    0xeac(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
        1158:   48 89 c7                mov    %rax,%rdi
        115b:   e8 f0 fe ff ff          call   1050 <puts@plt>
        1160:   90                      nop
        1161:   5d                      pop    %rbp
        1162:   c3                      ret    
    
    0000000000001163 <main>:
        1163:   f3 0f 1e fa             endbr64 
        1167:   55                      push   %rbp
        1168:   48 89 e5                mov    %rsp,%rbp
        116b:   b8 00 00 00 00          mov    $0x0,%eax
        1170:   e8 d4 ff ff ff          call   1149 <sayWords>
        1175:   b8 00 00 00 00          mov    $0x0,%eax
        117a:   5d                      pop    %rbp
        117b:   c3                      ret    
    
    

    汇编代码其实我也不是特别的熟悉,虽然大学时学过,但是所学知识已经连本带利全部返还学校了。
    但还是简单和大家介绍一下上面输出的内容的含义:

    • 最左边的一列表示地址偏移,程序再编译后可能假设基地址为0,所以我们可以把偏移地址作为史记地址来分析。
    • 中间一列则是程序指令的16进制表示。
    • 最右侧的一列则是把中间这列的指令翻译为汇编代码,

    针对最右侧的汇编指令,以mian函数为例:

    • 第一条指令 endbr64是intel CPU提供的硬件保护指令,一般放在函数开头
    • push属于压栈操作,mov属于赋值指令,rsp,rbp,eax都是CPU 特殊功能的寄存器,用来存储数据,这三条指令属于函数调用前的栈堆栈指针和返回值的准备设置。
    • 然后调用函数sayWords,然后返回

    我们看到0x1060的入口地址所指向的不是main函数。而是_start函数,这不难理解,所谓入口函数只是对开发者而言的,实际上真正进入开发者的逻辑之前,程序需要一些准备工作,设置好运行环境,之后才能正式调用main函数。

    关于计算机是如何执行函数的,因为篇幅所限不在此展开。

    数据段

    数据段用于存储数据,比如代码里的全局变量和局部静态变量等,都存储在该段。

    由于之前的demo中没有定义我们自己的变量,因此连夜改代码紧急定义一个变量来看看。

    // x.c
    #include<stdio.h>
    
    int global_value = 17;
    int global_value2 = 0xffeebbaa;
    void sayWords(){
    printf("hello owrld from C \n");
    printf("number: %d  %d",global_value,global_value2);
    }
    
     int main(){
        sayWords();
    
        return 0;
     }
    

    然后进行编译

    gcc -o x.o x.c
    

    然后打印可执行文件中的数据段的16进制表示

    $ objdump -s x.o
    
    Contents of section .data:
     4000 00000000 00000000 08400000 00000000  .........@......
     4010 11000000 aabbeeff                           ....           
    
    

    我们发现地址(偏移)为0x4010的位置的8个字节为:0x00000011,0xffeebbaa。0x11转换为10进制就是17。而0xffeebbaa就是我们给第二个变量赋的值。

    你可能会好奇为什么是0x00000011,0xffeebbaa,而不是0x11000000,0xaabbeeff,这个涉及到计算机硬件中字节存储方式的区别:大端序列和小端序列

    • 大端字节序:高位字节在前,低位字节在后
    • 小端字节序:低位字节在前,高位字节在后

    简单来说,大端字节序列和人类的书写阅读方式相吻合,小端字节序列则相反。
    比如正常人写一个数字100,是从左往右写,并且也是从左往右阅读理解的。这就是大端序列。而小端则会写成001,然后从右往左来读,最终也是读成100。

    如果我们翻阅前一篇文章的elf-header的信息,会发现这个文件是小端存储的,所以我们按照小端存储的方式来读取数据,也就是0x00000011,0xffeebbaa。

    总之数据段主要存储的是数据。

    符号表段

    看完数据段大家肯定也不禁有些疑惑,难道数据段真的只存数据啊,多一分都不存的,我们知道全局变量除了它所表示得数据之外,还有变量名和引用,这个在数据表里并未体现。那么关于变量名的相关信息存放在哪里呢?在符号表里。

    其实符号表不只存储了变量的引用符号,包括函数(无论内部定义还是外部定义),段,文件信息等都存储在符号表里。

    我们可以通过如下命令查看符号表

    $ readelf -s x.o
    
    # dynsym 动态符号表 保存动态链接过程中,保存符号引用的表(外部依赖库的函数,变量)
    Symbol table '.dynsym' contains 8 entries:
       Num:    Value          Size Type    Bind   Vis       Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
         1: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND __libc_start_main@GLIBC_2.34
         2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND _ITM_deregisterTMCloneTable
         3: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND puts@GLIBC_2.2.5
         4: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND printf@GLIBC_2.2.5
         5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND __gmon_start__
         6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND _ITM_registerTMCloneTable
         7: 0000000000000000     0 FUNC    WEAK   DEFAULT   UND __cxa_finalize@GLIBC_2.2.5
    
    #symtab是 符号表 包含了动态符号表的内容
    Symbol table '.symtab' contains 40 entries:
       Num:    Value          Size Type    Bind   Vis       Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
         1: 0000000000000000     0 FILE    LOCAL  DEFAULT   ABS Scrt1.o
         2: 000000000000038c    32 OBJECT  LOCAL  DEFAULT     4 __abi_tag
         3: 0000000000000000     0 FILE    LOCAL  DEFAULT   ABS crtstuff.c
         4: 00000000000010b0     0 FUNC    LOCAL  DEFAULT    16 deregister_tm_clones
         5: 00000000000010e0     0 FUNC    LOCAL  DEFAULT    16 register_tm_clones
         6: 0000000000001120     0 FUNC    LOCAL  DEFAULT    16 __do_global_dtors_aux
         7: 0000000000004018     1 OBJECT  LOCAL  DEFAULT    26 completed.0
         8: 0000000000003db8     0 OBJECT  LOCAL  DEFAULT    22 __do_global_dtors_aux_fini_array_entry
         9: 0000000000001160     0 FUNC    LOCAL  DEFAULT    16 frame_dummy
        10: 0000000000003db0     0 OBJECT  LOCAL  DEFAULT    21 __frame_dummy_init_array_entry
        11: 0000000000000000     0 FILE    LOCAL  DEFAULT   ABS x.c
        12: 0000000000000000     0 FILE    LOCAL  DEFAULT   ABS crtstuff.c
        13: 0000000000002130     0 OBJECT  LOCAL  DEFAULT    20 __FRAME_END__
        14: 0000000000000000     0 FILE    LOCAL  DEFAULT   ABS 
        15: 0000000000003dc0     0 OBJECT  LOCAL  DEFAULT    23 _DYNAMIC
        16: 0000000000002028     0 NOTYPE  LOCAL  DEFAULT    19 __GNU_EH_FRAME_HDR
        17: 0000000000003fb0     0 OBJECT  LOCAL  DEFAULT    24 _GLOBAL_OFFSET_TABLE_
        18: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND __libc_start_main@GLIBC_2.34
        19: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND _ITM_deregisterTMCloneTable
        20: 0000000000004000     0 NOTYPE  WEAK   DEFAULT    25 data_start
        21: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND puts@GLIBC_2.2.5
        22: 0000000000004018     0 NOTYPE  GLOBAL DEFAULT    25 _edata
        23: 00000000000011c0     0 FUNC    GLOBAL HIDDEN     17 _fini
        24: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND printf@GLIBC_2.2.5
        25: 0000000000004014     4 OBJECT  GLOBAL DEFAULT    25 global_value2  #全局变量
        ...
        ...
        30: 0000000000004010     4 OBJECT  GLOBAL DEFAULT    25 global_value #全局变量
        31: 0000000000004020     0 NOTYPE  GLOBAL DEFAULT    26 _end
        32: 0000000000001169    60 FUNC    GLOBAL DEFAULT    16 sayWords
        33: 0000000000001080    38 FUNC    GLOBAL DEFAULT    16 _start
        34: 0000000000004018     0 NOTYPE  GLOBAL DEFAULT    26 __bss_start
        35: 00000000000011a5    25 FUNC    GLOBAL DEFAULT    16 main
        36: 0000000000004018     0 OBJECT  GLOBAL HIDDEN     25 __TMC_END__
        37: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND _ITM_registerTMCloneTable
        38: 0000000000000000     0 FUNC    WEAK   DEFAULT   UND __cxa_finalize@GLIBC_2.2.5
        39: 0000000000001000     0 FUNC    GLOBAL HIDDEN     12 _init
    
    

    针对上述的符号表,linux也定义了一个数据结构专门用来表示他们

    
    typedef struct {
            Elf64_Word      st_name; # 4byte 指向符号字符串表的偏移
            unsigned char   st_info; # 符号类型与板定的信息、
            unsigned char   st_other;
            Elf64_Half      st_shndx; #符号所在的段,如果不再文件的段内,则表示其他信息
            Elf64_Addr      st_value; # 8byte 符号对应的值,绝对值或者地址
            Elf64_Xword     st_size; # 符号大小
    
    } Elf64_Sym;
    
    

    我们看到自己定义的变量在符号表的第25行和第30行,而他们在表中的Ndx=25,说明在SHT段表中的第25段,我们打印一下x.o的SHT发现,第25段就是数据段。我们也可以看到这两个全局变量的value分别就等于他们在data段中对应数值的地址。

    除此之外,我们还发现符号表中有ndx=UND的项,und就是undefined的缩写,表示该符号还未定义。一般是在我们引用外部共享库的符号或者函数时会出现,因为这些符号都定义在外部,在本文件中确实没有定义。比如printf@GLIBC_2.2.5,puts@GLIBC_2.2.5,这些都是定义在外部的函数,只是被我们引用了而已,此刻并不知道这个引用的函数的真实地址,所以他们的地址value=0。需要等到被引用的库加载到内存之后程序才能正确获取到它的地址,然后再修正过来。

    而与之相反的,我们自己内部定义的函数sayWords则是有正确的地址和所属的段的数组下标。

    对于那些从外部共享库中引入的符号,我们往往称作导入符号。而那些定义在本地的,可以被外部所调用的符号,我们称作导出符号

    重定位相关

    我们说符号表中的那些ndx=UND的项,往往是源代码引入了外部共享库的字段或者函数造成的,因为在编译期间无法获知从外部共享库导入的符号,所以对应的地址往往也是0.那么什么时候确定外部符号的地址的呢?在程序运行时动态链接共享库的时候对共享库进行重定位。

    这个链接工作由链接器来完成。我们在上一篇文章的SHT和PHT中都看到一个.interp,这个段就是用来加载ld-linux-x86-64.so这个加载器的

    dynamic段

    那么链接器是如何完成外部符号的重定位过程的呢?

    首先,链接器需要知道当前程序到底依赖了哪个共享库,才能把对应的共享库链接进来。这些信息在.dynamic段中,此外,dynamic段内还包含动态符号表和动态链接重定位表的地址。链接器从这个段中就能确定依赖的哪个库以及导入了哪些符号。以及完成重定位之后该把地址填到哪里。

    总之dynamic表可以算作动态链接的入口。

    而dynamic段的数据结构也比较简单

    typedef struct {
      Elf64_Sxword d_tag;       /* entry tag value */
      union {
        Elf64_Xword d_val;
        Elf64_Addr d_ptr;
      } d_un;
    } Elf64_Dyn;
    

    由一个d_tag表示类型,然后还有一个数值d_val或者 地址d_ptr(union结构体表示内部元素共用一个内存空间,可以理解为二选一)

    而d_tag的取值情况如下

    
    /* This is the info that is needed to parse the dynamic section of the file */
    #define DT_NULL     0
    #define DT_NEEDED   1
    #define DT_PLTRELSZ 2
    #define DT_PLTGOT   3
    #define DT_HASH     4 
    #define DT_STRTAB   5 #动态链接符号表(.dynsym)的地址,d_ptr指向dynsym的地址
    #define DT_SYMTAB   6 #动态链接符号字符串表(.dynstr) d_ptr指向地址
    #define DT_RELA     7 #动态链接重定位表
    #define DT_RELASZ   8
    #define DT_RELAENT  9
    #define DT_STRSZ    10
    #define DT_SYMENT   11
    #define DT_INIT     12 #初始代码的地址
    #define DT_FINI     13 #结束代码的地址
    #define DT_SONAME   14
    #define DT_RPATH    15
    #define DT_SYMBOLIC 16
    #define DT_REL          17 #动态链接符号表
    #define DT_RELSZ    18
    #define DT_RELENT   19
    #define DT_PLTREL   20
    #define DT_DEBUG    21
    #define DT_TEXTREL  22
    #define DT_JMPREL   23
    #define DT_ENCODING 32
    #define OLD_DT_LOOS 0x60000000
    #define DT_LOOS     0x6000000d
    #define DT_HIOS     0x6ffff000
    #define DT_VALRNGLO 0x6ffffd00
    #define DT_VALRNGHI 0x6ffffdff
    #define DT_ADDRRNGLO    0x6ffffe00
    #define DT_ADDRRNGHI    0x6ffffeff
    #define DT_VERSYM   0x6ffffff0
    #define DT_RELACOUNT    0x6ffffff9
    #define DT_RELCOUNT 0x6ffffffa
    #define DT_FLAGS_1  0x6ffffffb
    #define DT_VERDEF   0x6ffffffc
    #define DT_VERDEFNUM    0x6ffffffd
    #define DT_VERNEED  0x6ffffffe
    #define DT_VERNEEDNUM   0x6fffffff
    #define OLD_DT_HIOS     0x6fffffff
    #define DT_LOPROC   0x70000000
    #define DT_HIPROC   0x7fffffff
    

    通过如下命令可以查看dynamic段的信息

    $ readelf -d x.o
    
    Dynamic section at offset 0x2dc0 contains 27 entries:
      Tag                Type           Name/Value
      0x0000000000000001 (NEEDED)       Shared library: [libc.so.6]
      0x000000000000000c (INIT)         0x1000
      0x000000000000000d (FINI)         0x11c0
      0x0000000000000019 (INIT_ARRAY)   0x3db0
      0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
      0x000000000000001a (FINI_ARRAY)   0x3db8
      0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
      0x000000006ffffef5 (GNU_HASH)     0x3b0
      0x0000000000000005 (STRTAB)       0x498 
      0x0000000000000006 (SYMTAB)       0x3d8
      0x000000000000000a (STRSZ)        148 (bytes)
      0x000000000000000b (SYMENT)       24 (bytes)
      0x0000000000000015 (DEBUG)        0x0
      0x0000000000000003 (PLTGOT)       0x3fb0
      0x0000000000000002 (PLTRELSZ)     48 (bytes)
      0x0000000000000014 (PLTREL)       RELA
      0x0000000000000017 (JMPREL)       0x630
      0x0000000000000007 (RELA)         0x570
      0x0000000000000008 (RELASZ)       192 (bytes)
      0x0000000000000009 (RELAENT)      24 (bytes)
      0x000000000000001e (FLAGS)        BIND_NOW 
      0x000000006ffffffb (FLAGS_1)      NOW PIE 
      0x000000006ffffffe (VERNEED)      0x540
      0x000000006fffffff (VERNEEDNUM)   1
      0x000000006ffffff0 (VERSYM)       0x52c
      0x000000006ffffff9 (RELACOUNT)    3
      0x0000000000000000 (NULL)         0x0
    
    

    通过上面的介绍,我们对打印出来的dynamic段的数据含义就不会太陌生了。有了dynamic段提供的动态链接相关的信息,连接器可以比较方便的完成动态链接。

    在此之前,我们先来聊聊共享库是如何提供导出符号的。

    共享库如何提供导出符号

    我们前面提到过导出符号和导入符号,其实一个共享库既有导出符号又有导入符号,这很正常。导出符号可以理解为是共享库自己定义的变量或者函数,然后准备该外部来调用。当它被编译为送文件时,函数往往被放入代码段。并且有一个偏移地址(so内假设基地址为0),其实这个代码段的地址就是程序最终想要定位到的地址。

    而这个地址往往被记录到so内的符号表中.symtab,就比如我们自己的代码里的sayWords函数,就有正常的偏移地址和所属段的数组下标。

    共享库就是通过符号表来对外提供导出符号的。而程序定位某个共享库的符号时,也是通过该共享库的符号表来获取符号的地址。

    地址无关代码与GOT

    我们回到可执行程序端,当我们在代码中引用外部共享库的函数时,这个引用也都会编译打包到代码段中,而代码段是只读的,不允许在运行时进行修改,而引用的外部符号只有在运行时才能够获取真实的地址,而改动又不能放在代码段中,因此只能通过设计一种机制:代码段中的调用外部函数时,把这个外部函数调用转换为跳转到数据段中的一个相对地址中去。这个部分就叫全局偏移表(Global Offset Table),在全局偏移表中记录具体函数的地址,然后跳转到对应的地址中就行了。

    由于在ELF文件中,代码段和数据段之间的偏移距离是固定的,所以使用相对位置跳转即可,这样就避免了在代码段中使用绝对地址了。而由于数据段是可读可写的,所以链接器在找到相关函数符号的具体地址之后,把地址填入到GOT表中即可。

    重定位表

    连接器通过dynamic表可以找到重定位表,.rela重定位表中统计了各个段中需要重定位的信息。

    重定位表的结构也很简单无需过多介绍:

    
    
    typedef struct elf64_rela {
     //r_offset是完成重定位之后所要修正的位置的地址
      Elf64_Addr r_offset;  /* Location at which to apply the action */
      Elf64_Xword r_info;   /* index and type of relocation */
      Elf64_Sxword r_addend;    /* Constant addend used to compute value */
       //低32位表示重定位入口的类型,高32位表示符号在符号表中的下标
    } Elf64_Rela;
    

    我们可以通过如下命令打印重定位表

    $ readelf -r x.o
    
    ...
    ...
    Relocation section '.rela.plt' at offset 0x630 contains 2 entries:
        Offset             Info             Type               Symbol's Value  Symbol's Name + Addend
    0000000000003fc8  0000000300000007 R_X86_64_JUMP_SLOT     0000000000000000 puts@GLIBC_2.2.5 + 0
    0000000000003fd0  0000000400000007 R_X86_64_JUMP_SLOT     0000000000000000 printf@GLIBC_2.2.5 + 0
    
    

    .rela或.rel开头的都叫重定位表,后面跟着的.plt或者.dyn则表示是针对哪个段进行重定位,比如。rela.plt就是针对plt段进行重定位。

    .rela和rel都是重定位表。两者的区别在于。重定位表项的数据结构上,rela表多了一项Addend(上表中的最后一项),此字段是用于计算偏移的常数。

    .rela.plt是对plt(Procedure Linkage Table)段的重定位,我们称作过程链接表。
    我们可以看到puts函数和printf函数的offset分别是0x3fc8 0x3fd0,这表示链接器重定位之后,要把真实地址修正到0x3fc8和0x3fd0处。然后我们查看这个地址位于哪里:

     [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
    
     [24] .got              PROGBITS        0000000000003fb0 002fb0 000050 08  WA  0   0  8
    
    

    我们发现就位于got表中。
    Info的值分别是0000000300000007和0000000400000007,根据签名对info的定义,低32位表示重定位入口的类型,高32位表示该符号在符号表中的下标(符号表中第几个)。所以puts函数和printf函数分别在符号表的第3个和第四个。我们再回看动态符号表对照一下看对不对:

    $ readelf -s x.o
    
    # dynsym 动态符号表 保存动态链接过程中,保存符号引用的表(外部依赖库的函数,变量)
    Symbol table '.dynsym' contains 8 entries:
       Num:    Value          Size Type    Bind   Vis       Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
         1: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND __libc_start_main@GLIBC_2.34
         2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND _ITM_deregisterTMCloneTable
         3: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND puts@GLIBC_2.2.5
         4: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND printf@GLIBC_2.2.5
         5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND __gmon_start__
         6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND _ITM_registerTMCloneTable
         7: 0000000000000000     0 FUNC    WEAK   DEFAULT   UND __cxa_finalize@GLIBC_2.2.5
         
    

    果然,第三个和第四个分别是puts函数和printf函数。

    重定位入口的类型是7,也就是R_X86_64_JUMP_SLOT,这个类型的意思就是找到真实地址之后,直接填入到偏移地址为对应的地址处即可处即可。

    其实一般而言是会同时存在got表和.plt.got表,一个存放数据的引用地址,一个存放函数的引用地址;假如你在编译时支持了延迟绑定(命令如下 gcc -o x.o -z lazy x.c),那么有关于函数符号的引用都会被单独放在.got.plt中,否则都会放在got表中。

    延迟绑定

    什么是延迟绑定呢?

    我们先来思考一个问题,假如程序在链接共享库的时候,就把一次性所有符号表都进行重定位,然后把地址更新进来,那么这显然会给运行时的带来一些性能问题。

    于是一种等我们用到了该函数时再重定位的思想就自然而然出现了。

    这就叫延迟绑定。

    那么延迟绑定的具体实原理大概是怎样的呢?

    每当代码中调用到某个共享库函数时,编译器就会生成关于一段关于这个函数调用的代码(plt表中的一项)。每次调用该函数都会直接跳转到这段生成的代码块的地址中。这段代码的逻辑大概是这样的(以printf为例):

    • 当(第一次)进入代码块时,先跳转到plt.got表中记录printf函数的位置,由于这个位置暂时没有printf的真实地址,里面默认填入的是代码块的下一个指令地址,
    • 后面指令执行的就是查找printf函数的逻辑(查找函数的地址一般存在plt表的第一项中),查找到之后把地址填写到plt.got表中对应的位置
    • 当再次进入这个代码块时,仍然跳转到plt.got中的对应项的位置,此时里面记录的地址已经是真实地址了,于是直接跳转真实地址执行函数。

    示意图如下

    第一次调用

    image.png

    第二次调用

    [图片上传失败...(image-906206-1712731693210)]

    至此,就实现了按需重定位。

    不过移动设备普遍使用的ARM处理器架构似乎并不支持动态绑定,因此往往是再动态链接时定位完所有的导入符号地址。

    符号字符串表

    前文说到符号表,符号表主要用于记录符号的引用相关的信息,比如符号的引用地址。实际上符号表的字符串表示都在符号字符串表(.strtab)中,其中动态符号表(包含引用的外部函数的符号)的字符串引用的是动态符号字符串表: .dynstr,我们可以简单看看动态符号表如何引用到动态符号字符串表的。

    我们利用readelf命令把dynstr段的16进制内容打印出来:

    $ readelf -s x.o
    
    Contents of section .dynstr:
     0498 005f5f63 78615f66 696e616c 697a6500  .__cxa_finalize.
     04a8 5f5f6c69 62635f73 74617274 5f6d6169  __libc_start_mai
     04b8 6e007075 74730070 72696e74 66006c69  n.puts.printf.li
     04c8 62632e73 6f2e3600 474c4942 435f322e  bc.so.6.GLIBC_2.
     04d8 322e3500 474c4942 435f322e 3334005f  2.5.GLIBC_2.34._
     04e8 49544d5f 64657265 67697374 6572544d  ITM_deregisterTM
     04f8 436c6f6e 65546162 6c65005f 5f676d6f  CloneTable.__gmo
     0508 6e5f7374 6172745f 5f005f49 544d5f72  n_start__._ITM_r
     0518 65676973 74657254 4d436c6f 6e655461  egisterTMCloneTa
     0528 626c6500                             ble.    
     
    

    字符串使用过ASCII来表示的,所以一个字节可以表示一个符号。字符串之间使用'\0'来分隔(ascii码为0x00)。比如从地址498开始的字符,0x00是'\0'空字符,0x5f则是'_',以此类推。

    那么动态符号表中的符号是如何关联到动态符号字符串表的呢?我们前面分析符号表的结构Elf64_Sym时,介绍过其中的st_name所指向的就是字符串表中的偏移。

    我们来举个例子。

    这是前面读到的符号表

    # 从readelf中读取出来的动态符号表的内容(这是转换后的数据)
    Symbol table '.dynsym' contains 8 entries:
       Num:    Value          Size Type    Bind   Vis       Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
         1: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND __libc_start_main@GLIBC_2.34
         2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND _ITM_deregisterTMCloneTable
         3: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND puts@GLIBC_2.2.5
         4: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND printf@GLIBC_2.2.5
         5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND __gmon_start__
         6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND _ITM_registerTMCloneTable
         7: 0000000000000000     0 FUNC    WEAK   DEFAULT   UND __cxa_finalize@GLIBC_2.2.5
    
    
    

    但是因为打印出来的数据其实已经是转换后的数据了,readlef命令直接帮我们把name一栏替换成了对应的符号名以及其所属的共享库和版本号。

    但是直接看动态符号表的16进制的表示有很蛋疼,不知道从何开始。

    
    # 动态符号表的16进制表示
    Contents of section .dynsym:
     03d8 00000000 00000000 00000000 00000000  ................
     03e8 00000000 00000000 10000000 12000000  ................
     03f8 00000000 00000000 00000000 00000000  ................
     0408 4f000000 20000000 00000000 00000000  O... ...........
     0418 00000000 00000000 22000000 12000000  ........".......
     0428 00000000 00000000 00000000 00000000  ................
     0438 27000000 12000000 00000000 00000000  '...............
     0448 00000000 00000000 6b000000 20000000  ........k... ...
     0458 00000000 00000000 00000000 00000000  ................
     0468 7a000000 20000000 00000000 00000000  z... ...........
     0478 00000000 00000000 01000000 22000000  ............"...
     0488 00000000 00000000 00000000 00000000  ................
    

    在此我们再引入一个二进制文件查看工具:010 Editor

    我们以printf函数为例

    image.png

    然后我们找到sym_name的项

    image.png

    然后我们知道sym_name=0x27(小端字节序),十进制就是39,这表示这一项所引用的字符串从动态符号表字符串中的第39个字符的位置开始。

    于是我们回到动态符号字符串表:

    $ readelf -s x.o
    
    Contents of section .dynstr:
     0498 005f5f63 78615f66 696e616c 697a6500  .__cxa_finalize.
     04a8 5f5f6c69 62635f73 74617274 5f6d6169  __libc_start_mai
     04b8 6e007075 74730070 72696e74 66006c69  n.puts.printf.li
     04c8 62632e73 6f2e3600 474c4942 435f322e  bc.so.6.GLIBC_2.
     04d8 322e3500 474c4942 435f322e 3334005f  2.5.GLIBC_2.34._
     04e8 49544d5f 64657265 67697374 6572544d  ITM_deregisterTM
     04f8 436c6f6e 65546162 6c65005f 5f676d6f  CloneTable.__gmo
     0508 6e5f7374 6172745f 5f005f49 544d5f72  n_start__._ITM_r
     0518 65676973 74657254 4d436c6f 6e655461  egisterTMCloneTa
     0528 626c6500                             ble.    
     
    

    我们从头开始数,发现第39刚好就是一个空字符串的分隔符,后面就是字符串printf。

    当然,其实这样很傻,我们可以让特定段的二进制展示字符串

    $ readelf --string-dump=7 x.o // 数字是SHT表的下标,7是.dynstr 所在的位置
    
    String dump of section '.dynstr':
    [     1] __cxa_finalize
    [    10] __libc_start_main
    [    22] puts
    [    27] printf
    [    2e] libc.so.6
    [    38] GLIBC_2.2.5
    [    44] GLIBC_2.34
    [    4f] _ITM_deregisterTMCloneTable
    [    6b] __gmon_start__
    [    7a] _ITM_registerTMCloneTable
    
    

    这样看就很直接了。

    后记

    其实ELF文件并不只有上面介绍的那些段,我们通过打印SHT表会发现一共大概有40个段,但是很多段并不是程序运行时的重点,甚至也不会被加载进内存,因此我们挑了比较重要段来讲,包括常见的代码段,数据段,符号段,同时围绕动态链接的重定位过程分析了相关联的几个段。可以说从整体到细节都有了一定的了解。

    基于对ELF文件的了解,接下来我们会讲讲native hook,以及它的实现和相关的背景知识。

    相关文章

      网友评论

          本文标题:linux系统的ELF文件解析(二)

          本文链接:https://www.haomeiwen.com/subject/ukefxjtx.html