开发 gem5 模型

在 gem5 中建模核心


什么是 ISA?

指令集架构(ISA)是软件和硬件之间的接口。

ISA 定义了:


40% bg


gem5 可以模拟的 ISA

实际上,你可能只会使用 ARM、RISC-V 和 x86。

其余的虽然可用,但测试或维护不够完善。


gem5 的 ISA-CPU 独立性

与真实硬件中 CPU 与其设计运行的 ISA 紧密耦合不同,gem5 通过解耦两者来简化问题。 这样做使得 gem5 的 CPU 模型变得与 ISA 无关(或者 ISA 变得与 CPU 模型无关)。

虽然这种独立性有限制,但目标是允许轻松添加和扩展新的 ISA 和 CPU 模型,而无需处理大量的代码更改和重写。 作为高级总结,这种独立性是通过为每个 ISA 设置一个独立的”解码器”来实现的,该解码器将指令转换为描述其行为的对象。

注意:我将在这里广泛使用”解码器”一词来描述解析指令的位和字节以确定其行为以及它应该如何与 CPU 模型交互的过程。在 gem5 中,这是 ISA 定义的一部分,用于”插入”到 CPU 模型中。

它的功能或职责与真实 CPU 中的解码器不同。


ISA-CPU 独立性示意图

45% bg


重要部分:StaticInst

这个复杂设计的重要要点是,无论解码器是为哪个 ISA 创建的,它都会将 CPU 接收到的指令解析为 StaticInst 对象。

StaticInst 是一个包含特定 ISA 指令的静态信息的对象,适用于该指令的所有实例。

它包含以下信息:


DynamicInst

DynamicInst 对象包含特定指令实例的信息。 它是从 StaticInst 对象中的信息构造的。

它包含以下信息:


ExecContext

ExecContext 接口提供了指令以标准化方式与 CPU 模型交互的方法。

DynamicInst 对象实现了 ExecContext 接口。


gem5 中指令的执行过程

在这个例子中,我们将使用 GDB 来跟踪 gem5 中指令的执行。

首先,我们将通过 GDB 运行 materials/03-Developing-gem5-models/05-modeling-cores/01-inst-trace.py 中的脚本。

使用 GDB,我们将在 Add::Add 函数和 Add::execute 函数上添加断点。

首先,使用 gdb 运行 gem5:

gdb gem5

然后,在表示 Add 指令的 StaticInst 对象的函数上添加断点。


gem5 中指令的执行过程

Add::Add 函数上添加断点。 这只是 Add 类的构造函数。它创建表示 Add 指令的 StaticInst 对象。

(gdb) break Add::Add

然后在 Add::execute 函数上添加断点。 这是用于执行 Add 指令的函数。

(gdb) break Add::execute

开始执行 gem5:

(gdb) run 01-inst-trace.py

RISC-V Add::Add 回溯

你应该已经到达 Add::Add 函数中的第一个断点:

Breakpoint 1, 0x0000555555a3b1b0 in gem5::RiscvISAInst::Add::Add(unsigned int) ()

接下来我们将进行回溯。回溯显示到达当前函数所调用的函数。 让我们看看最后 10 个被调用的函数:

(gdb) bt 10

输出:

0 {PC} in gem5:: RiscvISAInst::Add: :Add(unsigned int) ()
1 {PC} in gem5:: RiscvISA:: Decoder: :decodeInst(unsigned long) ()
2 {PC} in gems:: RiscvISA: :Decoder: : decode(unsigned long, unsigned long)
3 {PC} in gem5:: RiscvISA: :Decoder:: decode (gem5:: PCStateBase&) ()
4 {PC} in gem5:: BaseSimpleCPU:: preExecute ()
5 {PC} in gem5:: TimingSimpleCPU:: IcachePort:: ITickEvent:: process () ()
6 {PC} in gem5:: EventQueue:: serviceone() ()
7 {PC] in gem5: :doSimLoop (gem5:: EventQueue*) ()
8 {PC} in gem5:: simulate(unsigned long) ()
9 {PC} in pybind11::pp_function:: initialize<gem5: :GlobalSimLoopExitEve ...

这里第 0 个函数调用是 Add::Add 函数。 每个后续索引是调用前一个函数的函数(即,第 1 个函数调用了第 0 个函数,第 2 个函数调用了第 1 个函数,等等)


第 5 个函数是 TimingSimpleCPU 模型的 process 函数,用于处理指令的函数。 索引 > 6 的函数是 gem5 的内部函数,在指令执行之前调用,我们在这里不需要关心。

4 {PC} in gem5:: BaseSimpleCPU:: preExecute ()

preExecute 是在 CPU 模型中执行指令之前调用的函数。它用于执行任何必要的设置。

你可以访问 gem5 仓库中的 “src/cpu/simple/base.cc” 来查看 BaseSimpleCPUpreExecute 函数。


回溯中的下一个函数是 RISC-V ISA 的解码器。

3 {PC} in gem5:: RiscvISA: :Decoder:: decode (gem5:: PCStateBase&) ()

这个函数是从 BaseSimpleCPUpreExecute 函数中的以下行调用的:

        //Decode an instruction if one is ready. Otherwise, we'll have to
        //fetch beyond the MachInst at the current pc.
        instPtr = decoder->decode(pc_state);

你可以跟踪这个调用到 Decoder:: decode,它可以在 gem5 仓库的 src/arch/riscv/decoder.cc 中找到。


StaticInstPtr
Decoder::decode(PCStateBase &_next_pc)
{
    if (!instDone)
        return nullptr;
    instDone = false;

    auto &next_pc = _next_pc.as<PCState>();

    if (compressed(emi)) {
        next_pc.npc(next_pc.instAddr() + sizeof(machInst) / 2);
        next_pc.compressed(true);
    } else {
        next_pc.npc(next_pc.instAddr() + sizeof(machInst));
        next_pc.compressed(false);
    }
    emi.vl      = next_pc.vl();
    emi.vtype8  = next_pc.vtype() & 0xff;
    emi.vill    = next_pc.vtype().vill;
    emi.rv_type = static_cast<int>(next_pc.rvType());

    return decode(emi, next_pc.instAddr());
}

这个函数在调用 Decoder::decode(ExtMachInst mach_inst, Addr addr) 之前将下一条指令加载到解码器中。


StaticInstPtr
Decoder::decode(ExtMachInst mach_inst, Addr addr)
{
    DPRINTF(Decode, "Decoding instruction 0x%08x at address %#x\n",
            mach_inst.instBits, addr);

    StaticInstPtr &si = instMap[mach_inst];
    if (!si)
        si = decodeInst(mach_inst);

    si->size(compressed(mach_inst) ? 2 : 4);

    DPRINTF(Decode, "Decode: Decoded %s instruction: %#x\n",
            si->getName(), mach_inst);
    return si;
}

解码函数

这个函数主要作为调用 Decoder::decodeInst 函数的简单包装器,加上设置大小和允许一些调试信息。

decodeInst 函数是回溯中的下一个函数,但它是_生成的_。

decideInst 函数是生成的代码,只有在你构建 gem5(scons build/ALL/gem5.opt -j$(nproc))时才会可用。 这些生成文件的副本已添加到 materials/03-Developing-gem5-models/05-modeling-cores/build-riscv-generated-files 供你参考。


这是 “decode-method.cc.inc” 的一个片段,移除了多余的行,以显示返回 Add 指令的语句路径:

// ...
case 0xc:
    switch (FUNCT3) {
    case 0x0:
        switch (KFUNCT5) {
        case 0x0:
            switch (BS) {
            case 0x0:
                // ROp::add(['\n   Rd = rvSext(Rs1_sd + Rs2_sd);\n   '],{})
                    return new Add(machInst);
                break;

这个解码函数接受机器指令并返回适当的 StaticInst 对象(Add(machInst))。 它只是一个巨大的映射表。


RISC-V Add::Execute 回溯

让我们在 GDB 中继续执行以到达下一个断点:

(gdb) c

如果成功,你应该看到以下输出:

Breakpoint 2, {PC} in gem5:: RiscvISAInst::Add::execute...

接下来,我们将进行回溯以查看到达当前函数所调用的函数:

(gdb) bt 5

如你所见,execute 函数是通过 TimingSimpleCPU 模型的 process 函数调用的。


以下代码可以在 “src/cpu/simple/timing.cc 中找到:

void
TimingSimpleCPU::IcachePort::ITickEvent::process()
{
    cpu->completeIfetch(pkt);
}

然后继续到:

// non-memory instruction: execute completely now
        Fault fault = curStaticInst->execute(&t_info, traceData);

这是调用 StaticInst 对象的 execute 函数的函数,它将执行指令的所有工作。 注意:这是因为 Add 是非内存指令。内存指令会立即执行。没有内存访问的指令被模拟为瞬时执行。


不同的内存访问和指令执行

StaticInst 对象有三个用于执行指令的函数:execute()initiateAcc()completeAcc()

execute() 用于通过单个函数调用执行指令。 这在两种情况下使用:运行原子模式时和指令是非内存指令时。

initiateAcc() 用于通过内存系统启动内存访问。 它在请求内存系统执行访问之前,完成实际指令操作访问之前的所有工作。然后,内存系统最终会调用 completeAcc() 来完成访问并触发指令的执行。

后两个函数用于内存指令,例如定时内存访问模式以及当指令是内存指令时(即,指令从内存加载,因此需要时序信息)。


指令-CPU 控制流(SimpleCPU)

75% bg


gem5 ISA 解析器

到目前为止,我们已经看到了指令如何在 gem5 中被解码然后执行。 然而,我们还没有看到这个解码过程是如何定义的,以及指令执行的行为是如何定义的。 这就是它变得复杂的地方…

ISA 规范和解析

“src/arch/*/isa” 目录包含 ISA 定义。 这是用我们称为 ISA 领域特定语言(ISA DSL)的专用语言编写的。

当构建 gem5 时,构建系统使用 src/arch/isa/isa_parser/isa_parser.py 脚本解析这些文件,该脚本生成必要的 CPP 代码。 这些生成的文件可以在 “build/ALL/arch/*/generated/” 中找到。 然后,gem5 构建系统将这些生成的文件编译到 gem5 二进制文件中。


重要的高级概念

bg right fit

ISA 定义的问题在于它非常间接,你可能会在试图理解 CPP 代码生成的小细节时迷失方向。

记住高级概念对于理解 ISA 是如何定义的以及指令是如何被解码和执行的更为重要。

痛苦的真相是,要扩展或添加到 ISA,大多数开发人员会使用 grep 查找类似的指令,并尝试理解涉及的模板,而不完全理解所有部分。


让我们尝试理解一条 RISC-V 指令

在下面,我们将查看 RISC-V 中的 LW 指令,以及它如何在 gem5 中被指定、解码和执行。


RISC-V 指令格式

要理解 RISC-V ISA 以及 gem5 RISC-V 解码器的工作原理,我们需要理解基本指令格式。 基本指令格式是 R、I、S、B、U 和 J 类型,它们使用以下格式:

55% bg



RISC-V 的”加载字”(LW)指令

加载字(指令:LW)是一条 I 类型指令,它将 32 位值从内存加载到寄存器中。 它由以下格式定义:

LW rd,offset(rs1) # rd = mem[rs1+imm]

它将源寄存器 rs1 的值加载到目标寄存器 rd + imm 中。如果 imm 为零,则 rs1 的完整字(32 位)被加载到 rd 中。这个 imm 值用于加载子字数据。但是,如果非零,imm 用于加载子字数据。imm 在加载到 rd 之前将 rs1 寄存器中的位进行移位。因此,如果 imm = 15rs1 的值在加载到 rd 之前会移位 15 位。


RISC-V 的 LW 指令分解

考虑以下指令:

000000000000000000000000100100110001100011

这是一条 LW 指令,属于 I 类型指令。 因此,指令按如下方式分解:

|   31  --  20   |  19 -- 15  |  14 -- 12  |  11 -- 7  |  6 -- 0 |
|  000000000001  |    00010   |    010     |   00011   |  000011 |
|       imm      |     rs1    |   func3    |    rd     |  opcode |

在这个例子中,地址为 2 的寄存器(rs100010)被加载到地址为 3 的寄存器(rd00011)中,偏移量为 1(imm)。funct3 是 LW 指令的功能代码(对于 LW 指令,这始终是 010),opcode 是 LW 指令的操作代码(也始终相同)。


注意:在 gem5 中,我们还提到 QUADRANTQUAD,它是操作码的最后两位(在这种情况下是 11),以及 OPCODE5,它指的是 opcode 右移 2 位(基本上是没有 QUADopcode,在这种情况下是 0000)。 因此 opcode = (OPCODE5 « 2 )+ QUAD


理解 LW 的解码

ISA 定义所做的是定义指令如何被分解,以及指令的”部分”(位域)如何用于解码指令。

转到 gem5 仓库中的 “src/arch/riscv/isa/bitfields.hsh” 目录。 下面是一个片段。

// Bitfield definitions.
//
def bitfield RVTYPE rv_type;

def bitfield QUADRANT <1:0>;
def bitfield OPCODE5 <6:2>;

这定义了位域,就像上一张幻灯片中描述的那样。 解码器使用这些位域来解码指令。


转到 “decoder.isa” 并搜索 lw 指令

以下显示了通过解析指令的 opcodefunct3 字段到达指令定义的路径:

# A reduced decoder.isa to focus just on the path to `lw`.
decode QUADRANT default Unknown::unknown() {
    0x3: decode OPCODE5 { # if QUADRANT == 0x03; then decode OPCODE5
        0x00: decode FUNCT3 { # if OPCODE5 == 0x00; then decode FUNCT3
            format Load { # This tells use to use the `Load` format when decoding (more on this later)
                0x2: lw({{ # if QU # if FUNCT3 == 0x02 then declare lw instruction
                    Rd_sd = Mem_sw;
                }});
            }
        }
    }
}

Rd_sd 是目标寄存器,Mem_sw 是要加载到目标寄存器中的内存地址。


从 LW ISA 定义生成代码

你可以并排比较 decoder.isa 和 decode-method.cc.inc,以查看 ISA 定义如何用于生成 CPP 解码器代码。

这是由 ISA 解析器脚本(isa_parser.py)完成的,gem5 构建系统使用它来生成 CPP 代码。

decode QUADRANT default Unknown::unknown() {

变成

using namespace gem5;
StaticInstPtr
RiscvISA::Decoder::decodeInst(RiscvISA::ExtMachInst machInst)
{
    using namespace RiscvISAInst;
  switch (QUADRANT) {

0x3: decode OPCODE5 {

变成

case 0x3:
      switch (OPCODE5) {

0x00: decode FUNCT3 {

变成

case 0x00:
    switch (FUNCT3) {

最后,

format Load {
    0x2: lw({{ # if QU # if FUNCT3 == 0x02 then declare lw instruction
        Rd_sd = Mem_sw;
    }});
}

变成

case 0x2:
    // Load::lw(['\n                    Rd_sd = Mem_sw;\n                '],{})
    return new Lw(machInst);
    break;

完整的翻译是:

using namespace gem5;
StaticInstPtr RiscvISA::Decoder::decodeInst(RiscvISA::ExtMachInst machInst) {
    using namespace RiscvISAInst;
    switch (QUADRANT) { case 0x3:
            switch (OPCODE5) { case 0x0:
                    switch(FUNCT3) {
                        case 0x2:
                            // Load::lw(['Rd_sd = Mem_sw;'],{})
                            return new Lw(machInst);
                            break;
                    }
            }
    }
}

生成的执行 LW 指令的函数

    setRegIdxArrays(
        reinterpret_cast<RegIdArrayPtr>(
            &std::remove_pointer_t<decltype(this)>::srcRegIdxArr),
        reinterpret_cast<RegIdArrayPtr>(
            &std::remove_pointer_t<decltype(this)>::destRegIdxArr));
            ;
    setDestRegIdx(_numDestRegs++, ((RD) == 0) ? RegId() : intRegClass[RD]);
    _numTypedDestRegs[intRegClass.type()]++;
    setSrcRegIdx(_numSrcRegs++, ((RS1) == 0) ? RegId() : intRegClass[RS1]);
    flags[IsInteger] = true;
    flags[IsLoad] = true;
    memAccessFlags = MMU::WordAlign;;
        offset = sext<12>(IMM12);;

如果你转到 “src/arch/riscv/isa/formats/mem.isa” 中 Load 的声明,你可以弄清楚这是如何构造的:

def format Load(memacc_code, ea_code = {{EA = rvZext(Rs1 + offset);}},
        offset_code={{offset = sext<12>(IMM12);}},
        mem_flags=[], inst_flags=[]) {{
    (header_output, decoder_output, decode_block, exec_output) = \
        LoadStoreBase(name, Name, offset_code, ea_code, memacc_code, mem_flags,
        inst_flags, 'Load', exec_template_base='Load')
}};

你可以通过这个来了解这个构造函数是如何生成的,但这有点像一个兔子洞。


从 “decoder-ns.hh.inc” 中,你可以看到为 Lw 指令生成的类定义:

    class Lw : public Load
    {
      private:
        RegId srcRegIdxArr[1]; RegId destRegIdxArr[1];

      public:
        /// Constructor.
        Lw(ExtMachInst machInst);

        Fault execute(ExecContext *, trace::InstRecord *) const override;
        Fault initiateAcc(ExecContext *, trace::InstRecord *) const override;
        Fault completeAcc(PacketPtr, ExecContext *,
                          trace::InstRecord *) const override;
    };

你可以继续探索

如前所述,ISA 定义是一个兔子洞,可能难以理解。

模板很复杂,通常建立在其他模板和 isa_parser.py 脚本中的专门翻译代码之上。

通过分析 ISA 定义和 isa_parser.py 脚本,你可以更好地理解 ISA 是如何定义的,以及指令是如何被解码和执行的。

生成的 CPP 代码可以通过与 ISA 定义进行比较来理解。

在 GDB 中使用断点来跟踪 gem5 中指令的执行是理解生成的代码如何用于解码和执行指令的好方法。


练习:实现 ADD16 指令

在这个练习中,你将在 gem5 RISC-V ISA 中实现 ADD16

ADD16 指令是一条 16 位加法指令,它将两个 16 位值相加并将结果存储在 16 位寄存器中。

格式:

| 31 -- 25 | 24 -- 20 | 19 -- 15 | 14 -- 12 | 11 -- 7 |  6 -- 0  |
|  0100000 |   rs2    |   rs1    |   000    |   rd    |  0110011 |
|  funct7  |          |          |  funct3  |         |  opcode  |

1 -- 0 (`11`) is the quadrant field.

语法

ADD16, Rs1, Rs2

目的:并行执行 16 位整数元素加法。

描述:这条指令将 Rs1 中的 16 位整数元素与 Rs2 中的 16 位整数元素相加,然后将 16 位元素写入 Rd 寄存器。


让我们运行 materials/03-Developing-gem5-models/05-modeling-cores/02-add16-instruction

这个文件运行 add16_test.c 的二进制文件。这是一个执行 add 16 指令的 C 程序。

我们还没有在 gem5 中实现这条指令。让我们运行这个脚本来查看输出。


add16_test.c 的重要部分

让我们看看 add16_test.c 文件做了什么。

 uint64_t num1 = 0xFFFFFFFFFFFFFFFF, num2 = 0xFFFFFFFFFFFFFFFF, output = 0;
printf("RISC-V Packed Addition using 0xFFFFFFFFFFFFFFFF and 0xFFFFFFFFFFFFFFFF \n");
asm volatile("add16 %0, %1,%2\n":"=r"(output):"r"(num1),"r"(num2):);
printf("Output is 0x%LX \n", output);
if (output == 0xFFFEFFFEFFFEFFFE) {
    printf("Test Passed! \n");
}

上面的代码片段将两个数字设置为 -1,然后我们运行 ADD16 指令。

我们测试指令运行后输出是否为 -2,并打印结果。


如我们所见,当我们运行

 gem5 ./add16_test.py

时,我们得到一个未知指令错误

src/arch/riscv/faults.cc:204: panic: Unknown instruction 0x4000010040e787f7 at pc (0x10636=>0x1063a).(0=>1)
Memory Usage: 1285988 KBytes
Program aborted at tick 18616032
--- BEGIN LIBC BACKTRACE ---

尝试自己将 ADD16 指令实现到 gem5 中。 当遇到困难时,最好的建议是找到类似的指令并尝试理解它们的工作原理。

可以找到帮助你入门的资源在 materials/03-Developing-gem5-models/05-modeling-cores/02-add16-instruction。 值得注意的是,这包含一个编译了 ADD16 指令的二进制文件,以及一个在 RISC-V 系统中运行二进制文件的配置文件。 这个配置会让你知道你是否正确实现了指令。


使用格式指定解码器

让我们反向工作,指定指令格式中的每个位域。

| 31 -- 25 | 24 -- 20 | 19 -- 15 | 14 -- 12 | 11 -- 7 |  6 -- 0  |
|  0100000 |   rs2    |   rs1    |   000    |   rd    |  0110011 |
|  funct7  |          |          |  funct3  |         |  opcode  |

由此,我们可以在 ISA 定义中指定解码器:

decode QUADRANT default Unknown::unknown() {
    0x3 : decode OPCODE5 {
        0x1d: decode FUNCT3 {
            format ROp {
                0x0: decode FUNCT7 {
                    0x20: // Add the ADD16 instruction here
                }
            }
        }
    }
}

注意ROp 格式用于寄存器-寄存器操作。 我为你找出了要使用的格式,但你可以在 ISA 定义中找到这个。


接下来,让我们将其添加到 RISC-V “decoder.isa” 文件中。

让我们在 decoder.isa 的第 2057 行添加这个

需要注意的是,此文件中已经定义了其他共享相同 QUADRANT 和 OPCODE5 值的指令。因此,我们只需要插入:

        0x1d: decode FUNCT3 {
            format ROp {
                0x0: decode FUNCT7 {
                    0x20:

到正确的位置。


接下来让我们添加指令名称:

                    0x20: add16({{

                    }});

花括号之间的空间是声明指令行为的地方。


最后我们添加代码。 这只是理解操作并执行适当操作的问题。 在我们的例子中,我们尽可能保持与 CPP 接近。

    0x20: add16({{
            uint16_t Rd_16 = (uint16_t)(Rs1_ud) +
                                    (uint16_t)(Rs2_ud);
        uint16_t Rd_32 = (uint16_t)((Rs1_ud >> 16) +
                                    (Rs2_ud >> 16));
        uint16_t Rd_48 = (uint16_t)((Rs1_ud >> 32) +
                                    (Rs2_ud >> 32));
        uint16_t Rd_64 = (uint16_t)((Rs1_ud >> 48) +
                                    (Rs2_ud >> 48));
        uint64_t result = Rd_64;
        result = result << 16 | Rd_48;
        result = result << 16 | Rd_32;
        result = result << 16 | Rd_16;
        Rd = result;
    }});

现在让我们再次运行 materials/03-Developing-gem5-models/05-modeling-cores/02-add16-instruction/add16_test.py 脚本并查看输出。

首先,让我们使用我们的更改构建 gem5。

在 gem5 目录中,执行以下命令

scons build/RISCV/gem5.opt -j 8

现在让我们运行 add16_test.py 脚本

../../https://github.com/gem5/gem5/blob/stable/build/RISCV/gem5.opt ./add16_test.py

如我们所见,测试通过了

src/sim/syscall_emul.cc:74: warn: ignoring syscall mprotect(...)
RISC-V Packed Addition using 0xFFFFFFFFFFFFFFFF and 0xFFFFFFFFFFFFFFFF
Output is 0xFFFEFFFEFFFEFFFE
Test Passed!