深入elf文件内部以及readelf工具的使用

inside ELF

  • elf: executable and linkable file.现代*nix操作系统目标文件大部分都是elf
  • 一个elf的视图如下:(.bss实际上不占文件空间)
    在这里插入图片描述

一个elf文件是由很多个sections构成的(包括符号表,也看作一个section,哪怕它是汇编器生成的)

  • 可以有重名的sections

段sections

最常见的四个段(sections)

  1. .rodata
  2. .data
  3. .bss:bss在文件中只是申明一个大小,并不占空间。(分配的堆内存就在这里)
  4. .text

使用objdump -h可以把elf文件的各个段的信息打出来,实际上就是从section header table读出的信息。

一个目标文件可以有多个同名的段。

常见的系统保留的段:

  • .rodata1(.rodata):只读段
  • .comment:编译器版本信息
  • .debug:调试信息
  • .dynamic:动态链接信息(共享库会有)
  • .hash:符号hash表
  • .line:调试时的行号(编译时候-g选项会生成)
  • .note: 额外的编译器信息
  • .strtab: 字符串表,用于符号表的访问(定义的函数名,全局变量名都在这里)
  • .symtab:符号表(给变量和字符串用的),每一个符号以及符号的地址
  • .shstrtab:段表字符串表(给段用的)

.symtab,.strtab,总是紧挨着的,.symtab结束后就是.strtab

.shstrtab例子

我们可以把shstrtab节的内容打出来(段的全部信息见下文中段头表)

在这里插入图片描述
这些实际上都是section名字的字符串。细心的话,可以发现上面只有18个字符串,没有.text.data段的字符串。这是因为.init.text已经包含字符串.text了。

.strtab例子

.strtab全是符号的名字字符串
在这里插入图片描述
.strtab和.shstrtab并不会有交集,但是在.symtab段记录了段的符号,这些段的Elf32_Symst_name实际上等于0,也就是空字符串。这两个段都是链接时候用的,不一定会加载到内存里面。

文件头ELF header

  • 可执行文件elf的开始52个字节总是固定的(即elf header),前16个字节总是magic

elf header简称ehdr

Elf header可以使用readelf -h查看
/Users/jaxxxxxo/Library/Application Support/typora-user-images/image-20200709183132261.png

相关具体信息定义在/usr/include/elf.h

typedef struct{
  unsigned char e_ident[16]; //16个字节,就是那个magic一直到ABI版本
  Elf32_Half e_type;//2字节,文件类型:可重定位,可执行,动态可链接(.so文件就是这种)
  Elf32_Half e_machine;//2字节,CPU平台,比如x86(常量名为EM_386)
  Elf32_Word e_version;//4字节,Elf版本,一般是常数1
  Elf32_Addr e_entry;//4字节,入口地址,对于可重定位的文件等于0
  Elf32_Off e_phoff;//4字节,程序头起点
  Elf32_Off e_shoff;//4字节,section header起始字节
  Elf32_Word e_flags;//4字节,一般都等于0
  Elf32_Half e_ehsize;//2字节,elf头文件大小,52字节,正好是本结构体的字节数
  Elf32_Half e_phentsize;//2字节,程序头大小
	Elf32_Half e_phnum;//2字节,程序头个数
  Elf32_Half e_shentsize;//2字节,section header table里每个条目的大小,一般等于40字节
  Elf32_Half e_shnum;//2字节,section header table里面的条数,即这个文件拥有的段的数量。上面的例子是21个
  Elf32_Half e_shstrndx;//2字节,section header string table index,段表字符串表的段(.shstrtab)所在段表中的下标(section header table的第几个条目,实际上就是第几个段)
}ELF32_Ehdr;

段头表 SECTION HEADER TABLE

有时候直接叫做section header,不加table

在这里插入图片描述

  • readelf查看section header时实际上就是从elf header拿到信息e_shoff,e_shnum,e_shentsize,再从文件里面去读取

section header简称shshdr

在一个section header table实际上就是一个ELF32_Shdr数组,ELF32_Shdr是一个占40字节的结构体

typedef struct{
	Elf32_Word sh_name;//段字符串在段字符串表(.sh)中的下标
  Elf32_Word sh_type;//段的类型
  ELF32_Word sh_flags;//段的标志,可以取SHF_WRITE,SHT_ALLOC,SHF_EXECINSTR
  ELF32_Word sh_addr;//如果该段可以被加载,这个值就是加载之后的虚拟地址否则等于0
  ELF32_Word sh_offset;//如果该段存在文件中,表示这个段在文件中的位置,否则没意义,如.bss段
  ELF32_Word sh_size;//这个段的长度,单位是字节
  ELF32_Word sh_link;//
  ELF32_Word sh_info;//
  ELF32_Word sh_addralign;//段地址对齐
  ELF32_Word sh_entsize;//项的长度
}ELF32_Shdr

段的类型

sh_type可以是

  • SHT_NULL:无效段

  • SHT_PROGBITS:程序段。代码段,数据段都是这种类型的

  • SHT_SYMTAB:符号表

  • SHT_STRTAB:字符串表

  • SHT_RELA:重定位表(.rel.text .rel.data)

  • SHT_HASH:符号表的HASH表

  • SHT_DYNAMIC:动态链接信息

  • SHT_NOTE:提示信息

  • SHT_NOBITS:表示该段在文件中没有内容,如.bss段

  • SHT_REL:该段包含了重定位信息

  • SHT_SHLIB:保留

  • SHT_DNYSYM:动态链接符号表

一个问题:SHT_RELASHT_REL区别是啥,有关动态链接的东西还是不太清楚

段的flags

  • SHF_WRITE:进程空间可写(0x1)
  • SHT_ALLOC:表示该段在进程空间需要分配空间,比如.bss,.text,.data(0x2)
  • SHF_EXECINSTR:该段在进程空间可执行(0x4)

常见的段的类型以及其标志位

名字类型标志
.bssSTH_NOBITSSHF_ALLOC+SHF_WRITE
.commentSHT_PROGBITSnone
dataSHT_PROGBITSSHF_ALLOC+SHF_WRITE
.debugSHT_PROGBITSnone
.dynamicSHT_DYNAMICSHF_ALLOC+SHF_WRITE
.hashSHT_HASHSHT_ALLOC
.lineSHT_PROGBITSnone
.noteSHT_NOTEnone
.rodataSHT_PROGBITSSHT_ALLOC
.shstrtabSHT_STRTABnone
.strtabSHT_STRTAB如果有可装载的段要用那么就是SHT_ALLOC
.symtabSHT_SYMTAB同上
.textSHT_PROGBITSSHT_ALLOC+SHF_EXECINSTR

可加载的sections都是alloc的

sh_link 和 sh_info

  • 只对段的类型与链接相关的有意义,如重定位表,符号表
sh_typesh_linksh_info
SHT_DYNAMIC该段所使用的字符串表在段表中的下标0
SHT_HASH该段使用的符号表早段表中的下标0
SHT_REL该段使用的相应符号表在段表中的下标该重定位表所作用的段在段表中的下标
SHT_RELA同上同上
SHT_SYMTAB操作系统相关操作系统相关
SHT_DYNSYM同上同上
其他SHN_UNDEF0

符号表

符号表特指.symtab

符号表中的符号可以分为下面几种

  • 定义在目标文件,被其他应用的符号
  • 本目标引用了但没定义的符号
  • 段名符号
  • 局部符号,比如定义在函数体内的static int s = 1;
  • 行号信息

符号表的定义

typedef struct{
  Elf32_Word st_name;//符号名字符串所在符号表的下标
  Elf32_Addr st_value;//符号对应的值,不同类型符号,值的意义不一样
  Elf32_Word st_size;//符号大小,该值是该数据类型的大小,比如double类型的符号是8字节,包括数组
 	unsigned char st_info;//符号类型与绑定信息
  unsigned char st_other;//目前没用
  Elf32_Half st_shndx;//符号所在的段(在段表中的下标)
}Elf32_Sym;//符号表每个struct大小为16字节
  • st_info低4位表示符号类型

    • 0:未知类型
    • 1:数据对象,如变量,数组
    • 2:函数
    • 3:表示一个段,高四位一定是0
    • 4:文件名,高四位一定是0,st_shndx一定是SHN_ABS=0xfff1
  • st_info高4位表示绑定信息:

    • 0:局部符号
    • 1:全局符号,对外部可见
    • 2:弱引用
  • 符号所在段st_shndx有三种情况

    • SHN_ABS=0xfff1表示该符号包含了一个绝对的值,比如文件名。
    • SHN_COMMON=0xfff2表示一般类型,COMMON块,比如一个未初始化的全局变量(.bss段里的符号都是这种)。
    • SHN_UNDEF表示未定义,本目标引用了但是定义在其他目标文件中

    要注意,.bss段里的符号类型是SHN_COMMON,它是没有值的。

.symtab例子

在这里插入图片描述
Elf32_Symst_name.strtab的下标

我们可以简要分析一下上述的.symtab,(用16进制的方式把内容输出来):

  • 每一行就是一个Elf32_Sym结构体,
  • 1-17(从0开始计数)行的symbol都是section,其st_info在最后一个word的倒数第三个字节(从0开始计数),比如第1行对应的最后一个word是03000100,最后两个字节分别是00 03(注意,高字节在高位,这是小端存放的方式)00st_other的值,03st_info的值。0是高4位,表示绑定信息,即局部符号。3是低4位表示该符号类型是一个段。
    • 我们看这个符号的st_value,它等于00100000刚好等于init.text段的addr。
    • 再关注这个符号的st_name,它等于0,在.strtab第0项是空字符串!!
    • st_shndx即最后两个byte,等于0001,即这个符号所在的段表下标等于1个。从readelf -S可以看见第二个段就是.init.text
    • 所以在可执行文件中,段的值出现了两次,一次是在section header table的sh_addr , 一次是在symtan里面的st_value。但是st_value在可重定位的目标文件里不见得是地址。

符号的值

不同类型的符号值的意义不一样

  • 在目标文件中,如果是符号定义且符号不是"COMMON",即符号的sh_shndx不等于COMMON,则这个值表示该符号在对应段的偏移(字节)。
  • 在目标文件中,如果符号是"COMMON"块的,则st_value表示该符号的对齐属性。
  • 在可执行文件中代表符号的虚拟地址

例子

在这里插入图片描述

在上述符号表中,1-17是段:

在这里插入图片描述
可以发现.symtab,.strtab.shstrtab没有在.symtab里面。因为这三个表里面压根没有变量。。。这三个段是给链接器用的。可执行文件里没有重定位表。

  • 我们来看readelf -s是怎么做的呢(readelf是怎么展示全部的符号信息的)。(下面是我的猜测)readelf首先要找到.symtab所在的段所在的文件偏移,这个信息存在section table的表项里面。readelf从section header table里找到SHT_SYMTAB类型的shdr entry,这个文件偏移就存在这个entry的sh_offset。在此之前,需要确定section header table所在的文件偏移,这个信息存在elf文件头的e_shoff。所以正着看,流程应该是下面:
    • 从elf文件头的e_shoff找到section header table
    • 遍历section header table的所有entry(entry的个数也在elf header里可以找到),如果找到sh_type==SHT_SYMTAB,就找到了一个符号表。
      • 找到符号表之后,就可以读出全部的符号信息。这些符号包括
      • 符号的name

弱符号和强符号

  • 编译器默认函数和初始化的全局变量位强符号,未初始化的全局变量位弱符号。
  • 通过gcc的__attribute__((weak))来定义一个强符号为弱符号。
  • 强弱符号是针对定义来说的,而不是针对引用

针对强弱符号的概念,链接器的处理规则如下:

  1. 不允许强符号被定义多次
  2. 如果一个符号在某个目标文件是强符号,在其他目标文件是弱符号,那么选择强符号作为链接是的引用解析
  3. 如果一个符号在所有目标文件中都是弱符号,选择占用空间最大的那个。比如定义了var在A.c定义成int类型,在B.c定义成double类型,则最后的符号是double类型

弱引用和强引用:生成可执行文件的所有符号要经过正确的决议。如果没有找到符号的定义,链接器就会报未定义错误,这种被称为强引用。与之对应的还有弱引用:如果符号有定义,则链接器将改符号的引用解析,如果未定义,则也不报错,认为其值等于0。

可以使用__attribute__((weakref))来定义一个引用为弱引用

objdump

  • -s 按照16进制打印所有section的值
  • -t 打印符号表
  • -h 打印所有section的信息,等价于readelf -S
  • -x 打印所有section的信息和符号表,程序的开始地址
  • -d 反汇编所有的指令

readelf

  • -h 显示elf header

  • -S 显示sections header

  • -s 显示符号表

  • -r 显示重定位表

  • -p --string-dump=<number|name> 把某个section的值按照string的形式展示出来,通常可以用这个把.strtab和.shstrtab打印出来

  • -x --hex-dump=<number|name> 把某个section按照16进制打出来

c++filt

这个工具可以解析c++符号

  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为了解析ELF文件并获取链接脚本,您可以使用类似libelf的库。Libelf是一种开放源代码库,可用于访问ELF文件的内容。以下是一个简短的C程序示例,该程序使用libelf库解析ELF文件并获取链接脚本: ``` #include <fcntl.h> #include <libelf.h> #include <stdio.h> int main() { //打开ELF文件 int fd = open("path/to/elf", O_RDONLY, 0); if (fd < 0) { perror("open failed"); return 1; } //加载ELF文件 Elf *e = elf_begin(fd, ELF_C_READ, NULL); if (e == NULL) { perror("elf_begin failed"); return 1; } //获取链接脚本的字符串节 size_t shstrndx; if (elf_getshdrstrndx(e, &shstrndx) != 0) { perror("elf_getshdrstrndx failed"); return 1; } Elf_Scn *scn = NULL; while ((scn = elf_nextscn(e, scn)) != NULL) { //获取节头 GElf_Shdr shdr; if (gelf_getshdr(scn, &shdr) != &shdr) { perror("gelf_getshdr failed"); return 1; } //获取节名 char *sec_name = elf_strptr(e, shstrndx, shdr.sh_name); if (sec_name == NULL) { perror("elf_strptr failed"); return 1; } //如果是链接脚本节 if (shdr.sh_type == SHT_PROGBITS && strcmp(sec_name, ".linker_script") == 0) { //获取数据 char *data = (char*)malloc(shdr.sh_size); if (data == NULL) { perror("malloc failed"); return 1; } if (gelf_getdata(scn, data, shdr.sh_size, shdr.sh_offset) == NULL) { perror("gelf_getdata failed"); return 1; } //打印链接脚本数据 printf("%s\n", data); free(data); } } //释放ELF 文件 elf_end(e); close(fd); return 0; } ``` 请注意,这只是一个简单的示例程序。实际上,解析ELF文件可能会更复杂,取决于所需的信息类型。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值