Elf第二讲,ELF程序头

Elf第二讲,ELF程序头

一丶简介

1.1 简介

ELF 程序头是对二进制文件中段的描述,是程序装载必须的一部分。段(segment) 是在内核装载时被解析的。主要作用就是描述磁盘上可执行文件的内存布局以及如何映射到内存中。可以通过引用原始的ELF头中名为: **e_phoff**(程序头表的偏移量)的偏移量来得到程序头表。

二丶程序头

2.1 程序头结构

2.1.1 结构体

在32位下,ELF程序头结构为:

typedef struct {
    Elf32_Word    p_type;        /* segment type */
    Elf32_Off    p_offset;    /* segment offset */
    Elf32_Addr    p_vaddr;    /* virtual address of segment */
    Elf32_Addr    p_paddr;    /* physical address - ignored? */
    Elf32_Word    p_filesz;    /* number of bytes in file for seg. */
    Elf32_Word    p_memsz;    /* number of bytes in mem. for seg. */
    Elf32_Word    p_flags;    /* flags */
    Elf32_Word    p_align;    /* memory alignment */
} Elf32_Phdr;

64位如下:

typedef struct {
    Elf64_Half    p_type;        /* entry type */
    Elf64_Half    p_flags;    /* flags */
    Elf64_Off    p_offset;    /* offset */
    Elf64_Addr    p_vaddr;    /* virtual address */
    Elf64_Addr    p_paddr;    /* physical address */
    Elf64_Xword    p_filesz;    /* file size */
    Elf64_Xword    p_memsz;    /* memory size */
    Elf64_Xword    p_align;    /* memory & file alignment */
} Elf64_Phdr;

其实核心字段就是 p_type p_offset p_vaddr 以及p_flags

其余的不会影响到我们。 这个结构主要描述了elf在被加载的时候内存要如何映射。

2.1.2 字段含义以及取值

字段 含义
p_type 描述了段的而类型
p_offset 描述了从文件到该段的文件偏移
p_vaddr 描述了段在内存中的偏移
p_paddr 描述了物理地址相关,在应用层无作用。
p_filesz p_offset描述了段在文件中的偏移。那么此成员就描述了在文件中所占的大小,可以为0
p_memsz 同上,描述了内存中映像所占的字节数。 可以为0
p_flags 此成员描述了段的标志
p_align 描述了对齐。对于可加载的段 p_vaddr和p_offset取值必须是合适的。此成员给出了段在文件中和内存中如何对齐。数值 0 1 标识不需要对齐。否则就必须是2的倍数。 p_vaddr和p_offset在取模后应该相等。

分别说一下取值结构:

  • p_type取值

    typedef enum <Elf32_Word> {
        PT_NULL                     = 0x0,
        PT_LOAD                     = 0x1,         //表示段是可以被加载到内存中执行的
        PT_DYNAMIC                  = 0x2,
        PT_INERP                    = 0x3,
        PT_NOTE                     = 0x4,
        PT_SHLIB                    = 0x5,
        PT_PHDR                     = 0x6,
        PT_TLS                      = 0x7,
        PT_NUM                      = 0x8,
        PT_LOOS                     = 0x60000000,
        PT_GNU_EH_FRAME             = 0x6474e550,
        PT_GNU_STACK                = 0x6474e551,
        PT_GNU_RELRO                = 0x6474e552,
        PT_LOSUNW                   = 0x6ffffffa,
        PT_SUNWBSS                  = 0x6ffffffa,
        PT_SUNWSTACK                = 0x6ffffffb,
        PT_HISUNW                   = 0x6fffffff,
        PT_HIOS                     = 0x6fffffff,
        PT_LOPROC                   = 0x70000000,
        PT_HIPROC                   = 0x7fffffff,
        // ARM Sections
        PT_SHT_ARM_EXIDX            = 0x70000001,
        PT_SHT_ARM_PREEMPTMAP       = 0x70000002,
        PT_SHT_ARM_ATTRIBUTES       = 0x70000003,
        PT_SHT_ARM_DEBUGOVERLAY     = 0x70000004,
        PT_SHT_ARM_OVERLAYSECTION   = 0x70000005
    } p_type32_e;
    typedef p_type32_e p_type64_e;
    

p_align取值:

typedef enum <Elf32_Word> {
    PF_None             = 0x0,    表示没有属性
    PF_Exec             = 0x1,    段可执行
    PF_Write            = 0x2,    可写
    PF_Write_Exec       = 0x3,    可写可执行
    PF_Read             = 0x4,    只读
    PF_Read_Exec        = 0x5,    读和执行
    PF_Read_Write       = 0x6,    读写
    PF_Read_Write_Exec  = 0x7     读写执行
} p_flags32_e;

2.1.3 PT_LOAD段

p_type描述了段的类型。 一个可执行文件至少要有一个PT_LOAD类型的段。 这类程序头描述的是可装载的段,

也就是说,这种类型的段会被装载或者映射到内存中。

一般来说,一个动态链接的ELF可执行文件通常包含两个可装载的段。 段类型都为PT_LOAD

  • 1.一个是存放程序代码的text段
  • 2.另一个是存放全局变量和动态链接信息的data段。

上面两个段则会根据p_align的对齐值在内存中对齐。并且映射到内存中。

linux 例子实战:

可以看到只要是LOAD的段都会按照对齐方式加载到内存中

提示:

​ 一般来说,TEXT段也称为代码段,权限一般都是可读可执行的。 对应取值就是PF_READ_EXEC

data段一般就是读写权限。 可以修改p_flags来让我们的程序权限增大。

2.1.4 PT_PHDR段

此段一般位于elf文件的第一个段。PT_PHDR段保存了程序头表本身的位置和大小。 phdr表保存了所有的phdr对文件(以及内存镜像)中段的描述信息。

可以使用

readelf -l <filename> 来查看phdr表的描述信息

如下:

Elf 文件类型为 DYN (共享目标文件)
Entry point 0x1070
There are 12 program headers, starting at offset 52

程序头:
Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
PHDR           0x000034 0x00000034 0x00000034 0x00180 0x00180 R   0x4
INTERP         0x0001b4 0x000001b4 0x000001b4 0x00013 0x00013 R   0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD           0x000000 0x00000000 0x00000000 0x00430 0x00430 R   0x1000
LOAD           0x001000 0x00001000 0x00001000 0x00284 0x00284 R E 0x1000
LOAD           0x002000 0x00002000 0x00002000 0x00144 0x00144 R   0x1000
LOAD           0x002ef4 0x00003ef4 0x00003ef4 0x0012c 0x00130 RW  0x1000
DYNAMIC        0x002efc 0x00003efc 0x00003efc 0x000f0 0x000f0 RW  0x4
NOTE           0x0001c8 0x000001c8 0x000001c8 0x00078 0x00078 R   0x4
GNU_PROPERTY   0x0001ec 0x000001ec 0x000001ec 0x00034 0x00034 R   0x4
GNU_EH_FRAME   0x002018 0x00002018 0x00002018 0x0003c 0x0003c R   0x4
GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
GNU_RELRO      0x002ef4 0x00003ef4 0x00003ef4 0x0010c 0x0010c R   0x1

Section to Segment mapping:
段节...
00
01     .interp
02     .interp .note.gnu.build-id .note.gnu.property .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt
03     .init .plt .text .fini
04     .rodata .eh_frame_hdr .eh_frame
05     .init_array .fini_array .dynamic .got .got.plt .data .bss
06     .dynamic
07     .note.gnu.build-id .note.gnu.property .note.ABI-tag
08     .note.gnu.property
09     .eh_frame_hdr
10
11     .init_array .fini_array .dynamic .got

2.1.4 PT_INTERP 段 link段

PT_INTERP 只将位置和大小信息存放在了一个以null(0)为终止符号的字符串中。 其作用就是指明程序解释器的位置。

其实主要作用可以理解为加载器。 我们写的程序都要导入一些函数你才能使用而这个路径下的加载器则会帮我们进行导入。比如说导入 libc。 如果我们自己写一个加载器替换,处理他的导入导出。那么这就叫做加载器劫持。

因为这个是系统帮你实现的。

如下:

三丶加载段

3.1.1 段加载原理

PT_LOAD段是最终加载到内存中的。其中他所用的到函数则是mmap函数

函数可以在: linux函数查询链接处找到mmap函数的使用方式以及定义。

下面说下简单的使用:

例子:

函数原型:
void *mmap(void * addr , size_t length , int prot , int flags , 
           int fd , off_t offset ); 
作用:
	mmap()在虚拟地址空间创建一个新的映射调用过程,新映射起始地址在addr中指定。长度参数指定了长度映射(必须大于0)
	如果addr为Null 则内核选择一个默认的页对齐后的地址给你。
	如果不是null 那么则会尝试在你给定的地址尝试。如果有了那么内核就会给你一个新的地址作为调用结果返回。
	mmap()使用后文件描符号 fd可以关闭,而不影响映射后的内存。
	prot则是权限,值为下:
		PROT_EXEC  : 代表内存页可执行
		PROT_READ  : 内存页可读
		PROT_WRITE :可写
		PROT_NONE  : 页面可能无法访问
	flags是参数标志:
		此标志的意思是你映射的内存是否其它进程可见(也就是是否共享)
		以及是否与文件映射在一起(意思就是修改内存文件是否对应修改)
		标志如下:
			MAP_SHARED: 共享映射,其它进程可见,且修改内存会修改到底层文件。
			MAP_SHARED_VALIDATE: (linux >= 4.15才能使用)提供了与MAP_SHARED一样的行为,唯一不同就是在内核验证处,查询文档即可。
			MAP_PRIVATE: 创建私有写时复制。更新映射的时候其它进程不可见,对于文件的修改不会修改到文件。
			MAP_FIXED : 将addr设置为绝对地址,意思就是如果不满足addr条件那么就出错。并不是让addr作为提示的地址了。
			
返回值:
		成功返回映射的地址的子真,失败返回-1(MAP_FAILED)

例子:

#include <stdio.h>
#include <stdlib.h> 
#include <sys/mman.h>
#include <dlfcn.h>
#include <sys/types.h>


int main(int argc, char **argv)
{

    FILE *fp = fopen("./abc.txt","rb");
    void *addr = mmap(
    (void*)0x80000000,
    0x1000,
    PROT_EXEC | PROT_READ | PROT_WRITE,
    MAP_PRIVATE | MAP_FIXED,
    fileno(fp),
    0);

    fclose(fp);
    printf("%p\n",addr);
    printf("file buffer is %c",*(u_int8_t*)addr);

return 0;
}

映射ABC.TXT到内存。并且设置地址为 0X80000000 设置为私有

最后打印输出内容

3.2 段加载实验

知道了ELF文件会使用PT_LOAD段。 那么我们则可以通过解析elf文件来加载一个elf文件的 PT_LOAD段。 实现Elf_loader 这跟在windows电脑上的 PEloader一样。

只需要加载 <elf.h> 头文件则可以解析elf文件了。

例子图:

代码

#include <cstddef>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <elf.h>

#define IMAGE_BASE 0x80000000

//获取phdr结构
Elf32_Phdr *get_Phdr(const char *filename, u_int32_t *out_phnumber, FILE **pFile)
{
	void *imageaddr = 0;
	FILE *fp = NULL;
	fp = fopen(filename, "rb");
	//解析ELF头  以及ELF 程序头来读取内存,当然读取0X1000个字节可以。为了学习这里解析

	Elf32_Ehdr header = {0}; //头
	if (NULL != fp)
	{

		fread(&header, sizeof(Elf32_Ehdr), 1, fp);
		/*
		printf("开始解析文件\n");
		printf("phsize = %x \n",header.e_phoff);
		printf("ph每一项大小为: %x \n",header.e_phentsize);
		printf("pheadr有 %x 项 \n",header.e_phnum);
		*/

		u_int32_t phheader_phoff = header.e_phoff;
		u_int32_t phheader_size = header.e_phentsize * header.e_phnum;
		Elf32_Phdr *phheaderAry = NULL;
		phheaderAry = (Elf32_Phdr *)malloc(phheader_size);
		if (phheaderAry == NULL)
		{
			fclose(fp);
			fp = NULL;
			return NULL;
		}
		fseek(fp, phheader_phoff, SEEK_SET);
		fread(phheaderAry, phheader_size, 1, fp);
		*out_phnumber = header.e_phnum;
		*pFile = fp;
		return phheaderAry;
	}
	return NULL;
}

//映射所有的PT_LOAD可加载段,所以便利PHDR进行映射
bool mapViewPtload()
{
	Elf32_Phdr *phdrbase = NULL;
	u_int32_t phdrnumber = 0;
	FILE *pFile = NULL;
	phdrbase = get_Phdr("./test.out", &phdrnumber, &pFile);
	printf("phdrbase = 0x%0x \n", phdrbase);
	printf("phdrnumbr = %d \n", phdrnumber);

	printf("start map view pt_load ......\n");
	/*
		1.遍历所有phdrnumber 找寻PT_LOAD类型的段
		2.开始映射 映射到addr中 
	*/
	Elf32_Phdr *phdrTmp = phdrbase;
	for (u_int32_t i = 0; i < phdrnumber; i++, phdrTmp++)
	{
		if (phdrTmp->p_type == PT_LOAD)
		{

			/*
			 1.映射文件中记录的PT段的数据映射到内存中
			 2.如果文件大小和内存大小在对齐后 文件大小-内存大小 大于0的情况
			 那么还需要进行内存映射  
			*/
			//对齐记录的内存中的偏移
			u_int32_t map_addr = phdrTmp->p_vaddr & 0xfffff000;
			//映射文件中p_offset所在的数据,映射p_filesz大小 到 mapaddr
			void *mapViewAddr = mmap(
				(void *)(IMAGE_BASE + map_addr),
				phdrTmp->p_filesz,
				PROT_EXEC | PROT_READ | PROT_WRITE,
				MAP_PRIVATE | MAP_FIXED,
				fileno(pFile),
				phdrTmp->p_offset & 0xfffff000);
			if (mapViewAddr == MAP_FAILED)
			{
				printf("映射失败 \n");
				return false;
			}
			//判断是否需要进行对齐
			uint32_t mem_size = (phdrTmp->p_memsz + 0xfff + (phdrTmp->p_vaddr & 0xfff)) & 0xfffff000;
			uint32_t file_size = ((phdrTmp->p_filesz + 0xfff) + (phdrTmp->p_vaddr & 0xfff)) & 0xfffff000;
			if (mem_size - file_size > 0)
			{
				//继续映射,但是映射不属于文件了
				mmap(
					(void *)(IMAGE_BASE + map_addr + file_size),
					mem_size - file_size,
					PROT_EXEC | PROT_READ | PROT_WRITE,
					MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS,
					fileno(pFile),
					phdrTmp->p_offset & 0xfffff000);
			}
			printf("mapview addr = %x \n", mapViewAddr);
		}
	}
	return true;
}

int main(int argc, char **argv)
{
	mapViewPtload();
	getchar();
	return 0;
}

四丶PT_DYNAMIC 中的 字符串表

4.1 数据解析

​ PT_DYNAMIC 程序头表我们需要看虚拟地址偏移。 如果加载到内存中就需要看虚拟机地址偏移。因为文件偏移不一定正确,虽然读出来是准的。

其中他的数据是一个结构体数组组成的。结构大概是如下类型(elf32为例)

struct {
	u_int32_t type;
	u_int32_t Va;
}

分别记录了偏移以及虚拟地址偏移。

其中type为 5(DT_STRTAB)的代表是字符串。通过查看他的地址偏移 可以直接在文件中看到

字符串的定义。

如下:

可以看到类型为5 偏移记录的是380 ,我文件中的地址和内存地址一样。所以我跳转到380查看。

补充: 请注意如果要解析这个数组有多项,请按照读取结尾为0为标志。而不是根据字段来进行计算得出的。

可以看到我所使用的字符串。

字符串表存在的意义就是给别人引用的。

五丶导入库表

5.1 导入库表

​ 导入库表,类型为DT_NEEDED(1) 在DYNAMIC表中,每一个为1的项都是导入库表。 每个导入库表记录了需要使用的so文件。

其中它所记录的偏移是虚拟地址。但是不是直接去虚拟地址查询的。

而是相对于字符串表的虚拟地址。 所以想要查询需要字符串表当作base

然后加上导入库表记录的偏移来查询使用。

查看下面的 DYN 中结构体数组表。

其中05代表字符串表。其记录的VA偏移是0x380。而在文件中也是0x380。

而表中第一项记录的是导入库表。 其偏移是0x82. 他记录的是相对于字符串表的偏移。所以我们需要 base+0x82 = VA 这样去查询。

例子:

0x380 + 0x82 = 0x402; 

直接跳转到0x402查看

可以看到引用的正是 LIBC.SO.6

六丶符号表

6.1 符号表简介

地址和字符串的对应关系叫做符号。导入以及导出都是一个符号。

比如导入的时候,比如我导入的是一个字符串名称,导入之后放到一个地址上。

在比如通过符号表可以找到函数地址并且使用函数地址。 主要应用就在这。

官方说法也叫做: 目标文件的符号表包含定位和重定位程序的符号定义和符号引用所需的信息。 符号表索引是此数组的下表。 这个是描述符号表节的

符号表

符号表的数据结构如下:

typedef struct {
        Elf32_Word      st_name;
        Elf32_Addr      st_value;
        Elf32_Word      st_size;
        unsigned char   st_info;
        unsigned char   st_other;
        Elf32_Half      st_shndx;
} Elf32_Sym;

typedef struct {
        Elf64_Word      st_name;
        unsigned char   st_info;
        unsigned char   st_other;
        Elf64_Half      st_shndx;
        Elf64_Addr      st_value;
        Elf64_Xword     st_size;
} Elf64_Sym;
字段 作用
st_name 表示目标文件的符号字符串表的索引,其中包含符号名称的字符串标识形式。如果该值为非零,那么就是标识指定的符号名称为字符串表的索引,否则就是符号表项没有名称。
st_value 关联符号的值,根据上下文。此值可以是绝对值或者地址。
st_size 许多符号具有关联大小。 例如目标文件的大小是目标文件中包含的字节数。如果符号没有大小或者大小未知,那么此值为0.
st_info 符号类型和帮顶属性。可查看下面的补充
st_other 符号的可见性。查看下面补充
st_shndx 所定义的每一个符号表项都与某节有关。此成员包含相关节头表索引。部分节索引会表示特殊含义。如果此成员包含 SHN_XINDEX,则实际节头索引会过大而无法放入此字段中。实际值包含在 SHT_SYMTAB_SHNDX 类型的关联节中。
  • st_info补充:

    代码处理为下 可以参考sys/elf.h:

    #define ELF32_ST_BIND(info)          ((info) >> 4)
    #define ELF32_ST_TYPE(info)          ((info) & 0xf)
    #define ELF32_ST_INFO(bind, type)    (((bind)<<4)+((type)&0xf))
    
    #define ELF64_ST_BIND(info)          ((info) >> 4)
    #define ELF64_ST_TYPE(info)          ((info) & 0xf)
    #define ELF64_ST_INFO(bind, type)    (((bind)<<4)+((type)&0xf))
    

    类型取值可以为下。

    名称 作用
    STB_LOCAL 0 局部符号,这些符号在包含其定义的目标文件的外部不可见。名称相同的局部符号可以存在于多个文件中而不会互相干扰。
    STB_GLOBAL 1 全局符号。 符号对于合并的所有目标文件都可见。一个文件的全局符号定义满足另一个文件对相同全局符号的为定义引用。
    STB_WEAK 2 弱符号,这些符号与全局符号类似,但其定义具有较低的优先级。
    STB_LOOS 10 STB_LOOS - STB_HIOS范围包含的值(包含这两个值)都是保留的。
    STB_HIOS 12 同上。
    STB_LOPROC 13 同上(STB_LOPROC-STB_HIPROC)
    STB_HIPROC 15 同上
  • st_other

    代码处理如下:

    #define ELF32_ST_VISIBILITY(o)       ((o)&0x3)
    #define ELF64_ST_VISIBILITY(o)       ((o)&0x3)
    

    符号可见性取值:

    名称 作用
    STV_DEFAULT 0 具有 STV_DEFAULT 属性的符号的可见性与符号的绑定类型指定的可见性相同。全局符号和弱符号在其定义组件(可执行文件或共享目标文件)外部可见。局部符号处于隐藏状态。另外,还可以替换全局符号和弱符号。可以在另一个组件中通过定义相同的名称插入这些符号。
    STV_INTERNAL 1 保留
    STV_HIDDEN 2 如果当前组件中定义的符号的名称对于其他组件不可见,则该符号处于隐藏状态。必须对这类符号进行保护。此属性用于控制组件的外部接口。由这样的符号命名的目标文件仍可以在另一个组件中引用(如果将目标文件的地址传到外部)。
    STV_PROTECTED 3 如果可执行文件或共享目标文件中包括可重定位目标文件,则该目标文件中包含的隐藏符号将会删除或转换为使用 STB_LOCAL 绑定。如果当前组件中定义的符号在其他组件中可见,但不能被替换,则该符号处于受保护状态。定义组件中对这类符号的任何引用都必须解析为该组件中的定义。即使在另一个组件中存在按缺省规则插入的符号定义,也必须进行此解析。具有 STB_LOCAL 绑定的符号将没有 STV_PROTECTED 可见性。
    STV_EXPORTED 4 此可见性属性确保符号保持为全局。不能使用任何其他符号可见性技术对此可见性进行降级或消除。具有 STB_LOCAL 绑定的符号将没有 STV_EXPORTED 可见性。
    STV_SINGLETON 5 此可见性属性确保符号保持为全局,并且符号定义的一个实例绑定到一个进程中的所有引用。不能使用任何其他符号可见性技术对此可见性进行降级或消除。具有 STB_LOCAL 绑定的符号将没有 STV_SINGLETON 可见性。不能直接绑定到 STV_SINGLETON
    STV_ELIMINATE 6 此可见性属性扩展 STV_HIDDEN。当前组件中定义为要消除的符号对其他组件不可见。该符号未写入使用该组件的动态可执行文件或共享目标文件的任何符号表中
  • 符号类型说明

    名称 作用
    STT_NOTYPE 0 未指定的符号类型
    STT_OBJECT 1 此符号与变量 数组等数据目标文件关联。
    STT_FUNC 2 此符号与函数或者其它可执行代码关联。
    STT_SECTION 3 此符号与节关联。 此类型的符号表各项主要用于重定位,并且通常具有STB_LOCAL绑定
    STT_FILE 4 通常,符号的名称会制定与目标文件关联的源文件的名称。文件符号具有STB_LOCAL绑定和节索引SHN_ABS.
    STT_COMMON 5 此符号标记未初始化的通用块。此符号的处理与 STT_OBJECT 的处理完全相同。
    STT_TLS 6 此符号指定线程局部存储实体。定义后,此符号可为符号指明指定的偏移,而不是实际地址。
    STT_LOOS 10 线程局部存储重定位只能引用 STT_TLS 类型的符号。从可分配节中引用 STT_TLS 类型的符号只能通过使用特殊线程局部存储重定位来实现。
    STT_HIOS 12 保留
    STT_LOPROC 13 保留
    STT_SPARC_REGISTER 13 保留
    STT_HIPROC 15 保留

6.2 符号表的说明

  • .symtab (SHT_SYMTAB)

    此符号表包含说明关联的 ELF 文件的每个符号。此符号表通常是不可分配的,因此在进程的内存映像中不可用。通过使用 mapfileELIMINATE 关键字可以从 .symtab 中消除全局符号。请参见删除符号SYMBOL_SCOPE / SYMBOL_VERSION 指令

  • .dynsym (SHT_DYNSYM)

    此表包含 .symtab 表中支持动态链接所需的符号的子集。此符号表可供分配,因此在进程的内存映像中可用。.dynsym 表以标准 NULL 符号开始,后跟文件全局符号。STT_FILE 符号通常不包含在此符号表中。如果重定位项需要,可能会包含 STT_SECTION 符号。

  • .SUNW_ldynsym (SHT_SUNW_LDYNSYM)

    扩充 .dynsym 表中包含的信息的可选符号表。.SUNW_ldynsym 表包含局部函数符号。此符号表可供分配,因此在进程的内存映像中可用。当不可分配的 .symtab 不可用,或已从文件中剥离时,调试器通过使用此节可在运行时上下文中产生精确的栈跟踪。此节还可以为运行时环境提供其他符号信息,以便与 dladdr(3C) 一起使用。

仅当 .dynsym 表存在时,才存在 .SUNW_ldynsym 表。当 .SUNW_ldynsym 节和 .dynsym 节同时存在时,链接编辑器会将其数据区域紧邻彼此放置,其中 .SUNW_ldynsym 放置在前面。这种放置方式可以使两个表看起来像是一个更大的连续符号表。此符号表遵从先前枚举的标准布局规则。

``

6.3 使用符号表

实战使用符号表

符号表 值为6(DT_SYMTAB) 所以我们可以在dynamic中的数据中 找类型为6的项。

通过其内存地址索引跳转到符号表(dynamic中的符号表,此表会映射到内存中)

图如下:

文件偏移260记录的则是符号表。 符号表是一个结构体。 elf32下是16字节长度

与字符串表一样,第一个结构不使用。

其中第二个结构开始 偏移是 0x58 这个0x58是st_name 他是记录的偏移。

偏移是相对于字符串表的偏移。 所以 字符串表base+0x58 才是此符号表的真实名字。

我的字符串表偏移是 0x2E0 在文件中寻得符号表的名字则是 0x2E0 + 0X58 = 0X338

其值正好就是 _ITM_deregisterTMCloneTable

正常来说 16个字节分布 分别是 名字(4个字节) 地址(4个字节) size(如果是函数地址则代表这个函数有多到) 。。。flag 不太重要。

所以符号表我们只需要关注前12个字节即可。

七丶导入表

7.1 导入表简介

导入表在dynamic数据中是10进制的23 16进制的0X17(DT_JMPREL)

导入表与其他数据表不同的是他有记录尺寸。 其它的数据表可能要找到数据为0才是最终数据。 而导入表有尺寸记录。 记录在dynamic中。 值为2 (DT_PLTRELSZ)

读出导入表大小之后需要 / 8(导入表结构大小) 这样就能得出有多个导入表了

结构如下(伪代码)

{
 uint_32_t addr;  导入的地址,导入的函数要写入到的addr中 例如 *(uint32*)addr = xxxxproc 是修改到addr中指向的地址中,而不是修改addr本身。
 unsigned char type; 类型。 
 unsigned char symtable1;
 unsigned char symtable2;   这三个1 2 3 和起来表示在符号表中的索引(符号表是数组组成的一行16个字节)。
 unsigned char symtable3;
}

符号表和导入表中的关联关系。

1.首先导入表中 查看导入的地址以及类型

2.查看符号表中的索引

3.去符号表中找到对应符号表。 通过符号表的字符串去找对应的函数。

posted @ 2021-10-03 14:02  Android_IBinary  阅读(2767)  评论(0编辑  收藏  举报