使用 C++23 从零实现 RISC-V 模拟器(4):完善 log 支持并支持更多指令

这篇具有很好参考价值的文章主要介绍了使用 C++23 从零实现 RISC-V 模拟器(4):完善 log 支持并支持更多指令。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

👉🏻 文章汇总「从零实现模拟器、操作系统、数据库、编译器…」:https://okaitserrj.feishu.cn/docx/R4tCdkEbsoFGnuxbho4cgW2Yntc

这一节内容解析了更多的指令,并且提供了更详细的 log 输出从而进一步的定位问题。

具体代码可以参考这个分支的代码:https://github.com/weijiew/crvemu/tree/lab4-load-store

1. 使用 C++17 的结构化绑定

接下来使用 C++17 的结构化绑定来消除重复的逻辑。

1.1 修改前

下面是修改前的代码,其中提取指令对应字段的逻辑是重复的。可以写成一个函数专门解析字段,统一管理,这样避免了变动的时候多次修改。

std::optional<uint64_t> executeAddi(Cpu& cpu, uint32_t inst) {
    uint32_t rd = (inst >> 7) & 0x1f;
    uint32_t rs1 = (inst >> 15) & 0x1f;
    int64_t immediate = static_cast<int32_t>(inst & 0xfff00000) >> 20;
    // ....
}

std::optional<uint64_t> executeSlli(Cpu& cpu, uint32_t inst) {
    uint32_t rd = (inst >> 7) & 0x1f;
    uint32_t rs1 = (inst >> 15) & 0x1f;
    int64_t immediate = static_cast<int32_t>(inst & 0xfff00000) >> 20;
    // ....
}

1.2 修改后

下面是修改后的代码,其中使用了 C++17 的结构化绑定的语法,如果不熟悉可以参考这篇文渣:C++17 结构化绑定 。

// 拆包解包
std::tuple<uint32_t, uint32_t, uint32_t> unpackInstruction(uint32_t inst) {
  return {
    (inst >> 7) & 0x1f,   // rd
    (inst >> 15) & 0x1f,  // rs1
    (inst >> 20) & 0x1f  // rs2
  };
}

std::optional<uint64_t> executeAddi(Cpu& cpu, uint32_t inst) {
  auto [rd, rs1, rs2] = unpackInstruction(inst);

  auto immediate = static_cast<int64_t>(static_cast<int32_t>(inst & 0xfff00000) >> 20);
  std::cout << "ADDI: x" << rd << " = x" << rs1 << " + " << immediate << std::endl;
  cpu.regs[rd] = cpu.regs[rs1] + immediate;
  return cpu.update_pc();
}

上面还用到了 tuple ,如果没有接触过可以参考这篇文章:C++11 tuple 。

这个 commit 列出了引入该特性的所有相关变动:feat: Add unpacking function for instruction parsing. 。

2. LOG 改造

这部分实现一个简单的 LOG 函数,将日志分级,并且不同级别的日志用不同颜色来打印,进一步完善输出,使得后续调试起来更高效。

此前是直接用 std::cout 输出,接下来用 LOG 来输出,具体的使用例子如何下。第一个参数为日志的级别,ERROR 用红色输出,INFO 用绿输出。

bool Dram::store(uint64_t addr, uint64_t size, uint64_t value) {
  if (size != 8 && size != 16 && size != 32 && size != 64) {
    LOG(ERROR, "Invalid size for store operation: ", size, " bytes.");
    return false;
  }

  uint64_t nbytes = size / 8;
  std::size_t index = (addr - DRAM_BASE);
  if (index + nbytes > dram.size()) {
    LOG(ERROR, "Invalid address range for store operation at DRAM address ", addr);
    return false;
  }

  for (uint64_t i = 0; i < nbytes; ++i) {
    dram[index + i] = (value >> (i * 8)) & 0xFF;
  }

  LOG(INFO, "DRAM store successful. Value: ", value, " at address ", addr, " with size ", size, " bytes.");
  return true;
}

接下来讲解如何实现 LOG 函数。

2.1 实现 LOG

简单来说下面的代码定义了一些变量来区分不同颜色,第一个参数为 LOG 的级别,分别为 “DEBUG”, “INFO”, “WARNING”, “ERROR” 并且对应不同的颜色。

ENABLE_DEBUG_PANIC 专门用来调试,出现 ERROR 的时候程序会终止,这个对调试帮助很大,否则需要从很多的日志中寻找 ERROR。

constexpr std::string_view RED = "\033[0;31m";
constexpr std::string_view GREEN = "\033[0;32m";
constexpr std::string_view YELLOW = "\033[0;33m";
constexpr std::string_view BLUE = "\033[0;34m";
constexpr std::string_view DIM = "\033[0;37m";
constexpr std::string_view NC = "\033[0m";  // No color

// Log 级别
enum LogLevel {
  DEBUG,
  INFO,
  WARNING,
  ERROR
};

// 接受任何可以通过 << 输出的类型
template <typename T>
concept Printable = requires(std::ostream& os, T s) {
  os << s;
};

// 是否启用 debug panic 输出
constexpr bool ENABLE_DEBUG_PANIC = true;

// 打印日志函数,接受可打印类型的参数(Printable... Args)
template <Printable... Args>
void print_log(std::ostream& os, LogLevel level, Args&&... s) {
  // 定义日志级别字符串数组
  const char* levelStrings[] = {"DEBUG", "INFO", "WARNING", "ERROR"};
  // 定义颜色字符串视图数组,对应不同的日志级别
  const std::string_view colors[] = {BLUE, GREEN, YELLOW, RED};

  // 输出带颜色的日志级别标识
  os << colors[level] << "[" << levelStrings[level] << "] ";
  // 使用折叠表达式将所有参数连接到输出流中
  (os << ... << s);
  // 输出颜色结束标识(假设NC被定义为颜色结束符号)
  os << NC << '\n';

  // 如果日志级别为 ERROR 并且启用了 debug panic 输出,则调用 abort
  if (level == ERROR && ENABLE_DEBUG_PANIC) {
    std::cerr << "Error occurred. Aborting program." << std::endl;
    std::abort();
  }
}

#define LOG(level, ...)                                             \
  do {                                                              \
    print_log(std::cout, level, "In function ", __FUNCTION__, " (", \
    __FILE__, ':', __LINE__, "): ", __VA_ARGS__);                   \
  } while (0)

这个 commit 列出了所有的相关改动:feat: add log.

3. 指令解析

需要解析的指令有点多就不全部列出来了,暂时提供下面几个吧。

3.1 解析 Lui

LUI(Load Upper Immediate)指令用于加载一个立即数的高位到目标寄存器中。让我们通过一个具体的RISC-V汇编代码例子来说明LUI指令的用法。

考虑以下RISC-V汇编代码片段:

lui x1, 0x12345

这条指令的含义是将立即数 0x12345 的高 20 位加载到寄存器 x1 中。现在,让我们详细解释一下:

  • lui: 这是LUI指令的助记符,表示Load Upper Immediate。

  • x1: 这是目标寄存器,即将要存储加载的高位数据的寄存器。在这个例子中,数据将被加载到寄存器 x1 中。

  • 0x12345: 这是立即数,即要加载的数值。在这个例子中,它是 0x12345。LUI指令加载的是这个数值的高 20 位。

因此,这条指令的效果是将 0x12345 的高 20 位加载到寄存器 x1 中,低 12 位填充为零。请注意,LUI指令不会修改目标寄存器的低 12 位。

这是一个简单的例子,但在实际的程序中,LUI指令通常用于构建大于 32 位的立即数,以进行后续的运算或数据访问。

使用场景

下面是 LUI 指令在 RISC-V 中有一些常见的应用场景,包括但不限于:

  1. 设置绝对地址: 在程序的初始化过程中,可以使用LUI指令将一个常数的高位加载到目标寄存器,从而设置某个全局变量或数据结构的绝对地址。
lui x1, 0x10000  # 将 0x10000 的高 20 位加载到 x1 寄存器,用于设置某个全局变量的地址
  1. 构建大立即数: LUI指令通常与后续的指令(例如ADDI)一起使用,以构建一个大于32位的立即数。这在一些需要处理大数据的计算中很有用。
lui x2, 0x12345  # 将 0x12345 的高 20 位加载到 x2 寄存器
addi x2, x2, 0x6789  # 将 0x6789 加到 x2 寄存器的低 12 位
  1. 数据访问: 在某些情况下,程序可能需要使用LUI指令加载某个数据结构的地址的高位,然后使用其他指令完成对该数据结构的访问。
lui x3, 0xA0000  # 将数据结构的地址的高 20 位加载到 x3 寄存器
lw x4, 0(x3)  # 通过 x3 寄存器访问数据结构

总的来说,LUI指令用于初始化全局变量、构建大立即数以及设置绝对地址等场景。在程序的早期阶段,它通常用于初始化阶段,为后续的指令提供合适的立即数或地址。

指令的二进制组成

LUI指令的二进制表示有固定的格式,具体结构如下:

imm[31:12] | rd | opcode

其中:

  • imm[31:12] 表示立即数的高 20 位,即 immediate 部分。
  • rd 表示目标寄存器,即将加载的值存放到的寄存器。
  • opcode 表示操作码,用于指定这是一条LUI指令。

让我们具体来看一个例子,假设我们有一条LUI指令,要将立即数 0x12345 的高 20 位加载到寄存器 x1 中:

  1. 将立即数转换成二进制:

    • 0x12345 的二进制表示是 0001 0010 0011 0100 0101
    • 取高 20 位,即 0001 0010 0011 0100 0101
  2. 寄存器选择:

    • 假设我们要加载到 x1 寄存器,其寄存器编号是 00001
  3. 操作码选择:

    • LUI指令的操作码是 0110111

将这些部分组合起来,我们得到这条LUI指令的二进制表示:

0001 0010 0011 0100 0101 | 00001 | 0110111

这就是将 0x12345 的高 20 位加载到 x1 寄存器的LUI指令的二进制表示。这个二进制序列可以被计算机硬件识别和执行。

代码及测试

下面是 LUI 具体的解析函数及其对应的单元测试。

std::optional<uint64_t> executeLui(Cpu& cpu, uint32_t inst) {
  auto [rd, rs1, rs2] = unpackInstruction(inst);
  auto immediate = static_cast<uint64_t>(inst & 0xfffff000);  // Extract the upper 20 bits
  LOG(INFO, "LUI: x", rd , " = ", immediate);
  cpu.regs[rd] = immediate;
  return cpu.update_pc();
}

TEST(RVTests, TestLui) {
  std::string code = start +
      "lui a0, 42 \n";      // Load 42 into a0
  Cpu cpu = rv_helper(code, "test_lui", 1);

  // Verify if x1 has the correct value
  EXPECT_EQ(cpu.regs[10], 42 << 12) << "Error: a0 should be the result of LUI instruction";
}

3.2 解析 AUIPC

AUIPC(Add Upper Immediate to PC)指令用于将一个立即数的高位与当前PC(程序计数器)值相加,并将结果存储到目标寄存器中。让我们通过一个具体的 RISC-V 汇编代码例子来说明 AUIPC 指令的用法。

考虑以下 RISC-V 汇编代码片段:

auipc x1, 0x12345

这条指令的含义是将立即数 0x12345 的高 20 位与当前 PC 值相加,并将结果存储到寄存器 x1 中。现在,让我们详细解释一下:

  • auipc: 这是 AUIPC 指令的助记符,表示 Add Upper Immediate to PC。

  • x1: 这是目标寄存器,即将要存储加载的高位数据与 PC 相加的结果的寄存器。在这个例子中,数据将被加载到寄存器 x1 中。

  • 0x12345: 这是立即数,即要加载的数值。在这个例子中,它是 0x12345。AUIPC 指令加载的是这个数值的高 20 位。

  • PC(程序计数器):AUIPC 指令会将当前 PC 的值作为基础,与立即数的高 20 位相加。这是 AUIPC 指令与 LUI 指令的主要区别。

因此,这条指令的效果是将 0x12345 的高 20 位与当前 PC 值相加,然后将结果存储到寄存器 x1 中,低 12 位填充为零。请注意,AUIPC 指令不会修改目标寄存器的低 12 位。

这是一个简单的例子,但在实际的程序中,AUIPC 指令通常用于构建全局变量的地址或进行跳转。

使用场景

下面是 AUIPC 指令在 RISC-V 中有一些常见的应用场景,包括但不限于:

  1. 构建全局变量地址: 在程序中,可以使用 AUIPC 指令将一个常数的高位与当前 PC 相加,从而构建全局变量的地址。
auipc x1, 0x10000  # 将 0x10000 的高 20 位与当前 PC 相加,用于构建全局变量的地址
  1. 跳转目标的构建: 在程序中,AUIPC 指令也常用于构建跳转指令的目标地址。
auipc x2, 0x20000  # 将 0x20000 的高 20 位与当前 PC 相加,用于构建跳转指令的目标地址
jalr x3, x2, 0    # 使用 jalr 指令跳转到 x2 寄存器指定的地址
  1. 构建大立即数: AUIPC 指令通常与后续的指令(例如 ADDI)一起使用,以构建一个大于 32 位的立即数。
auipc x4, 0x12345  # 将 0x12345 的高 20 位与当前 PC 相加,用于构建大立即数的高位
addi x4, x4, 0x6789  # 将 0x6789 加到 x4 寄存器的低 12 位

总的来说,AUIPC 指令用于构建全局变量地址、跳转目标地址以及构建大立即数等场景。在程序中,它通常用于初始化阶段,为后续的指令提供合适的地址或立即数。

指令的二进制组成

AUIPC 指令的二进制表示有固定的格式,具体结构如下:

imm[31:12] | rd | opcode

其中:

  • imm[31:12] 表示立即数的高 20 位,即 immediate 部分。
  • rd 表示目标寄存器,即将加载的值存放到的寄存器。
  • opcode 表示操作码,用于指定这是一条 AUIPC 指令。

让我们具体来看一个例子,假设我们有一条 AUIPC 指令,要将立即数 0x12345 的高 20 位与当前 PC 相加,并将结果加载到寄存器 x1 中:

  1. 将立即数转换成二进制:

    • 0x12345 的二进制表示是 0001 0010 0011 0100 0101
    • 取高 20 位,即 0001 0010 0011 0100 0101
  2. 寄存器选择:

    • 假设我们要加载到 x1 寄存器,其寄存器编号是 00001
  3. 操作码选择:

    • AUIPC 指令的操作码是 0010111

将这些部分组合起来,我们得到这条 AUIPC 指令的二进制表示:

0001 0010 0011 0100 0101 | 00001 | 0010111

这就是将 0x12345 的高 20 位与当前 PC 相加,并将结果加载到 x1 寄存器的 AUIPC 指令的二进制表示。这个二进制序列可以被计算机硬件识别和执行。

代码及测试

下面是 AUIPC 具体的解析函数及其对应的单元测试。

std::optional<uint64_t> executeAUIPC(Cpu& cpu, uint32_t inst) {
  auto [rd, rs1, rs2] = unpackInstruction(inst);
  auto imm = static_cast<int64_t>(static_cast<int32_t>(inst & 0xfffff000));

  LOG(INFO, "AUIPC: x", rd, " = pc + ", imm);
  cpu.regs[rd] = cpu.pc + imm;
  return cpu.update_pc();
}

TEST(RVTests, TestAUIPC) {
  std::string code = start +
      "auipc a0, 42 \n";      // Load 15 into x2
  Cpu cpu = rv_helper(code, "test_auipc", 1);

  EXPECT_EQ(cpu.regs[10], DRAM_BASE + (42 << 12)) << "Error: a0 should be the result of AUIPC instruction";
}

在这个测试中添加了一个名为 executeAuipc 的函数,该函数负责解析并执行 AUIPC 指令。同时编写了一个名为 TestAuipc 的单元测试,用于验证 AUIPC 指令的正确性。

3.3 解析 JAL

JAL(Jump and Link)指令用于实现无条件跳转到目标地址,并将跳转前的地址保存到寄存器中,通常用于实现函数调用和子程序的跳转。让我们通过一个具体的 RISC-V 汇编代码例子来说明 JAL 指令的用法。

考虑以下 RISC-V 汇编代码片段:

jal x1, target_label

这条指令的含义是无条件跳转到 target_label 处,并将跳转前的地址保存到寄存器 x1 中。现在,让我们详细解释一下:

  • jal: 这是 JAL 指令的助记符,表示 Jump and Link。

  • x1: 这是目标寄存器,即将要存储跳转前地址的寄存器。在这个例子中,地址将被保存到寄存器 x1 中。

  • target_label: 这是跳转的目标地址,通常是代码中的标签或具体的地址。

因此,这条指令的效果是无条件跳转到 target_label 处,并将跳转前的地址保存到寄存器 x1 中。

使用场景

JAL 指令在 RISC-V 中有一些常见的应用场景,包括但不限于:

  1. 函数调用: 在程序执行中,JAL 指令通常用于实现函数的跳转和调用。通过 JAL 指令,程序可以跳转到函数的入口地址,并在跳转前将返回地址保存到寄存器中。
jal ra, my_function  # 跳转到 my_function 函数,并将返回地址保存到 ra 寄存器中
  1. 异常处理: 在一些情况下,当程序发生异常或中断时,JAL 指令可以用于跳转到相应的异常处理程序,并保存当前执行位置。
jal a0, exception_handler  # 跳转到异常处理程序,并将返回地址保存到 a0 寄存器中
  1. 程序跳转: JAL 也可以用于实现程序中的跳转,例如在某个条件下跳转到指定的标签或地址。
jal x2, target_label  # 无条件跳转到 target_label,并将跳转前的地址保存到 x2 寄存器中

总体来说,JAL 指令常用于实现程序的跳转和函数调用,通过保存返回地址,使得程序能够在需要时返回到跳转前的位置。

指令的二进制组成

JAL 指令的二进制表示有固定的格式,具体结构如下:

imm[20] | imm[10:1] | imm[11] | imm[19:12] | rd | opcode

其中:

  • imm[20] 表示立即数的最高位,即 immediate 部分的符号位。
  • imm[10:1] 表示立即数的中间部分,即 immediate 的位 10 到位 1。
  • imm[11] 表示立即数的第 11 位。
  • imm[19:12] 表示立即数的最低部分,即 immediate 的位 19 到位 12。
  • rd 表示目标寄存器,即将跳转前地址保存到的寄存器。
  • opcode 表示操作码,用于指定这是一条 JAL 指令。

让我们具体来看一个例子,假设我们有一条 JAL 指令,要无条件跳转到 target_label 处,并将跳转前的地址保存到寄存器 x1 中:

  1. 计算跳转偏移:

    • 计算 target_label 相对于当前指令的偏移量。
    • 假设 target_label 的地址是 0x40001234,当前指令的地址是 0x40001000。
    • 则偏移量为 0x1234 - 0x1000 = 0x234.
  2. 将偏移量转换成二进制:

    • 0x234 的二进制表示是 0010 0011 0100.
  3. 符号扩展:

    • 将二进制偏移量符号扩展为 21 位,即 0000 0000 0010 0011 0100.
  4. 寄存器选择:

    • 假设我们要将返回地址保存到 x1 寄存器,其寄存器编号是 00001
  5. 操作码选择:

    • JAL 指令的操作码是 1101111

将这些部分组合起来,我们得到这条 JAL 指令的二进制表示:

0 | 0000 0000 0010 0011 0100 | 1 | 00001 | 1101111

这就是无条件跳转到 target_label 处,并将跳转前的地址保存到 x1 寄存器中的 JAL 指令的二进制表示。这个二进制序列可以被计算机硬件识别和执行。

代码及测试

下面是 JAL 具体的解析函数及其对应的单元测试。

std::optional<uint64_t> executeJAL(Cpu& cpu, uint32_t inst) {
  auto [rd, rs1, rs2] = unpackInstruction(inst);
  auto imm = ((inst & 0x80000000) ? 0xfff00000 : 0) |
             ((inst >> 20) & 0x7fe) |
             ((inst >> 9) & 0x800) |
             ((inst >> 12) & 0xff);

  LOG(INFO, "JAL: x", rd, " = pc + 4; pc = pc + ", imm);
  cpu.regs[rd] = cpu.pc + 4;
  return cpu.pc + imm;
}

TEST(RVTests, TestJAL) {
      std::string code = start +
        "jal a0, 42\n";
      Cpu cpu = rv_helper(code, "test_jal", 1);
      EXPECT_EQ(cpu.regs[10], DRAM_BASE + 4)
          << "Error: a0 should be the result of JAL instruction";
      EXPECT_EQ(cpu.pc, DRAM_BASE + 42)
          << "Error: pc should be the target address after JAL instruction";
}

3.4 解析 JALR

JALR(Jump and Link Register)指令用于实现通过寄存器间接跳转,并将跳转前的地址保存到目标寄存器中,通常用于实现函数调用和子程序的跳转。让我们通过一个具体的 RISC-V 汇编代码例子来说明 JALR 指令的用法。

考虑以下 RISC-V 汇编代码片段:

jalr x1, x2, 0x100

这条指令的含义是通过寄存器 x2 中的地址加上立即数 0x100 进行跳转,并将跳转前的地址保存到寄存器 x1 中。现在,让我们详细解释一下:

  • jalr: 这是 JALR 指令的助记符,表示 Jump and Link Register。

  • x1: 这是目标寄存器,即将要存储跳转前地址的寄存器。在这个例子中,地址将被保存到寄存器 x1 中。

  • x2: 这是用于间接跳转的寄存器,其内容是跳转目标地址。

  • 0x100: 这是立即数偏移,表示相对于寄存器 x2 中的地址的偏移。

因此,这条指令的效果是通过寄存器间接跳转到 x2 + 0x100 处,并将跳转前的地址保存到寄存器 x1 中。

使用场景

JALR 指令在 RISC-V 中有一些常见的应用场景,包括但不限于:

  1. 函数调用: 在程序执行中,JALR 指令通常用于实现函数的间接跳转和调用。通过 JALR 指令,程序可以跳转到由寄存器指定的函数入口地址,并在跳转前将返回地址保存到另一个寄存器中。
jalr ra, x3, 0x0  # 通过 x3 寄存器中的地址间接跳转,并将返回地址保存到 ra 寄存器中
  1. 异常处理: 在一些情况下,当程序发生异常或中断时,JALR 指令可以用于间接跳转到相应的异常处理程序,并保存当前执行位置。
jalr a0, x4, 0x200  # 通过 x4 寄存器中的地址间接跳转,并将返回地址保存到 a0 寄存器中
  1. 程序跳转: JALR 也可以用于实现程序中的间接跳转,例如在某个条件下跳转到由寄存器指定的地址。
jalr x5, x6, -8  # 通过 x6 寄存器中的地址间接跳转,并将返回地址保存到 x5 寄存器中,同时减去偏移 8

总体来说,JALR 指令常用于实现程序的间接跳转和函数调用,通过保存返回地址,使得程序能够在需要时返回到跳转前的位置。

指令的二进制组成

JALR 指令的二进制表示有固定的格式,具体结构如下:

imm[11:0] | rd | funct3 | rs1 | opcode

其中:

  • imm[11:0] 表示立即数的偏移,即 immediate 部分。
  • rd 表示目标寄存器,即将跳转前地址保存到的寄存器。
  • funct3 表示功能码,用于指定具体的 JALR 操作。
  • rs1 表示用于间接跳转的寄存器。
  • opcode 表示操作码,用于指定这是一条 JALR 指令。

让我们具体来看一个例子,假设我们有一条 JALR 指令,要通过寄存器 x2 中的地址加上立即数 0x100 进行跳转,并将跳转前的地址保存到寄存器 x1 中:

  1. 将立即数转换成二进制:

    • 0x100 的二进制表示是 0001 0000 0000.
  2. 寄存器选择:

    • 假设我们要将返回地址保存到 x1 寄存器,其寄存器编号是 00001
    • 用于间接跳转的寄存器是 x2,其寄存器编号是 00010
  3. 功能码选择:

    • 假设 JALR 的功能码是 110
  4. 操作码选择:

    • JALR 指令的操作码是 1100111

将这些部分组合起来,我们得到这条 JALR 指令的二进制表示:

0001 0000 0000 | 00001 | 110 | 00010 | 1100111

这就是通过寄存器 x2 中的地址加上立即数 0x100 进行跳转,并将跳转前的地址保存到寄存器 x1 中的 JALR 指令的二进制表示。这个二进制序列可以被计算机硬件识别和执行。

代码及测试

下面是 JALR 具体的解析函数及其对应的单元测试。文章来源地址https://www.toymoban.com/news/detail-828369.html

std::optional<uint64_t> executeJALR(Cpu& cpu, uint32_t inst) {
  auto [rd, rs1, rs2] = unpackInstruction(inst);
  auto imm = static_cast<int64_t>(static_cast<int32_t>(inst & 0xfff00000) >> 20);

  uint64_t t = cpu.pc + 4;
  uint64_t new_pc = (cpu.regs[rs1] + imm) & ~1;

  LOG(INFO, "JALR: x", rd, " = pc + 4; pc = (x", rs1, " + ", imm, ") & ~1");
  cpu.regs[rd] = t;
  return new_pc;
}

TEST(RVTests, TestJALR) {
      std::string code = start +
        "addi a1, zero, 42\n"
        "jalr a0, -8(a1)\n";
      Cpu cpu = rv_helper(code, "test_jalr", 2);

      EXPECT_EQ(cpu.regs[10], DRAM_BASE + 8)
          << "Error: a0 should be the result of JALR instruction";
      EXPECT_EQ(cpu.pc, 34)
          << "Error: pc should be 34 after JALR instruction";
}

到了这里,关于使用 C++23 从零实现 RISC-V 模拟器(4):完善 log 支持并支持更多指令的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 华为ensp模拟器如何使用VMware虚拟机桥接虚拟网卡实现上网(保姆级教程)

    华为ensp模拟器如何使用VMware虚拟机桥接虚拟网卡实现上网(保姆级教程)

    我对ensp的小云朵真是又爱又恨,爱是因为它可以桥接虚拟网卡,对实验的验证性得到了进一步的升华,恨是因为不会设置。 现在有很多学校的奇葩老师都要求nat要ping通百度,然后告诉学生用云彩可以实现,但又有几个老师告诉该如何设置呢? 我在不会前也是一直在找资料,

    2024年02月06日
    浏览(7)
  • Android:【1】一文教你使用Camille+夜神模拟器实现安卓应用隐私合规辅助检测

    Android:【1】一文教你使用Camille+夜神模拟器实现安卓应用隐私合规辅助检测

    碎碎念: 不好意思最近课业上比较繁忙,一直忙于一些测试,没有按时来填坑。今天带来的是基于课程项目要求,需要完成的安卓应用行为检测Demo。 因为环境的配置花费了一些时间,做一个简单的记录,方便后续进行优化。 目录 1 工具的介绍和准备 1.1 Camille 1.2 夜神模拟器

    2024年02月01日
    浏览(23)
  • RISC-V单板计算机模拟和FPGA板多核IP实现

    🎯使用单板计算机 Visionfive 2 或模拟器测试RISC-V汇编 🎯RISC-V汇编加载和算术。🎯使用GNU MAKE汇编RISC-V指令,ESP32使用CMake编译执行指令。🎯RISC-V汇编功能和使用释义:控制指令,内存管理,功能块和堆栈。 🎯 使用RISC-V汇编代码控制GPIO。🎯RISC-V汇编指令嵌入C 代码,C代码调

    2024年04月12日
    浏览(11)
  • mac使用Simulator打开IOS模拟器(不使用Xcode打开IOS模拟器)

    mac使用Simulator打开IOS模拟器(不使用Xcode打开IOS模拟器)

    提示:打开模拟器的前提必须是安装了Xcode。 在mac电脑上有Xcode可以用来模拟不同类型的手机进行来解决ios兼容问题,这里记录一下不使用Xcode打开ios模拟器的方法 快捷键:command+空格。搜索simulator,第一个就是模拟器 使用以下命令打开模拟器 结果如下图

    2024年02月15日
    浏览(15)
  • 使用Android自带模拟器模拟车载多屏显示

    使用Android自带模拟器模拟车载多屏显示

    由于车机设备资源不够,可采用 Android Studio 自带模拟器来模拟车机多屏显示,如下图所示模拟一个Activity在不同屏幕显示 更新Android Studio: 配置AVD参数 Android SDK:12 开启第二块屏幕 按如下截图4个步骤,设置Second Display的分辨率和屏幕密度 查看当前屏幕ID信息,用下面adb命令

    2024年02月16日
    浏览(43)
  • 通过模拟器实现APP抓包

    通过模拟器实现APP抓包

    本教程将跳过工具安装部分,请正确食用 😉 我的环境: 操作系统:win11 模拟器版本:雷电安卓模拟器稳定版-安卓7.1(32位) V5.0.46 抓包工具:Wireshark-Version 4.0.1 抓包工具:charles-Version 4.6.3 工具安装地址: 雷电安卓模拟器👈点击跳转官网地址下载 Wireshark👈点击跳转官网地址

    2024年02月02日
    浏览(10)
  • 人生重开模拟器(Python实现)

    人生重开模拟器(Python实现)

    人生重开模拟器是由VickScarlet上传至GitHub的一款简单的文字网页游戏。 玩家点击“立即重开”并设置角色的初始属性后,程序就会随机为玩家生成对应的人生经历。 下面我们实现一个简化版的人生重开模拟器,主要目的在于熟悉Python的语法。 初始界面可以自由发挥,这里博

    2024年02月02日
    浏览(8)
  • C#实现键盘鼠标模拟器

    C#实现键盘鼠标模拟器

    下面程序可指定一连串重复动作,按顺序执行   using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Text.RegularExpressions; using System.Windows.Forms; namespace Simulator {     public partial class Form1 : Form     {         [System.Runtime.InteropServices.DllImp

    2024年02月16日
    浏览(15)
  • Android 使用模拟器模拟Linux操作系统

    Android 使用模拟器模拟Linux操作系统

    在Android手机上使用模拟器模拟ubuntu等操作系统,便于测试 Termux:是一款 Android 终端模拟器和 Linux 环境应用程序,无需 root 或设置即可直接运行。虽然酷安和谷歌菜市场都能下载,但这些渠道都很久没更新了,建议到 F - Droid 下载。 下载链接: https://f-droid.org/packages/com.termu

    2024年02月07日
    浏览(12)
  • viple模拟器使用(四):unity模拟器中实现两距离局部最优迷宫算法

    viple模拟器使用(四):unity模拟器中实现两距离局部最优迷宫算法

    两距离:指的是左侧距离和右侧距离 局部最优: 对当前状态来说最好的选择 ,至于整体能不能达到最优,是无法确定的。 从节点1到节点5,一共有3条路 第1条路线:1→2→4→5,对应的花销是:2+3+4=9; 第2条路线:1→3→4→5,对应的花销是:1+5+4=10; 第3条路线:1→3→5,对

    2024年02月05日
    浏览(23)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包