elf处理原理和相关基础设施

项目工程

Posted by Bruce Lee on 2024-08-25

关于我

欢迎来到我的博客!这里汇集了我对编程和技术的洞见和总结。本站内容分为几个主要类别,涵盖从具体技术实现到编程理念的广泛话题。

主要内容分类

  • 项目工程:深入探讨技术的实现细节和理解。
  • C/C++:围绕C/C++语言的技术点和编程技巧进行详细总结。
  • 程序员哲学:分享程序员在职业生涯中应该具备的哲学理念和思考方式。

想要了解更多具体内容,您可以访问文章分类页面。

联系我

如果您有任何问题或想要交流,欢迎通过关于页面与我联系。

感谢您的阅读和支持,希望我的博客能为您的技术旅程带来帮助!


elf处理原理

后续会详细讲解如何在加载器中直接作用elf文件结构来处理可执行程序文件.

libelf.h头文件,这个对应的库中,有关于读取,修改,写入elf文件的函数和数据结构.可以用来加载elf文件,来获取段和符号的信息.

gelf.h是libelf库的一部分,提供的通用接口,无视底层架构的差异(32位或者64位)

首先需要Elf对象,用于对整个elf文件进行引用.指向这个elf文件.
然后定义一个Elf_Scn对象scn,用于对elf文件中的每一个节进行引用.
然后定义一个GElf_Shdr对象shdr,描述elf中的节头信息.
然后定义一个Elf_Data对象data,用于获取节中的数据信息.
然后定义一个fd的int型变量,用于获取文件描述符,用于打开文件.

传入elf文件信息,然后调用open函数打开,返回fd参数.调用open函数中,传入filename和O_RDONLY宏(在fcntl.h头文件中定义)

然后调用elf_version函数用于初始化检查,确保elf库是最新的,并且能用.该函数用于设置libelf库所使用的ELF版本,并检查库的兼容性.

EV_CURRENT宏定义在libelf文件中,表示其最新版本.如果elf_version函数返回EV_NONE,表示错误.

然后调用elf_begin函数,传入fd,ELF_C_READ参数,用于关联elf文件,并且返回一个Elf对象elf,用于引用整个elf文件.

ELF_C_READ表示只读.

如果错误,调用了elf_errmsg函数传入(-1),用于打印具体的错误信息,-1表示最新的.

然后准备好了之后,开始进入while循环,调用elf_nextscn函数,来以节为单位进行索引.循环遍历所有的节,来查找是否含有符号表节.

传入elf对象,和scn去承载节的信息.

如果返回值scn不等于NULL,说明获取了一个有效节.

然后调用gelf_getshdr函数,传入scn对象,和shdr来承载节头信息.

然后调用shdr中的sh_type变量与SHT_SYMTAB宏进行比较.表示符号表,在libelf中定义的.

如果这是一个符号表,那么就进入数据处理代码块.

使用Elf_Data对象的data变量来通过调用elf_getdata函数(传入scn节对象)来获取整个节的数据信息.

然后这里就是获取了所有的符号表条目信息.

通过使用shdr.sh_size和shdr.sh_entsize变量,获取了条目数量.

然后使用动态申请内存,获得了symtable_value变量指向的uint32_t [2]单元空间,数量为count个.symtable_name指向了const char*单元空间,数量也是count个.

然后进入count次数循环.

定义一个GElf_Sym类型sym变量,用于调用gelf_getsym去获取符号表条目信息.

然后通过sym中的st_info变量来获取符号信息.通过调用GELF_ST_TYPE来从st_info中提取信息,然后与STT_FUNC进行比较,为了获取func类型的符号条目.

找到了FUNC类型的符号表条目,然后就是通过elf_strptr函数去获取字符串表中的对应的符号表的字符串名称等信息.

需要传入shdr.sh_link,这个节头信息,指示了符号表的名称在字符串表中的偏移位置,传入sym.st_name表示这个条目在整个符号表名称中的偏移位置.

然后获取name.

其条目的地址和大小,是通过sym.st_value和sym.st_size变量来获取.

综上,然后处理这些信息,存储在symtable_name和symtable_value变量中.

用于在cpu-exec文件中进行使用.

首先需要在cpu-exec.c文件中extern那几个在monitor中定义的变量.

然后根据自己的喜好,排版输出格式,我是直接定义了ANSI_GREEN和ANSI_BLUE宏,用于输出CALL和RET关键字的不同颜色.

定义了ftrace_exec函数,用于比较.为何要传入old_pc.因为在s->dnpc是下一条实际执行指令.在程序启动的时候,pc指向的是当前要执行的指令,这个指令是函数首地址,可以不漏掉任何初始函数.

ftrace_exec中的逻辑上述分析过了.

在exec_once中调用ftrace_exec函数.

bug:在symtable_value/name中出现的过多的不相关数据,原因是count的计数与func的计数混合了.

重新来一个for循环,用symtable_count来技术func,用count来计数全部条目

这里没有应用链表来动态申请内存.

在monitor中,对一系列的变量定义,函数定义,以及头文件进行条件宏定义.
在init_monitor函数中,对init_ftrace函数的调用也同样处理.

在cpu-exec中,对extern,ftrace_exec,以及对其的调用也同样处理

在nemu中进行make menuconfig之后,需要在am中的nemu.md中进行同样的手动操作,进行定义FTRACE,只需要注释掉CONFIG_FTRACE的定义就行了

消失的符号以及字符串表

首先宏名称是不可能出现符号表中,在预编译阶段,宏已经被展开消失了.

局部变量在编译器优化的过程一部分是消失了,或者在堆栈中,没有名字.符号表中存储的是全局变量,函数,文件的名字.

一个符号,是一个对物理内存地址的映射,这个内存地址一般是不会发生变化的.这样的实体才有符号.

如果编写

1
2
3
4
5
6
#include <stdio.h>
int a = 0;
int main()
{
printf("hey %d", a);
}

经过多层编译:

1
2
3
$gcc -S main.c -o main.s
$gcc -c main.s -o main.o
$gcc main.o -o main.elf

然后

1
readelf -a main.elf

或者对比:

1
readelf -p .strtab main.elf

可以看到在符号表中与字符串表中的文件名位置,不在同一个偏移位置的.

关于符号表在不同阶段的作用

符号表是在词法分析阶段生成的,然后在语法分析阶段,是用不上的,语法分析是建立语法树.符号表是在语义分析阶段用的上,然后在中间代码生成,然后与静态/动态库进行链接的过程是可以用的上的,在最后的执行阶段是用不上的.

所以如果将elf文件中的符号表使用strip -s main.elf命令删除,是不影响运行./main.elf的

如果将main.o目标文件中的符号表删除,那么在链接的时候,就会出错,找不到符号.

AM基础设施

AM的思想确实保证了klib(我们针对nemu中编写的运行时环境)都是运行在AM之上的代码,也是架构无关的.因为在klib中,我们并没有体现什么关于RISCV32中的具体信息.

对于make[1]: *** [run] Error 1错误信息来源

其实make工具通过子程序去编译其他目录下的makefile文件,这是在主程序中,运行run命令,发生了错误,错误码是1,可能是操作系统返回的.

在klib.h中定义__NATIVE_USE_KLIB__,然后呢,在klib的实现代码中,进行的__NATIVE_USE_KLIB__的条件包含代码.

如果定义了,那么就会包含这些代码.

然后在am的makefile中有:

1
LIBS := $(sort $(LIBS) am klib)

这里klib中有代码,自然就会被编译进去.

文档中,允许使用putch,我看到了在am.h中,是申明了putch函数,

nemu中的遗留问题

mul-longlong测试文件出bad
am-kernels/kernels/hello在运行在nemu中,出现out of bound

目前测试的目标平台

根据上述的分析.
如果在ARCH中传入native,那么就是直接运行在真机上,并且链接的是glibc库,这个功能就是测试我们的测试文件是否正确.

如果一切不变,知识在klib.h文件中将__NATIVE_USE_KLIB__宏定义,那么就不是链接glibc库,而是连接klib库,这样子就是运行我们的测试程序,并且测试程序链接的库就是自己编写的klibc.如果保证了测试程序的正确性,那么这个行为就是验证klibc库的正确性.

如果上述一切不变,ARCH参数改为riscv32-nemu,就会直接运行在我们的nemu上,如果保证了我们的上层程序和实现的klib库是正确的,那么就可以验证我们nemu实现是否正确.

bug排除完毕

首先就是关于mul-longlong测试程序的bug,在完成difftest之后,重点就是投放到了指令实现的问题上,关于这个执行过程,difftest指向了mulh指令.

在经过返回验证,返回编写c代码原则mulh指令的扩展问题后,感觉没有任何问题之后.还是向乐桐请教,关于符号扩展,我的实现其实还是真有问题,但是具体问题,下次分析,主要解决方法就是使用了框架代码中给定的SEXT宏.

关于klib库,和klib-test中的测试程序,主要就是strlen函数的实现有问题,导致了后续的连环错误.其次就是测试程序本身的错误.但是现在都排除了.

mul-longlong的错误,在表现上,是a5的值,写进了a4中.但是本身问题是mulh指令中的两个源操作数在扩展问题上的错误.

关于diff-test

最后写下关于difftest的实现,这个过程比较顺利,按照regs中的顺序,比较代码是按照isa_reg_display的for循环来实现的.

其中,也是调用了difftest_check_reg函数进行的内部比较和返回.

在函数检查过程中

主要是运用__attribute__((optimize(“O0”)))来禁止优化函数.

其中使用__attribute__((aligned(4)))来进行4对齐.

同时,使用嵌入汇编代码来验证mulh的实现.

主要是如下这样:

1
2
3
4
uint32_t a = 0xaeb1c2aa;
uint32_t b = 0x4500ff2b;
uint32_t result;
asm("mulh %0, %1, %2" : "=r"(result) : "r"(a), "r"(b));

用于指定源操作数和目的操作数.


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !