RISC-V ISA Simulator系列之fesvr<8>

深入解析 FESVR(Front-End Server)

url: https://github.com/riscv/riscv-isa-sim.git
commid: fcbdbe7946079650d0e656fa3d353e3f652d471f

代码链接

目录

  1. FESVR 概述
  2. FESVR 代码结构分析
  3. 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 需要完成任务包含了以下内容:

  1. 解析 ELF 头,获取 .text.data 段等信息。
  2. 读取 ELF 文件内容,并写入模拟器的目标内存。
  3. 设置 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 类的对象 topstd::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 宏判断)来定义不同的内存访问宏 MEMMEM_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 类型的变量 addrlen,用于存储内存地址和长度信息。
  • 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 方法返回 falsejtag 对象的 done 方法返回 falsetop 对象的 exit_o 信号的最低位为 0dtmjtag 可能分别代表调试传输模块和 JTAG(Joint Test Action Group)接口模块,exit_o 可能是一个退出信号。
  • 在循环内部,首先将时钟信号 clk_i 设置为 0,然后调用 top 对象的 eval 方法对模块进行评估,更新模块的状态。
  • 接着将时钟信号 clk_i 设置为 1,再次调用 eval 方法进行评估。这样就模拟了一个时钟周期的变化,不断驱动模块进行状态更新。

下一篇:RISC-V ISA Simulator系列之fesvr<9>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值