深入解析 FESVR(Front-End Server)
url: https://github.com/riscv/riscv-isa-sim.git
commid: fcbdbe7946079650d0e656fa3d353e3f652d471f
目录
- FESVR 概述
- FESVR 代码结构分析
- ELF 加载机制
在RISC-V ISA Simulator系列之fesvr<1-7>中我们已经完成了
1. FESVR 概述
2. FESVR 代码结构分析
1. ELF 相关文件
2. HTIF(Host-Target Interface)
3. 设备模拟
4. 系统调用
5. 调试和仿真管理
6. FESVR 相关
7. 终端和交互
内容的,下面我们继续完成下列内容。
3. ELF 加载机制
3.1 ELF 加载的作用
当我们在ariane_tb.cpp 文章中的main函数中了解到ariane_testharness要运行一个 RISC-V 二进制程序(ELF 格式)时,fesvr
需要完成任务包含了以下内容:
- 解析 ELF 头,获取
.text
、.data
段等信息。 - 读取 ELF 文件内容,并写入模拟器的目标内存。
- 设置 RISC-V 目标程序的 入口地址(PC)。
3.2 ELF 加载流程
ELF 加载的代码主要在 elfloader.cc
中,关键函数为 load_elf()
:
void load_elf(const char* filename, memif_t* memif, reg_t* entry)
{
std::ifstream file(filename, std::ios::binary);
if (!file)
throw std::runtime_error("ELF file not found");
elf_header_t elf_header;
file.read((char*)&elf_header, sizeof(elf_header_t));
if (elf_header.magic != ELF_MAGIC)
throw std::runtime_error("Invalid ELF file");
*entry = elf_header.entry;
load_program_segments(file, memif, elf_header);
}
其中:
- 读取 ELF 头信息,检查 ELF 格式是否正确。
- 获取程序的 入口地址(entry point)。
- 解析段信息,并加载到目标内存。
3.3 关键数据结构
ELF 头的结构定义如下:
struct elf_header_t {
uint32_t magic;
uint8_t class;
uint8_t data;
uint8_t version;
uint8_t osabi;
uint64_t entry;
};
ELF 加载后,fesvr
通过 memif_t
将程序数据写入模拟器内存。
下面我们以ariane_tb.cpp 文章中的main函数中为例,来介绍readelf以及相关内容:
mian函数代码
int main(int argc, char **argv) {
std::clock_t c_start = std::clock();
auto t_start = std::chrono::high_resolution_clock::now();
bool verbose;
bool perf;
unsigned random_seed = (unsigned)time(NULL) ^ (unsigned)getpid();
uint64_t max_cycles = -1;
int ret = 0;
bool print_cycles = false;
// Port numbers are 16 bit unsigned integers.
uint16_t rbb_port = 0;
#if VM_TRACE
FILE * vcdfile = NULL;
char * fst_fname = NULL;
uint64_t start = 0;
#endif
char ** htif_argv = NULL;
int verilog_plusargs_legal = 1;
while (1) {
static struct option long_options[] = {
{"cycle-count", no_argument, 0, 'c' },
{"help", no_argument, 0, 'h' },
{"max-cycles", required_argument, 0, 'm' },
{"seed", required_argument, 0, 's' },
{"rbb-port", required_argument, 0, 'r' },
{"verbose", no_argument, 0, 'V' },
#if VM_TRACE
{"vcd", required_argument, 0, 'v' },
{"dump-start", required_argument, 0, 'x' },
{"fst", required_argument, 0, 'f' },
#endif
HTIF_LONG_OPTIONS
};
int option_index = 0;
#if VM_TRACE
int c = getopt_long(argc, argv, "-chpm:s:r:v:f:Vx:", long_options, &option_index);
#else
int c = getopt_long(argc, argv, "-chpm:s:r:V", long_options, &option_index);
#endif
if (c == -1) break;
retry:
switch (c) {
// Process long and short EMULATOR options
case '?': usage(argv[0]); return 1;
case 'c': print_cycles = true; break;
case 'h': usage(argv[0]); return 0;
case 'm': max_cycles = atoll(optarg); break;
case 's': random_seed = atoi(optarg); break;
case 'r': rbb_port = atoi(optarg); break;
case 'V': verbose = true; break;
case 'p': perf = true; break;
#if VM_TRACE
case 'v': {
vcdfile = strcmp(optarg, "-") == 0 ? stdout : fopen(optarg, "w");
if (!vcdfile) {
std::cerr << "Unable to open " << optarg << " for VCD write\n";
return 1;
}
break;
}
case 'f': {
fst_fname = optarg;
break;
}
case 'x': start = atoll(optarg); break;
#endif
// Process legacy '+' EMULATOR arguments by replacing them with
// their getopt equivalents
case 1: {
std::string arg = optarg;
if (arg.substr(0, 1) != "+") {
optind--;
goto done_processing;
}
if (arg == "+verbose")
c = 'V';
else if (arg.substr(0, 12) == "+max-cycles=") {
c = 'm';
optarg = optarg+12;
}
#if VM_TRACE
else if (arg.substr(0, 12) == "+dump-start=") {
c = 'x';
optarg = optarg+12;
}
#endif
else if (arg.substr(0, 12) == "+cycle-count")
c = 'c';
// If we don't find a legacy '+' EMULATOR argument, it still could be
// a VERILOG_PLUSARG and not an error.
else if (verilog_plusargs_legal) {
const char ** plusarg = &verilog_plusargs[0];
int legal_verilog_plusarg = 0;
while (*plusarg && (legal_verilog_plusarg == 0)){
if (arg.substr(1, strlen(*plusarg)) == *plusarg) {
legal_verilog_plusarg = 1;
}
plusarg ++;
}
if (!legal_verilog_plusarg) {
verilog_plusargs_legal = 0;
} else {
c = 'P';
}
goto retry;
}
// If we STILL don't find a legacy '+' argument, it still could be
// an HTIF (HOST) argument and not an error. If this is the case, then
// we're done processing EMULATOR and VERILOG arguments.
else {
static struct option htif_long_options [] = { HTIF_LONG_OPTIONS };
struct option * htif_option = &htif_long_options[0];
while (htif_option->name) {
if (arg.substr(1, strlen(htif_option->name)) == htif_option->name) {
optind--;
goto done_processing;
}
htif_option++;
}
std::cerr << argv[0] << ": invalid plus-arg (Verilog or HTIF) \""
<< arg << "\"\n";
c = '?';
}
goto retry;
}
case 'P': break; // Nothing to do here, Verilog PlusArg
// Realize that we've hit HTIF (HOST) arguments or error out
default:
if (c >= HTIF_LONG_OPTIONS_OPTIND) {
optind--;
goto done_processing;
}
c = '?';
goto retry;
}
}
done_processing:
if (optind == argc) {
std::cerr << "No binary specified for emulator\n";
usage(argv[0]);
return 1;
}
int htif_argc = 1 + argc - optind;
htif_argv = (char **) malloc((htif_argc) * sizeof (char *));
htif_argv[0] = argv[0];
for (int i = 1; optind < argc;) htif_argv[i++] = argv[optind++];
const char *vcd_file = NULL;
Verilated::commandArgs(argc, argv);
jtag = new remote_bitbang_t(rbb_port);
dtm = new preload_aware_dtm_t(htif_argc, htif_argv);
signal(SIGTERM, handle_sigterm);
std::unique_ptr<Variane_testharness> top(new Variane_testharness);
read_elf(htif_argv[1]);
#if VM_TRACE
Verilated::traceEverOn(true); // Verilator must compute traced signals
#if VM_TRACE_FST
std::unique_ptr<VerilatedFstC> tfp(new VerilatedFstC());
if (fst_fname) {
std::cerr << "Starting FST waveform dump into file '" << fst_fname << "'...\n";
top->trace(tfp.get(), 99); // Trace 99 levels of hierarchy
tfp->open(fst_fname);
}
else
std::cerr << "No explicit FST file name supplied, using RTL defaults.\n";
#else
std::unique_ptr<VerilatedVcdFILE> vcdfd(new VerilatedVcdFILE(vcdfile));
std::unique_ptr<VerilatedVcdC> tfp(new VerilatedVcdC(vcdfd.get()));
if (vcdfile) {
std::cerr << "Starting VCD waveform dump ...\n";
top->trace(tfp.get(), 99); // Trace 99 levels of hierarchy
tfp->open("");
}
else
std::cerr << "No explicit VCD file name supplied, using RTL defaults.\n";
#endif
#endif
for (int i = 0; i < 10; i++) {
top->rst_ni = 0;
top->clk_i = 0;
top->rtc_i = 0;
top->eval();
#if VM_TRACE
if (vcdfile || fst_fname)
tfp->dump(static_cast<vluint64_t>(main_time * 2));
#endif
top->clk_i = 1;
top->eval();
#if VM_TRACE
if (vcdfile || fst_fname)
tfp->dump(static_cast<vluint64_t>(main_time * 2 + 1));
#endif
main_time++;
}
top->rst_ni = 1;
// Preload memory.
#if (VERILATOR_VERSION_INTEGER >= 5000000)
// Verilator v5: Use rootp pointer and .data() accessor.
#define MEM top->rootp->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__i_tc_sram_wrapper__DOT__i_tc_sram__DOT__sram.m_storage
#define MEM_USER top->rootp->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__gen_mem_user__DOT__i_tc_sram_wrapper_user__DOT__i_tc_sram__DOT__sram.m_storage
#else
// Verilator v4
#define MEM top->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__i_tc_sram_wrapper__DOT__i_tc_sram__DOT__sram
#define MEM_USER top->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__gen_mem_user__DOT__i_tc_sram_wrapper_user__DOT__i_tc_sram__DOT__sram
#endif
long long addr;
long long len;
size_t mem_size = 0xFFFFFF;
while(get_section(&addr, &len))
{
if (addr == 0x80000000)
read_section_void(addr, (void *) MEM , mem_size);
if (addr == 0x84000000)
try {
read_section_void(addr, (void *) MEM_USER , mem_size);
} catch (...){
std::cerr << "No user memory instanciated ...\n";
}
}
while (!dtm->done() && !jtag->done() && !(top->exit_o & 0x1)) {
top->clk_i = 0;
top->eval();
#if VM_TRACE
if (vcdfile || fst_fname)
tfp->dump(static_cast<vluint64_t>(main_time * 2));
#endif
top->clk_i = 1;
top->eval();
#if VM_TRACE
if (vcdfile || fst_fname)
tfp->dump(static_cast<vluint64_t>(main_time * 2 + 1));
#endif
// toggle RTC
if (main_time % 2 == 0) {
top->rtc_i ^= 1;
}
main_time++;
}
#if VM_TRACE
if (tfp)
tfp->close();
if (vcdfile)
fclose(vcdfile);
#endif
if (dtm->exit_code()) {
fprintf(stderr, "%s *** FAILED *** (tohost = %d) after %ld cycles\n", htif_argv[1], dtm->exit_code(), main_time);
ret = dtm->exit_code();
} else if (jtag->exit_code()) {
fprintf(stderr, "%s *** FAILED *** (tohost = %d, seed %d) after %ld cycles\n", htif_argv[1], jtag->exit_code(), random_seed, main_time);
ret = jtag->exit_code();
} else if (top->exit_o & 0xFFFFFFFE) {
int exitcode = ((unsigned int) top->exit_o) >> 1;
fprintf(stderr, "%s *** FAILED *** (tohost = %d) after %ld cycles\n", htif_argv[1], exitcode, main_time);
ret = exitcode;
} else {
fprintf(stderr, "%s *** SUCCESS *** (tohost = 0) after %ld cycles\n", htif_argv[1], main_time);
}
if (dtm) delete dtm;
if (jtag) delete jtag;
std::clock_t c_end = std::clock();
auto t_end = std::chrono::high_resolution_clock::now();
if (perf) {
std::cout << std::fixed << std::setprecision(2) << "CPU time used: "
<< 1000.0 * (c_end-c_start) / CLOCKS_PER_SEC << " ms\n"
<< "Wall clock time passed: "
<< std::chrono::duration<double, std::milli>(t_end-t_start).count()
<< " ms\n";
}
return ret;
}
这里我们要重点分析下面部分代码:
std::unique_ptr<Variane_testharness> top(new Variane_testharness);
read_elf(htif_argv[1]);
// Preload memory.
#if (VERILATOR_VERSION_INTEGER >= 5000000)
// Verilator v5: Use rootp pointer and .data() accessor.
#define MEM top->rootp->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__i_tc_sram_wrapper__DOT__i_tc_sram__DOT__sram.m_storage
#define MEM_USER top->rootp->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__gen_mem_user__DOT__i_tc_sram_wrapper_user__DOT__i_tc_sram__DOT__sram.m_storage
#else
// Verilator v4
#define MEM top->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__i_tc_sram_wrapper__DOT__i_tc_sram__DOT__sram
#define MEM_USER top->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__gen_mem_user__DOT__i_tc_sram_wrapper_user__DOT__i_tc_sram__DOT__sram
#endif
long long addr;
long long len;
size_t mem_size = 0xFFFFFF;
while(get_section(&addr, &len))
{
if (addr == 0x80000000)
read_section_void(addr, (void *) MEM , mem_size);
}
while (!dtm->done() && !jtag->done() && !(top->exit_o & 0x1)) {
top->clk_i = 0;
top->eval();
top->clk_i = 1;
top->eval();
这段代码主要完成了几个关键步骤:创建测试平台对象、读取 ELF 文件、根据 Verilator 版本定义内存访问宏、预加载内存数据,最后在满足特定条件下进行时钟信号的模拟和模块评估。
1. 创建测试平台对象
std::unique_ptr<Variane_testharness> top(new Variane_testharness);
- 这行代码使用
std::unique_ptr
智能指针创建了一个Variane_testharness
类的对象top
。std::unique_ptr
确保了对象的所有权是唯一的,当top
超出作用域时,会自动释放其所指向的对象,避免内存泄漏。
2. 读取 ELF 文件
read_elf(htif_argv[1]);
- 调用
read_elf
函数,传入htif_argv[1]
作为参数,该函数的作用是读取指定的 ELF(Executable and Linkable Format)文件。ELF 文件通常包含可执行程序、目标文件或共享库等信息,这里是为后续的模拟或测试加载程序代码和数据。
在文章《core-v-verif系列之cva6 verilator Model之ariane_tb.cpp 函数read_elf》我们分析了readelf的功能,read_elf 函数的主要功能是读取 ELF(Executable and Linkable Format)文件,并解析其内容,将程序头和节表信息存储到全局变量中,同时提取符号表信息。
3. 根据 Verilator 版本定义内存访问宏
#if (VERILATOR_VERSION_INTEGER >= 5000000)
// Verilator v5: Use rootp pointer and .data() accessor.
#define MEM top->rootp->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__i_tc_sram_wrapper__DOT__i_tc_sram__DOT__sram.m_storage
#define MEM_USER top->rootp->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__gen_mem_user__DOT__i_tc_sram_wrapper_user__DOT__i_tc_sram__DOT__sram.m_storage
#else
// Verilator v4
#define MEM top->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__i_tc_sram_wrapper__DOT__i_tc_sram__DOT__sram
#define MEM_USER top->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__gen_mem_user__DOT__i_tc_sram_wrapper_user__DOT__i_tc_sram__DOT__sram
#endif
- 这是一个条件编译块,根据 Verilator 的版本(通过
VERILATOR_VERSION_INTEGER
宏判断)来定义不同的内存访问宏MEM
和MEM_USER
。Verilator 是一个用于 Verilog 硬件描述语言的仿真工具,不同版本可能对内部结构的访问方式有所不同。这里定义的宏用于方便后续对特定内存区域的访问。
4. 代码定义解释
#define MEM top->rootp->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__i_tc_sram_wrapper__DOT__i_tc_sram__DOT__sram.m_storage
这行代码使用了C++的预处理器指令 #define
来定义一个宏 MEM
。宏的作用是将 MEM
替换为 top->rootp->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__i_tc_sram_wrapper__DOT__i_tc_sram__DOT__sram.m_storage
。
top
:通常是Verilator生成的顶层模块实例的指针。在Verilator仿真中,top
指向整个硬件设计的顶层模块。rootp
:在Verilator v5及以后的版本中,引入了$root
包装器,rootp
是指向这个根对象的指针。ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__i_tc_sram_wrapper__DOT__i_tc_sram__DOT__sram
:这是Verilator生成的内部层次化命名,用于表示硬件设计中的特定模块和信号路径。__DOT__
是Verilator用来表示Verilog中的点号(.
),__BRA__
和__KET__
分别表示方括号([
和]
)。这里表示从顶层模块开始,通过一系列的子模块层次,最终定位到i_tc_sram
模块中的sram
信号。.m_storage
:表示sram
信号的存储成员,通常是一个数组或向量,用于存储SRAM的内容。
5. Verilator如何生成该路径
1. 解析Verilog代码
Verilator首先会解析用户提供的Verilog代码,构建一个内部的抽象语法树(AST)来表示硬件设计的结构。在这个过程中,Verilator会识别出所有的模块、信号和层次结构。
2. 生成C++代码
基于解析得到的AST,Verilator会生成对应的C++代码来模拟硬件行为。在生成过程中,Verilator会为每个模块和信号生成相应的C++类和成员变量。
3. 层次化命名
为了在C++代码中表示Verilog代码中的层次化结构,Verilator会使用特定的命名规则。例如,使用 __DOT__
来表示点号(.
),__BRA__
和 __KET__
来表示方括号([
和 ]
)。这样,Verilator就可以将Verilog中的层次化路径转换为C++代码中的成员访问路径。
4. rootp
指针
在Verilator v5及以后的版本中,引入了 $root
包装器和 rootp
指针,用于提供对整个硬件设计的根对象的访问。通过 rootp
指针,可以方便地访问顶层模块及其子模块的信号。
5. 示例
假设Verilog代码中有如下模块层次结构:
module ariane_testharness;
// ...
i_sram sram_inst;
// ...
endmodule
module i_sram;
// ...
gen_cut #(0) gen_cut_inst;
// ...
endmodule
module gen_cut #(parameter INDEX = 0);
// ...
i_tc_sram_wrapper wrapper_inst;
// ...
endmodule
module i_tc_sram_wrapper;
// ...
i_tc_sram sram_inst;
// ...
endmodule
module i_tc_sram;
reg [31:0] sram [0:1023];
// ...
endmodule
Verilator会将上述Verilog代码转换为C++代码,并生成类似 top->rootp->ariane_testharness__DOT__i_sram__DOT__gen_cut__BRA__0__KET____DOT__i_tc_sram_wrapper__DOT__i_tc_sram__DOT__sram
的路径来表示 i_tc_sram
模块中的 sram
信号。
总结
Verilator通过解析Verilog代码、生成C++代码和使用特定的命名规则,将Verilog中的层次化结构转换为C++代码中的成员访问路径。在Verilator v5及以后的版本中,使用 rootp
指针来提供对整个硬件设计的根对象的访问。
相关文章
core-v-verif系列之cva6 verilator Model之Variane_testharness.sv相关模块sram
core-v-verif系列之cva6 verilator Model之tc_sram.sv
4. 预加载内存数据
long long addr;
long long len;
size_t mem_size = 0xFFFFFF;
while(get_section(&addr, &len))
{
if (addr == 0x80000000)
read_section_void(addr, (void *) MEM , mem_size);
}
- 定义了两个
long long
类型的变量addr
和len
,用于存储内存地址和长度信息。 mem_size
被初始化为0xFFFFFF
,表示内存大小。- 使用
while
循环调用get_section
函数,该函数会不断返回内存区域的地址和长度信息。 - 当地址
addr
等于0x80000000
时,调用read_section_void
函数,将该地址对应的内存区域的数据读取到之前定义的MEM
所指向的内存中。
5. 时钟信号模拟和模块评估
while (!dtm->done() && !jtag->done() && !(top->exit_o & 0x1)) {
top->clk_i = 0;
top->eval();
top->clk_i = 1;
top->eval();
}
- 这是一个循环,循环条件是
dtm
对象的done
方法返回false
、jtag
对象的done
方法返回false
且top
对象的exit_o
信号的最低位为0
。dtm
和jtag
可能分别代表调试传输模块和 JTAG(Joint Test Action Group)接口模块,exit_o
可能是一个退出信号。 - 在循环内部,首先将时钟信号
clk_i
设置为0
,然后调用top
对象的eval
方法对模块进行评估,更新模块的状态。 - 接着将时钟信号
clk_i
设置为1
,再次调用eval
方法进行评估。这样就模拟了一个时钟周期的变化,不断驱动模块进行状态更新。
下一篇:RISC-V ISA Simulator系列之fesvr<9>