BLOG

Record, summarize, and improve.

gem5 document

gem5 SimpleCPU BaseSimpleCPU AtomicSimpleCPU TimingSimpleCPU O3CPU Pipeline stages Execute-in-execute model Template Policies ISA independence Interaction with ThreadContext Backend Pipeline Compute Instructions Load Instruction Store Instruction Branch Misspeculation Memory Order Misspeculation Minor What is Minor? Design philosophy Multithreading Data structures Model structure Key data structures Instruction and line identity: InstId (dyn_inst.hh) Instructions: MinorDynInst (dyn_inst.hh) ForwardLineData (pipe_data.hh) ForwardInstData (pipe_data.hh) Fetch1::FetchRequest (fetch1.hh) LSQ::LSQRequest (execute.hh) The pipeline Event handling: MinorActivityRecorder (activity.hh,pipeline.hh) Each pipeline stage Fetch1 stage Fetch2 stage Branch prediction Decode stage Execute stage Functional units Functional unit FIFOs Issue Commit Advance Scoreboard Execute::inFlightInsts LSQ Draining Debug options MinorTrace and minorview.py MinorTrace format MinorTrace - Ticked unit cycle state MinorInst - summaries of instructions issued by Decode MinorLine - summaries of line fetches issued by Fetch1 minorview.py minor.pic format TraceCPU 概述 Elastic Trace Generation Trace file formats跟踪文件格式 protobuf 格式的 Elastic Trace 字段 Scripts and options脚本和选项 Replay with Trace CPU Scripts and options脚本和选项 Visualization可视化 O3 管道查看器 Building EXTRAS Execution basic gem5 bootcamp 2022 module on instruction execution StaticInsts DynInsts Microcode support ExecContext ThreadContext ProxyThreadContext Difference vs. ExecContext ThreadState Faults Registers Register types - float, int, misc Indexing - register spaces stuff PCs Register Indexing Types of Register Indices Relative Unified Flattened Combining Register Index Types Illustrations Caveats ISA and CPU Independence ISA Parser The decode section Specifying instruction formats Decode block defaults Preprocessor directive handling The declaration section Format definitions Template definitions Output blocks Let blocks Bitfield definitions Operand and operand type definitions Namespace declaration ISA parser Formats operands decode tree let blocks microcode assembler microops macroops directives rom object Lots more stuff Code parsing Bitfield operators Operand type qualifiers Instruction operands The CodeBlock class The InstObjParams class M5ops Building M5 and libm5 The m5 Utility (FS mode) Other M5 ops Using gem5 ops in Java code Using gem5 ops with Fortran code Linking M5 to your C/C++ code Using the “_addr” version of M5ops Debugger-based Debugging Debugging Python with PDB Using Valgrind Debugging Simulated Code Getting a cross-architecture gdb Target-specific instructions ARM Target Trace-based Debugging Introduction The Exec debug flag Reducing trace file size The tracediff and rundiff utilities Comparing traces across machines Internal Exec tracing implementation (InstTracer) Comparing traces with a real machine Native Trace statetrace utility Tuning Caveats ISA support Garnet Synthetic Traffic Related Files How to run Parameterized Options Implementation of Garnet Synthetic Traffic Learning gem5 SimObject 增加新的调试标志 增加事件 数据包 端口

gem5

包括两个简单的单 CPI 模型,一个无序模型和一个有序流水线模型。内存系统可以灵活地构建在高速缓存和交叉开关或提供更灵活的内存系统建模的 Ruby 模拟器之外

SimpleCPU

SimpleCPU是一个纯粹的功能型、顺序型模型,适合于没有必要建立详细模型的情况。这可以包括热身期、驱动主机的客户系统,或者只是测试以确保程序的运行。
它最近被重新编写,以支持新的内存系统,现在被分成了三种:

BaseSimpleCPU

BaseSimpleCPU 有几个用途:

  • 保存架构状态,统计信息在 SimpleCPU 模型中通用。
  • 定义用于检查中断、设置获取请求、处理预执行设置、处理执行后操作以及将 PC 推进到下一条指令的函数。这些功能在 SimpleCPU 模型中也很常见。
  • 实现 ExecContext 接口。

BaseSimpleCPU 不能单独运行。您必须使用从 BaseSimpleCPU 继承的类之一,AtomicSimpleCPU 或 TimingSimpleCPU

AtomicSimpleCPU

AtomicSimpleCPU 是使用原子内存访问的 SimpleCPU 版本(有关详细信息,请参阅内存系统)。它使用来自原子访问的延迟来估计整体缓存访问时间。AtomicSimpleCPU 派生自 BaseSimpleCPU,实现了读写内存的功能,也实现了 tick,它定义了每个 CPU 周期发生的事情。它定义了用于连接内存的端口,并将 CPU 连接到缓存

Image in a image block

TimingSimpleCPU

TimingSimpleCPU 是使用定时内存访问的 SimpleCPU 版本(有关详细信息,请参阅内存系统)。它在缓存访问时停止并等待内存系统在继续之前做出响应。与AtomicSimpleCPU 一样,TimingSimpleCPU 也是从BaseSimpleCPU 派生而来,实现了相同的功能集。它定义用于连接到内存的端口,并将CPU连接到高速缓存。它还定义了处理从内存发出的访问响应所必需的功能。

Image in a image block

O3CPU

O3CPU是我们为v2.0版本开发的新的详细模型。它是一个松散地基于Alpha 21264的失序CPU模型。本页将给你一个关于O3CPU模型、流水线阶段和流水线资源的总体概述。我们努力使代码保持良好的文档化,所以请浏览代码,以了解O3CPU各部分工作的确切细节。

Pipeline stages

  • Fetch

    每个周期提取指令,根据选择的策略选择从哪个线程提取。这个阶段是首次创建DynInst的地方。也处理分支预测。

  • Decode

    每个周期对指令进行解码。也处理早期解决与PC有关的无条件分支。

  • Rename

    使用一个有空闲列表的物理寄存器文件重命名指令。如果没有足够的寄存器可以重命名,或者后端资源已经用完,就会停滞。在这个时候也会处理任何序列化的指令,在重命名中停顿,直到后端资源耗尽。

  • Issue/Execute/Writeback

    我们的模拟器模型在调用execute()函数时可以处理执行和写回,因此我们将这三个阶段合并为一个阶段。此阶段(IEW)负责将指令分派到指令队列,告知指令队列发出指令,并执行和写回指令。

  • Commit

    每个周期提交指令,处理指令可能引起的任何故障。也处理在分支预测错误的情况下重定向前端的问题。

Execute-in-execute model

对于O3CPU,我们已经努力使其在时间上高度精确。为了做到这一点,我们使用了一个在流水线的执行阶段实际执行指令的模型。大多数模拟器模型会在流水线的起点或终点执行指令;SimpleScalar和我们以前的详细CPU模型都是在流水线的起点执行指令,然后将其传递给计时后端。这带来了两个潜在的问题:首先,计时后端有可能出现错误,而这些错误不会在程序结果中显示出来。第二,通过在流水线的开始执行,所有的指令都是按顺序执行的,失序的负载交互会丢失。我们的模型能够避免这些缺陷,并提供一个准确的定时模型。

Template Policies

O3CPU大量使用模板策略来获得一定程度的多态性,而不需要使用虚拟函数。它使用模板策略将一个 "Impl "传递给O3CPU中使用的几乎所有的类。这个Impl定义了流水线的所有重要类,如特定的Fetch类、Decode类、特定的DynInst类型、CPU类等等。它允许任何使用它作为模板参数的类能够获得Impl中定义的任何类的完整类型信息。通过获取完整的类型信息,就不需要传统的虚拟函数/基类,而这通常是用来提供多态性的。主要的缺点是,CPU必须在编译时完全定义,而且模板化的类需要手动实例化。参见src/cpu/o3/impl.hhsrc/cpu/o3/cpu_policy.hh,了解Impl类的例子。

ISA independence

O3CPU的设计试图将依赖ISA的代码和独立于ISA的代码分开。管道阶段和资源都是独立于ISA的,低级别的CPU代码也是如此。依赖ISA的代码实现了ISA特有的功能。例如,AlphaO3CPU实现了Alpha特有的功能,如错误中断的硬件返回(hwrei())或读取中断标志。较低级别的CPU,即FullO3CPU,处理协调所有的流水线阶段和处理其他与ISA无关的动作。我们希望这种分离能使未来的ISA更容易实现,因为希望只有高层的类需要重新定义。

Interaction with ThreadContext

ThreadContext为外部对象访问CPU内的线程状态提供了接口。然而,由于O3CPU是一个失序的CPU,这一点略显复杂。虽然很好地定义了任何给定周期的架构状态,但并没有很好地定义如果架构状态被改变会怎样。因此,对ThreadContext进行读取是可行的,但对ThreadContext进行写入并改变寄存器状态需要CPU刷新整个流水线。这是因为可能有一些飞行指令依赖于被改变的寄存器,而且不清楚它们是否应该查看寄存器的更新。因此,对ThreadContext的访问有可能会导致CPU模拟的速度减慢。

Backend Pipeline

Compute Instructions

计算指令比较简单,因为它们不访问内存,不与LSQ交互。下面是一个高级调用链(只有重要的功能),并对每个功能进行了描述。

Rename::tick()->Rename::RenameInsts()
IEW::tick()->IEW::dispatchInsts()
IEW::tick()->InstructionQueue::scheduleReadyInsts()
IEW::tick()->IEW::executeInsts()
IEW::tick()->IEW::writebackInsts()
Commit::tick()->Commit::commitInsts()->Commit::commitHead()
  • 重命名(Rename::renameInsts())。正如其名称所暗示的,寄存器被重命名,指令被推送到IEW阶段。它检查IQ/LSQ是否能容纳新的指令。
  • Dispatch (IEW::dispatchInsts())。这个函数将重命名的指令插入到IQ和LSQ中。
  • Schedule (InstructionQueue::scheduleReadyInsts()) IQ管理就绪列表中的就绪指令(操作数就绪),并将它们安排到一个可用的FU上。FU的延迟在这里被设置,指令在FU完成后被送去执行。
  • 执行(IEW::executeInsts())。在这里,计算指令的execute()函数被调用并被发送到commit。请注意execute()将把结果写到destiniation寄存器中。
  • 写回(IEW::writebackInsts())。这里InstructionQueue::wakeDependents()被调用。依赖指令将被添加到准备好的列表中进行调度。
  • 提交(Commit::commitInsts())。一旦指令到达ROB的头部,它将被提交并从ROB中释放。
Load Instruction

加载指令在执行前与计算指令共享同一路径。

IEW::tick()->IEW::executeInsts()
  ->LSQUnit::executeLoad()
    ->StaticInst::initiateAcc()
      ->LSQ::pushRequest()
        ->LSQUnit::read()
          ->LSQRequest::buildPackets()
          ->LSQRequest::sendPacketToCache()
    ->LSQUnit::checkViolation()
DcachePort::recvTimingResp()->LSQRequest::recvTimingResp()
  ->LSQUnit::completeDataAccess()
    ->LSQUnit::writeback()
      ->StaticInst::completeAcc()
      ->IEW::instToCommit()
IEW::tick()->IEW::writebackInsts()
  • LSQUnit::executeLoad() 将通过调用指令的 initiateAcc() 函数来启动访问。通过执行上下文接口,initiateAcc() 将调用 initiateMemRead() 并最终定向到 LSQ::pushRequest()
  • LSQ::pushRequest()将分配一个LSQRequest来跟踪所有状态,并开始翻译。当翻译完成后,它将记录虚拟地址并调用LSQUnit::read()
  • LSQUnit::read()将检查负载是否与之前的任何存储有关联。
    • 如果它可以转发,那么它将为下一个周期安排WritebackEvent
    • 如果它被别名但不能转发,它就会调用InstructionQueue::rescheduleMemInst()LSQReuqest::discard()
    • 否则,它将数据包发送到缓存。
  • LSQUnit::writeback()将调用StaticInst::completeAcc(),它将向目标寄存器写入一个加载值。然后该指令被推送到提交队列中。 IEW::writebackInsts()将标记它完成,并唤醒它的依赖。从这里开始,它与计算指令共享相同的路径。
Store Instruction

存储指令与加载指令类似,但只是在提交后回写到缓存中。

IEW::tick()->IEW::executeInsts()
  ->LSQUnit::executeStore()
    ->StaticInst::initiateAcc()
      ->LSQ::pushRequest()
        ->LSQUnit::write()
    ->LSQUnit::checkViolation()
Commit::tick()->Commit::commitInsts()->Commit::commitHead()
IEW::tick()->LSQUnit::commitStores()
IEW::tick()->LSQUnit::writebackStores()
  ->LSQRequest::buildPackets()
  ->LSQRequest::sendPacketToCache()
  ->LSQUnit::storePostSend()
DcachePort::recvTimingResp()->LSQRequest::recvTimingResp()
  ->LSQUnit::completeDataAccess()
    ->LSQUnit::completeStore()
  • LSQUnit::read()不同,LSQUnit::write()只会复制存储数据,但不会将数据包发送到缓存中,因为存储还没有提交。
  • 在提交存储后,LSQUnit::commitStores()将把SQ条目标记为canWB,这样LSQUnit::writebackStores()将向缓存发送存储请求。
  • 最后,当响应回来时,LSQUnit::completeStore()将释放SQ条目。
Branch Misspeculation

IEW::executeInsts()中处理了分支误判问题。它将通知提交阶段开始压制错误分支上的ROB中的所有指令。

IEW::tick()->IEW::executeInsts()->IEW::squashDueToBranch()

Memory Order Misspeculation

InstructionQueue有一个MemDepUnit来跟踪内存顺序的依赖性。如果MemDepUnit指出有依赖性,IQ就不会安排指令。
LSQUnit::read()中,LSQ将搜索可能的别名存储,如果可能的话,就转发。否则,加载将被阻断,并通过通知MemDepUnit,重新安排在阻断存储完成时进行。
LSQUnit::executeLoad/Store()都会调用LSQUnit::checkViolation()来搜索LQ可能的误判。如果发现,它将设置LSQUnit::memDepViolator,并且IEW::executeInsts()将稍后开始压制错误的指令。

IEW::tick()->IEW::executeInsts()
  ->LSQUnit::executeLoad()
    ->StaticInst::initiateAcc()
    ->LSQUnit::checkViolation()
  ->IEW::squashDueToMemOrder()

Minor

What is Minor?

Minor是一个具有固定流水线但可配置数据结构和执行行为的无序处理器模型。它旨在用于建立具有严格的无序执行行为的处理器模型,并允许通过MinorTrace/minorview.py格式/工具可视化指令在流水线中的位置。其目的是提供一个框架,将模型与特定的、所选择的具有类似能力的处理器进行微观架构上的关联。

本文件包含了对Minor gem5内序处理器模型的结构和功能的描述。

建议想了解 Minor 的内部组织、设计决策、C++ 实现和 Python 配置的人阅读。假设对gem5和它的一些内部结构比较熟悉。本文件旨在与Minor的源代码一起阅读,并解释其一般结构,而不是太刻意地去命名每一个函数和数据类型。

Design philosophy

Multithreading

该模型目前不具备多线程的能力,但在关键的地方有THREAD注释,在这些地方需要对阶段数据进行排列以支持多线程。

Data structures

避免了用大量的生命周期信息来装饰数据结构。只有指令(MinorDynInst)包含相当一部分数据内容,其值在构造时没有被设置。

所有内部结构在构建时都有固定的大小。在队列和FIFO(MinorBuffer,FUPipeline)中保存的数据应该有一个BubbleIF接口,以便为每种类型提供不同的 ‘bubble’/no 数据值选项。

Inter-stage的 "struct "数据被打包在结构中,通过值传递。只有MinorDynInst、ForwardLineData中的line数据以及内存接口对象Fetch1::FetchRequest和LSQ::LSQRequest在运行模型时被::new分配。

Model structure

MinorCPU类的对象是由模型提供给gem5的。 MinorCPU实现了(cpu.hh)的接口,并可以提供数据和指令接口以连接到缓存系统。该模型的配置方式与其他 gem5 模型类似,通过 Python 进行配置。该配置被传递给 MinorCPU::pipeline(属于 Pipeline 类),它实际上实现了处理器流水线。

从MinorCPU向下看,主要单元所有权的层次结构是这样的。

  • MinorCPU
    • Pipeline - pipeline的容器,拥有循环的 "tick "事件机制和空转(循环跳过)机制。
      • Fetch1 - 指令获取单元,负责获取缓存行(或从I-缓存接口获取部分行)。
      • Fetch2 - line到指令分解
      • Decode - 指令到微操作分解
      • Execute - 指令执行和数据存储器接口
        • LSQ - 内存参考指令的加载存储队列
        • LSQ::DcachePort - 从 Execute 到 D-cache 的接口

Key data structures

Instruction and line identity: InstId (dyn_inst.hh)

InstId包含序列号和线程号,这些序列号和线程号描述了单个被取走的缓存行和指令的生命周期和指令流关系。
InstId以下列形式之一打印。

- T/S.P/L - for fetched cache lines
- T/S.P/L/F - for instructions before Decode
- T/S.P/L/F.E - for instructions from Decode onwards

for example:

- 0/10.12/5/6.7

InstId's fields are:

Field Symbol Generated by Checked by Function
InstId::threadId T Fetch1 任何地方都需要线程号 Thread number (currently always 0).
InstId::streamSeqNum S Execute Fetch1, Fetch2, Execute (丢弃lines/insts) 由Execute选择的流序列号。流序列号在Execue的PC(分支、异常)变化后会发生变化,并用于分离前、后brnach instrucion流。
InstId::predictionSeqNum Fetch2 Fetch2 (在预测后丢弃line) 预测序列号代表分支预测决定。这被Fetch2用来根据Fetch2所做的最后跟踪的分支预测来标记line/指令/。Fetch2可以向Fetch1发出信号,它应该改变它的获取地址,并用新的预测序列号标记行(只有当Fetch1期望的流序列号与请求的序列号一致时,它才会这样做)。
InstId::lineSeqNum Fetch1 (just for debugging) 该缓存行或该指令提取的行的取数序列号。
InstId::fetchSeqNum Fetch2 Fetch2 (as the inst. sequence number for branches) 当行被分解为指令时,由Fetch2分配的指令获取顺序。
InstId::execSeqNum Decode Execute (to check instruction identify in queues/FUs/LSQ 微操作分解后的指令顺序

序列号字段都是相互独立的,虽然,例如,指令的InstId::execSeqNum总是>= InstId::fetchSeqNum,但这种比较是没有用的。

每个序列号字段的起始阶段都为该字段保留一个计数器,该计数器可以被递增,以产生新的、唯一的数字。

Instructions: MinorDynInst (dyn_inst.hh)

MinorDynInst表示一条指令在流水线上的进展情况。一条指令可以是三种情况。

Things Predicate Explanation
A bubble MinorDynInst::isBubble() 没有任何指示,只是一个空间填充物
A fault MinorDynInst::isFault() 在一个指令的包裹下,有一个故障顺着pipeline传递下来
A decoded instruction MinorDynInst::isInst() 指令实际上是在Fetch2中传递给gem5解码器的,所以创建时是完全解码的。MinorDynInst::staticInst是解码后的指令形式。

指令是使用gem5 RefCountingPtr(base/refcnt.hh)包装器进行参考计算的。因此,它们在代码中通常显示为 MinorDynInstPtr。请注意,由于RefCountingPtr初始化为nullptr,而不是支持BubbleIF::isBubble的对象,将原始的MinorDynInstPtrs从stage.hh传递到Queues和其他类似结构而不进行装箱是危险的。

ForwardLineData (pipe_data.hh)

ForwardLineData用于从Fetch1向Fetch2传递缓存行。像MinorDynInsts一样,它们可以是bubbles(ForwardLineData::isBubble()),也可以是携带故障的,或者可以包含由Fetch1获取的line(部分行)。ForwardLineData携带的数据由一个从内存返回的数据包对象拥有,并且是明确的内存管理,一旦处理完毕(由Fetch2删除数据包)就必须删除。

ForwardInstData (pipe_data.hh)

ForwardInstData在其ForwardInstData::insts向量中最多可以包含ForwardInstData::width()指令。这个结构用于在Fetch2、Decode和Execute之间携带指令,并在Decode和Execute中存储输入缓冲区向量。

Fetch1::FetchRequest (fetch1.hh)

FetchRequests代表I-cache行获取请求。它们用于Fetch1的内存队列中,并在穿越内存系统时被推入/弹出Packet::senderState
FetchRequests 包含一个用于获取访问的内存系统请求(mem/request.hh),如果请求到达内存,则包含一个数据包(Packet,mem/packet.hh),以及一个可以填充TLB源预取故障(如果有)的故障字段。

LSQ::LSQRequest (execute.hh)

LSQRequests与FetchRequests相似,但用于D-cache访问。它们携带与内存访问相关的指令。

The pipeline

------------------------------------------------------------------------------
    Key:

    [] : inter-stage BufferBuffer
    ,--.
    |  | : pipeline stage
    `--'
    ---> : forward communication
    <--- : backward communication

    rv : reservation information for input buffers

                ,------.     ,------.     ,------.     ,-------.
 (from  --[]-v->|Fetch1|-[]->|Fetch2|-[]->|Decode|-[]->|Execute|--> (to Fetch1
 Execute)    |  |      |<-[]-|      |<-rv-|      |<-rv-|       |     & Fetch2)
             |  `------'<-rv-|      |     |      |     |       |
             `-------------->|      |     |      |     |       |
                             `------'     `------'     `-------'
------------------------------------------------------------------------------

四个流水线阶段通过MinorBuffer FIFO(stage.hh,最终来自TimeBuffer)结构连接在一起,允许对阶段间的延迟进行建模。在前进方向的相邻阶段之间有一个MinorBuffers(例如:从Fetch1到Fetch2传递line),在Fetch2和Fetch1之间有一个向后方向的缓冲器,携带分支预测。

阶段Fetch2、Decode和Execute有输入缓冲区,每个周期可以接受来自前一阶段的输入数据,如果该阶段还没有准备好处理这些数据,可以保留这些数据。输入缓冲区以接收到的相同形式存储数据,因此Decode和Execute的输入缓冲区包含来自其前一阶段的输出指令向量(ForwardInstData(pipe_data.hh)),指令和bubbles的位置与单个缓冲区条目相同。

阶段输入缓冲区为其前一阶段提供了一个Reservable(stage.hh)接口,以允许在其输入缓冲区中保留插槽,并向后传达其输入缓冲区的占用情况,以允许前一阶段计划其是否应在给定周期内进行输出。

Event handling: MinorActivityRecorder (activity.hh,pipeline.hh)

Minor 本质上是一个可以循环调用的模型,可以根据流水线活动跳过某些周期。外部事件主要通过回调接收(例如Fetch1::IcachePort::recvTimingResp),并导致流水线被唤醒以服务推进请求队列。

Ticked(sim/ticked.hh)是一个基类,将evaluate成员函数和提供的SimObject结合在一起。它提供了Ticked::start/stop 接口来启动和暂停定时发出的时钟事件。Pipeline是Ticked的派生类。在evaluate调用期间,阶段可以通过调用MinorCPU::activityRecorder->activity()(用于非可调用相关活动)或MinorCPU::wakeupOnEvent()(用于与阶段回调相关的“唤醒”活动)来发出它们在下一周期仍有工作要做的信号。

Pipeline::evaluate包含对每个单元的evaluate调用,以及测试流水线空闲,可以关闭时钟滴答,如果没有单元表示它可能在下一个周期活动。

在Pipeline(pipeline.hh)中,阶段以相反的顺序进行评估(因此也会以相反的顺序::evaluate),并且可以在每个周期写入后立即读取它们的后向数据,从而允许输出决策是“完美的”(允许整个流水线的同步停滞)。 Fetch2到Fetch1的分支预测也可以在0周期内传输,使fetch1到Fetch2的Backward Delay成为唯一可配置的延迟,可以设置为0周期。

可以调用MinorCPU::activateContext和MinorCPU::suspendContext接口来启动和暂停线程(以MT意义上的线程),以及启动和暂停流水线。执行指令可以调用此接口(间接通过ThreadContext)来空闲CPU/它们的线程。

Each pipeline stage

一般来说,一个阶段(每个周期)的行为是:

    evaluate:
        push input to inputBuffer
        setup references to input/output data slots

        do 'every cycle' 'step' tasks

        if there is input and there is space in the next stage:
            process and generate a new output
            maybe re-activate the stage

        send backwards data

        if the stage generated output to the following FIFO:
            signal pipe activity

        if the stage has more processable input and space in the next stage:
            re-activate the stage for the next cycle

        commit the push to the inputBuffer if that data hasn't all been used
evaluate:
	      将输入推送到输入缓冲区
	      设置输入/输出数据槽的引用

				执行“每个周期”的“步骤”任务

				如果有输入并且下一阶段有空间:
			       处理并生成新的输出
			       可能重新激活阶段

				发送向后数据

				如果阶段生成输出到以下FIFO:
	            信号管道活动

				如果阶段有更多可处理的输入并且下一阶段有空间:
	            重新激活阶段以进行下一个周期

				如果该数据尚未全部使用,则提交推送到输入缓冲区

执行阶段与这个模型不同,因为它的前向输出(分支)数据是无条件地发送给Fetch1和Fetch2的。为了允许这种行为,Fetch1和Fetch2必须无条件地接受这些数据。

Fetch1 stage

Fetch1负责从I-cache中获取缓存行或部分缓存行,并将其传递给Fetch2以被分解为指令。它可以从Execute和Fetch2接收“流变更”指示,以表明它应该更改其内部获取地址并用新流或预测序列号标记新获取的行。当Execute和Fetch2同时发出流变更时,Fetch1会接受Execute的变更。

Fetch1发出的每一行都将带有唯一的行序号,可用于调试流变更。

从I-cache获取时,Fetch1将请求从当前获取地址(Fetch1::pc)到参数fetch1LineSnapWidth中设置的“数据快照”大小的结尾。随后的自主行获取将以快照边界和fetch1LineWidth大小获取整行。

Fetch1只有在可以预留Fetch2输入缓冲区的空间时才会发起内存获取。该输入缓冲区为系统提供了获取队列/ LFL。

Fetch1包含两个队列:请求和传输,用于处理将行获取的地址(通过TLB)转换和容纳获取/响应访存的请求/响应的阶段。

Fetch1的请求将作为新分配的FetchRequest对象推入请求队列,一旦它们被通过调用itb->translateTiming发送到ITLB。

TLB的响应将请求从请求队列移动到传输队列。如果每个队列中有多个条目,则可以为不在请求队列头部的请求获得TLB响应。在这种情况下,TLB响应将被标记为请求对象中的状态更改为Translated,将请求推进到传输(和内存系统)的工作留给调用Fetch1 :: stepQueues,该调用在接收到任何事件后的周期中被调用。

Fetch1 :: tryToSendToTransfers - layout:文档标题:执行基础doc:gem5文档parent:cpu_modelspermalink:/documentation/general_docs/cpu_models/execution_basics -

负责在两个队列之间移动请求并向内存发出请求。 TLB查找失败(预取中止)继续占用队列中的空间,直到它们在传输的头部恢复。

来自内存的响应会更改请求对象的状态为Complete,Fetch1::evaluate可以检索响应数据,将其打包在ForwardLineData对象中,并将其转发到Fetch2的输入缓冲区。

由于总是在Fetch2::inputBuffer中预留空间,因此将输入缓冲区的大小设置为1会导致非预取行为。

发生流变更时,可以无条件丢弃已翻译的请求队列成员和已完成的传输队列成员,以便为新传输腾出空间。

Fetch2 stage

Fetch2从Fetch1接收一行到它的输入缓冲区。缓冲区内头行的数据被迭代并分离成单独的指令,这些指令被打包成一个指令向量,可以传递给解码器。如果在整个输入行或分解的指令中发现故障,打包指令可以提前中止。

Branch prediction

Fetch2 包含分支预测机制。这是对gem5(cpu/pred/...)提供的分支预测器接口的一个封装。

对发现的任何控制指令进行分支预测。如果试图对一条指令进行预测,MinorDynInst::triedToPredict标志将被设置在该指令上。

当预测到有分支时,MinorDynInst::predictedTaken标志被设置,MinorDynInst::predictedTarget被设置为预测的目标PC值。然后,预测的分支指令被打包到Fetch2的输出向量中,预测序列号被递增,并且分支被传达给Fetch1。

在发出预测信号后,Fetch2将丢弃其输入缓冲区的内容,并拒绝任何与该分支具有相同流序列号但具有不同预测序列号的新行。这允许拒绝按顺序获取的行,而不忽视由来自Execute的 "真正 "分支的流变化产生的新行(它将有一个新的流序列号)。

由Fetch1数据包提供给Fetch2的程序计数器值只有在流的变化时才会更新。Fetch2::havePC表明PC是否将从下一个被处理的输入行中提取。Fetch2::havePC是必要的,以允许通过解码跟踪换行指令。

由Execute处理的分支(和预测分支的指令)将产生BranchData(pipe_data.hh)数据,解释分支的结果,并转发给Fetch1和Fetch2。 Fetch1使用这些数据来改变流(并更新其流序列号和新行的地址)。 Fetch2使用它来更新分支预测器。 对于在提交过程中被丢弃的指令,Minor不会将分支数据传达给分支预测器。

BranchData::BranchReason(pipe_data.hh)对可能的分支情况进行编码。

Branch enum val. In Execute Fetch1 reaction Fetch2 reaction
No Branch (output bubble data) - -
CorrectlyPredictedBranch Predicted, taken - Update BP as taken branch
UnpredictedBranch Not predicted, taken and was taken New stream Update BP as taken branch
BadlyPredictedBranch Predicted, not taken New stream to restore to old Inst. source Update BP as not taken branch
BadlyPredictedBranchTarget Predicted, taken, but to a different target than predicted one New stream Update BTB to new target
SuspendThread Hint to suspend fetch Suspend fetch for this thread (branch to next inst. as wakeup fetch addr -
Interrupt Interrupt detected New stream -

参数decodeInputWidth设定了每个周期可以装入输出的指令数量。如果参数fetch2CycleInput为真,Decode可以尝试在每个周期从其输入缓冲区的一个以上条目中获取指令。

Decode stage

解码从Fetch2(通过其输入缓冲器)获取指令向量,并将这些指令分解为微操作(如果需要),并将它们打包到其输出指令向量中。

参数executeInputWidth设置每个周期可以打包到输出的指令数量。如果参数decodeCycleInput为真,Decode可以尝试在每个周期从其输入缓冲区的一个以上条目中获取指令。

Execute stage

Execute提供所有的指令执行和内存访问机制。一条指令通过Execute可能需要多个周期,其精确时间由功能单元流水线FIFO来模拟。

一个指令矢量(可能包括故障 "指令")由解码器提供给Execute,并可以在发布之前在Execute输入缓冲区中排队。设置参数executeCycleInput允许Execute检查一个以上的输入缓冲区条目(一个以上的指令向量)。输入向量中的指令数量可以用executeInputWidth设置,输入缓冲区的深度可以用参数executeInputBufferSize设置。

Functional units

执行阶段包含构成CPU计算核心的每个功能单元的管线。功能单元是通过executeFuncUnits参数配置的。每个功能单元都有它所支持的指令类的数量,指令发布之间的规定延迟,以及从指令发布到(可能的)提交的延迟,还有一个可选择的定时注释,能够实现更复杂的定时。

每个活动周期,Execute::evaluation都会执行这个动作。

    Execute::evaluate:
        push input to inputBuffer
        setup references to input/output data slots and branch output slot

        step D-cache interface queues (similar to Fetch1)

        if interrupt posted:
            take interrupt (signalling branch to Fetch1/Fetch2)
        else
            commit instructions
            issue new instructions

        advance functional unit pipelines

        reactivate Execute if the unit is still active

        commit the push to the inputBuffer if that data hasn't all been used

Functional unit FIFOs

功能单元被实现为SelfStallingPipelines(stage.hh)。这些是具有两个不同的 "推 "和 "弹 "线的TimeBuffer FIFO。它们响应SelfStallingPipeline::advance的方式与TimeBuffers相同,除非在FIFO的远端,即'pop',有数据。一个 "停滞 "标志被提供出来,用于发出停滞信号并允许停滞被清除。其目的是为每个功能单元提供一个流水线,在指令被处理和流水线被明确地解除滞留之前,绝不会将指令从该流水线中推进。

“发出"、"提交 "和 "前进 "等动作都是针对功能单元的。

Issue

发出指令涉及到对输入缓冲区指令和功能单元的头部进行迭代,以尝试按顺序发出指令。每个周期可以发出的指令数量受参数executeIssueLimit、executeCycleInput的设置、流水线空间的可用性以及用于选择可以发出指令的流水线的策略的限制。

目前,唯一的发行策略是用给定的指令依次对每个流水线进行严格的轮流访问。为了获得更大的灵活性,需要可能有更好的(和更具体的政策)。

内存操作指令遍历其功能单元,以执行其EA计算。在'提交'时,ExecContext::initiateAcc执行阶段被执行,任何内存访问被发出(通过。 ExecContext::{read,write}Mem调用LSQ::pushRequest)到LSQ。

请注意,故障的发布就像它们是指令一样,并且可以(目前)发布给任何功能单元。

每个发出的指令也被推入Execute::inFlightInsts队列。内存参考指令被推入Execute::inFUMemInsts队列。

Commit

指令通过检查Execute::inFlightInsts队列的头部(该队列用指令发出的功能单元编号来装饰)来提交。然后,可以在其功能单元中找到的指令被执行并从Execute::inFlightInsts中弹出。

内存操作指令被提交到内存队列中(如上所述),并退出其功能单元管道,但不会从Execute::inFlightInsts队列中弹出。Execute::inFUMemInsts队列在内存操作通过功能单元时为其提供排序(保持发行顺序)。在进入LSQ时,指令会从Execute::inFUMemInsts中弹出。

如果参数executeAllowEarlyMemoryIssue被设置,内存操作可以在到达Execute::inFlightInsts的头部之前,但在满足其依赖性之后,从它们的FU发送到LSQ。 MinorDynInst::instToWaitFor被标记为最新的依赖指令execSeqNum,该指令需要被提交给内存操作以进展到LSQ。

一旦有了内存响应(通过测试Execute::inFlightInsts与LSQ::findResponse的头部),提交将处理该响应(ExecContext::completeAcc)并从Execute::inFlightInsts中弹出指令。

任何分支、故障或中断都会引起流序列号的变化,并向Fetch1/Fetch2发出分支信号。只有具有当前流序列号的指令将被发出和/或提交。

Advance

所有未停顿的流水线被推进,此后可能成为停顿。如果任何管道中还有任何指令,下一个周期的潜在活动就会发出信号。

Scoreboard

记分牌(Scoreboard)是用来控制指令的发布。它包含了飞行指令的数量,这些指令将写入每个通用的CPU整数或浮点寄存器。只有当记分牌上的指令数为0时,指令才会被发出,这些指令将被写入一个指令的源寄存器。

一旦指令被发出,指令的每个目标寄存器的计分板计数将被递增。

指令结果的估计交付时间在记分牌中被标记出来,方法是将发出的FU的长度加到当前时间。每个FU上的计时参数提供了一个额外的规则列表,用于计算交付时间。这些都记录在 MinorCPU.py 的参数注释中。

在提交时,(对于内存操作,内存响应提交)指令的源寄存器的记分牌计数器会被减去。 将被减去。

Execute::inFlightInsts

Execute::inFlightInsts 队列中总是包含正确的指令发布顺序中 Execute 中所有正在执行的指令。Execute::issue 是唯一一个会将指令推入队列的过程。Execute::commit 是唯一一个可以弹出指令的过程。

LSQ

LSQ可以在许多保守的情况下支持多个未完成的内存事务。

有三个队列来包含请求:请求,传输和存储缓冲区。请求和传输队列的操作方式类似于Fetch1中的队列。存储缓冲区用于解耦完成存储操作的延迟与后续加载之间的关系。

请求在其说明离开其功能单元时发送到DTLB。在请求的头部,可以发送可缓存的加载请求到内存并发送到传输队列。可缓存的存储将未经处理地发送到传输队列,并与其他事务一起保持顺序进行。

LSQ :: tryToSendToTransfers中的条件决定何时可以将请求发送到内存。

所有不可缓存的事务,拆分事务和锁定事务都按顺序处理请求的头部。此外,存储缓冲区中的存储结果可以将其数据转发到可缓存的加载(不需要从内存执行读取),但是在传输队列的存储流入存储缓冲区之前,不能将可缓存的加载发送到传输队列。

在传输结束时,可以从Execute中取出已完成(故障,可缓存存储或已发送到内存并收到响应)的请求,并将其提交(ExecContext :: completeAcc),对于存储,将其发送到存储缓冲区。

屏障指令不会阻止可缓存的加载进行到内存,但会导致流变更,从而丢弃该加载。如果存储在屏障的阴影中,但在新的指令流到达Execute之前,则不会将存储提交到存储缓冲区。由于所有其他内存事务都在请求的末尾延迟,直到它们在Execute :: inFlightInsts的头部,它们将被任何屏障流变更丢弃。

提交后,LSQ :: BarrierDataRequest请求将插入存储缓冲区以跟踪每个屏障,直到所有先前的内存事务从存储缓冲区流出。在屏障排出之前,不会从FU的末端发出任何其他内存事务。

Draining

排水主要由执行阶段处理。当通过调用 MinorCPU::drain 启动时,Pipeline::evaluation 在每个周期检查每个单元的排水状态,并保持管道的活动,直到排水完成。是Pipeline发出了排空完成的信号。 Execute 由 MinorCPU::drain 触发,并从 Execute::NotDraining 状态开始,按以下顺序,开始步入其 Execute::Drain 状态机。

State Meaning
Execute::NotDraining Not trying to drain, normal execution 不尝试排空,正常执行
Execute::DrainCurrentInst Draining micro-ops to complete inst. 耗尽微操作以完成安装。
Execute::DrainHaltFetch Halt fetching instructions 停止获取指令
Execute::DrainAllInsts Discarding all instructions presented 丢弃所有显示的指令

当完成后,一个被耗尽的执行单元将处于Execute::DrainAllInsts状态,它将继续丢弃指令,但对模型其他部分的耗尽状态一无所知。

Debug options

该模型提供了一些调试标志,可以通过--debug-flags选项传递给gem5。
可用的标志是:

组标志 Minor 启用所有以 Minor 开头的标志

Debug flag Unit which will generate debugging output
Activity Debug ActivityMonitor actions
Branch Fetch2 and Execute branch prediction decisions
MinorCPU CPU global actions such as wakeup/thread suspension
Decode Decode
MinorExec Execute behaviour
Fetch Fetch1 and Fetch2
MinorInterrupt Execute interrupt handling
MinorMem Execute memory interactions
MinorScoreboard Execute scoreboard activity
MinorTrace Generate MinorTrace cyclic state trace output (see below)
MinorTiming MinorTiming instruction timing modification operations

组标志Minor启用所有以Minor开头的标志

MinorTrace and minorview.py

调试标志 MinorTrace 导致逐个周期的状态数据被打印出来,然后可以被 minorview.py 工具处理和查看。这个输出是非常冗长的,所以建议只用于小的例子。

MinorTrace format

MinorTrace 输出三种类型的行:

MinorTrace - Ticked unit cycle state

For example:

 110000: system.cpu.dcachePort: MinorTrace: state=MemoryRunning in_tlb_mem=0/0

对于每个时间步长,MinorTrace标志将导致为模型中每个命名的元素打印一条MinorTrace线。

MinorInst - summaries of instructions issued by Decode

Decode

For example:

 140000: system.cpu.execute: MinorInst: id=0/1.1/1/1.1 addr=0x5c \\
                             inst="  mov r0, #0" class=IntAlu

目前只对已提交的指令生成MinorInst行。

MinorLine - summaries of line fetches issued by Fetch1

Fetch1

For example:

  92000: system.cpu.icachePort: MinorLine: id=0/1.1/1 size=36 \\
                                vaddr=0x5c paddr=0x5c

minorview.py

Minorview(util/minorview.py)可用于可视化由MinorTrace创建的数据。

usage: minorview.py [-h] [--picture picture-file] [--prefix name]
                   [--start-time time] [--end-time time] [--mini-views]
                   event-file

Minor visualiser

positional arguments:
  event-file

optional arguments:
  -h, --help            show this help message and exit
  --picture picture-file
                        markup file containing blob information (default:
                        <minorview-path>/minor.pic)
  --prefix name         name prefix in trace for CPU to be visualised
                        (default: system.cpu)
  --start-time time     time of first event to load from file
  --end-time time       time of last event to load from file
  --mini-views          show tiny views of the next 10 time steps

原始调试输出可以作为事件文件传递给 minorview.py。它将挑出MinorTrace行,并使用其他在仿真中被命名为单元的行(如上例中的system.cpu.dcachePort),当单元被点击在可视化显示器上时,将作为 "注释 "出现。

点击一个包含指令或线条的单元,会出现一个气泡,提供从MinorInst/MinorLine行中得到的额外信息。

  • start-time和end-time允许只加载调试文件的部分。
  • prefix允许提供要检查的CPU的名称前缀。它的默认值是 "system.cpu"。

在可视化器中,开始、结束、后退、前进、播放和停止按钮可以用来控制显示的模拟时间。

对角线上的彩色块显示的是它们所代表的指令或行的InstId。注意Fetch1和f1ToF2.F中的行只显示行的id字段,Fetch2、f2ToD和decode.inputBuffer中的指令还没有执行序列号。T/S.P/L/F.E按钮可以用来切换InstId的部分内容,使其更容易理解显示内容。有用的组合是。

Combination Reason
E 只显示最终执行序列号
F/E 显示与指令相关的数字
S/P 仅显示与流相关的数字(观察流序列随分支变化而不随预测分支变化)
S/E 显示指令及其流

右边的键显示了所有可显示的颜色(有些颜色的选择相当糟糕!)。

Symbol Meaning
U Uknown data
B Blocked stage
- Bubble
E Empty queue slot
R Reserved queue slot
F Fault
r Read (used as the leftmost stripe on data in the dcachePort)
w Write “ “
0 to 9 last decimal digit of the corresponding data
    ,---------------.         .--------------.  *U
    | |=|->|=|->|=| |         ||=|||->||->|| |  *-  <- Fetch queues/LSQ
    `---------------'         `--------------'  *R
    === ======                                  *w  <- Activity/Stage activity
                              ,--------------.  *1
    ,--.      ,.      ,.      | ============ |  *3  <- Scoreboard
    |  |-\\[]-\\||-\\[]-\\||-\\[]-\\| ============ |  *5  <- Execute::inFlightInsts
    |  | :[] :||-/[]-/||-/[]-/| -. --------  |  *7
    |  |-/[]-/||  ^   ||      |  | --------- |  *9
    |  |      ||  |   ||      |  | ------    |
[]->|  |    ->||  |   ||      |  | ----      |
    |  |<-[]<-||<-+-<-||<-[]<-|  | ------    |->[] <- Execute to Fetch1,
    '--`      `'  ^   `'      | -' ------    |        Fetch2 branch data
             ---. |  ---.     `--------------'
             ---' |  ---'       ^       ^
                  |   ^         |       `------------ Execute
  MinorBuffer ----' input       `-------------------- Execute input buffer
                    buffer

阶段显示当前正在生成/处理的指令的颜色。

阶段之间的前向FIFO显示当前时刻(左侧)推入它们的数据、正在传输的数据以及可在其输出(右侧)获得的数据。

Fetch2和Fetch1之间的反向FIFO显示分支预测数据。

通常,所有显示的数据都是在指示的时间点结束周期活动之前但在阶段间FIFO被滴答之前正确的。因此,每个FIFO都有一个额外的槽来显示断言的新输入数据以及当前FIFO中的所有数据。

每个阶段的输入缓冲区显示在相应阶段的下方,并以水平条状显示这些缓冲区的内容。默认情况下标记为保留(青色)的条带是保留供上一阶段填充的。因此,所有保留或占用槽的输入缓冲区将阻止上一阶段生成输出。

取指队列和LSQ显示接口队列中的行/指令,并以框架顶部的两种条纹颜色显示TLB和内存中的行/指令数量。

在Execute内部,水平条表示单个FU管道。左侧的垂直条是输入缓冲区,右侧的条是本周期提交的指令。 Execute的背景显示本周期在其原始FU管道位置提交的指令。

Execute块顶部的条带显示Execute正在提交的当前streamSeqNum。 Fetch1顶部的类似条纹显示该阶段的预期streamSeqNum,Fetch2顶部的条纹显示其发行predictionSeqNum。

记分牌显示正在执行的指令数,这些指令将把结果提交到所示位置的寄存器中。记分牌包含每个整数和浮点寄存器的槽。

Execute::inFlightInsts 队列显示了 Execute 中所有正在执行的指令,最旧的指令(即下一个要提交的指令)在最右边。

Stage activity "显示每个阶段的信号活动(如E/1)(CPU的杂项活动在左边)。

Activity "显示阶段和管道活动的计数。

minor.pic format

minor.pic文件(src/minor/minor.pic)描述了展示台上模型块的布局。它的格式在提供的minor.pic文件中描述。

TraceCPU

概述

Trace CPU 模型回放弹性跟踪,这些跟踪是由附加到 O3 CPU 模型的 Elastic Trace Probe 生成的依赖性和时序注释跟踪。Trace CPU 模型的重点是以快速且合理准确的方式实现内存系统(高速缓存层次结构、互连和主内存)性能探索,而不是使用详细但缓慢的 O3 CPU 模型。这些跟踪是为在 SE 和 FS 模式下模拟的单线程基准测试而开发的。通过将 Trace CPU 与经典内存系统以及不同的缓存设计参数和 DRAM 内存类型连接起来,它们已与 15 个内存敏感的 SPEC 2006 基准测试和少数 HPC 代理应用程序相关联。一般来说,elastic traces可以移植到其他仿真环境。

出版

使用 Elastic Traces 探索系统性能:快速、准确和便携”, Radhika Jagtap、Stephan Diestelhorst、Andreas Hansson、Matthias Jung 和 Norbert Wehn SAMOS,2016 年

跟踪生成和重放方法

Image in a image block

Elastic Trace Generation

Elastic Trace Probe Listener 侦听插入到 O3 CPU 管道阶段的探测点。它监控每条指令,并通过记录数据 Read-After-Write 依赖关系以及加载和存储之间的顺序依赖关系来创建依赖图。它将指令获取请求跟踪和弹性数据内存请求跟踪写入两个单独的文件,如下所示。

Image in a image block
Trace file formats跟踪文件格式

弹性数据内存跟踪和获取请求跟踪都使用 google protobuf 编码。

protobuf 格式的 Elastic Trace 字段
字段 描述
required uint64 seq_num 用作跟踪依赖项的 id 的指令编号
required RecordType type RecordType 枚举值:INVALID、LOAD、STORE、COMP
optional uint64 p_addr 如果指令是加载/存储,则为物理内存地址
optional uint32 size 如果指令是加载/存储,则以字节为单位的数据大小
optional uint32 flags 访问的标志或属性,例如。不可缓存
required uint64 rob_dep 顺序(ROB)依赖关系的过去指令号
required uint64 comp_delay 最后一个依赖项完成与指令执行之间的执行延迟
repeated uint64 reg_dep 与 RAW 数据相关的过去指令编号
optional uint32 weight 考虑被过滤掉的提交指令
optional uint64 pc 指令地址,即程序计数器
optional uint64 v_addr 指令是加载/存储时的虚拟内存地址
optional uint32 asid 地址空间 ID

Python 中的解码脚本可用于以util/decode_inst_dep_trace.pyASCII 格式输出跟踪。

ASCII 中的跟踪示例

1,356521,COMP,8500::
2,35656,1,COMP,0:,1:
3,35660,1,LOAD,1748752,4,74,500:,2:
4,35660,1,COMP,0:,3:
5,35664,1,COMP,3000::,4
6,35666,1,STORE,1748752,4,74,1000:,3:,4,5
7,35666,1,COMP,3000::,4
8,35670,1,STORE,1748748,4,74,0:,6,3:,7
9,35670,1,COMP,500::,7

指令获取跟踪中的每条记录都有以下字段。

字段 描述
required uint64 tick 访问时间戳
required uint32 cmd 读或写(在这种情况下总是读)
required uint64 addr 物理内存地址
required uint32 size 以字节为单位的数据大小
optional uint32 flags 访问的标志或属性
optional uint64 pkt_id 访问的id
optional uint64 pc 指令地址,即程序计数器

Python 中的解码脚本util/decode_packet_trace.py可用于以 ASCII 格式输出跟踪。

编译依赖项

您需要安装 google protocol buffer,因为跟踪是使用它来记录的。

sudo apt-get install protobuf-compiler

sudo apt-get install libprotobuf-dev

Scripts and options脚本和选项
  • SE模式
    • build/ARM/gem5.opt [gem5.opt options] -d bzip_10Minsts configs/example/se.py [se.py options] --cpu-type=arm_detailed --caches --cmd=$M5_PATH/binaries/arm_arm/linux/bzip2 --options=$M5_PATH/data/bzip2/lgred/input/input.source -I 10000000 --elastic-trace-en --data-trace-file=deptrace.proto.gz --inst-trace-file=fetchtrace.proto.gz --mem-type=SimpleMemory
  • FS 模式:为您感兴趣的区域创建检查点并从检查点恢复,但启用 O3 CPU 模型和跟踪
    • build/ARM/gem5.opt --outdir=m5out/bbench ./configs/example/fs.py [fs.py options] --benchmark bbench-ics
    • build/ARM/gem5.opt --outdir=m5out/bbench/capture_10M ./configs/example/fs.py [fs.py options] --cpu-type=arm_detailed --caches --elastic-trace-en --data-trace-file=deptrace.proto.gz --inst-trace-file=fetchtrace.proto.gz --mem-type=SimpleMemory --checkpoint-dir=m5out/bbench -r 0 --benchmark bbench-ics -I 10000000

Replay with Trace CPU

上面生成的执行跟踪随后被 Trace CPU 使用,如下图所示。

Image in a image block

Trace CPU 模型继承自 Base CPU,并与数据和指令 L1 高速缓存接口。Trace CPU 的图表解释了主要的逻辑和控制块,如下所示。

Image in a image block
Scripts and options脚本和选项
  • 示例文件夹中的跟踪回放脚本可用于回放 SE 和 FS 生成的跟踪
    • build/ARM/gem5.opt [gem5.opt options] -d bzip_10Minsts_replay configs/example/etrace_replay.py [options] --cpu-type=trace --caches --data-trace-file=bzip_10Minsts/deptrace.proto.gz --inst-trace-file=bzip_10Minsts/fetchtrace.proto.gz --mem-size=4GB
字段 描述
required uint64 seq_num 访问时间戳
required RecordType type 读或写(在这种情况下总是读)
optional uint64 p_addr 如果指令是加载/存储,则为物理内存地址
optional uint32 size 如果指令是加载/存储,则以字节为单位的数据大小
optional uint32 flags 访问的标志或属性,例如。不可缓存
required uint64 rob_dep 顺序(ROB)依赖关系的过去指令号
required uint64 comp_delay 最后一个依赖项完成与指令执行之间的执行延迟
repeated uint64 reg_dep 与 RAW 数据相关的过去指令编号
optional uint32 weight 考虑被过滤掉的提交指令
optional uint64 pc 指令地址,即程序计数器
optional uint64 v_addr 指令是加载/存储时的虚拟内存地址
optional uint32 asid 地址空间 ID

Visualization可视化

此页面包含有关集成或可与 gem5 一起使用的不同类型的信息可视化的信息。

O3 管道查看器

o3 管道查看器是乱序 CPU 管道的基于文本的查看器。它显示指令何时被提取 (f)、解码 (d)、重命名 (n)、分派 (p)、发出 (i)、完成 (c) 和退役 (r)。这对于理解流水线在合理的小代码序列中停滞或压缩的位置非常有用。在环绕的彩色查看器旁边是当前指令退出的标记、该指令的 pc、它的反汇编以及该指令的 o3 序列号。

Image in a image block

要生成上面看到的输出行,您首先需要使用 o3 cpu 运行一个实验:

./build/ARM/gem5.opt --debug-flags=O3PipeView --debug-start=<first tick of interest> --debug-file=trace.out configs/example/se.py --cpu-type=detailed --caches -c <path to binary> -m <last cycle of interest>

然后你可以运行脚本来生成类似于上面的跟踪(在这种情况下,500 是每个时钟 (2GHz) 的滴答数):

./util/o3-pipeview.py -c 500 -o pipeview.out --color m5out/trace.out

您可以通过 less 管道文件来查看彩色输出:

less -r pipeview.out

当 CYCLE_TIME (-c) 错误时,输出中的右方括号可能不会与同一列对齐。CYCLE_TIME 的默认值为 1000。请注意。

该脚本有一些额外的集成帮助:(键入“./util/o3-pipeview.py –help”以获得帮助)。

Building EXTRAS

EXTRAS SCons选项是一种在gem5中添加功能而不将文件添加到gem5源代码树中的方法。具体而言,它允许您确定一个或多个目录,这些目录将被编译到gem5中,就像它们出现在gem5树的 "src "部分一样,而不要求代码实际位于 "src "之下。它的存在是为了允许用户编译没有或不能随gem5分发的额外功能(通常是额外的SimObject类)。这对于维护不适合纳入gem5源码树的本地代码,或由于不兼容的许可证而无法纳入的第三方代码是非常有用的。由于EXTRAS位置完全独立于gem5仓库,你也可以将代码放在不同的版本控制系统下。

EXTRAS功能的主要缺点是,就其本身而言,它只支持向gem5添加代码,不支持修改任何gem5的基本代码。

EXTRAS功能的一个用途是支持EIO跟踪。EIO的跟踪读取器是根据SimpleScalar许可证授权的,由于该许可证与gem5的BSD许可证不兼容,读取这些跟踪的代码不包括在gem5发行版中。相反,EIO代码是通过一个单独的 "担保 "库发布的。

下面的例子展示了如何编译EIO代码。通过添加或修改extra路径,任何其他合适的extra都可以被编译进来。要编译使用EXTRAS的代码,只需执行以下程序

scons EXTRAS=/path/to/encumbered build/<ISA>/gem5.opt

在这个目录的根部,你应该有一个SConscript,它使用M5其他部分使用的Source()SimObject()scons函数来编译适当的源,并添加任何感兴趣的SimObjects。如果你想添加一个以上的目录,你可以将EXTRAS设置为一个用冒号分隔的路径列表。

请注意,EXTRAS是一个 "粘性 "参数,所以在向scons提供了一个值之后,只要没有被覆盖,这个值就会在以后针对同一构建目录(本例中为build/<ISA>)的scons调用中被重复使用。因此,你只需要在第一次构建一个特定的配置时指定EXTRAS,或者当你想覆盖一个先前指定的值时。要运行带有EXTRAS的回归,请使用类似以下的命令行。

./util/regress --scons-opts = "EXTRAS=/path/to/encumbered" -j 2 quick

Execution basic

gem5 bootcamp 2022 module on instruction execution

gem5训练营(2022年)有一场关于学习gem5中指令如何工作以及如何在gem5中添加新指令的会议。会议中展示的幻灯片可以在这里找到。
关于gem5指令的bootcamp模块录制的youtube视频可以在这里找到。

StaticInsts

StaticInst提供二进制指令的所有静态信息和方法。
它持有以下信息/方法。

  • 告诉你这是哪种指令(整数、浮点、分支、内存障碍,等等)。
  • 该指令的操作类别
  • 源寄存器和目的寄存器的数量
  • 使用的整数和浮点寄存器的数量
  • 将二进制指令解码为StaticInst的方法
  • 虚拟函数execute(),它定义了一条指令的具体结构动作(例如,读取r1、r2,将它们相加并存储在r3。)
  • 处理启动和完成内存操作的虚拟函数
  • 对于将内存操作分成两个操作的模型,分别执行地址计算和内存访问的虚拟函数
  • 拆解指令的方法,将其以人类可读的格式打印出来。(例如:addq r1 r2 r3)

它没有动态信息,如指令的PC或源寄存器的值或结果。这使得StaticInst与独特的二进制机器指令的映射是1比1。我们利用这一事实,将二进制指令与StaticInst的映射缓存在hash_map中,允许我们只对二进制指令进行一次解码,其余时间则直接使用StaticInst。

每条ISA指令都派生自StaticInst,并实现了自己的构造函数、execute()函数,如果它是一条内存指令,则实现了内存访问函数。参见ISA_description_system以了解关于如何指定这些ISA指令的细节。

DynInsts

DynInst是用来保存指令的动态信息的。这对于更详细的模型或失序模型来说是必要的,这两种模型可能需要超出StaticInsts的额外信息,以便正确执行指令。它所存储的一些动态信息包括。

  • 该指令的PC
  • 源寄存器和目的寄存器的重名索引
  • 预测的下一个PC
  • 指令的结果
  • 该指令的线程编号
  • 该指令在哪个CPU上执行
  • 该指令是否被压制

此外,DynInst还提供了ExecContext接口。当ISA指令被执行时,DynInst被作为ExecContext传入,处理ISA对CPU状态的所有访问。

详细的CPU模型可以从DynInst派生,并创建他们自己的特定的DynInst子类,实现任何可能需要的额外状态或功能。参见 src/cpu/o3/alpha/dyn_inst.hh 以了解这方面的例子。

Microcode support

ExecContext

ExecContext描述了ISA用来访问CPU状态的接口。尽管有一个src/cpu/exec_context.hh文件,但它纯粹是为了说明问题,类并不从它派生。相反,ExecContext是一个隐含的接口,由ISA承担。

ExecContext接口提供了以下方法

  • 读取和写入PC信息
  • 读取和写入整数、浮点和控制寄存器
  • 读取和写入内存
  • 记录并返回内存访问的地址,预取,并触发系统调用
  • 触发一些全系统模式的功能
  • ExecContext接口的实例实现包括。
  • SimpleCPU
  • DynInst

关于指令集如何实现的更多细节,见ISA描述页。

ThreadContext

ThreadContext是对CPU之外的任何东西的线程的所有状态的接口。它提供了读取或写入外部对象可能需要的状态的方法,如PC、下一个PC、整数和FP寄存器,以及IPRs。它还提供了获取重要的线程相关类指针的函数,如ITB、DTB、System、内核统计和内存端口。它是一个抽象的基类;CPU必须通过从它派生或使用模板化的ProxyThreadContext类来创建自己的ThreadContext。

ProxyThreadContext

ProxyThreadContext类提供了一种实现ThreadContext的方法,而不需要从它派生出来。ThreadContext是一个抽象类,所以任何从它派生出来并使用其接口的东西都将支付虚拟函数调用的开销。创建这个类是为了让用户定义的Thread对象能够在任何使用ThreadContext的地方被使用,而当它被自己使用时,无需支付虚拟函数调用的开销。用户定义的对象必须简单地提供所有与正常ThreadContext相同的功能,而ProxyThreadContext将转发所有对用户定义对象的调用。关于使用ProxyThreadContext的例子,请看SimpleThread的代码。

Difference vs. ExecContext

ThreadContext与ExecContext略有不同。ThreadContext提供了对单个线程状态的访问;ExecContext提供了ISA对CPU的访问(意味着它在SMT系统上是隐式多线程的)。此外,ThreadState是一个抽象类,它确切地定义了接口;ExecContext是一个更隐含的接口,它必须被实现,以便ISA可以访问它需要的任何状态。访问状态的函数调用在二者之间略有不同。ThreadContext提供了读/写寄存器方法,该方法接收了一个架构寄存器索引。ExecContext提供读/写寄存器方法,接收一个StaticInst和一个索引,其中索引指的是该StaticInsts的第i个源或目标寄存器。此外,ExecContext提供了访问内存的读写方法,而ThreadContext不提供任何访问内存的方法。

ThreadState

ThreadState类用于保存各CPU模型中通用的线程状态,如线程ID、线程状态、内核统计、内存端口指针,以及一些完成指令数量的统计。每个CPU模型都可以从ThreadState派生出来,并在此基础上,加入被认为合适的线程状态。这方面的一个例子是SimpleThread,其中所有线程的架构状态都被添加进去了。然而,所有线程的状态都集中在ThreadState派生类中是没有必要的(甚至在某些情况下是不可行的)。DetailedCPU在ThreadState之外的自己的类中保存寄存器值和重命名地图。ThreadState只是用来提供一种更方便的方式来集中定位一些状态,并提供跨CPU模型的共享。

Faults Registers Register types - float, int, misc

Indexing - register spaces stuff

请看寄存器索引,以获得更彻底的处理。

CPU模型中的扁平化和寄存器索引的 "镍币之旅"。

首先,一条指令已经确定它需要这样那样的寄存器,这是由它的编码决定的(或者它总是使用某个寄存器,或者...)。为了便于讨论,我们假设我们谈论的是SPARC,寄存器是%g1,并且第二组globals是活动的。从指令的角度来看,未平移的寄存器是%g1,很可能,它只是由索引1表示。

接下来,我们需要从指令对寄存器文件的看法映射到实际的存储位置。可以把这看作是虚拟内存。指令在索引空间内工作,这就像一个虚拟地址空间,它需要被映射到扁平化的空间,这就像物理内存。在这里,索引1可能被映射到,比如说,9,其中0-7是第一组globals,8-15是第二组。

这就是CPU介入的地方。索引9指的是指令期望访问的一个实际的寄存器,而CPU的工作就是让它发生。在这之前,所有的工作都是由ISA完成的,CPU无法了解;在这之后,所有的工作都由CPU完成,ISA也无法了解。

CPU可以自由地像简单的CPU那样直接提供一个寄存器,即拥有一个数组,并只是代表指令读写第9个元素。另外,CPU还可以做一些复杂的事情,比如重命名并将扁平化的索引进一步映射到一个物理寄存器中,如O3

所有这些的一个重要属性,如果你考虑到虚拟内存的类比,是扁平化之前的索引空间的大小与扁平化之后的大小没有关系。虚拟内存空间可能非常大(估计有间隙),并映射到一个较小的物理空间,或者它可能很小,并映射到一个较大的物理空间,其中多余的是用于,例如,其他时间使用的其他虚拟空间。你需要确保你使用正确的尺寸(扁平化后)来确定你的表的大小,因为这就是可能的选择空间。

还有一个棘手的部分来自于这样一个事实,即我们在索引中添加了偏移量,以区分ints和floats和miscs。这些偏移量在扁平化前的世界里可能是一件事,但在扁平化后的世界里却需要是另一件事,以防止东西落在彼此的上面而不留下空隙。在这里很容易犯错误,这也是我不喜欢用这种偏移的想法来保持不同类型的分离的原因之一。我宁愿看到一个二维的索引,其中第二个坐标是一个寄存器类型。但是,在今天的世界上,这是一个你必须保持跟踪的东西。

PCs

Register Indexing

gem5中的CPU寄存器索引是一个复杂的问题,因为它需要支持多种ISA,有时具有非常不同的寄存器语义(寄存器窗口、条件代码、基于模式的备用寄存器集等)。此外,随着新的ISA的加入,这种支持也在逐步发展,所以旧的代码可能无法利用较新的功能或术语。

Types of Register Indices

在CPU模型中,有三种内部使用的寄存器索引:相对的、统一的和扁平化的。

Relative

相对的寄存器索引是指在机器指令中被编码的索引。每一类寄存器(整数、浮点等)都有一个单独的索引空间,从0开始。 寄存器类别是由操作码暗示的。因此,在源寄存器字段中的 "1 "值可能意味着整数寄存器1(例如,"%r1")或浮点寄存器1(例如,"%f1"),这取决于指令的类型。

Unified

虽然相对寄存器索引有利于保持指令编码的紧凑性,但它们是不明确的,因此对于管理依赖关系等事情并不方便。为了避免这种模糊性,解码器通过添加特定类别的偏移量将相对寄存器索引映射到一个统一的寄存器空间,从而将每个相对索引范围重新定位到一个独特的位置。整数寄存器不做修改,继续从零开始。浮点寄存器的索引被偏移(至少)整数寄存器的数量,所以第一个FP寄存器(例如,"%f0")得到的统一索引要大于最后一个整数寄存器的索引。类似地,杂项(又称控制)寄存器被映射到FP寄存器索引空间的末端之后。

Flattened

统一的寄存器索引提供了对所有寄存器的明确描述,这些寄存器在执行过程中的某一点可以作为指令操作数访问。不幸的是,由于一些ISA的复杂特性,它们并不总是能明确地识别指令所参考的实际状态。例如,在带有寄存器窗口的ISA中(特别是SPARC),一个特定的寄存器标识符,如"%o0",在 "保存 "或 "恢复 "操作后会指向一个与之前不同的寄存器。有几个ISA的寄存器在正常操作中是隐藏的,但是当中断发生时,它们会被映射到普通的寄存器上(例如ARM的特定模式寄存器),或者在明确的监督者控制下(例如SPARC的 "备用球")。

我们通过维护一个扁平化的寄存器空间来解决这个问题,该空间为每一个独特的寄存器存储位置提供一个独特的索引。例如,SPARC的扁平化寄存器空间的整数部分为全局和备用全局,以及每个可用的寄存器窗口提供了不同的索引。从统一的或相对的寄存器索引转换到扁平化的寄存器索引的 "扁平化 "过程因ISA而异。在一些ISA上,这种映射是微不足道的,而其他ISA则使用表格查询来进行转换。

统一寄存器索引和扁平化寄存器索引生成之间的一个关键区别是,前者总是可以静态地完成,而后者往往取决于动态处理器状态。也就是说,从相对指数到统一指数的转换只取决于指令本身提供的上下文(这很方便,因为转换是在解码器中完成的)。相反,对扁平化寄存器索引的映射可能取决于处理器的状态,如SPARC上的中断级别或当前窗口指针。

Combining Register Index Types

虽然修改寄存器索引的典型过程是相对的->统一的->扁平的,但事实证明,相对的与统一的和扁平的与非扁平的是正交的属性。相对与统一表示索引是相对于其寄存器类别(整数、FP或杂项)的基础寄存器,还是加入了其类别的基础偏移。Flattened vs. unlattened表示索引是否被调整以考虑到运行时的环境,如寄存器窗口的调整或替代的寄存器文件模式。因此,一个相对扁平化的寄存器索引是一个已经考虑了运行时环境的索引,但仍然是相对于其类的基本偏移量表示的。

一组特定于类的偏移量被用来从相对索引中生成统一的索引,而不管这些索引是扁平化的还是未扁平化的。因此,即使在使用扁平化地址时,偏移量也必须大到足以分离寄存器类别。因此,未扁平化的统一寄存器空间往往是不连续的。

Illustrations

作为一个例子,考虑一个假设的架构,有四个整数寄存器(%r0-%r4),三个FP寄存器(%f0-%f2),和两个杂项/控制寄存器(%msr0-%msr1)。此外,该架构支持一套完整的备用整数和FP寄存器,用于快速上下文切换。

由此产生的寄存器文件布局,以及统一的扁平化的寄存器文件索引,显示在右边。虽然图片中的索引范围是0到15,但实际的有效索引集取决于索引的类型和(对于相对索引)寄存器的类别,以及。

在这个例子中,备用FP寄存器文件中的寄存器%f1可以通过相对扁平化的索引4以及相对未扁平化的索引1、统一未扁平化的索引9或统一扁平化的索引12来引用。请注意,相对指数和统一指数之间的差值总是8(不管扁平化如何),而未扁平化和扁平化指数之间的差值是3(不管相对与统一的状态如何)。

Image in a image block

Caveats

  • 尽管不幸的是,gem5代码并不总是清楚某个特定的函数期望哪种类型的寄存器索引,但其名称包含了一个寄存器类的函数(例如readIntReg())期望一个相对的寄存器索引,而期望一个扁平化索引的函数通常在函数名称中带有 "扁平"。
  • 虽然一般情况很复杂,但普通情况可能很简单。例如,因为整数寄存器从统一寄存器空间的开始,相对和统一寄存器的索引对于整数寄存器是相同的。此外,在一个没有(或很少使用)备用整数寄存器的架构中,未扁平化和扁平化的指数(几乎总是)也是一样的,这意味着在这种情况下,所有四种类型的寄存器指数都是可以互换的。虽然这种情况似乎是一种简化,但它也倾向于隐藏使用了错误的寄存器索引类型的bug。
  • 上面的描述是为了说明这些索引类型的典型用法。可能会有不完全遵循这一描述的例外情况,但我已经厌倦了在每一句话中都写 "典型"。
  • 相对 "和 "统一 "这两个术语是为了在本文档中使用而发明的,所以在代码中你不太可能看到它们,直到代码开始赶上本页面。
  • 这个讨论只涉及到架构寄存器。一个失序的CPU模型,如O3,通过将这些架构寄存器(使用扁平化的寄存器索引)重命名到底层物理寄存器文件,增加了另一层复杂性。

ISA and CPU Independence

gem5试图保持CPU模型的ISA独立性,以使其更容易与不同的CPU模型使用任何ISA。gem5依靠两个通用接口使这种独立性成为可能:静态指令和执行上下文(两者都在上面讨论)。静态指令允许CPU管理指令,执行上下文允许ISA或指令与CPU交互。以下图片提供了gem5中哪些组件是依赖ISA或独立的高层次概述。

Image in a image block

上图来源: Modular ISA-Independent Full-System Simulation (Ch 5 of Processor and System-on-Chip Simulation), G. Black, N. Binkert, and S. Reinhardt, A. Saidi. Link.

ISA Parser

gem5 ISA描述语言是一种专门为生成gem5所需的类定义和解码器功能而设计的定制语言。本节提供了对该语言本身的实用、非正式的概述。该语言的正式语法被嵌入到解析器的 "yacc "部分(在isa_parser.py中寻找以p_开头的函数)。解析器的第二个主要部分是处理类似C语言的代码规范,以提取指令特征;这方面的内容将在代码解析部分介绍。在最高层次上,ISA描述文件被分为两部分:声明部分和解码部分。解码部分规定了解码器的结构并定义了解码器返回的具体指令。声明部分定义了支持解码器所需的全局信息(类、指令格式、模板等)。由于解码部分是描述文件的重点,我们将从这里开始讨论。

The decode section

描述中的解码部分是一组嵌套的解码块。一个解码块指定要解码的机器指令的一个字段,以及为该字段的特定值提供的结果。解码块在语法和语义上都类似于C语言的开关语句。事实上,描述文件中的每个解码块都会在生成的解码函数中产生一个开关语句。让我们从一个(略微简化的)例子开始。

decode OPCODE {
  0: add({{ Rc = Ra + Rb; }});
  1: sub({{ Rc = Ra - Rb; }});
}

一个解码块以关键字decode开始,后面是要解码的指令字段的名称。后者必须在文件的声明部分用位域定义来定义(见位域定义)。解码块的其余部分是一个用大括号括起来的语句列表。最常见的语句是一个整数常数和一个冒号,后面是一个指令定义。这条语句相当于C语言开关中的 "case "语句(但注意,为了简洁起见,省略了 "case "关键词)。一个用逗号分隔的整数常量列表可以用来让一个解码语句适用于一组比特字段值中的任何一个。

指令定义的语法类似于C语言的函数调用,指令的助记符代替了函数名。逗号分隔的参数在处理指令定义时被使用。在上面的例子中,指令定义都有一个参数,即 "代码字面"。代码字头在操作上类似于字符串常量,但由双括号({{和}})划定。代码字头可以跨越多行而不需要转义行末字符。不进行反斜线转义处理(例如,\t是按字面意思理解的,不产生制表符)。分隔符的选择是为了使包含在代码字面中的类C代码能够被emacs的C模式很好地格式化。

解码语句可以指定一个嵌套的解码块来代替指令定义。在这种情况下,如果外部块指定的位域与给定的值相匹配,内部块指定的位域将被检查,并执行额外的切换。

在C语言中,使用关键字default代替整数常数来定义一个默认动作也是合法的。然而,更常见的是使用解码块缺省语法,在下面的解码块缺省一节中讨论。

Specifying instruction formats

当ISA描述文件被处理时,每条指令的定义事实上都调用了一个函数,为解码文件生成适当的C++代码。被调用的函数是由指令格式决定的。指令格式决定了给指令定义的参数的数量和类型,以及如何处理它们以生成相应的输出。请注意,在这里使用的术语 "指令格式 "仅指这些定义处理功能之一,而不一定与ISA定义的机器指令格式一一对应。在前面的例子中,有一个过度简化的现象,就是没有指定指令格式。因此,解析器不知道如何处理指令定义。

指令格式可以通过两种方式指定。可以在助记符之前给出明确的格式说明,用双冒号(::)分开,如下所示。

decode OPCODE {
  0: Integer::add({{ Rc = Ra + Rb; }});
  1: Integer::sub({{ Rc = Ra - Rb; }});
}

在这个例子中,两条指令的定义都将用Integer格式来处理。一个更常见的方法是使用格式块来指定一组定义的格式,如下所示。

decode OPCODE {
  format Integer {
    0: add({{ Rc = Ra + Rb; }});
    1: sub({{ Rc = Ra - Rb; }});
  }
}

在这个例子中,"Integer "的格式适用于内括号内的所有指令定义。因此,这两个例子在功能上是等同的。对格式块的使用没有什么限制。一个格式块可以只包括解码块中语句的一个子集。格式块和显式格式规范可以自由混合,后者优先。格式块和解码块可以任意嵌套在彼此之间。请注意,闭合括号总是与最近的格式块或解码块绑定,这使得语法上不可能产生不完全嵌套在封闭块内的格式块或解码块。

在指令定义发生的任何地方,如果没有明确的格式说明,将使用与最里面的封闭格式块相关的格式。如果定义中没有明确的格式,也没有封闭的格式块,将产生一个运行时错误。

Decode block defaults

解码块的默认情况可以通过default:标签来指定,就像C语言的开关语句一样。然而,在ISA描述中,未指定的情况对应于未知或非法的指令编码是很常见的。为了避免在每个解码块中都有default:情况的要求,该语言允许一个替代的default:语法,为当前解码块和任何没有明确默认的嵌套解码块指定一个默认情况。这种替代的缺省是通过在比特字段规范之后(在开括号之前)给出缺省关键字和指令定义来指定的。指定最外层的解码块,如下所示。

decode OPCODE default Unknown::unknown() {
   [...]
}

因此(几乎)等同于在每一个没有指定默认情况的解码块中加入default: Unknown::unknown();在每个没有指定默认情况的解码块内。

注意:每次遇到指令定义时都会调用适当的格式定义(见_格式定义)。因此,有一个单一的块级默认值和每个嵌套块内的默认值之间存在语义上的差异,即前者将调用一次格式定义,而后者可能导致格式定义的多次调用。如果格式定义产生头、解码器或执行器输出,那么该输出将被多次包含在相应的文件中,这通常会导致C++被编译时出现多个定义错误。如果绝对有必要多次调用一条指令的格式定义,那么格式定义应该写成只产生解码块输出,所有需要的头、解码器和执行输出都应该用_output块产生一次(见_输出块)。

Preprocessor directive handling

解码块也可以包含C预处理器指令。这些指令不被解析器处理;相反,它们被传递到C++输出,以便在C++解码器编译时进行处理。解析器不识别任何特定的指令;任何在第一列中带有#的行都被视为预处理器指令。这些指令被复制到所有的输出流中(头文件、解码器和执行文件;见格式定义。这些指令保持其相对于解码块内指令定义所产生的代码的位置。净结果是,例如,围绕一组指令定义的#ifdef/#endif对将同时包含这些定义产生的声明和解码函数中相应的case语句。因此,#ifdef和类似的结构可以用来划分指令定义,这些指令定义将根据预处理器的符号(如FULL_SYSTEM)有条件地编译到模拟器中。应该强调的是,#ifdef并不影响ISA描述解析器。在#ifdef/#else/#endif结构中,条件两部分的所有指令定义都将被处理。只有在随后的C++编译解码器的过程中,才会选择其中的一个或另一个定义集。

The declaration section

如上所述,ISA描述的解码部分(由一个外部解码块组成)前面是声明部分。声明部分的主要目的是定义指令格式和其他将在解码块中使用的支持元素,以及几乎逐字传递给生成的输出的支持C++代码。本节介绍了出现在声明部分的组件。 格式定义、模板定义、输出块、让块、位域定义、操作数和操作数类型定义,以及命名空间声明。

Format definitions

An instruction format is basically a Python function that takes the arguments supplied by an instruction definition (found inside a decode block) and generates up to four pieces of C++ code. The pieces of C++ code are distinguished by where they appear in the generated output.

  1. The ‘‘header output’’ goes in the header file (decoder.hh) that is included in all the generated source files (decoder.cc and all the per-CPU-model execute .cc files). The header output typically contains the C++ class declaration(s) (if any) that correspond to the instruction.
  2. The ‘‘decoder output’’ goes before the decode function in the same source file (decoder.cc). This output typically contains definitions that do not need to be visible to the execute() methods: inline constructor definitions, non-inline method definitions (e.g., for disassembly), etc.
  3. The ‘‘exec output’’ contains per-CPU model definitions, i.e., the execute() methods for the instruction class.
  4. The ‘‘decode block’’ contains a statement or block of statements that go into the decode function (in the body of the corresponding case statement). These statements take control once the bit pattern specified by the decode block is recognized, and are responsible for returning an appropriate instruction object.

The syntax for defining an instruction format is as follows:

  1. 指令格式基本上是一个Python函数,它接收由指令定义提供的参数(在解码块中找到),并生成最多四段C++代码。这些C++代码片段通过它们在生成的输出中出现的位置来区分。
  2. 头部输出 "出现在头文件(decoder.hh)中,该文件包含在所有生成的源文件(decoder.cc和所有按CPU模型执行的.cc文件)中。头文件的输出通常包含与指令对应的C++类声明(如果有的话)。解码器输出''在同一源文件(decoder.cc)中的解码函数之前。这个输出通常包含不需要被execute()方法看到的定义:内联构造函数定义、非内联方法定义(例如,用于反汇编),等等。
  3. exec输出''包含每个CPU模型的定义,即指令类的execute()方法。
  4. 解码块''包含进入解码函数的语句或语句块(在相应的case语句的主体中)。一旦解码块指定的位模式被识别,这些语句就会接管控制权,并负责返回一个适当的指令对象。

定义指令格式的语法如下。

def format FormatName(arg1, arg2) {{
    [code omitted]
}};

在这个例子中,该格式被命名为 "FormatName"。(根据惯例,指令格式名称以大写字母开头,并使用混合大小写)。使用这种格式的指令定义将被期望提供两个参数(arg1和arg2)。该语言还支持Python的可变参数机制:如果最后一个参数以星号开始(例如*rest),它将从调用地点接收所有其他未绑定的参数列表。
注意,格式定义中倒数第二的语法标记(在分号之前)只是一个代码字面(字符串常数),如上所述。在这种情况下,代码字面的文字是一个Python代码块。这个Python代码将在每个使用指定格式的指令定义中被调用。

除了明确的参数外,Python代码还提供了两个额外的参数:name,与指令助记符绑定;Name,是第一个字母大写的助记符(对于根据助记符形成C++类名很有用)。

The format code block specifies the generated code by assigning strings to four special variables: header_outputdecoder_outputexec_output, and decode_block. Assignment is optional; for any of these variables that does not receive a value, no code will be generated for the corresponding section. These strings may be generated by whatever method is convenient. In practice, nearly all instruction formats use the support functions provided by the ISA description parser to specialize code templates based on characteristics extracted automatically from C-like code snippets. Discussion of these features is deferred to the Code parsing page.

格式化代码块通过给四个特殊变量分配字符串来指定生成的代码:header_outputDecoder_outputexec_outputdecode_block。赋值是可选的;对于这些变量中的任何一个,如果没有收到一个值,就不会为相应的部分生成代码。这些字符串可以通过任何方便的方法生成。在实践中,几乎所有的指令格式都使用ISA描述解析器提供的支持功能,根据从类似C语言的代码片断中自动提取的特征来专门设计代码模板。关于这些特征的讨论将推迟到代码解析页。

尽管ISA描述完全独立于任何特定的模拟器CPU模型,但一些C++代码(特别是执行输出)必须为每个模型稍作专门化。这种特殊化是通过自动替换CPU模型的特定符号来处理的。这些符号以CPU_开头,被解析器特别处理。目前只有一个特定于模型的符号,CPU_exec_context,它被评估为模型的执行环境类名称。与模板一样 (见模板定义),对 CPU 特定符号的引用使用基于 Python 键的格式字符串;因此对 CPU_exec_context 符号的引用在字符串中显示为 %(CPU_exec_context)s

如果分配给header_outputdecoder_outputdecode_block的字符串包含一个CPU特定的符号参考,该字符串将为每个CPU模型复制一次,每个实例将根据该模型替换其CPU特定的符号。然后将产生的字符串连接起来,形成最终的输出。分配给exec_output的字符串总是为每个CPU模型复制和替换一次,不管它们是否包含CPU特定的符号引用。这些实例不会被串联,而是被单独跟踪,并被放在单独的每个CPU模型文件中(例如,simple_cpu_exec.cc)。

Template definitions

正如上面格式定义一节所讨论的,指令格式的目的是处理指令定义的参数并生成若干C++代码片断。这些代码片断通常是通过专门的代码模板生成的。描述语言为定义这些模板提供了一个简单的语法:关键字def模板、模板名称、模板主体(一个代码字面)和分号。按照惯例,模板名称以大写字母开始,使用混合大小写,并以 "Declare"(用于声明(头输出)模板)、"Decode"(用于解码块模板)、"Constructor"(用于解码器输出模板)或 "Exece"(用于执行输出模板)结束。例如,最简单有用的解码模板如下。

def template BasicDecode {{
    return new %(class_name)s(machInst);
}};

一个指令格式将通过用实际的类名代替%(class_name)s来为一个特定的指令专门设计这个模板。(模板的特殊化依赖于Python的字符串格式操作符%。术语%(class_name)s是C的%s格式字符串的扩展,表示应该替换符号class_name的值)。然后,产生的代码将使C++解码函数在识别特定指令时创建一个指定类别的新对象。

模板在解析器中被表示为 Python 对象。模板通常通过调用模板对象的 subst() 方法来生成一个字符串。这个方法需要一个参数,指定模板中的替换符号 (例如,%(class_name)s) 与特定值的映射。如果参数是一个字典,那么字典本身就指定了这个映射。否则,参数必须是另一个 Python 对象,并且该对象的属性被用作映射。在实践中,subst()的参数几乎总是解析器的 InstObjParams 类的一个实例;见 InstObjParams 类。除了subst()参数指定的符号之外,模板还可以引用其他模板(例如%(BasicDecode)s);这些模板也会被subst()插值到结果中。

对CPU模型特定符号的模板引用(见格式定义)不会被subst()扩展,而是完整地传递。这一特性允许它们以后根据结果是否被分配到exec_output或其他输出部分而被适当地扩展。然而,当一个包含CPU模型特定符号的模板被另一个模板引用时,那么前一个模板会被复制,并在插值前扩展成一个单一的字符串,就像分配给header_output或Decoder_output的模板一样。这个策略保证只有直接包含CPU模型特定符号的模板会被复制,而不是间接包含这种符号的模板。这最后一个特性被用来将每个CPU的execute()方法的声明插到指令类声明模板中(见Alpha ISA描述中的BasicExecDeclare模板)。

Output blocks

输出块允许ISA描述包括几乎逐字复制到输出文件的C++代码。这些块对于定义在多个指令对象中共享的类和局部函数非常有用。一个输出块有以下格式。

output <destination> {{
    [code omitted]
}};

<目的地>关键字必须是头、解码器或执行中的一个。代码字面内的代码被当作是分配给指令格式内的header_output decoder_outputexec_output变量,包括对CPU模型特定符号的特殊处理。对代码字面进行的唯一额外处理是比特域运算符的替换,如指令定义中使用的比特域运算符(见比特域运算符,以及对模板的引用插值。

Let blocks

Let 块提供了全局 Python 代码。这些块由关键字 let 和一个代码字头 (双括号分隔的字符串) 以及一个分号组成。代码字面被 Python 解释器立即执行。解释器保持着跨let块的执行上下文,因此在一个let块中定义的变量和函数可以在随后的let块中访问。这个上下文在执行指令格式定义时也被使用。let块的主要目的是定义共享的Python数据结构和函数,以便在指令格式中使用。解析器将有限的定义集输出到这个执行上下文中,包括定义的模板集 (见模板定义,InstObjParams 和 CodeBlock 类 (见代码解析),以及标准的 Python string 和 re (正则表达式) 模块。

Bitfield definitions

位域定义为机器指令中的位域提供一个名称。这些名称通常被用作解码块中的位域规范。这些名称也被用于解码器文件中的其他C++代码,包括指令类定义和解码代码。在这些例子中演示了比特字段定义的语法。

def bitfield OPCODE <31:26>;
def bitfield IMM <12>;
def signed bitfield MEMDISP <15:0>;

指定的位范围包括两端,第0位是最不重要的位;因此,例子中的OPCODE位域从32位指令中提取最重要的6位。一个索引值可以提取一个位域,IMM。默认情况下,提取的值是零扩展的;如果有额外的带符号的关键字,如MEMDISP的例子,提取的值将被符号扩展。位域的实现是基于预处理器宏和C++模板函数的,所以结果值的大小将取决于上下文。

为了充分了解位域定义的使用范围,我们需要深入了解一下。一个位域定义简单地生成一个C++预处理器宏,从隐式变量machInst中提取指定的位域。 解码函数的机器指令参数也被称为machInst;因此,任何使用位域名称最终出现在解码函数内(如解码块的参数或指令格式输出的解码片)都将隐式引用当前正在解码的指令。存储在StaticInst对象中的二进制机器指令也被命名为machInst,所以任何在指令对象的成员函数中使用位域名称都会引用这个存储值。这个数据成员在StaticInst构造函数中被初始化,所以即使在派生对象的构造函数中使用比特字段名也是安全的。

Operand and operand type definitions

这些语句指定了可以在表达指令功能操作的代码块中使用的操作数类型。参见操作数类型限定符和指令解析。

Namespace declaration

声明部分的最后一个部分是命名空间声明,由关键字namespace和一个标识符以及一个分号组成。在声明部分必须出现一个命名空间的声明。由此产生的C++解码函数,由解码块中的指令定义产生的声明,以及在名字空间声明之后出现的任何声明语句的内容将被放置在具有指定名称的C++名字空间中。在命名空间声明之前出现的声明语句的内容将在命名空间之外。

ISA parser

Formats
operands
decode tree
let blocks
microcode assembler
microops
macroops
directives
rom object
Lots more stuff

Code parsing

在很大程度上,ISA描述机制的力量和灵活性源于这样一个事实:从解码块中提供的简短指令定义到所产生的C++代码的映射是在通用编程语言(Python)中进行的。(这个功能是由上面格式定义中描述的 "指令格式 "定义完成的。从技术上讲,ISA描述语言允许任何任意的Python代码来执行这种映射。然而,解析器提供了一个Python类和函数库,旨在自动从指令操作的简要描述中推断出指令的特性,并生成填充声明和解码模板所需的字符串。这个库大约占isa_parser.py中代码的一半。

指令行为用C++描述,有两个扩展:位域操作符和操作符类型限定符。为了避免在ISA描述系统中建立一个完整的C++解析器(或者反过来限制可用于指令描述的C++),这些扩展使用正则表达式匹配和替换来实现。因此,对它们的使用有一些语法上的限制。下面两节将依次讨论这些扩展。第三节讨论了操作数解析,这是解析器自动推断大多数指令特性的技术。最后两节讨论Python类,指令格式通过这些类与库交互。 CodeBlock,它分析和封装指令描述代码;指令对象参数类InstObjParams,它封装了要被替换到模板中的全部参数。

Bitfield operators

可以使用<:>后缀操作符对r值进行简单的位域提取。位的编号与全局位域定义中使用的一致(见位域定义)。例如,Ra<7:0>提取了寄存器Ra的低8位。单位字段可以通过去掉后一个操作数来指定,例如Rb<31:>。与全局位域定义不同,冒号不能去掉,因为这样就很难区分位域操作符和模板参数。此外,位索引参数必须是标识符或整数常数;不允许使用表达式。位操作符将适用于其左侧的语法标记,或者,如果该标记是一个封闭的小括号,则适用于小括号中的表达式。

Operand type qualifiers

指令操作数(如寄存器)的有效类型可以通过在操作数名称上附加一个句号和一个类型限定符来指定。类型限定符的列表是特定于体系结构的;ISA描述中的def operand_types语句被用来指定它。该说明是以Python字典的形式出现的,它将类型扩展映射到类型名。例如,Alpha ISA的定义如下。

def operand_types {{
    'sb' : 'int8_t',
    'ub' : 'uint8_t',
    'sw' : 'int16_t',
    'uw' : 'uint16_t',
    'sl' : 'int32_t',
    'ul' : 'uint32_t',
    'sq' : 'int64_t',
    'uq' : 'uint64_t',
    'sf' : 'float',
    'df' : 'double'
}};

因此,Alpha 32 位加法指令 addl 可以定义为:

Rc.sl = Ra.sl + Rb.sl;

操作是使用指定的类型进行的;结果将从指定的类型转换为适当的寄存器值(在这种情况下,通过符号将32位的结果扩展为64位,因为Alpha的整数寄存器是64位大小)。

类型限定符只允许在公认的指令操作数上使用(见指令操作数)。

Instruction operands

解析器提供的大部分自动化是基于它对指令定义代码中使用的操作数的识别。大多数相关的指令特性可以从操作数中推断出来:浮点指令与整数指令可以通过使用的寄存器来识别,从内存位置读取的指令是一个加载指令,等等。结合上述的位域操作数和类型限定符,大多数指令可以用一行代码来描述。此外,模拟器CPU模型之间的大部分差异在于操作数的访问机制;通过自动生成这些访问的代码,一个描述就可以满足各种情况。
ISA描述通过def operands语句提供了一个公认的指令操作数及其特征的列表。这个语句指定了一个Python字典,将操作数串映射到一个五元素的元组中。元组中的元素指定操作数如下

  1. 操作数类别,必须是 "IntReg"、"FloatReg"、"Mem"、"NPC "或 "ControlReg "中的一个字符串,分别表示整数寄存器、浮点寄存器、内存位置、下一个程序计数器(NPC)或控制寄存器。
  2. 操作数的默认类型(在def operand_types块中定义的扩展字符串)。
  3. 表示操作数的具体实例如何被解码的指定符(例如,一个位域名称)。
  4. 一个字符串或三个字符串,表示在使用该操作数时可以推断出的指令标志,以及
  5. 一个排序优先级,用于控制反汇编中操作数的顺序。

例如,Alpha ISA操作数特征图的一个简化子集如下

def operands {{
    'Ra': ('IntReg', 'uq', 'RA', 'IsInteger', 1),
    'Rb': ('IntReg', 'uq', 'RB', 'IsInteger', 2),
    'Rc': ('IntReg', 'uq', 'RC', 'IsInteger', 3),
    'Fa': ('FloatReg', 'df', 'FA', 'IsFloating', 1),
    'Fb': ('FloatReg', 'df', 'FB', 'IsFloating', 2),
    'Fc': ('FloatReg', 'df', 'FC', 'IsFloating', 3),
    'Mem': ('Mem', 'uq', None, ('IsMemRef', 'IsLoad', 'IsStore'), 4),
    'NPC': ('NPC', 'uq', None, ( None, None, 'IsControl'), 4)
}};

名为Ra的操作数是一个整数寄存器,默认类型为uq(无符号四字),使用指令中的RA位域,意味着IsInteger指令标志,并且排序优先级为1(在任何操作数列表中置于首位)。

对于指令标志元素,一个单一的字符串(如'IsInteger')意味着一个无条件推断的指令标志。如果标志操作数是一个三联体,第一个元素是无条件的,第二个元素在操作数是源时被推断,第三个元素在它是目的时被推断。因此,内存引用的('IsMemRef', 'IsLoad', 'IsStore')元素表明,任何带有内存操作数的指令都被标记为内存引用。此外,如果内存操作数是一个源,指令被标记为加载,而如果操作数是一个目标,指令被标记为存储。同样,NPC操作数的(None, None, 'IsControl')元组表示任何写入NPC的指令都是控制指令,但是仅仅引用NPC作为源的指令不会收到任何默认标志。

注意,描述代码解析使用正则表达式,这限制了解析器推断部分操作数性质的能力。特别是,目标操作数与源操作数的区别仅仅在于测试操作数是否出现在赋值运算符(=)的左侧。以不同方式被赋值的目的操作数,例如通过引用传递给其他函数,仍然必须出现在赋值的左侧,才能被正确识别为目的操作数。解析器也不能识别C语言的复合赋值,例如+=。如果一个操作数既是源码又是目的码,它必须同时出现在=的左边和右边。

基于正则表达式的代码解析的另一个限制是,代码块中的控制流不被识别。结合CPU模型中如何进行寄存器更新的细节,这意味着目的地不能被有条件地更新。如果一个特定的寄存器被识别为目标寄存器,该寄存器将总是在execute()方法的末尾被更新,因此,代码必须沿着代码块内每个可能的代码路径为该寄存器分配一个有效的值。

The CodeBlock class

一个指令格式通过将字符串传递给CodeBlock构造函数来请求处理一个包含指令描述代码的字符串。构造函数执行所有需要的分析和处理,将结果存储在返回的对象中。在CodeBlock的字段中,包括:

  • orig_code: 原始代码字符串。
  • code: 一个包含合法的C++代码的处理过的字符串,通过替换位域操作符和混合操作符类型限定符(s/./_/),使之成为有效的C++标识符,从而从原始代码中衍生出来.
  • constructor: 指令对象的构造器代码,初始化各种C++对象字段,包括操作数和操作数的寄存器索引.
  • exec_decl: 代码来声明与操作数相对应的C++变量,以用于执行仿真函数中。
  • _rd: 代码将实际操作数的值读入源操作数的相应C++变量。名称的第一部分表示相关的CPU型号(目前支持简单和dtld)。
  • _wb: 代码将C++变量的内容写回适当的寄存器或内存位置。同样,名字的第一部分反映了CPU的模型。
  • _mem_rd_nonmem_rd_mem_wb_nonmem_wb: 同上,但内存和非内存操作数是分开的。
  • flags: 操作数隐含的指令标志集。
  • op_class: 仅仅根据操作数类型,对指令的操作类别(见OpClass)进行基本猜测。
The InstObjParams class

InstObjParams类的实例封装了所有需要代入代码模板的参数,作为模板的subst()方法的参数使用(见模板定义)。

class InstObjParams(object):
    def __init___(self, parser, 
                  mem, class_name, base_class = '',
                  snippets = {}, opt_args = []):

前三个构造函数参数用于填充对象的助记符、class_name和(可选)base_class成员。第四个(可选)参数是一个CodeBlock对象;所提供的CodeBlock对象的所有成员都被复制到新对象中,使它们可以被模板替换。任何其余的参数都被解释为额外的指令标志(附加到从CodeBlock参数继承的标志列表中,如果有的话),或者作为一个操作类(覆盖CodeBlock的任何op_class)。

M5ops

本页解释了M5中可以用来做检查点等的特殊操作码。m5工具程序(在我们的磁盘镜像和util/m5/*中)在命令行中提供了一些这样的功能。在许多情况下,最好是直接在你感兴趣的应用程序的源代码中插入操作。你应该能够与适当的libm5.a文件链接,m5ops.h头文件有所有函数的原型。关于M5ops的使用教程是作为gem5 2022年训练营的一部分进行的。这个活动的录音可以在这里找到。

Building M5 and libm5

为了为你的目标ISA构建m5和libm5.a,在util/m5/目录下运行以下命令。

scons build/{TARGET_ISA}/out/m5

目标 ISA 列表如下所示

  • x86
  • arm (arm-linux-gnueabihf-gcc)
  • thumb (arm-linux-gnueabihf-gcc)
  • sparc (sparc64-linux-gnu-gcc)
  • arm64 (aarch64-linux-gnu-gcc)
  • riscv (riscv64-linux-gnu-gcc)

注意,如果你使用的是X86系统的其他ISA,你需要安装交叉编译器。交叉编译器的名称在上面的列表中的括号内显示。

更多细节见util/m5/README.md。

The m5 Utility (FS mode)

m5工具(见util/m5/)可以在FS模式下使用,以发出特殊指令来触发模拟的特定功能。它目前提供了以下选项。

  • initparam: 已被弃用,仅用于旧的二进制兼容。
  • exit [delay]: 在延迟纳秒内停止模拟。
  • resetstats [delay [period]]。以延迟纳秒为单位重置仿真统计;每隔纳秒重复一次。
  • dumpstats [delay [period]]。以延迟纳秒为单位将模拟统计资料保存到文件中;每隔纳秒重复一次。
  • dumpresetstats [delay [period]]:与dumpstats相同;resetstats
  • checkpoint [delay [period]]。以延迟纳秒创建一个检查点;每隔纳秒重复一次。
  • readfile。打印由配置参数system.readfile指定的文件。rcS文件就是这样被复制到仿真环境中的。
  • debugbreak。在模拟器中调用debug_break()(导致模拟器获得SIGTRAP信号,在用GDB调试时很有用)。
  • switchcpu:引起一个 "switch cpu "类型的退出事件,如果需要的话,允许Python切换到不同的CPU型号。
  • workbegin: 引起一个 "workbegin "类型的退出事件,可用于标记一个投资回报率的开始。
  • workend。引起一个类型为 "workend "的退出事件,可用于标记一个投资回报率的终止。

Other M5 ops

这些是其他的M5操作,在命令行的形式下并不实用。

  • quiesce。不再安排CPU的tick()调用,直到某个异步事件将其唤醒(中断)。
  • quiesceNS:同上,但如果之前没有被唤醒,会在若干纳秒后自动唤醒。
  • quiesceCycles。与上述相同,但使用CPU周期而不是纳秒
  • quisceTIme: CPU被静止的时间量。
  • addsymbol: 添加一个符号到模拟器的符号表。例如,当一个内核模块被加载时

Using gem5 ops in Java code

这些操作也可以在Java代码中使用。这些操作允许在java程序中调用gem5的操作,例如以下内容。

import jni.gem5Op;

public  class HelloWorld {

   public static void main(String[] args) {
       gem5Op gem5 = new gem5Op();
       System.out.println("Rpns0:" + gem5.rpns());
       System.out.println("Rpns1:" + gem5.rpns());
   }

   static {
       System.loadLibrary("gem5OpJni");
   }
}

在构建时,你需要确保classpath包括gem5OpJni.jar。

javac -classpath $CLASSPATH:/path/to/gem5OpJni.jar HelloWorld.java

而在运行时,你需要确保java和库的路径都被设置。

java -classpath $CLASSPATH:/path/to/gem5OpJni.jar -Djava.library.path=/path/to/libgem5OpJni.so HelloWorld

Using gem5 ops with Fortran code

gem5的特殊操作码(psuedo指令)可用于Fortran程序。在Fortran代码中,人们可以添加对调用特殊操作码的C函数的调用。在创建最终的二进制文件时,将Fortran程序和C程序(用于操作码)的对象文件一起编译。我发现这里提供的文档很有用。请阅读这一节 -- 编译一个混合的C-Fortran程序。

Linking M5 to your C/C++ code

为了将m5与你的代码连接起来,首先按照上面一节的描述建立libm5.a。

Then

  • 在您的源文件中包含 gem5/m5ops.h
  • gem5/include 添加到编译器的包含搜索路径
  • gem5/util/m5/build/{TARGET_ISA}/out 加入链接器搜索路径。
  • 链接到 libm5.a

例如,这可以通过在你的Makefile中添加以下内容来实现。

CFLAGS += -I$(GEM5_PATH)/include
LDFLAGS += -L$(GEM5_PATH)/util/m5/build/$(TARGET_ISA)/out -lm5

这是一个简单的 Makefile 示例:

TARGET_ISA=x86

GEM5_HOME=$(realpath ./)$(info   GEM5_HOME is $(GEM5_HOME))

CXX=g++

CFLAGS=-I$(GEM5_HOME)/include

LDFLAGS=-L$(GEM5_HOME)/util/m5/build/$(TARGET_ISA)/out -lm5

OBJECTS= hello_world

all: hello_worldhello_world:$(CXX) -o $(OBJECTS) hello_world.cpp $(CFLAGS) $(LDFLAGS)

clean:rm -f $(OBJECTS)

Using the “_addr” version of M5ops

m5ops的"_addr "版本触发了与默认m5ops相同的模拟特定功能,但它们使用不同的触发机制。下面是m5工具README.md的一段话,解释了触发机制。

`The bare function name as defined in the header file will use the magic instruction based trigger mechanism, what would have historically been the default.

Some macros at the end of the header file will set up other declarations which mirror all of the other definitions, but with an “_addr” and “_semi” suffix. These other versions will trigger the same gem5 operations, but using the “magic” address or semihosting trigger mechanisms. While those functions will be unconditionally declared in the header file, a definition will exist in the library only if that trigger mechanism is supported for that ABI.`

为了使用"_addr "版本的m5ops,你需要包括m5_mmap.h头文件,将 "magic "地址(ex. "0xFFFF0000" for x86)传递给m5op_addr,然后调用map_m5_mem()来打开/dev/mem。你可以通过在原m5ops函数的末尾添加"_addr "来插入m5ops。

下面是一个使用"_addr "版本的m5ops的简单例子。

#include <gem5/m5ops.h>
#include <m5_mmap.h>
#include <stdio.h>
#define GEM5
int main(void) {
#ifdef GEM5
    m5op_addr = 0xFFFF0000;
    map_m5_mem();
    m5_work_begin_addr(0,0);
#endif
print("hello world!");

#ifdef GEM5
    m5_work_end_addr(0,0);
#endif
}

当你在FS模式下用KVM CPU插入m5ops运行应用程序时,可能会出现这个错误。

illegal instruction (core dumped)

这是因为m5ops指令对主机来说不是有效的指令。使用"_addr "版本的m5ops可以解决这个问题,所以如果你想把m5ops整合到你的应用程序中,或者在与KVM CPU一起运行时使用m5二进制工具,可能有必要使用"_addr "版本。

Debugger-based Debugging

如果仅靠跟踪是不够的,你需要使用调试器(如gdb)详细检查gem5正在做什么。如果你达到这一点,你肯定希望使用 gem5.debug 二进制文件。理想情况下,通过查看跟踪记录,你至少可以缩小你认为出错的周期范围。达到这一点的最快方法是使用DebugEvent,它可以进入gem5的事件队列,并在达到指定周期时通过向进程发送SIGTRAP信号强制进入调试器。你需要在调试器下启动gem5,或者将调试器连接到gem5进程中,这样才能发挥作用。
当你使用 --debug-break=100 参数调用 gem5 时,你可以创建一个或多个 DebugEvents。你也可以使用schedBreak()函数从调试器提示中创建新的DebugEvents。下面的示例会话说明了这两种方法。

% gdb m5/build/<ISA>/gem5.debug
GNU gdb 6.1
Copyright 2002 Free Software Foundation, Inc.
[...]
(gdb) run --debug-break=2000 configs/run.py
Starting program: /z/stever/bk/m5/build/<ISA>/gem5.debug --debug-break=2000 configs/run.py
M5 Simulator System
[...]
warn: Entering event queue @ 0.  Starting simulation...

Program received signal SIGTRAP, Trace/breakpoint trap.
0xffffe002 in ?? ()
(gdb) p curTick
$1 = 2000
(gdb) c
Continuing.

(gdb) call schedBreak(3000)
(gdb) c
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0xffffe002 in ?? ()
(gdb) p _curTick
$3 = 3000
(gdb)

gem5包括一些专门用于从调试器中调用的函数(例如,使用gdb call 命令,如上面的schedBreak()例子)。其中许多是 "转储 "函数,显示模拟器的内部数据结构。例如,eventq_dump()显示在主事件队列中安排的事件。大多数其他的转储函数都与特定的对象有关,比如指令队列和详细的CPU模型中的ROB。这些函数包括

Function Effect
schedBreak(<tick>) Schedule a SIGTRAP to occur at <tick>
setDebugFlag("<flag>") Enable a debug flag from the debugger
clearDebugFlag("<flag>") Disable a debug flags from the debugger
eventqDump() Print out all events on the event queue
takeCheckpoint(<tick>) Create a checkpoint at cycle <tick>
SimObject::find("system.qualified.name") Returns the pointer to the object with the specified name

Debugging Python with PDB

你可以用 Python 调试 (PDB) 来调试配置脚本,就像你调试其他 Python 脚本一样。你可以在你的配置脚本执行前进入PDB,方法是给gem5二进制文件提供-pdb参数。另一种方法是在你的配置脚本(例如fs.py或se.py)中,在你想进入调试器的地方放上下面一行。
import pdb; pdb.set_trace()
注意,src下的Python文件被编译到gem5二进制文件中,所以如果你在这些文件中添加这一行(或做其他改动),你必须重新构建二进制文件。另外,你可以将M5_OVERRIDE_PY_SOURCE环境变量设置为 "true"(见 src/python/importer.py)。
关于使用PDB的更多细节,请参见官方PDB文档。

Using Valgrind

Valgrind是一个动态分析工具,(主要)用于对目标应用程序进行剖析,检测运行时错误的来源,以及检测内存泄漏。
为了使Valgrind发挥作用,目标gem5二进制文件必须已被编译为包括调试信息。因此,必须使用gem5.debug二进制文件。由于Valgrind在使用tcmalloc时存在困难,编译gem5.debug时必须不使用--without-tcmalloc标志。
scons --without-tcmalloc build/{ISA}/gem5.debug
要使用 Valgrind 运行检查,请执行以下操作。
valgrind --leak-check=yes --suppressions=util/valgrind-suppressions build/{Target ISA}/gem5.debug {gem5 arguments}
上述内容将运行gem5并做两件事。
如果收到运行时错误,给出堆栈跟踪。
提供有关潜在内存泄漏的信息。
util/valgrind-suppressions文件包含一组由Valgrind报告的警告,但gem5开发者并不认为这是一个问题。 众所周知,Valgrind会提供误报。util/valgrind-suppressions应在这些误报被发现时进行更新。关于抑制Valgrind警告的更多信息可以在Valgrind用户手册中找到。
如果收到一个运行时错误,Valgrind将返回一个类似以下的输出(摘自Valgrind快速入门指南)。

==19182== Invalid write of size 4
==19182==    at 0x804838F: f (example.c:6)
==19182==    by 0x80483AB: main (example.c:11)

在这个输出中。
19182是进程ID
无效写入是什么样的错误。
这个错误下面是堆栈跟踪。在这个例子中,泄漏发生在example.c的第6行。这一行包含在函数f中,该函数被第11行的main方法调用(也在example.c中)。
0x804838F是代码地址。这通常并不重要。
Valgrind也可能返回关于内存泄漏的警告,例如。

==19182== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==19182==    at 0x1B8FF5CD: malloc (vg_replace_malloc.c:130)
==19182==    by 0x8048385: f (a.c:5)
==19182==    by 0x80483AB: main (a.c:11)

堆栈跟踪将告诉你哪里发生了内存泄漏。如果Valgrind指出一个内存块 "肯定丢失",那么就有内存泄漏。然而,如果Valgrind说一个块 "可能丢失",Valgrind有理由相信内存泄漏,但也许不是(这通常是在代码对指针做复杂处理的情况下)。
如果Valgrind返回的输出结果难以确定根本原因,可以尝试用-track-origins=yes来运行Valgrind。这将增加执行时间,但会提供更多信息。
关于更多的高级功能,应该查阅《Valgrind用户手册》。

Debugging Simulated Code

gem5内置支持gdb的远程调试器接口。如果你对监控模拟机器上的代码(FS模式下的内核或SE模式下的程序)感兴趣,你可以在主机平台上启动gdb,让它与模拟的gem5系统对话,就像它是一台真实的机器/进程一样(只是更好,因为gem5的执行是确定性的,gem5的远程调试器接口保证不会扰乱模拟系统的执行)。
如果你模拟的系统使用的是与你运行的主机不同的ISA,你将需要一个跨架构的gdb;见下文的说明。如果你模拟的是你主机的本地ISA,你很可能只需使用预装的本地gdb。
当gem5运行时,每个CPU在一个TCP端口上监听一个远程调试连接。第一个分配的端口一般是7000,不过如果一个端口正在使用,就会尝试下一个端口。
为了连接远程调试器,必须要有一份内核和源代码的拷贝。同时,为了查看内核的调用堆栈,你必须确保Linux在构建时启用了必要的调试配置参数。要运行远程调试器,请按以下步骤操作(假设host=localhost,port=7000)。

gdb-multiarch <path-to-linux>/vmlinux
GNU gdb (Ubuntu 8.2-0ubuntu1~18.04) 8.2
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

(gdb) target remote <host>:<port>

gem5模拟器已经在运行,目标远程命令连接到已经运行的模拟器,并在执行中停止它。你可以设置断点并使用调试器来调试内核。也可以使用远程调试器来调试控制台代码。设置是类似的,但如何设置将留待以后的工作。
如果你同时使用远程调试器和模拟器上的调试器,可以通过调用debugger()从主调试器中触发远程调试器。在你这样做之前,你需要弄清楚你要调试的CPU(cpu id),并将current_debugger设置为该cpuid。如果你只有一个cpu,那么它将是cpuid 0,然而如果有多个cpu,你将需要将cpu id与远程gdb会话的相应端口号相匹配。例如,使用下面来自gem5的样本输出,调用cpu 3的内核调试器需要内核调试器监听端口7001。

%./build/<ISA>/gem5.debug configs/example/fs.py
...
making dual system
Global frequency set at 1000000000000 ticks per second
Listening for testsys connection on port 3456
Listening for drivesys connection on port 3457
0: testsys.remote_gdb.listener: listening for remote gdb #0 on port 7002
0: testsys.remote_gdb.listener: listening for remote gdb #1 on port 7003
0: testsys.remote_gdb.listener: listening for remote gdb #2 on port 7000
0: testsys.remote_gdb.listener: listening for remote gdb #3 on port 7001
0: drivesys.remote_gdb.listener: listening for remote gdb #4 on port 7004
0: drivesys.remote_gdb.listener: listening for remote gdb #5 on port 7005
0: drivesys.remote_gdb.listener: listening for remote gdb #6 on port 7006
0: drivesys.remote_gdb.listener: listening for remote gdb #7 on port 7007

Getting a cross-architecture gdb

要在gem5中使用远程调试器,最重要的部分是你要把gdb编译成与你要模拟的目标系统一起工作。推荐的方法是安装gdb-multiarch包,提供一个可用于多个ISA(archs)的单一gdb二进制文件

% sudo apt-get update -y
% sudo apt-get install -y gdb-multiarch

可以在主机上编译一个非本地架构的gdb作为替代。必须做的就是在编译gdb时加入--target=选项进行配置。你也可以用交叉编译器得到预编译的调试器。参见 "下载 "以获得一些包含调试器的交叉编译器的链接。

% wget <http://ftp.gnu.org/gnu/gdb/><gdb-version>.tar.gz
% tar xfz <gdb-version>.tar.gz
% cd <gdb-version>
% ./configure --target=<isa>
<configure output....>
% make
<make output...this may take a while>

最终结果是 gdb/gdb,它将用于远程调试。

Target-specific instructions

ARM Target

如果你打算调试一个ARM内核,你需要一个合理的新版本的gdb(7.1或更高)。此外,你必须像这样手动指定tspecs(端口号可能不同)。tspec文件可以在gdb源代码中找到。

set remote Z-packet on
set tdesc filename path/to/features/arm-with-neon.xml
symbol-file <path to vmlinux used for gem5>
target remote <ip addr of host running gem5 or if local host 127.0.0.1>:7000

Trace-based Debugging

Introduction

最简单的调试方法是让gem5打印出它正在做的事情的痕迹。模拟器包含许多DPRINTF语句,用于打印描述潜在的有趣事件的跟踪信息。每个DPRINTF都与一个调试标志相关联(例如,Bus, Cache, Ethernet, Disk 等)。要打开某个特定标志的信息,请使用--debug-flags命令行参数。可以通过给出一个字符串列表来指定多个标志,例如。
build/<ISA>/gem5.opt --debug-flags=Bus, Cache configs/examples/fs.py
将打开一组与指令执行有关的调试标志,但不包括Tick(计时)信息。如果你想在两次运行中比较相同指令的执行情况,但以不同的速率执行,这很有用。
请注意,gem5.fast二进制文件不支持跟踪;使其比gem5.opt更快的部分原因是DPRINTF代码被编译出来。
--debug-flags命令行选项应放在gem5可执行文件之后,但在模拟脚本之前。这是因为调试标志是由gem5本身处理的,而命令行选项是在模拟脚本之前还是之后,决定了它们是针对gem5还是脚本的。

Debugging Options
-----------------
--debug-break=TIME[,TIME]
                        Tick to create a breakpoint
--debug-help            Print help on debug flags
--debug-flags=FLAG[,FLAG]
                        Sets the flags for debug output (-FLAG disables a
                        flag)
--debug-start=TIME      Start debug output at TIME (must be in ticks)
--debug-file=FILE       Sets the output file for debug [Default: cout]
--debug-ignore=EXPR     Ignore EXPR sim objects

调试/跟踪标志的完整列表可以通过运行gem5的--debug-help选项查看。
如果你发现感兴趣的事件没有被追踪到,请随时自己添加DPRINTFs。你可以通过在任何SConscript文件(最好是离你使用新标志的地方最近的文件)中添加DebugFlag()命令来添加新的调试标志。如果你在一个C++源文件中使用调试标志,你需要在该文件中包含头文件debug/<name of debug flag>.hh
对于更复杂的bug,跟踪可以简单地确定模拟中需要更深入调查的点,非常有用。--debug-break选项可以让你在调试器下重新运行你的仿真,并在跟踪所确定的某个特定点上停止。你也可以安排断点,并在调试器中启用或禁用调试标志。更多信息请参见基于调试器的调试页面。

The Exec debug flag

Exec复合调试标志非常有用,因为它在gem5中开启了指令追踪功能。 它使模拟器在每条指令执行完毕后打印出其反汇编版本,以及其他有用的信息,如时间、PC、地址(如果是内存指令)等。这些单独的信息可以通过基本调试标志Exec控制来打开或关闭。例如,你可以通过关闭ExecSymbol标志(例如,--debug-flags=Exec,-ExecSymbol)来禁止使用函数符号名来代替绝对PC地址(如果它们可用的话)。
如果某些所谓的无害变化导致gem5停止正常工作,你可以使用src/util目录下的tracediff脚本比较变化前后的跟踪输出。脚本中的注释描述了如何使用它。

Reducing trace file size

Trace file can become very large very quickly, but they also compress very well (e.g. about 90%). If you’d like to make gem5 output a compressed trace, just add a .gz extension to the output file name. For example --debug-file=trace.out will produce an uncompressed file as normal, but --debug-file=trace.out.gz will produce a gzip compressed file. You can use the zcat program and pipes to process the output. The editor vim also can uncompress gzip compressed files in memory.

追踪文件可以很快变得非常大,但它们的压缩效果也非常好(例如,大约90%)。如果你想让gem5输出压缩后的跟踪文件,只需在输出文件名中添加一个.gz扩展名。例如,--debug-file=trace.out会像平常一样产生一个未压缩的文件,但--debug-file=trace.out.gz会产生一个gzip压缩的文件。你可以使用zcat程序和管道来处理这些输出。编辑器vim也可以在内存中解压缩gzip压缩文件。

The tracediff and rundiff utilities

tracediffrundiff 工具允许对 gem5 的两个跟踪数据流进行简单的比较,以发现任何差异。这对于调试回归测试失败的原因、弄清为什么你的小代码改动似乎会导致一些不相关的执行问题,或者比较CPU模型的执行情况都非常方便。
这两个工具都可以在util目录中找到。 rundiff是一个简单的类似diff的程序。与常规的diff不同,这个脚本在比较其输入之前不会读入整个输入,因此它可以用于从其他程序(如gem5追踪)输送的冗长输出。tracediffrundiff的一个前端,提供了一个简单的方法来运行两个类似的gem5副本并对其输出进行比较。它采用带有嵌入式替代命令的普通gem5命令行,并在不同的子目录中执行两个替代命令,其输出被输送到rundiff。
脚本参数的处理方式统一如下。
如果参数不包含"|"字符,它将被附加到两个命令行上。
如果参数中有一个"|"字,那么"|"字的任何大小的文本都会被附加到各自的命令行中。注意,你必须给参数加引号,或者用反斜杠转义'|',这样shell就不会认为你在做一个管道或者在它周围加引号。
带有'#'字符的参数在这些字符处被分割开来,作为独立的术语处理替代物('|'s),然后粘贴回一个参数(没有'#')。(这是受C语言预处理程序中'##'标记粘贴操作的启发)。
换句话说,参数应该像你想运行的命令行一样,用'|'来列出你想在两次运行中不同的部分的备选方案。
例如

% tracediff gem5.opt --opt1 '--opt2|--opt3' --opt4
# would compare these two runs:
gem5.opt --opt1 --opt2 --opt4
gem5.opt --opt1 --opt3 --opt4

% tracediff 'path1|path2#/m5.opt' --opt1 --opt2
# would compare these two runs:
path1/gem5.opt --opt1 --opt2
path2/gem5.opt --opt1 --opt2

如果你想只在一个运行中添加参数,只需在一边加上'|'和文本(--onlyOn1|)。你也可以把多个参数放在一起做(|-a -b -c只在第二次运行中添加三个参数)。
tracediff的-n参数允许你预览两个生成的命令行,而不运行它们。
为了使tracediff发挥作用,必须启用一些跟踪标志。最常用的跟踪标志是 --debug-flags=Exec,-ExecTicks,它从每条跟踪中移除时间戳,当出现轻微的时间变化时,它适合于进行差异。
Tracediff在比较CPU模型时也很有用,当一个模型失败而另一个没有失败时。在这种情况下,最好在问题发生之前创建一个检查点(这可以通过创建一堆检查点并找到一个故障点来实现)。如果故障发生在内核代码中,使用-ExecUser调试标志,另一方面,如果故障发生在用户代码中,尝试使用-ExecKernel调试标志,在跟踪中隔离用户代码。然后,你可以比较追踪结果,看看何时执行出现分歧。

Comparing traces across machines

有时,gem5 的执行在不同的环境中会出现莫名其妙的差异,而您希望使用 rundiff 来帮助确定它们的分歧所在。与其尝试在同一台机器上重现这些环境,你可以使用netcat和rundiff来比较网络上不同系统上运行的gem5实例的跟踪。
首先,在一台机器上启动rundiff,配置为比较来自gem5本地实例的跟踪输出和netcat "服务器 "的输出。由于网络可能是瓶颈,我们将压缩穿越netcat的跟踪,这意味着我们需要在它到达时解压缩。例如(任意选择端口号33335)。

util/rundiff 'gem5.opt --debug-flag=Exec <gem5 args> |' 'nc -d -l 33335 | gunzip -c |' >& tracediff.out &

现在去第二台机器,在那里启动gem5的副本,并将其压缩的跟踪输出发送到第一台机器上运行的netcat实例。比如说。

gem5.opt --debug-flag=Exec <gem5 args> |& gzip -c |& nc <hostname> 33335

Internal Exec tracing implementation (InstTracer)

上面的 "基于跟踪的调试 "部分谈到了如何使用Exec跟踪标志来打印每条指令完成时的信息。这个功能实际上是由一个InstTracer对象实现的,该对象在指令执行时收集指令的信息。这些对象可以被替换掉,不同的对象可以用它们收集的信息做不同的事情。例如,IntelTrace对象以不同的格式打印出一个与外部工具兼容的跟踪。这些对象还可以做更多的事情,而不仅仅是打印一个跟踪。 NativeTrace对象通过套接字向状态跟踪工具(如下所述)逐条指令发送关于架构状态的信息,以验证执行情况。 InstTracer对象是SimObjects,它被分配给每个CPU的tracer参数。如果你想安装一个不同的跟踪器,只要把它分配给感兴趣的CPU上的那个参数。
在编写你自己的InstTracer时,你至少要写两个不同的类,一个继承于InstTracer,一个继承于InstRecordInstTracer类的主要职责是生成与特定指令相关的InstRecord对象。通过子类化InstTracer,你将能够返回你自己的专门版本的InstRecord,这是真正做大部分工作的类。
InstRecord类有许多字段,保存着指令的历史信息。例如,InstRecord记录了指令的PC,如果它访问了内存,它使用了什么地址,它产生的 "数据 "值(多个数据值不被处理),等等。InstRecord函数也有一个指向ThreadContext的指针,可以用来读出架构状态。当一条指令执行完毕后,InstRecord的dump()虚拟函数被调用来处理该记录。对于默认的InstTracer,这是打印指令的汇编语言形式等的地方,也就是你打开Exec时看到的输出。对于NativeTrace,这是收集架构状态的地方,以便将其发送给statetrace。

Comparing traces with a real machine

状态跟踪工具与gem5同时运行,将真实机器上工作负载的执行与gem5中的执行进行比较。 在模拟器和真实系统中,工作负载被允许一次运行一条指令。在每条指令之后,架构状态被收集和比较,任何差异都被报告。要把它设置好并产生有用的结果可能很棘手(如下所述),但它是一个非常有价值的调试工具,因为它往往能迅速准确地指出问题的来源,可能为每个错误节省许多痛苦的调试时间。

Native Trace

在gem5中,一个NativeTrace InstTracer对象(如上所述)需要安装在将运行相关工作负载的CPU上。当执行开始时,追踪器将等待状态追踪工具连接到它。然后,在每个指令执行后,它使用InstRecord对象中的ThreadContext指针来收集当前运行进程的架构状态。它还会通过它们建立的连接读取由状态跟踪收集的架构状态。两个版本的状态被比较,任何有意义的差异都被报告。状态的确切构成以及如何进行比较是非常依赖于ISA的,所以每个ISA都定义了自己的NativeTrace版本。这些专门的类可以处理一些事情,比如当寄存器可能变成未定义时的预期差异,或者由于某种原因执行跳过的情况。

statetrace utility

statetrace工具在util目录下找到,负责在真实机器上运行工作负载。它使用 Linux 内核提供的 ptrace 机制来单步追踪目标进程并访问其状态。它使用scons,但独立于gem5的其他部分所使用的scons。要构建适合特定 ISA 的 statetrace 版本,请使用 build/${ARCH}/statetrace 目标,其中 ${ARCH} 由感兴趣的 ISA 替代。目前公认的 ${ARCH} 值是 amd64、arm、i686 和 sparc。您可以使用 CXX scons 参数覆盖任何 ISA 的编译器,并使用 ${ARCH}CXX 覆盖某一特定 ISA 的编译器。例如,要构建 arm 版本的 statetrace,你可以运行。

cd util/statetrace scons ARMCXX=arm-softfloat-linux-gnueabi-g++ build/arm/statetrace

statetrace 接受四个标志,-h 用于打印帮助,--host 用于指定 gem5 监听的 IP 和端口,-i 用于打印初始堆栈帧上的内容,-nt 用于禁用跟踪。 -nt通常与-i一起使用,以获得一个进程的初始堆栈信息,而不运行它。命令行选项的末端用两个破折号标记。接下来,写上你希望statetrace运行的命令行。
程序名称和参数的确切文本很重要,因为这些将被传递到进程的堆栈中。较长的数值会在堆栈中占用更多的空间,这就把其他项目移到了不同的地址,而statetrace则会被许多不重要的差异所堵塞。例如,如果你需要在gem5子目录下运行一个在你的主目录中找到的程序,你运行这个命令。

statetrace -- ~/gem5/my_benchmark arg1 arg2

You must also override arg0 in gem5 to be ~/gem5/my_benchmark.

Tuning

statetrace 是一个非常敏感的系统,模拟执行和真实执行之间的任何细微差别都可能产生很多很多虚假的差异。为了从 statetrace 中获得有用的信息,你需要调整真实系统和 gem5,使一切都能完美匹配。我通常会创建一个补丁,其中包含我为状态跟踪对 gem5 所做的所有修改。然后我可以轻松地删除它们,或在发现和修复问题时重新应用它们。Mercurial queues 对管理该补丁和我的修复补丁很有用。以下是一个不完整的列表,列出了你可能需要纠正的差异。
地址随机化。为了提高安全性,Linux会随机化进程的地址空间,在它们的堆栈和堆区中移动。这使得攻击者更难预测内存的样子,但它也彻底打败了状态跟踪。要禁用它,在/proc/sys/kernel/randomize_va_space中回车0。你几乎肯定需要root权限才能这样做。
argv值。请确保在gem5和真实系统中对程序的每个参数使用完全相同的文本。这包括arg0,即程序名称。
文件块大小。Glibc使用与一个文件相关的块大小来决定如何缓冲它。不同的行为会导致执行失败,并使状态跟踪无法工作。你可以在 src/sim/syscall_emul.hh 的 convertStatBuf 和 convertStat64Buf 函数中改变 gem5 报告的块大小。
初始堆栈内容。根据你的Linux版本,初始堆栈的内容可能是不同的。你可以使用 -i 和 -nt 选项来打印出真实机器上初始堆栈的内容。 statetrace 试图解释初始堆栈,以便你能更容易地看到其中的内容。你需要调整gem5设置堆栈的方式,以符合你的真实系统。这段代码通常在适当的arch目录下一个名为process.cc的文件中。gem5的代码经过精心构建,使其设置的堆栈尽可能与Linux相同,但底层机制会发生变化。另外,Linux在初始堆栈上放置了一个辅助向量集合。这些是类型、值对,让内核在进程启动时向其提供额外的信息。Linux不时地引入一种新的辅助向量类型并将其添加到堆栈中。你可能需要深入研究Linux的源代码并模拟任何新的条目。

Caveats

因为状态跟踪对执行中的任何变化都非常敏感,所以它不能用于那些行为方式不是很可预测的程序。例如,如果一个程序从/dev/random中读入一个随机值,并在计算中使用它(或者在控制流中更糟糕),那么这个程序就不能使用。不太明显的是,如果该程序依赖于系统时间,而系统时间是不可预测的,那么它也不能被使用。一般说来,许多基准试图做到非常确定,以便它们可以用来产生可重复的数据。这使得它们与statetrace配合得很好。
状态跟踪不能用于操作系统层面,至少有两个主要原因。首先,在可预见的未来,没有任何系统实现或将实现单步操作系统。第二,真正的操作系统不是决定性的。来自硬件设备的中断几乎肯定会在不可预知的时间出现,一些设备会返回不可预知的数据,而gem5更不可能与该级别的系统行为完全匹配,在该级别上,固件和其他实现细节不再被抽象。其次,系统级的相关状态量通常比用户级的大,特别是在x86这样复杂的ISA中。收集、比较和传输所有这些额外的状态将大大影响性能。

并非所有trace的实现都能正常工作。例如,当我上次在ARM上使用状态跟踪时,某些函数调用了由内核设置的内存区域,该区域对各种操作有内核特定的实现。Ptrace依靠的是软件断点,它的工作原理是将程序中的下一条指令替换为会被捕获的指令。由于该内存区域确实属于内核,Ptrace无法修改它来安装断点。该进程 "逃脱 "了单步执行,并迅速运行完成,让gem5等待更新,但更新却没有到来。
statetrace无法跟踪内存的变化。由于内存非常大,而且没有一个方便的方法来检测对它的修改,statetrace只跟踪基于寄存器的架构状态。如果一条指令正确地改变了寄存器,但将错误的值存储到内存和/或错误的地址,那么这个问题在很多指令中可能都检测不到。幸运的是,这类错误是例外。
要将执行情况与真正的机器进行比较,最好是有一台真正的机器供你使用。不过,在qemu这样的仿真器中运行状态跟踪还是很有可能的。这可能会慢一点,而且是与模拟器而不是真实的硬件进行比较,但它仍然可以帮助识别错误。

ISA support

目前SPARC、ARM和x86都支持状态。ARM的支持目前是最复杂的,只在连接中发送状态的差异,这提高了性能,并且只在差异开始或停止时打印,这减少了输出并提高了可读性。这些功能计划被移植到其他ISA上。希望这些代码能够被分解出来,放到基础的NativeTrace类中,这样所有的ISA都能轻松地使用它。

Garnet Synthetic Traffic

Garnet合成流量提供了一个框架,用于模拟Garnet网络的受控输入。这对于网络测试/调试,或只用合成流量进行网络模拟是很有用的。
注意:Garnet合成流量注入器只适用于Garnet_standalone一致性协议。

Related Files

  • configs/example/garnet_synth_traffic.py : 调用网络测试器的文件
  • src/cpu/tester/garnet_sythetic_taffic/GarnetSyntheticTraffic.* : 实现测试器的文件

How to run

首先用Garnet_standalone一致性协议构建gem5。这个协议是与ISA无关的,因此我们用NULL ISA来构建它。

scons build/NULL/gem5.debug PROTOCOL=Garnet_standalone

Example command:

./build/NULL/gem5.debug configs/example/garnet_synth_traffic.py  \\
--num-cpus=16 \\
--num-dirs=16 \\
--network=garnet2.0 \\
--topology=Mesh_XY \\
--mesh-rows=4  \\
--sim-cycles=1000 \\
--synthetic=uniform_random \\
--injectionrate=0.01

Parameterized Options

System Configuration Description
--num-cpus cpus的数量。这是网络中源(注入)节点的数量。
--num-dirs 目录的数量。这是网络中目标(弹射)节点的数量。
--network 网络模型:简单或garnet2.0。使用garnet2.0来运行合成流量。
--topology 连接cpu和dirs与网络路由器/交换机的拓扑结构。
--mesh-rows 网格中的行数。仅当-topology为Mesh时有效* MeshDirCorners*。
Network Configuration Description
--router-latency 石榴石路由器中管道阶段的默认数量。必须是>=1。可以在拓扑文件中以每个路由器为基础被覆盖。
--link-latency 网络中每个链接的默认延迟。必须是>=1。可以在拓扑文件中以每个链路为基础进行覆盖。
--vcs-per-vnet 每个虚拟网络的 VC 数。
--link-width-bits 石榴石网络内所有链接的宽度(比特)。默认=128。
Traffic Injection Configuration Description
--sim-cycles 模拟应该运行的周期总数。
--synthetic 要注入的合成流量的类型。目前支持以下合成流量模式:uniform_random、tornado、bit_complement、bit_reverse、bit_rotation、neighbour、shuffle和transpose。
--injectionrate 流量注入率,单位是包/节点/周期。小数点后的精度可以由-精度控制,在garnet_synth_traffic.py中默认设置为3,它可以取0到1之间的任何小数值。
--single-sender-id 只从这个发送者注入。要从所有节点发送,设置为-1。
--single-dest-id 只向这个目的地发送。要向合成流量模式指定的所有目的地发送,设置为-1。
--num-packets-max 每个cpu节点要注入的最大数据包数量。默认值是-1(一直注入到模拟周期)。
--inj-vnet 只在这个vnet(0、1或2)中注入。0和1是1-lit,2是5-lit。设置为-1可以在所有网内随机注入。

Implementation of Garnet Synthetic Traffic

合成流量注入器是在GarnetSnytheticTraffic.cc中实现的。生成和发送数据包的步骤顺序如下。

  • 每个周期,每个cpu执行一个概率等于-injectionrate的bernouli试验,以确定是否产生一个数据包。
  • 如果-num-packets-max为非负数,每个cpu在生成-num-packets-max数量的数据包后停止生成新数据包。注射器在-sim-cycles之后终止。
  • 如果cpu必须生成一个新的数据包,它会根据合成流量类型(-synthetic)计算新数据包的目的地。
  • 这个目的地被嵌入到数据包地址中块偏移后的位中。
  • 生成的数据包被随机标记为ReadReq,或INST_FETCH,或WriteReq,并被发送到Ruby Port(src/mem/ruby/system/RubyPort.hh/cc)。
  • Ruby端口将数据包分别转换为RubyRequestType:LD、RubyRequestType:IFETCH和RubyRequestType:ST,并将其发送给Sequencer,后者又将其发送给Garnet_standalone缓存控制器。
  • 缓存控制器从数据包地址中提取目标目录。
  • 缓存控制器将LD、IFETCH和ST分别注入到虚拟网络0、1和2。
  • LD和IFETCH是作为控制包(8字节)注入的,而ST是作为数据包(72字节)注入的。
  • 该数据包穿越网络并到达目录。
  • 目录控制器只是简单地丢弃它。
  • *gem5模拟器目前提供了四种不同的CPU模型:AtomicSimple、TimingSimple、InOrder和O3。**AtomicSimple和TimingSimple是非流水线CPU模型,AtomicSimple是一种最小的单IPC CPU模型,适用于快速功能模拟;TimingSimple与之类似,但是使用了存储器访问时序模型,用以统计存储器访问延迟;InOrder是一个按序流水线CPU模型,该模式下,可以配置硬件支持的线程数量;O3是一个乱序流水线CPU模型,可以支持超标量结构和SMT。InOrder与O3都是execute-in-execute(指令的执行只在执行阶段)的设计。

gem5支持两种执行模式:System-call Emulation(SE)和Full-System(FS)

SE模式中,当程序执行系统调用时,gem5会捕捉到,同时模拟调用,通常是传递给主机操作系统。通过模拟大部分的系统调用,避免了对外设和OS进行建模的需要。SE模式下,没有线程调度器,线程必须静态的映射到cores,因此会限制多线程应用。SPEC CPU基准测试通常在SE模式下运行

FS(全系统)模式对一个完整系统,包括OS和外设,进行了建模,支持执行用户和内核指令。FS模式中,gem5提供了一个适合运行操作系统的裸机环境,包括中断,异常等,并不是所有ISAs都支持此模式。

FS相对于SE,精度更高,可以执行更多类型的负载。虽然SPEC CPU基准测试通常在SE模式下运行,但是在FS模式下运行它们将提供与OS更实际的交互。因此需要许多OS服务或I/O设备的工作负载可能只在FS模式下运行。

Image in a image block

支持两种存储系统模型:Classic和Ruby。

Classic模型(来自M5)提供了一个快速且易于配置的内存系统;

Ruby模型(来自GEMS)提供了一种灵活且能够精确模拟的内存系统,支持cache一致性。Ruby内存模型支持大量的互连拓扑结构,同时包括两种不同的网络模型。组件之间的链接使用一个简单的python文件声明,然后通过最短路径分析创建路由表。在确定链接和路由表后,根据不同的网络模型进行实现。两种网络模型为:

1、Simple网络模型:只对链接,路由延迟和链路带宽,并没有对路由器资源争用和流量控制建模。

2、Garnet网络模型:对路由建立了详细的模型,包括相关的资源竞争和流量控制。

gem5提供了两种特定领域的语言,一种用于描述ISA(继承自M5),另一种用于描述cache的一致性协议(继承自GEMS)。

ISA DSL

用于统一二进制指令的解码和它们的语义规范。gem5通过使用一个通用的C++基类来描述指令,从而实现了ISA的独立。每种ISA会重新基类中继承的方法,例如execute()。ISA描述语言允许用户简洁地指定所需的c++代码。

Cache Coherence DSL(SLICC)

SLICC是一种DSL,用于灵活的实现cache一致性协议。SLICC目前支持AMD Opteron的基于广播的一致性协议和CMP的目录协议。SLICC将cache,mem,DMA控制器定义为单独的per-memory-block的状态机,这些状态机组合称为整个的协议。gem5中的SLICC将协议定义为一组状态、事件、转换和操作,同时将状态机特定的逻辑和协议无关的组件(例如cache)绑定在一起。gem5的SLICC会自动生成python和c++文件,同时也支持局部变量,以简化编程提高性能

标准化接口是面向对象的基础。有两个核心的接口:端口(port)接口和消息缓冲(message buffer)接口。

  • 端口接口:连接内存对象,包括cpu和caches,caches到总线,总线到外设和内存。该接口支持三种机制来访问数据,
    1. timing模式,用来建模带有详细时序的内存访问,会有request和response的消息机制;
    2. atomic模式,用来获取时序信息,但是没有消息机制,状态会直接发生变化;
    3. 功能模式,存储操作不会改变时序信息。
  • 消息缓冲接口:Ruby使用端口接口来连接cpu和外设,同时使用消息缓冲接口连接Ruby内部对象。两个接口非常类似。

gem5 master and slave ports

timing, atomic, and functional

Packet

  • MemReq
  • MemCmd

Learning gem5

python

import m5
from m5.objects import *

# 创建System对象,它是我们模拟系统中所有其他对象的父对象,
# System对象包含许多功能(不是时序级别)信息,如物理内存范围、根时钟域、根电压域、内核(在全系统仿真中)等
system = System()

# 创建一个时钟域,并在该域上设置时钟频率,必须为这个时钟域指定一个电压域,只使用电压域的默认选项
system.clk_domain = SrcClockDomain()
system.clk_domain.clock = '1GHz'
system.clk_domain.voltage_domain = VoltageDomain()

# 使用时序模式进行内存模拟,几乎总是会使用时序模式进行内存模拟,除非在特殊情况下,例如快速转发和从检查点恢复,设置一个大小为 512 MB 的单个内存范围
system.mem_mode = 'timing'
system.mem_ranges = [AddrRange('512MB')]

# 创建一个最简单的基于时序的 CPU TimingSimpleCPU,此 CPU 模型在单个时钟周期内执行一条指令
system.cpu = TimingSimpleCPU()

# 创建系统范围的内存总线
system.membus = SystemXBar()

# 将I/D cache连接到membus
system.cpu.icache_port = system.membus.slave
system.cpu.dcache_port = system.membus.slave

# 创建一个 I/O 控制器并将其连接到内存总线
# 将PIO和中断端口连接到内存总线是x86特定的要求。其他ISA(例如 ARM)不需要这3条
system.cpu.createInterruptController()
system.cpu.interrupts[0].pio = system.membus.master
system.cpu.interrupts[0].int_master = system.membus.slave
system.cpu.interrupts[0].int_slave = system.membus.master
# 需要将系统中的一个特殊端口连接到 membus。该端口是一个仅功能端口,允许系统读写内存
system.system_port = system.membus.slave

# 创建一个内存控制器并将其连接到 membus。对于这个系统,我们将使用一个简单的 DDR3 控制器,它将负责我们系统的整个内存范围。
system.mem_ctrl = DDR3_1600_8x8()
system.mem_ctrl.range = system.mem_ranges[0]
system.mem_ctrl.port = system.membus.master

# 创建进程,将CPU设置为使用进程作为工作负载
process = Process()
process.cmd = ['tests/test-progs/hello/bin/x86/linux/hello']
system.cpu.workload = process
system.cpu.createThreads()

# 实例化系统并开始执行,创建Root对象,实例化过程会遍历我们在python中创建的所有SimObjects,并创建C++等价物
root = Root(full_system = False, system = system)
m5.instantiate()

print("Beginning simulation!")
exit_event = m5.simulate()

print('Exiting @ tick {} because {}'
      .format(m5.curTick(), exit_event.getCause()))
Image in a image block
# 创建L1I/D cache L2 cache
class L1ICache(L1Cache):
    size = '16kB'

class L1DCache(L1Cache):
    size = '64kB'

class L2Cache(Cache):
    size = '256kB'
    assoc = 8
    tag_latency = 20
    data_latency = 20
    response_latency = 20
    mshrs = 20
    tgts_per_mshr = 12

# 将CPU连接到缓存
def connectCPU(self, cpu):
    # need to define this in a base class!
class L1ICache(L1Cache):
    size = '16kB'
    def connectCPU(self, cpu):
        self.cpu_side = cpu.icache_port

class L1DCache(L1Cache):
    size = '64kB'
    def connectCPU(self, cpu):
        self.cpu_side = cpu.dcache_port

# 将缓存连接到bus
def connectBus(self, bus):
    self.mem_side = bus.slave

# 将L2 cache连接到mem和cpu
def connectCPUSideBus(self, bus):
    self.cpu_side = bus.master

def connectMemSideBus(self, bus):
    self.mem_side = bus.slave

# 将l1 I/D cache和l2 cache通过l2bus连接
system.l2bus = L2XBar()

system.cpu.icache.connectBus(system.l2bus)
system.cpu.dcache.connectBus(system.l2bus)

system.l2cache = L2Cache()
system.l2cache.connectCPUSideBus(system.l2bus)
system.l2cache.connectMemSideBus(system.membus)

# 通过命令行将参数传递给gem5
from optparse import OptionParser

parser = OptionParser()
parser.add_option('--l1i_size', help="L1 instruction cache size")
parser.add_option('--l1d_size', help="L1 data cache size")
parser.add_option('--l2_size', help="Unified L2 cache size")

(options, args) = parser.parse_args()
---
system.cpu.icache = L1ICache(options)
system.cpu.dcache = L1DCache(options)
...
system.l2cache = L2Cache(options)
---
def __init__(self, options=None):
    super(L1Cache, self).__init__()
    pass
def __init__(self, options=None):
    super(L1ICache, self).__init__(options)
    if not options or not options.l1i_size:
        return
    self.size = options.l1i_size
def __init__(self, options=None):
    super(L1DCache, self).__init__(options)
    if not options or not options.l1d_size:
        return
    self.size = options.l1d_size
def __init__(self, options=None):
    super(L2Cache, self).__init__()
    if not options or not options.l2_size:
        return
    self.size = options.l2_size

build/X86/gem5.opt configs/tutorial/two_level_opts.py --l2_size='1MB' --l1d_size='128kB'
Image in a image block

SimObject

每个 SimObject 都有一个与之关联的 Python 类。这个 Python 类描述了可以从 Python 配置文件控制的 SimObject 参数

from m5.params import *
from m5.SimObject import SimObject

class HelloObject(SimObject):
    type = 'HelloObject'
    cxx_header = "learning_gem5/hello_object.hh"

type 是您使用 Python SimObject 包装的,需要与 C++ 类名相同

cxx_header 是包含用作类型参数的类的声明文件

所有 SimObjects 的构造函数都假定它将接受一个参数对象。这个参数对象是由构建系统自动创建的,此参数类型的名称是根据您的对象名称自动生成的,对于我们的“HelloObject”,参数类型的名称是“HelloObjectParams“

增加新的调试标志

在 SConscript 文件中声明

DebugFlag('Hello')

在.cc文件中增加

#include "debug/Hello.hh"

最后

DPRINTF(Hello, "Created the hello object**\\n**");

调试打印语句

DPRINTF(Flag, __VA_ARGS__) //打印格式化字符串
DTRACE(Flag) //如果启用标志 ( Flag ),则返回 true,否则返回 false
DDUMP(Flag, data, count) //打印长度为count字节的二进制数据data
DPRINTFS(Flag, SimObject, __VA_ARGS__) //比DPRINTF多一个simobject参数
DPRINTFR(Flag, __VA_ARGS__) //输出调试语句而不打印名称

DDUMPN(data, count)
DPRINTFN(__VA_ARGS__)
DPRINTFNR(__VA_ARGS__)

增加事件

class HelloObject : public SimObject
{
  private:
    void processEvent();

    EventFunctionWrapper event;

  public:
    HelloObject(HelloObjectParams *p);

    void startup();
};

HelloObject::HelloObject(HelloObjectParams *params) :
    SimObject(params),
		event([this]{processEvent();}, name()) {
    DPRINTF(Hello, "Created the hello object\\n");
}

void
HelloObject::startup()
{
		//100个tick之后执行
    schedule(event, 100);
}
class HelloObject(SimObject):
    type = 'HelloObject'
    cxx_header = "learning_gem5/hello_object.hh"

		# Param.<TypeName>声明一个类型的参数TypeName
    time_to_wait = Param.Latency("Time before firing the event")
    number_of_fires = Param.Int(1, "Number of times to fire the event before "
                                   "goodbye")

HelloObject::HelloObject(HelloObjectParams *params) :
    SimObject(params),
    event(*this),
    myName(params->name),
    latency(params->time_to_wait),
    timesLeft(params->number_of_fires)
{
    DPRINTF(Hello, "Created the hello object with the name %s\\n", myName);
}

class HelloObject : public SimObject
{
  private:
    void processEvent();

    EventWrapper<HelloObject, &HelloObject::processEvent> event;

    std::string myName;

    Tick latency;

    int timesLeft;

  public:
    HelloObject(HelloObjectParams *p);

    void startup();
};

root.hello = HelloObject(time_to_wait = '2us')
root.hello = HelloObject()
root.hello.time_to_wait = '2us'

// 为一次迭代填充缓冲区。如果缓冲区未满,此
// 函数会将另一个事件排入队列以继续填充。
void  fillBuffer ();

数据包

数据包还有一个MemCmd,它是数据包的当前命令。该命令可以在数据包的整个生命周期内发生变化(例如,一旦满足内存命令,请求就会变成响应)。最常见MemCmd的是ReadReq(读取请求)、ReadResp(读取响应)、WriteReq(写入请求)、WriteResp(写入响应)。缓存和许多其他命令类型也有写回请求 ( WritebackDirtyWritebackClean)。

  • 当两者都可以接受请求和响应时,简单的主从交互。概述了主从端口之间最简单的交互。此图显示了计时模式下的交互。其他模式要简单得多,在主从之间使用简单的调用链。
Image in a image block

为了发送请求包,主控调用sendTimingReq. 反过来,(并且在同一个调用链中),该函数recvTimingReq在从属设备上调用,PacketPtr其唯一参数相同。

Image in a image block
  • 当主设备忙时的简单主从交互显示了主设备在从设备尝试发送响应时正忙的情况。在这种情况下,从属设备sendTimingResp在收到一个recvRespRetry.
Image in a image block

端口

从端口:

  • AddrRangeList getAddrRanges() 返回所有者负责的非重叠地址范围的列表。**
  • Tick recvAtomic(PacketPtr pkt) CPU 尝试进行原子内存访问时调用
  • void recvFunctional(PacketPtr pkt) CPU 进行功能访问时调用,这在系统调用仿真模式中用于从主机文件系统加载文件
  • bool recvTimingReq(PacketPtr pkt) 此函数在此端口的对等方调用时调用sendTimingReq。它采用单个参数,即请求的数据包指针。如果数据包被接受,此函数返回 true。如果此函数返回 false,则该对象必须在将来的某个时间调用sendReqRetry,以便通知对等端口它能够接受被拒绝的请求。
  • void recvRespRetry() 该函数在对端端口调用时调用sendRespRetry。执行此函数时,该端口应sendTimingResp再次调用以重试向其对等主端口发送响应。

主端口:

  • bool recvTimingResp( PacketPtr pkt ) 此函数在此端口的从对等方调用时调用sendTimingResp。如果此对象可以接受响应,则此函数返回 true。否则,在将来的某个时刻,该对象必须调用sendRespRetry以通知其对等方它现在能够接收响应。
  • void recvReqRetry() 当对等端口调用时调用此函数,这sendReqRetry意味着该对象应尝试重新发送先前失败的数据包
  • void recvRangeChange()sendRangeChange上面类似,每当对等端口想要通知此对象它接受的地址范围正在更改时,都会调用此函数。该函数通常只在内存系统初始化期间调用,而不是在仿真执行时调用

构造函数需要初始化所有端口。每个端口的构造函数都有两个参数:名称和指向其所有者的指针

SimpleMemobj::SimpleMemobj(SimpleMemobjParams *params) :
    MemObject(params),
    instPort(params->name + ".inst_port",this),
    dataPort(params->name + ".data_port",this),
    memPort(params->name + ".mem_side",this)
{
}