秋花最是黄葵好
天然嫩态迎秋早
关于ELF文件
首次接触到elf,我也是非常懵逼,可执行文件就叫可执行文件嘛!为什么搞个可执行与可链接格式文件?这里我也不说太多废话,因为解释和说明文档,无论你百度和google,都能找到一大堆解释,很全。我说的更多是关于自己使用的时候要用到的知识理论,以及怎么读取。
ELF文件中常用到的有,ELF头,节和表。
ELF头更多的是记录文件的一些信息,比如程序入口地址、文件类型(可执行还是共享库),文件头的大小等。
比较简单的可以使用readelf工具,首先读取一下ELF头:
我这个ubuntu语言是中文,导致都翻译成中文了,其实英文看上去会习惯点。
比较重要的就是ELF文件中的各个段,有很多种方式获取各个段名称,这是我之前写的一个简单的C++程序获取的段名:
各个段都有相应的作用,用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;
}
看一下输出:
可以看到程序的各个段,也有我们熟知的text
、data
、bss
段,也有我们后面要用的符号段:.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;
}
看到输出结果:
可以看到所有函数符号都打印出来了,还打印了函数的起始指令地址和函数的指令在函数中所有指令地址上的范围,这里可以说明一下,直接看汇编会更清楚一点,这里以_start函数举例。
使用objdump -d server > server.txt
拿到汇编指令,然后打开server.txt
,找到_start函数的指令。
在之前图中可以看到,_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差不多,但是代码长度长了一半多啊,: