elf文件和读取elf信息

profile

Posted by bbkgl on January 6, 2020

秋花最是黄葵好

天然嫩态迎秋早

关于ELF文件

首次接触到elf,我也是非常懵逼,可执行文件就叫可执行文件嘛!为什么搞个可执行与可链接格式文件?这里我也不说太多废话,因为解释和说明文档,无论你百度和google,都能找到一大堆解释,很全。我说的更多是关于自己使用的时候要用到的知识理论,以及怎么读取。

ELF文件中常用到的有,ELF头,节和表。

ELF头更多的是记录文件的一些信息,比如程序入口地址、文件类型(可执行还是共享库),文件头的大小等。

比较简单的可以使用readelf工具,首先读取一下ELF头:

20200208170928.png

我这个ubuntu语言是中文,导致都翻译成中文了,其实英文看上去会习惯点。

比较重要的就是ELF文件中的各个段,有很多种方式获取各个段名称,这是我之前写的一个简单的C++程序获取的段名:

20200208174928.png

各个段都有相应的作用,用readelf读取一下。

共有 37 个节头,从偏移量 0x8740 开始:

节头:
  [] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       0000000000000024  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002c0  000002c0
       0000000000000120  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000004003e0  000003e0
       000000000000010c  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           00000000004004ec  000004ec
       0000000000000018  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400508  00000508
       0000000000000040  0000000000000000   A       6     2     8
  [ 9] .rela.dyn         RELA             0000000000400548  00000548
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400560  00000560
       00000000000000a8  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         0000000000400608  00000608
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000400630  00000630
       0000000000000080  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         00000000004006b0  000006b0
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         00000000004006c0  000006c0
       0000000000000492  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         0000000000400b54  00000b54
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000400b60  00000b60
       0000000000000009  0000000000000000   A       0     0     4
  [17] .eh_frame_hdr     PROGBITS         0000000000400b6c  00000b6c
       0000000000000094  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         0000000000400c00  00000c00
       0000000000000274  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       0000000000601de8  00001de8
       0000000000000010  0000000000000000  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       0000000000601df8  00001df8
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .jcr              PROGBITS         0000000000601e00  00001e00
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .dynamic          DYNAMIC          0000000000601e08  00001e08
       00000000000001f0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000601ff8  00001ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000602000  00002000
       0000000000000050  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000602050  00002050
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000602060  00002060
       0000000000000008  0000000000000000  WA       0     0     1
  [27] .comment          PROGBITS         0000000000000000  00002060
       0000000000000035  0000000000000001  MS       0     0     1
  [28] .debug_aranges    PROGBITS         0000000000000000  00002095
       00000000000000a0  0000000000000000           0     0     1
  [29] .debug_info       PROGBITS         0000000000000000  00002135
       0000000000002c0d  0000000000000000           0     0     1
  [30] .debug_abbrev     PROGBITS         0000000000000000  00004d42
       00000000000006a1  0000000000000000           0     0     1
  [31] .debug_line       PROGBITS         0000000000000000  000053e3
       0000000000000413  0000000000000000           0     0     1
  [32] .debug_str        PROGBITS         0000000000000000  000057f6
       0000000000001f8b  0000000000000001  MS       0     0     1
  [33] .debug_ranges     PROGBITS         0000000000000000  00007781
       0000000000000090  0000000000000000           0     0     1
  [34] .shstrtab         STRTAB           0000000000000000  000085e4
       000000000000015a  0000000000000000           0     0     1
  [35] .symtab           SYMTAB           0000000000000000  00007818
       00000000000008b8  0000000000000018          36    57     8
  [36] .strtab           STRTAB           0000000000000000  000080d0
       0000000000000514  0000000000000000           0     0     1

有我们熟知的.text段,.data段,.bss段,.rodata段等,也有调试专用的.debug_info,.debug_line等。

讲一下我熟悉的几个段:

  • .text,就是代码段,存储汇编指令
  • .bss,保存的是未初始化的全局静态变量和局部静态变量
  • .data,数据段,保存的是已经初始化了的全局静态变量和局部静态变量,当然了,只是C/C++这种编译型,没有虚拟机的语言,其他解释型语言应该是自己虚拟机决定
  • .rodata,段存放的是只读数据,包括只读变量(const修饰的变量和字符串常量),这个可以自己弄几个变量验证以下2333
  • .debug_info,如果要用到调试信息的话,这个就是最主要的参照,比如存储了从机器码到源代码的映射,当然也存储了源代码
  • .debug_line,存储的是汇编指令地址到文件名+行号的映射
  • .symtab,符号表段,存储有当前可执行文件里加载的所有符号,包括常见的变量,函数等
  • .dynsym,动态库的符号表段,同上类似

基本常用的就上面这几个,其他的我也不了解。

如何读取ELF的符号表

我知道的其实有挺多方式读取的,命令行工具比如readelf,objdump等,自己写程序读的话就是用libelf,libdwarf,libelfin库。当然了,如果用libelf,libdwarf库的话,嘿嘿,我倒是研究过,但是很痛苦啊。。。相对来说,libelfin库就很好用了,下面大部分内容是我写的另一篇博客读取动态库符号表以及指令地址转化中的一部分。

使用libelfin读取符号表

首先可以遍历elf文件中的各个段:

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
#include <elf/elf++.hh>
#include <string>
#include <fcntl.h>
#include <unistd.h>

int main() {
    std::string fname = "/home/bbkgl/vimcode/server";
    int fd = open(fname.c_str(), O_RDONLY);
    elf::elf ef(elf::create_mmap_loader(fd));
    for (const auto &sec : ef.sections()) { 
        printf("Symbol table '%s':\n", sec.get_name().c_str());
    }
    close(fd);
    return 0;
}

看一下输出:

20200208174928.png

可以看到程序的各个段,也有我们熟知的textdatabss段,也有我们后面要用的符号段:.symtab.dynamic,也就是符号表。

所以得从这两个section中读取符号,同样使用libelfin库,接着上面的程序写。

稍微加上几行就好了,但是注意这里我进行了筛选,得到只有函数的符号,并使用重载的elf::to_string()去打印符号类型,筛选后应该都是func

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
#include <elf/elf++.hh>
#include <string>
#include <unistd.h>
#include <fcntl.h>

int main() {
    std::string fname = "/home/bbkgl/vimcode/server";
    int fd = open(fname.c_str(), O_RDONLY);
    elf::elf ef(elf::create_mmap_loader(fd));
    for (const auto &sec : ef.sections()) {
        if (sec.get_hdr().type != elf::sht::symtab && sec.get_hdr().type != elf::sht::dynsym)
            continue;
        for (const auto &sym : sec.as_symtab()) {
            auto &data = sym.get_data();
            if (data.type() != elf::stt::func) continue;
            printf("%016lx %8ld %10s %s\n", data.value, data.size,
                   elf::to_string(data.type()).c_str(), sym.get_name().c_str());
        }
    }
    close(fd);
    return 0;
}

看到输出结果:

20200207230414.png

可以看到所有函数符号都打印出来了,还打印了函数的起始指令地址和函数的指令在函数中所有指令地址上的范围,这里可以说明一下,直接看汇编会更清楚一点,这里以_start函数举例。

使用objdump -d server > server.txt拿到汇编指令,然后打开server.txt,找到_start函数的指令。

20200207232645.png

在之前图中可以看到,_start函数的起始指令地址为0x0000000000400bb0,地址范围大小为42,0x400bb0 + 42 = 0x400bda,正好就是最后一条指令,所以打印的符号表中的意义就很明了了。

当然也会发现有很多函数的起始指令地址是0,地址范围大小也是0,说明这些函数是动态库中导入的,详细的说明可以见读取动态库符号表以及指令地址转化

使用libelf库读取符号表

其实原理和步骤其实是一样的,只是用libelf库相对来说会复杂很多。

然后我就直接贴代码了,注意的是libelf会依赖于libz:

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
#include <elf/elf++.hh>
#include <string>
#include <fcntl.h>
#include <libelf.h>
#include <unistd.h>
#include <gelf.h>

int main() {
    std::string fname = "/home/bbkgl/vimcode/server";
    int fd = open(fname.c_str(), O_RDONLY);
    // Before the first call to elf_begin() , a program must call elf_version() to coordinate versions.
    if (elf_version(EV_CURRENT) == EV_NONE) {
        printf("EV_NONE");
    }

    Elf *elf = elf_begin(fd, ELF_C_READ_MMAP, nullptr);
    if (elf != nullptr) {
        /* 确定是文件类型是否是ELF文件 */
        if (elf_kind (elf) == ELF_K_ELF) {
            printf("elf exe\n");
        } else if (elf_kind (elf) == ELF_K_AR) { /* 目标是库文件 */
            printf("lib\n");
        }
        Elf_Scn     *scn = nullptr;
        GElf_Shdr   shdr;
        Elf_Data    *data;
        int         ii, count;
        while ((scn = elf_nextscn(elf, scn)) != nullptr) {
            gelf_getshdr(scn, &shdr);
            if (shdr.sh_type == SHT_SYMTAB || shdr.sh_type == SHT_DYNSYM) {
                data = elf_getdata(scn, nullptr);
                count = shdr.sh_size / shdr.sh_entsize;
                // sym.st_info == 18的时候是func类型
                for (ii = 0; ii < count; ++ii) {
                    GElf_Sym sym;
                    gelf_getsym(data, ii, &sym);
                    // 只打印函数符号表
                    if (ELF32_ST_TYPE(sym.st_info) != STT_FUNC) continue;
                    char *name = nullptr;
                    int status = 99;
                    printf("%016lx %4d %s\n", sym.st_value, sym.st_size, elf_strptr(elf, shdr.sh_link, sym.st_name));
                }
            }
        }
        /* 关闭elf结构句柄 */
        elf_end (elf);
    }
    close(fd);
    return 0;
}

下面是输出结果,其实和之前libelfin差不多,但是代码长度长了一半多啊,:

20200207235325.png