type
date
status
slug
summary
tags
category
password
icon
elf目标文件的格式
从nemu读入的.bin文件考虑
.bin文件是elf目标文件的一种,是可以直接被计算机识别而运行的;.bin文件只有代码和数据,简言之,就是一系列指令的集合。
复盘nemu读取镜像文件的过程
首先,nemu会解析程序运行时的输入参数,如果存在输入镜像文件,则会将该文件打开,将其读入(客户机的)物理内存当中;其次,返回该镜像文件的大小;
关键点在于向物理内存中加载数据,nemu是利用一个字节数组来模拟物理内存的,因此我们需要将客户机地址映射到宿主机地址当中,就需要用的guest_to_host函数,利用客户机物理地址减去客户机物理内存基地址得到偏移量,将它加到宿主机数组地址上就实现了简单的映射:
当然,如果没有给出镜像文件,那么则默认读入内嵌的默认镜像。
紧接着,这里的物理内存,又是如何组织的呢?我在load_img函数中注意到了:
即,镜像文件的存放位置,要从RESET_VECTOR(这是一个客户机物理地址)开始,查看了一下该值的大小,发现:
即,内存的基地址就是
CONFIG_MBASE
,那为什么不是0而是0x80000000呢?这里为了得到RESET_VECTOR
还是用PMEM_LEFT + CONFIG_PC_RESET_OFFSET
得到的,这里的OFFSET有什么含义吗,难道也可能不为0?为了解决上述疑问,我重新复盘了以下pa1的讲义:

image-20240127224027017
也就是说只有x86的物理内存是从0开始编址的,riscv32则是从0x80000000开始,也就是说物理内存的第一个单元的地址是0x80000000而不是0;对于第二个问题,可能就是操作系统的问题了,或者说一些其他的考虑,才将偏移量设置为一个其它的值。
那么,这个.bin文件中程序和数据的组织结构如何?
这么一想,还真有点困难,难以理解,我们可以先参考是一下默认的镜像数据:

其实还是挺清晰的,第一部分是程序,第二部分是数据,实现了基本的代码和数据分离。
如果在文件中加上其它的东西?(gcc)
为什么?因为我们还有其它的需求。
添加调试信息
- addr2line 工具可以根据调试信息将地址转换为源文件位置(gdb);
添加权限管理
例如,代码应该是可读可执行不可写的;数据是可读可写不可执行的;需要直到代码和数据之间的边界。
我们需要一些信息将这些内容进行标记和区分。
添加入口地址
可以添加入口地址,使得每次不一定都从头开始执行。
元数据
数据的数据。
工具-Binutils(生成/解析二进制文件的工具集)
- ld
- as
- objcopy
- objdump
- ………….
思考一下,如何设计一个elf文件?
基本要求:程序与数据分离;
其他要求:记录每一部分在文件中的起始位置;记录每一部分的长度;记录各个部分的属性,例如权限;记录每个部分的名字;
我们可以用C语言实现一个结构体:
我们将这个数据结构记录的程序对象称为section(节),这个数据结构本身(元数据)称为section
header(节头)。
将所有的节的名称集中存放
将元数据的字符串集中放在另一个区域中,节头中的
name
替换成一个指针,用于记录字符串在文件中的位置;进一步考虑,将这个区域当做一个新的节,节头中的name
换成一个偏移量,用于记录字符串在新节中的位置;在ELF格式便是使用的此方案,该节叫做.strtab(string
table)。节可以通过节头找到,那如何找到节头?
ELF格式使用的方案是,将节头的位置记录在ELF头中,并约定ELF头位于ELF文件的起始位置(固定位置)。
找到第一个节头后,如何找到下一个?
ELF格式使用的方案是数组,将多个节头连续存放,形成一张“节头表”,并将节头的数量记录在ELF头中。
帮助文档
man 5 elf
- https://refspecs.linuxfoundation.org/elf/gabi41.pdf
- https://refspecs.linuxbase.org/
静态连接原理
为什么需要链接
无论是编译还是汇编都无法实现跨节对数据和函数的引用,因此这一部分工作就自然交给连接器来完成。
链接的两大原理
符号解析(symbol resolution)
用于处理符号的引用,将符号的引用与符号的定义进行关联。
那什么算是一个符号呢?上面说了,链接的一个工作就是要完成跨节(考虑到每个目标文件的程序和数据都分布在不同的一个节中,因此跨节就是跨文件,例如一个文件中程序节中引用了另一个文件中程序节中的一个函数)的数据与函数的引用,因此我们可以认为全局性的变量和函数都属于符号,相反静态变量以及自动化局部变量则不属于符号。
重定位(relocation)
将不同文件中相同的节进行合并。
- 合并.o文件;
- 确定每个符号的地址;
- 在相关指令中填入新地址;
符号解析基本原理
每次汇编都生成一个符号表,并将该符号表存储在目标文件的.symtab节中,该节的每个entry中都记录一个符号的相关信息,包括:
- 符号名(即.strtab节中的便宜量);
- 符号在程序中的地址;
- 符号的大小;
- 符号所属的节:
- 如果符号在相应节中定义,则标记为D(已定义);
- 否则标记为U(未定义);
- 其他属性;
链接器维护两个集合:已定义集合和未定义集合,并依次对各个目标文件的符号表进行扫描,并根据相应条件进行如下操作:

实现pa2中的ftrace
在运行程序时传入对应elf文件
添加命令行选项并实现文件加载


在am的makefile文件中添加相应参数

实现读取ELF头函数并简单测试

根据ELF头读出节头表
所谓节头表,其实就是一个节头结构体数组,因此我们只需要从其偏移量开始读取节头数目的结构体即可。读出之后,我们需要从节头表中找到.symtab、.strtab两个节的偏移量和大小,因此要遍历数组中所有元素的sh_name属性,而这一属性所保存的内容是其名称字符串在.shstrtab节中的偏移量。
所以现在我们需要先定位.shstrtab节的位置,其实通过elf头中的e_shstrndx成员以及节头表数组就可以直接得到了。

读出节头字符串表并最终得出符号表和符号字符串表

到这里就已经将符号表和字符串表都读入程序当中了,其实还是挺简单的,但是由于对elf文件的不熟悉,绕来绕去的,一度把自己给绕晕了,自己捋了好久才把逻辑捋出来。下一步就要考虑如何建立ftrace缓冲区的问题了。
- 作者:Tdotd
- 链接:https://www.tdotd.top//article/elf-learn
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。