背景
在学习ELF文件的过程中,看到Matrix中有类似于如下的代码来遍历ELF文件的e->phdr程序头表的内容。
// 根据e_phoff找到Program Header的位置
elf_file->elf_phdr = (ELF_Phdr *) (elf_file->base_address + elf_file->elf_ehdr->e_phoff);
// ELF文件Program Header的结束位置
void *phdr_end_addr = elf_file->elf_phdr + elf_file->elf_ehdr->e_phnum;
// 从phdr开始到结束寻找,开始找到PT_LOAD的段
for (ELF_Phdr *phdr_start_addr = elf_file->elf_phdr; phdr_start_addr < phdr_end_addr; phdr_start_addr++) {
// 开始遍历程序头
// 在C中,数组指针的++代表指向下个元素
auto *program_head_table = (ELF_Phdr *) phdr_start_addr;
if (program_head_table->p_type == PT_LOAD) {
LOGE(TAG, "TYPE:%u, PT LOAD FIND", program_head_table->p_type);
}
LOGE(TAG, "TYPE:%u", program_head_table->p_type);
}
问题
elf_file->elf_phdr:程序头表的起始位置
elf_file->elf_ehdr->e_phnum:程序头表中段的总数
elf_file->elf_ehdr->e_phentsize:程序头表中每个Entry的大小
问题1:为何elf_file->elf_phdr + elf_file->elf_ehdr->e_phnum就能得到程序头表的结束地址呢?而不是简单的地址加上对应的数字?
问题2:在for循环中,phdr_start_addr++为何能找到程序头表中的下一个段结构呢?
问题3:在elf_file->base_address + elf_file->elf_ehdr->e_phoff计算ELF_Phdr的地址时,是直接加的,和问题1中的有何区别?
日志验证
在代码中加入日志,打印phdr的起始与结束地址,以及entry的总数与每个entry的大小。
e->phdr start addr:534289383488
e->phdr end addr:534289383936
e->phdr entry count:8, entry size:56
Program Header: index:0...addr:534289383488
Program Header: index:1...addr:534289383544
Program Header: index:2...addr:534289383600
Program Header: index:3...addr:534289383656
Program Header: index:4...addr:534289383712
Program Header: index:5...addr:534289383768
Program Header: index:6...addr:534289383824
Program Header: index:7...addr:534289383880
可以看到:
结束地址减去起始地址正好是所有Entry的大小
end addr-start addr = 534289383936 - 534289383488 = 448
entry count * entry size = 8 * 56 = 448
在for循环中,phdr_start_addr++获取到就是对应index的起始地址
Entry之间的间隔大小正好为56,534289383544 - 534289383488 = 56是一个Entry的大小
原因
对于问题3而言,由于elf_file->base_address是ELF_Addr类型,本身就是无符号的32位或者64位整形,所以直接加减即可。
typedef __u64 Elf64_Addr;
typedef __u32 Elf32_Addr;
而elf_file->elf_phdr则是(Elf64_Phdr *),也就是指针类型,而对于指针来说,+1或者-1都是对于整个结构体而言,所以对于指针的操作,每次加1或者-1都会偏移sizeof(struct)的大小。
而这个计算,都是在编译期间编译器已经计算好了的。
(void *)的计算
C和C++中不允许(void *)pointer与整数相加,否则在编译的时候会报错误:
arithmetic on a pointer to void
原因是C和C++禁止在(void *)的指针中进行运算,因为在运行的时候会有很多字节对齐的操作,如果没有指定类型的话,无法知道对应的结构体的大小,所以禁止(void *)指针的计算。
结论
对于地址的加减来说,可以正常按数加减,而且加减完后可以赋值给结构体指针。
而对于指针的加减来说,是对于指针对应的结构体大小而言的,每次加减都是计算的N个结构体大小的偏移。
例如:phdr_start_addr++可以理解成:
phdr_start_addr = phdr_start_addr + sizeof(ELF_Phdr); // 计算地址的方式