深入解析 FESVR(Front-End Server)
url: https://github.com/riscv/riscv-isa-sim.git
commid: fcbdbe7946079650d0e656fa3d353e3f652d471f
目录
- FESVR 概述
- FESVR 代码结构分析
- ELF 加载机制
1. FESVR 概述
fesvr
(Front-End Server)是 riscv-isa-sim
(Spike 模拟器)中的一个核心组件,它负责 模拟器与外部环境之间的交互。主要功能包括:
- ELF 加载:将 RISC-V 二进制程序加载到模拟器内存。
- 系统调用处理:拦截并模拟 RISC-V 目标的
ecall
,并在宿主机执行相应的系统调用。 - 与 Spike 交互:通过 HTIF(Host-Target Interface)进行内存访问、I/O 操作等。
在 RISC-V 仿真环境中,fesvr
扮演 仿真程序的桥梁,确保 Spike 能够正确模拟 RISC-V 目标程序的执行。
2. FESVR 代码结构分析
fesvr
的代码主要位于 riscv-isa-sim/fesvr/
目录,主要文件及作用如下:
fesvr
目录下的源码文件清单,涉及 ELF 加载、设备仿真、HTIF 交互、系统调用 等多个模块。下面是各文件的简要介绍:
1. ELF 相关文件
elf.h
:ELF 格式的定义,包含 ELF 头结构等信息。elfloader.h
/elfloader.cc
:解析 ELF 文件,并将程序加载到目标内存。elf2hex.cc
:是将 ELF 转换为 HEX 格式的工具。
1.1 elf.h
elf.h文件内容如下:
// See LICENSE for details.
#ifndef _ELF_H
#define _ELF_H
#include <stdint.h>
#define ET_EXEC 2
#define EM_RISCV 243
#define EM_NONE 0
#define EV_CURRENT 1
#define IS_ELF(hdr) \
((hdr).e_ident[0] == 0x7f && (hdr).e_ident[1] == 'E' && \
(hdr).e_ident[2] == 'L' && (hdr).e_ident[3] == 'F')
#define ELF_SWAP(hdr, val) (IS_ELFLE(hdr)? from_le((val)) : from_be((val)))
#define IS_ELF32(hdr) (IS_ELF(hdr) && (hdr).e_ident[4] == 1)
#define IS_ELF64(hdr) (IS_ELF(hdr) && (hdr).e_ident[4] == 2)
#define IS_ELFLE(hdr) (IS_ELF(hdr) && (hdr).e_ident[5] == 1)
#define IS_ELFBE(hdr) (IS_ELF(hdr) && (hdr).e_ident[5] == 2)
#define IS_ELF_EXEC(hdr) (IS_ELF(hdr) && ELF_SWAP((hdr), (hdr).e_type) == ET_EXEC)
#define IS_ELF_RISCV(hdr) (IS_ELF(hdr) && ELF_SWAP((hdr), (hdr).e_machine) == EM_RISCV)
#define IS_ELF_EM_NONE(hdr) (IS_ELF(hdr) && ELF_SWAP((hdr), (hdr).e_machine) == EM_NONE)
#define IS_ELF_VCURRENT(hdr) (IS_ELF(hdr) && ELF_SWAP((hdr), (hdr).e_version) == EV_CURRENT)
#define PT_LOAD 1
#define SHT_NOBITS 8
typedef struct {
uint8_t e_ident[16];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint32_t e_entry;
uint32_t e_phoff;
uint32_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} Elf32_Ehdr;
typedef struct {
uint32_t sh_name;
uint32_t sh_type;
uint32_t sh_flags;
uint32_t sh_addr;
uint32_t sh_offset;
uint32_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint32_t sh_addralign;
uint32_t sh_entsize;
} Elf32_Shdr;
typedef struct
{
uint32_t p_type;
uint32_t p_offset;
uint32_t p_vaddr;
uint32_t p_paddr;
uint32_t p_filesz;
uint32_t p_memsz;
uint32_t p_flags;
uint32_t p_align;
} Elf32_Phdr;
typedef struct
{
uint32_t st_name;
uint32_t st_value;
uint32_t st_size;
uint8_t st_info;
uint8_t st_other;
uint16_t st_shndx;
} Elf32_Sym;
typedef struct {
uint8_t e_ident[16];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint64_t e_entry;
uint64_t e_phoff;
uint64_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} Elf64_Ehdr;
typedef struct {
uint32_t sh_name;
uint32_t sh_type;
uint64_t sh_flags;
uint64_t sh_addr;
uint64_t sh_offset;
uint64_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint64_t sh_addralign;
uint64_t sh_entsize;
} Elf64_Shdr;
typedef struct {
uint32_t p_type;
uint32_t p_flags;
uint64_t p_offset;
uint64_t p_vaddr;
uint64_t p_paddr;
uint64_t p_filesz;
uint64_t p_memsz;
uint64_t p_align;
} Elf64_Phdr;
typedef struct {
uint32_t st_name;
uint8_t st_info;
uint8_t st_other;
uint16_t st_shndx;
uint64_t st_value;
uint64_t st_size;
} Elf64_Sym;
#endif
上述代码定义了一系列用于处理 ELF(Executable and Linkable Format)文件的宏和结构体,ELF 是一种在 Unix 和类 Unix 系统中广泛使用的二进制文件格式,用于可执行文件、目标文件、共享库等。下面详细分析各个结构定义的作用:
- 宏定义
- 文件类型和常量:
ET_EXEC
:值为 2,表示该 ELF 文件是一个可执行文件。EM_RISCV
:值为 243,代表 ELF 文件的目标架构是 RISC - V。EM_NONE
:值为 0,表示无特定的目标架构。EV_CURRENT
:值为 1,代表当前的 ELF 版本。
- ELF 文件检查宏:
IS_ELF(hdr)
:用于检查给定的 ELF 头hdr
是否为有效的 ELF 文件,通过检查e_ident
数组的前四个字节是否为0x7f
、'E'
、'L'
、'F'
来判断。ELF_SWAP(hdr, val)
:根据 ELF 文件的字节序(大端或小端),调用from_le
或from_be
函数对值val
进行字节序转换。
- ELF 文件特性检查宏:
IS_ELF32(hdr)
:检查给定的 ELF 头是否表示一个 32 位的 ELF 文件。IS_ELF64(hdr)
:检查给定的 ELF 头是否表示一个 64 位的 ELF 文件。IS_ELFLE(hdr)
:检查给定的 ELF 头是否表示一个小端字节序的 ELF 文件。IS_ELFBE(hdr)
:检查给定的 ELF 头是否表示一个大端字节序的 ELF 文件。IS_ELF_EXEC(hdr)
:检查给定的 ELF 头是否表示一个可执行的 ELF 文件。IS_ELF_RISCV(hdr)
:检查给定的 ELF 头是否表示目标架构为 RISC - V 的 ELF 文件。IS_ELF_EM_NONE(hdr)
:检查给定的 ELF 头是否表示无特定目标架构的 ELF 文件。IS_ELF_VCURRENT(hdr)
:检查给定的 ELF 头是否使用当前的 ELF 版本。
- 程序头表和节头表相关宏:
PT_LOAD
:值为 1,表示程序头表中的一个加载段。SHT_NOBITS
:值为 8,表示节头表中的一个不占用文件空间的节。
- 结构体定义
2.1 32 位 ELF 头结构体
Elf32_Ehdr
typedef struct {
uint8_t e_ident[16];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint32_t e_entry;
uint32_t e_phoff;
uint32_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} Elf32_Ehdr;
- 作用:该结构体用于表示 32 位 ELF 文件的文件头,包含了文件的基本信息,如文件类型、目标架构、版本、入口地址、程序头表和节头表的偏移等。
- 字段解释:
e_ident
:一个长度为 16 的数组,包含了 ELF 文件的标识信息,如文件类型、字节序、版本等。e_type
:表示 ELF 文件的类型,如可执行文件、目标文件等。e_machine
:表示目标架构。e_version
:表示 ELF 文件的版本。e_entry
:表示程序的入口地址。e_phoff
:表示程序头表在文件中的偏移。e_shoff
:表示节头表在文件中的偏移。e_flags
:包含一些与目标架构相关的标志。e_ehsize
:表示 ELF 文件头的大小。e_phentsize
:表示程序头表中每个条目的大小。e_phnum
:表示程序头表中的条目数量。e_shentsize
:表示节头表中每个条目的大小。e_shnum
:表示节头表中的条目数量。e_shstrndx
:表示节名表在节头表中的索引。
2.2 32 位节头结构体
Elf32_Shdr
typedef struct {
uint32_t sh_name;
uint32_t sh_type;
uint32_t sh_flags;
uint32_t sh_addr;
uint32_t sh_offset;
uint32_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint32_t sh_addralign;
uint32_t sh_entsize;
} Elf32_Shdr;
- 作用:该结构体用于表示 32 位 ELF 文件中的节头,每个节头描述了一个节的信息,如节的名称、类型、大小、在文件中的偏移等。
- 字段解释:
sh_name
:节名在节名表中的索引。sh_type
:节的类型,如代码节、数据节等。sh_flags
:节的标志,如是否可执行、是否可写等。sh_addr
:节在内存中的地址。sh_offset
:节在文件中的偏移。sh_size
:节的大小。sh_link
:与该节相关的其他节的索引。sh_info
:与该节相关的额外信息。sh_addralign
:节的地址对齐要求。sh_entsize
:如果节中包含固定大小的条目,该字段表示每个条目的大小。
2.3 32 位程序头结构体
Elf32_Phdr
typedef struct
{
uint32_t p_type;
uint32_t p_offset;
uint32_t p_vaddr;
uint32_t p_paddr;
uint32_t p_filesz;
uint32_t p_memsz;
uint32_t p_flags;
uint32_t p_align;
} Elf32_Phdr;
- 作用:该结构体用于表示 32 位 ELF 文件中的程序头,每个程序头描述了一个程序段的信息,如段的类型、在文件中的偏移、在内存中的地址等。
- 字段解释:
p_type
:段的类型,如加载段、动态链接段等。p_offset
:段在文件中的偏移。p_vaddr
:段在虚拟内存中的地址。p_paddr
:段在物理内存中的地址(在大多数情况下与p_vaddr
相同)。p_filesz
:段在文件中占用的大小。p_memsz
:段在内存中占用的大小。p_flags
:段的标志,如是否可执行、是否可写等。p_align
:段的地址对齐要求。
2.4 32 位符号表条目结构体
Elf32_Sym
typedef struct
{
uint32_t st_name;
uint32_t st_value;
uint32_t st_size;
uint8_t st_info;
uint8_t st_other;
uint16_t st_shndx;
} Elf32_Sym;
- 作用:该结构体用于表示 32 位 ELF 文件中的符号表条目,每个符号表条目描述了一个符号的信息,如符号的名称、值、大小等。
- 字段解释:
st_name
:符号名在符号名表中的索引。st_value
:符号的值,通常是一个地址。st_size
:符号所占用的大小。st_info
:符号的类型和绑定信息。st_other
:保留字段。st_shndx
:符号所在节的索引。
2.5 64 位 ELF 头结构体
Elf64_Ehdr
typedef struct {
uint8_t e_ident[16];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint64_t e_entry;
uint64_t e_phoff;
uint64_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} Elf64_Ehdr;
- 作用:与
Elf32_Ehdr
类似,用于表示 64 位 ELF 文件的文件头,只是部分字段使用了 64 位整数类型。
2.6 64 位节头结构体
Elf64_Shdr
typedef struct {
uint32_t sh_name;
uint32_t sh_type;
uint64_t sh_flags;
uint64_t sh_addr;
uint64_t sh_offset;
uint64_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint64_t sh_addralign;
uint64_t sh_entsize;
} Elf64_Shdr;
- 作用:与
Elf32_Shdr
类似,用于表示 64 位 ELF 文件中的节头,部分字段使用了 64 位整数类型。
2.7 64 位程序头结构体
Elf64_Phdr
typedef struct {
uint32_t p_type;
uint32_t p_flags;
uint64_t p_offset;
uint64_t p_vaddr;
uint64_t p_paddr;
uint64_t p_filesz;
uint64_t p_memsz;
uint64_t p_align;
} Elf64_Phdr;
- 作用:与
Elf32_Phdr
类似,用于表示 64 位 ELF 文件中的程序头,部分字段使用了 64 位整数类型。
2.8 64 位符号表条目结构体
Elf64_Sym
typedef struct {
uint32_t st_name;
uint8_t st_info;
uint8_t st_other;
uint16_t st_shndx;
uint64_t st_value;
uint64_t st_size;
} Elf64_Sym;
- 作用:与
Elf32_Sym
类似,用于表示 64 位 ELF 文件中的符号表条目,部分字段使用了 64 位整数类型。
综上所述,这些结构体和宏定义提供了一套完整的接口,用于解析和处理 32 位和 64 位的 ELF 文件。
elf.h
:ELF 格式的定义,包含 ELF 头结构等信息。elfloader.h
/elfloader.cc
:解析 ELF 文件,并将程序加载到目标内存。elf2hex.cc
:是将 ELF 转换为 HEX 格式的工具。
1.2 elfloader.h/elfloader.cc
elfloader.h
#ifndef _ELFLOADER_H
#define _ELFLOADER_H
#include "elf.h"
#include "memif.h"
#include <map>
#include <string>
class memif_t;
std::map<std::string, uint64_t> load_elf(const char* fn, memif_t* memif, reg_t* entry,
reg_t load_offset, unsigned required_xlen = 0);
#endif
elfloader.cc
load_elf 函数实现
// See LICENSE for license details.
#include "config.h"
#include "elf.h"
#include "memif.h"
#include "byteorder.h"
#include <cstring>
#include <string>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <assert.h>
#include <unistd.h>
#include <stdexcept>
#include <stdlib.h>
#include <stdio.h>
#include <vector>
#include <map>
#include <cerrno>
std::map<std::string, uint64_t> load_elf(const char* fn, memif_t* memif, reg_t* entry,
reg_t load_offset, unsigned required_xlen = 0)
{
int fd = open(fn, O_RDONLY);
struct stat s;
if (fd == -1)
throw std::invalid_argument(std::string("Specified ELF can't be opened: ") + strerror(errno));
if (fstat(fd, &s) < 0)
abort();
size_t size = s.st_size;
char* buf = (char*)mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (buf == MAP_FAILED)
throw std::invalid_argument(std::string("Specified ELF can't be mapped: ") + strerror(errno));
close(fd);
assert(size >= sizeof(Elf64_Ehdr));
const Elf64_Ehdr* eh64 = (const Elf64_Ehdr*)buf;
assert(IS_ELF32(*eh64) || IS_ELF64(*eh64));
unsigned xlen = IS_ELF32(*eh64) ? 32 : 64;
if (required_xlen != 0 && required_xlen != xlen) {
throw incompat_xlen(required_xlen, xlen);
}
assert(IS_ELFLE(*eh64) || IS_ELFBE(*eh64));
assert(IS_ELF_EXEC(*eh64) || IS_ELF_DYN(*eh64));
assert(IS_ELF_RISCV(*eh64) || IS_ELF_EM_NONE(*eh64));
assert(IS_ELF_VCURRENT(*eh64));
if (IS_ELF_EXEC(*eh64)) {
load_offset = 0;
}
std::vector<uint8_t> zeros;
std::map<std::string, uint64_t> symbols;
#define LOAD_ELF(ehdr_t, phdr_t, shdr_t, sym_t, bswap) \
do { \
ehdr_t* eh = (ehdr_t*)buf; \
phdr_t* ph = (phdr_t*)(buf + bswap(eh->e_phoff)); \
*entry = bswap(eh->e_entry) + load_offset; \
assert(size >= bswap(eh->e_phoff) + bswap(eh->e_phnum) * sizeof(*ph)); \
for (unsigned i = 0; i < bswap(eh->e_phnum); i++) { \
if (bswap(ph[i].p_type) == PT_LOAD && bswap(ph[i].p_memsz)) { \
reg_t load_addr = bswap(ph[i].p_paddr) + load_offset; \
if (bswap(ph[i].p_filesz)) { \
assert(size >= bswap(ph[i].p_offset) + bswap(ph[i].p_filesz)); \
memif->write(load_addr, bswap(ph[i].p_filesz), \
(uint8_t*)buf + bswap(ph[i].p_offset)); \
} \
if (size_t pad = bswap(ph[i].p_memsz) - bswap(ph[i].p_filesz)) { \
zeros.resize(pad); \
memif->write(load_addr + bswap(ph[i].p_filesz), pad, zeros.data()); \
} \
} \
} \
shdr_t* sh = (shdr_t*)(buf + bswap(eh->e_shoff)); \
assert(size >= bswap(eh->e_shoff) + bswap(eh->e_shnum) * sizeof(*sh)); \
assert(bswap(eh->e_shstrndx) < bswap(eh->e_shnum)); \
assert(size >= bswap(sh[bswap(eh->e_shstrndx)].sh_offset) + \
bswap(sh[bswap(eh->e_shstrndx)].sh_size)); \
char* shstrtab = buf + bswap(sh[bswap(eh->e_shstrndx)].sh_offset); \
unsigned strtabidx = 0, symtabidx = 0; \
for (unsigned i = 0; i < bswap(eh->e_shnum); i++) { \
unsigned max_len = \
bswap(sh[bswap(eh->e_shstrndx)].sh_size) - bswap(sh[i].sh_name); \
assert(bswap(sh[i].sh_name) < bswap(sh[bswap(eh->e_shstrndx)].sh_size)); \
assert(strnlen(shstrtab + bswap(sh[i].sh_name), max_len) < max_len); \
if (bswap(sh[i].sh_type) & SHT_NOBITS) continue; \
assert(size >= bswap(sh[i].sh_offset) + bswap(sh[i].sh_size)); \
if (strcmp(shstrtab + bswap(sh[i].sh_name), ".strtab") == 0) \
strtabidx = i; \
if (strcmp(shstrtab + bswap(sh[i].sh_name), ".symtab") == 0) \
symtabidx = i; \
} \
if (strtabidx && symtabidx) { \
char* strtab = buf + bswap(sh[strtabidx].sh_offset); \
sym_t* sym = (sym_t*)(buf + bswap(sh[symtabidx].sh_offset)); \
for (unsigned i = 0; i < bswap(sh[symtabidx].sh_size) / sizeof(sym_t); \
i++) { \
unsigned max_len = \
bswap(sh[strtabidx].sh_size) - bswap(sym[i].st_name); \
assert(bswap(sym[i].st_name) < bswap(sh[strtabidx].sh_size)); \
assert(strnlen(strtab + bswap(sym[i].st_name), max_len) < max_len); \
symbols[strtab + bswap(sym[i].st_name)] = bswap(sym[i].st_value) + load_offset; \
} \
} \
} while (0)
if (IS_ELFLE(*eh64)) {
if (memif->get_target_endianness() != endianness_little) {
throw std::invalid_argument("Specified ELF is little endian, but system uses a big-endian memory system. Rerun without --big-endian");
}
if (IS_ELF32(*eh64))
LOAD_ELF(Elf32_Ehdr, Elf32_Phdr, Elf32_Shdr, Elf32_Sym, from_le);
else
LOAD_ELF(Elf64_Ehdr, Elf64_Phdr, Elf64_Shdr, Elf64_Sym, from_le);
} else {
#ifndef RISCV_ENABLE_DUAL_ENDIAN
throw std::invalid_argument("Specified ELF is big endian. Configure with --enable-dual-endian to enable support");
#else
if (memif->get_target_endianness() != endianness_big) {
throw std::invalid_argument("Specified ELF is big endian, but system uses a little-endian memory system. Rerun with --big-endian");
}
if (IS_ELF32(*eh64))
LOAD_ELF(Elf32_Ehdr, Elf32_Phdr, Elf32_Shdr, Elf32_Sym, from_be);
else
LOAD_ELF(Elf64_Ehdr, Elf64_Phdr, Elf64_Shdr, Elf64_Sym, from_be);
#endif
}
munmap(buf, size);
return symbols;
}
这个 load_elf
函数的主要功能是解析 ELF(Executable and Linkable Format)文件,并将其加载到目标内存接口 memif_t
中,同时提取 ELF 文件中的符号表。
该函数广泛用于 RISC-V 仿真器(如 Spike)和其他基于 FESVR(Front-End Server)的系统中,以支持程序加载和符号解析。
1. 函数定义
std::map<std::string, uint64_t> load_elf(const char* fn, memif_t* memif, reg_t* entry,
reg_t load_offset, unsigned required_xlen = 0)
参数解析
-
fn
:ELF 文件路径 -
memif
:目标内存接口,memif_t
对象用于模拟访存操作 -
entry
:ELF 的入口地址指针,存储加载的程序的入口地址 -
load_offset
:加载偏移量,决定 ELF 代码和数据在模拟内存中的最终地址 -
required_xlen
(可选):指定 ELF 文件的位数(32/64 位),用于检查是否与系统兼容返回值
-
std::map<std::string, uint64_t>
:返回 ELF 符号表,映射符号名称到其地址(用于调试和符号解析)
2. 打开 ELF 文件
int fd = open(fn, O_RDONLY);
if (fd == -1)
throw std::invalid_argument(std::string("Specified ELF can't be opened: ") + strerror(errno));
- 以只读方式打开 ELF 文件
- 如果失败,抛出
std::invalid_argument
异常,并返回错误信息(strerror(errno)
)
3. 获取 ELF 文件大小
struct stat s;
if (fstat(fd, &s) < 0)
abort();
size_t size = s.st_size;
- 使用
fstat
获取 ELF 文件大小,存入s.st_size
- 失败时直接
abort()
终止程序
4. 使用
mmap
映射 ELF 文件
char* buf = (char*)mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (buf == MAP_FAILED)
throw std::invalid_argument(std::string("Specified ELF can't be mapped: ") + strerror(errno));
close(fd);
mmap
将 ELF 文件映射到进程地址空间- 失败时抛出异常
- 关闭文件描述符
fd
(映射后fd
不再需要)
5. 解析 ELF 头
assert(size >= sizeof(Elf64_Ehdr));
const Elf64_Ehdr* eh64 = (const Elf64_Ehdr*)buf;
assert(IS_ELF32(*eh64) || IS_ELF64(*eh64));
- 确保 ELF 文件大小足够大
- 解析 ELF 头(
Elf64_Ehdr
结构体) - 确认是 32 位或 64 位 ELF(
IS_ELF32
和IS_ELF64
宏)
进一步检查
unsigned xlen = IS_ELF32(*eh64) ? 32 : 64;
if (required_xlen != 0 && required_xlen != xlen) {
throw incompat_xlen(required_xlen, xlen);
}
- 如果
required_xlen
设定(非 0),但 ELF 位数不匹配,则抛出异常
6. 解析 ELF 类型
assert(IS_ELFLE(*eh64) || IS_ELFBE(*eh64));
assert(IS_ELF_EXEC(*eh64) || IS_ELF_DYN(*eh64));
assert(IS_ELF_RISCV(*eh64) || IS_ELF_EM_NONE(*eh64));
assert(IS_ELF_VCURRENT(*eh64));
- 检查 ELF 文件的字节序(小端/大端)
- 检查 ELF 类型(可执行
EXEC
或动态库DYN
) - 检查 ELF 目标架构(RISC-V 或未指定)
- 检查 ELF 版本(
VCURRENT
)
7. 处理
load_offset
if (IS_ELF_EXEC(*eh64)) {
load_offset = 0;
}
- 如果 ELF 是可执行文件(EXEC),则
load_offset = 0
- 如果是动态库(DYN),可能需要动态调整
load_offset
8. 解析 ELF
ELF 加载主要依赖于LOAD_ELF
宏:
#define LOAD_ELF(ehdr_t, phdr_t, shdr_t, sym_t, bswap) ...
该宏负责:
- 读取 ELF 头
- 解析程序头表(Program Header Table)
- 将
PT_LOAD
段映射到内存 - 解析符号表
(1) 加载程序段
for (unsigned i = 0; i < bswap(eh->e_phnum); i++) {
if (bswap(ph[i].p_type) == PT_LOAD && bswap(ph[i].p_memsz)) {
reg_t load_addr = bswap(ph[i].p_paddr) + load_offset;
if (bswap(ph[i].p_filesz)) {
memif->write(load_addr, bswap(ph[i].p_filesz), (uint8_t*)buf + bswap(ph[i].p_offset));
}
if (size_t pad = bswap(ph[i].p_memsz) - bswap(ph[i].p_filesz)) {
zeros.resize(pad);
memif->write(load_addr + bswap(ph[i].p_filesz), pad, zeros.data());
}
}
}
- 遍历 程序头表
- 处理
PT_LOAD
段 - 将 ELF 段写入
memif
模拟的内存 - 填充未初始化的
.bss
段
(2) 解析符号表
if (strtabidx && symtabidx) {
char* strtab = buf + bswap(sh[strtabidx].sh_offset);
sym_t* sym = (sym_t*)(buf + bswap(sh[symtabidx].sh_offset));
for (unsigned i = 0; i < bswap(sh[symtabidx].sh_size) / sizeof(sym_t); i++) {
symbols[strtab + bswap(sym[i].st_name)] = bswap(sym[i].st_value) + load_offset;
}
}
- 找到
.symtab
和.strtab
- 提取符号信息
- 存入
symbols
映射表
9. 清理
munmap(buf, size);
return symbols;
- 解除
mmap
映射 - 返回解析出的符号表
总结
- 打开 ELF 文件并映射到内存
- 解析 ELF 头部信息
- 确定 ELF 类型、架构和字节序
- 解析并加载程序段到
memif_t
- 解析符号表并返回
- 清理资源
代码作用
这个 load_elf
函数广泛用于:
- Spike 仿真器加载 RISC-V ELF
- FESVR(Front-End Server)加载 Linux 用户态 ELF
- 调试工具解析 ELF 符号
1.3 elf2hex.cc
#include <iostream>
#include "htif_hexwriter.h"
#include "memif.h"
#include "elfloader.h"
int main(int argc, char** argv)
{
if(argc < 4 || argc > 5)
{
std::cerr << "Usage: " << argv[0] << " <width> <depth> <elf_file> [base]" << std::endl;
return 1;
}
unsigned width = atoi(argv[1]);
if(width == 0 || (width & (width-1)))
{
std::cerr << "width must be a power of 2" << std::endl;
return 1;
}
unsigned long long int base = 0;
if(argc==5) {
base = atoll(argv[4]);
if(base & (width-1))
{
std::cerr << "base must be divisible by width" << std::endl;
return 1;
}
}
unsigned depth = atoi(argv[2]);
if(depth == 0 || (depth & (depth-1)))
{
std::cerr << "depth must be a power of 2" << std::endl;
return 1;
}
htif_hexwriter_t htif(base, width, depth);
memif_t memif(&htif);
reg_t entry;
load_elf(argv[3], &memif, &entry, 0);
std::cout << htif;
return 0;
}
这段 C++ 代码的主要作用是加载 ELF 文件,并将其内容转换成 HEX 格式进行输出。代码涉及 ELF 解析 (load_elf
)、内存接口 (memif_t
)、以及 HEX 输出 (htif_hexwriter_t
)。
1. 解析命令行参数
if(argc < 4 || argc > 5)
{
std::cerr << "Usage: " << argv[0] << " <width> <depth> <elf_file> [base]" << std::endl;
return 1;
}
- 代码要求至少 3 个参数(
width
、depth
、elf_file
),可选的第 4 个参数是base
地址。 argc
过少或过多都会报错。
2. 解析
width
unsigned width = atoi(argv[1]);
if(width == 0 || (width & (width-1)))
{
std::cerr << "width must be a power of 2" << std::endl;
return 1;
}
width
由atoi
解析,要求是 2 的幂(如 8、16、32、64 等)。(width & (width-1))
用于检测width
是否是 2 的幂。
3. 解析
base
(可选参数)
unsigned long long int base = 0;
if(argc==5) {
base = atoll(argv[4]);
if(base & (width-1))
{
std::cerr << "base must be divisible by width" << std::endl;
return 1;
}
}
base
是 ELF 加载到的内存基址,默认为 0。- 如果提供了
base
,需要保证其是width
的整数倍,否则报错。
4. 解析
depth
unsigned depth = atoi(argv[2]);
if(depth == 0 || (depth & (depth-1)))
{
std::cerr << "depth must be a power of 2" << std::endl;
return 1;
}
depth
代表存储深度,也必须是 2 的幂。
初始化关键对象
htif_hexwriter_t htif(base, width, depth);
memif_t memif(&htif);
-
htif_hexwriter_t htif(base, width, depth);
htif_hexwriter_t
是一个用于 HEX 文件输出的类,表示一个带 HEX 输出能力的 HTIF(Host-Target Interface)。- 其参数:
base
:内存起始地址。width
:存储单元宽度(字节)。depth
:存储深度。
-
memif_t memif(&htif);
memif_t
是一个内存接口封装,用于与htif_hexwriter_t
进行交互,实现存储操作。
加载 ELF 文件
reg_t entry;
load_elf(argv[3], &memif, &entry, 0);
load_elf
解析 ELF 文件,并加载其内容到memif
代表的存储区域。entry
存储 ELF 文件的入口地址。
内部执行流程:
- 解析 ELF 文件头,判断 ELF 格式(32-bit/64-bit、字节序)。
- 遍历 ELF 程序头表,将 LOAD 段加载到
memif
。 - 遍历 ELF 符号表,提取符号信息(函数地址、变量地址等)。
- 返回符号表,同时
entry
变量获得 ELF 入口地址。
输出 HEX 文件
std::cout << htif;
htif_hexwriter_t
内部应该重载了operator<<
,支持直接输出 HEX 格式数据。- 这里
std::cout << htif;
的作用是将 ELF 加载的内容以 HEX 格式输出。
总结
1. 代码作用
这段代码的主要功能是:
- 解析 ELF 文件,并将其内容加载到
memif
代表的虚拟存储区域。 - 通过
htif_hexwriter_t
将内存中的 ELF 数据转换为 HEX 文件格式,并输出。
2. 关键流程
- 解析命令行参数:
width
(存储单元宽度)depth
(存储深度)elf_file
(ELF 文件路径)base
(可选的内存基址)
- 初始化 HEX 文件存储接口
htif_hexwriter_t
。 - 通过
memif_t
作为内存接口,调用load_elf
加载 ELF 数据。 - 解析 ELF 文件,将 LOAD 段写入
memif
。 - 提取 ELF 符号表,返回符号地址信息。
- 通过
std::cout
输出 HEX 格式的存储数据。
3. 适用场景
- 该工具适用于将 RISC-V ELF 可执行文件转换为 HEX 格式,用于 FPGA 加载或嵌入式调试。
- 适合用作 RISC-V 模拟器、FPGA 代码加载器的一部分。
下一篇:RISC-V ISA Simulator系列之fesvr<2>