第5/2部分建筑。 1:RocketChip大道和湿滑的仪器仪表跑道的十字路口

在前面的四个部分中,已为使用RISC-V RocketChip内核进行实验做准备,即将RISC-V RocketChip内核通过Altera FPGA(现在为Intel)移植到“非标准”电路板上。 最后,在最后一部分中 ,结果证明可以在此板上运行Linux。 你知道这一切使我感到有趣吗? 我必须同时使用RISC-V,C和Scala的汇编程序,而在所有这些程序中,Scala是最低级别的语言(因为在上面编写了处理器)。


让我们在本文中也不要让C令人反感。 此外,如果Scala + Chisel捆绑包仅用作特定领域的语言来对硬件进行明确描述,那么今天我们将学习如何以指令形式将简单的C函数“拉”入处理器。


最终目标是通过类似于QInst的方式实现琐碎的类似于AFL的仪器的实现,而单独的指令的实现只是副产品。


显然,有(没有一个)商业OpenCL到RTL转换器。 我还遇到了有关某个RISC-V的COPILOT项目的信息,该项目具有相似的目标(更为先进),但是搜索结果非常糟糕,此外,它很有可能也是商业产品。 我主要对OpenSource解决方案感兴趣,但是即使有,也可以尝试自己实现,仍然很有趣-至少作为一个简化的培训示例,然后了解它的发展方式...


免责声明 (除了关于“用灭火器跳舞”的常规警告之外):我强烈建议您不要鲁applying地应用生成的软件核心,尤其是对不受信任的数据-到目前为止,我没有那么大的信心,甚至不理解为什么要处理的数据不能流程和/或核心之间的某些边界情况下的“流程”。 好吧,关于数据可能“跳动”的事实,我认为是显而易见的。 一般来说,仍然存在验证和验证...


首先,我怎么称呼“简单功能”? 出于本文的目的,这意味着一个函数,其中所有转换(有条件的和无条件的)仅将指令计数器增加一个恒定值。 也就是说,所有可能的过渡图都是(有向的)无环的,没有“动态”边缘。 本文框架的最终目标是能够从程序中获得一个简单的功能,并用汇编程序插件代替它,然后在合成阶段将其“缝”入处理器,可以选择使其成为另一条指令的副作用。 具体来说,本文中不会显示分支,但是在最简单的情况下,创建分支并不困难。


学习理解C(实际上不是)


首先,您需要了解我们将如何解析C? 没错,没办法-我学会解析ELF文件并没有白费:您只需要将C / Rust /其他代码编译成eBPF字节码,然后就已经对其进行了解析。 某些困难是由于以下事实造成的:在Scala中,您不能仅连接elf.h并读取结构字段。 当然,您可以尝试使用JNAerator-如有必要,它们可以对库进行绑定-不仅可以结构,还可以生成通过JNA工作的代码(不要与JNI混淆)。 作为一名真正的程序员,我将编写自行车,并仔细地编写头文件中的枚举和偏移量常量。 结果和中间结构由案例类的以下结构描述:


 sealed trait SectionKind case object RegularSection extends SectionKind case object SymtabSection extends SectionKind case object StrtabSection extends SectionKind case object RelSection extends SectionKind final case class Elf64Header( sectionHeaders: Seq[ByteBuffer], sectionStringTableIndex: Int ) final case class Elf64Section( data: ByteBuffer, linkIndex: Int, infoIndex: Int, kind: SectionKind ) final case class Symbol( name: String, value: Int, size: Int, shndx: Int, isInstrumenter: Boolean ) final case class Relocation( relocatedSection: Int, offset: Int, symbol: Symbol ) final case class BpfInsn( opcode: Int, dst: Int, src: Int, offset: Int, imm: Either[Long, Symbol] ) final case class BpfProg( name: String, insns: Seq[BpfInsn] ) 

我不会特别描述解析过程-这只是从java.nio.ByteBuffer进行的无聊字节传输-所有有趣的事情已经在解析ELF文件文章中进行了描述。 我只能说您需要仔细处理opcode == 0x18 (将64位立即数加载到寄存器中),因为它一次占用两个8字节的字(也许还有其他此类操作码,但我还没有遇到它们) ,并且这并不总是加载与重定位相关的内存地址,就像我最初想的那样。 例如, 0x0101010101010101诚实地使用64位常量 0x0101010101010101 。 为什么我不对补丁下载的文件进行“诚实”重定位-因为我想查看符号形式的字符(对双关语很抱歉),因此以后可以用寄存器替换COMMON部分中的字符,而无需使用带有特殊处理地址类型的拐杖(和这意味着,即使在具有恒定/非恒定UInt舞蹈中也是如此。


我们根据一组说明来构建硬件


因此,通过假设,所有可能的执行路径都排在指令列表的下方,这意味着数据沿着定向的非循环图流动,并且其所有边缘都是静态定义的。 同时,我们具有纯粹的组合逻辑(即,途中没有寄存器),这些逻辑是通过寄存器上的操作获得的,以及使用内存进行加载/存储操作期间的延迟。 因此,在一般情况下,可能无法在一个时钟周期内完成操作。 我们将做简单的事情:我们将以UInt的形式将值传输到,但是就像(UInt, Bool) :对中的第一个元素是值,第二个是其正确性的标志。 也就是说,只要地址不正确,从内存中读取就没有多大意义,并且一般而言是不可能的。


eBPF字节码执行模型假定某种类型的RAM具有64位寻址,以及一组16个(甚至10个)64位寄存器。 提出了一种原始的递归算法:


  • 我们从上下文中开始,在该上下文中,指令的操作数位于r1r2中,其余为零,都有效(更确切地说,有效性等于协处理器指令的“就绪”)
  • 如果看到算术逻辑指令,则从上下文中提取其操作数寄存器,调用自身作为列表的尾部,并在上下文中将输出操作数替换为一对(data1 op data2, valid1 && valid2)
  • 如果遇到分支,我们只需递归地构建两个分支:如果分支发生,否则
  • 如果遇到加载或保存到内存的问题,我们就会以某种方式摆脱困境:我们执行已传输的回调,并假设在执行该指令期间一旦valid语句无法被调用的不变性。 保存操作的有效性由我们与globalValid标志进行AND运算,该标志必须在返回控件之前进行设置。 同时,我们必须沿线进行读写,以正确处理增量和其他修改。

因此,将尽可能并行地而不是逐步地执行操作。 同时,请您注意,对内存特定字节的所有操作自然应该完全排序,否则结果是不可预测的,UB。 即 *addr += 1这是正常现象,直到读取完成(直到现在我们仍然不知道要写什么),才会真正开始写入,但是*addr += 1; return *addr; *addr += 1; return *addr; 我通常会安全地给出或类似的值。 也许值得调试(也许它隐藏了一些更棘手的问题),但是无论如何,这种吸引力本身就是一个主意,因为您必须跟踪工作已经完成的内存地址,但是我有一个愿望valid可能,请静态验证值。 这正是固定大小的全局变量将要执行的操作。


结果是一个抽象类BpfCircuitConstructor ,它没有实现的方法doMemLoaddoMemStoreresolveSymbol


 trait BpfCircuitConstructor { // ... sealed abstract class LdStType(val lgsize: Int) { val byteSize = 1 << lgsize val bitSize = byteSize * 8 val mask: UInt = if (bitSize == 64) mask64 else ((1l << bitSize) - 1).U } case object u8 extends LdStType(0) case object u16 extends LdStType(1) case object u32 extends LdStType(2) case object u64 extends LdStType(3) def doMemLoad(addr: UInt, tpe: LdStType, valid: Bool): (UInt, Bool) def doMemStore(addr: UInt, tpe: LdStType, data: UInt, valid: Bool): Bool sealed trait Resolved { def asPlainValue: UInt def load(ctx: Context, offset: Int, tpe: LdStType, valid: Bool): LazyData def store(offset: Int, tpe: LdStType, data: UInt, valid: Bool): Bool } def resolveSymbol(sym: BpfLoader.Symbol): Resolved // ... } 

CPU核心整合


对于初学者,我决定采用一种简单的方法:使用标准的RoCC(火箭定制协处理器)协议连接到处理器内核。 据我了解,这不是对所有RISC-V兼容内核的常规扩展,而是仅对Rocket和BOOM(伯克利custom0计算机)的扩展,因此,当在编译器上拖动上游工作时,汇编器custom0助记符被排除在外- custom3负责加速器命令。


通常,每个Rocket / BOOM处理器内核最多可以通过配置添加四个RoCC加速器,还有实现示例:


Configs.scala:


 class WithRoccExample extends Config((site, here, up) => { case BuildRoCC => List( (p: Parameters) => { val accumulator = LazyModule(new AccumulatorExample(OpcodeSet.custom0, n = 4)(p)) accumulator }, (p: Parameters) => { val translator = LazyModule(new TranslatorExample(OpcodeSet.custom1)(p)) translator }, (p: Parameters) => { val counter = LazyModule(new CharacterCountExample(OpcodeSet.custom2)(p)) counter }) }) 

相应的实现在LazyRoCC.scala文件中。


加速器实现表示内存控制器已经熟悉的两个类:在这种情况下,其中一个是从LazyRoCC继承的, LazyRoCC是从LazyRoCC继承的。 第二类具有RoCCIO类型的io端口,其中包含cmd请求端口, resp响应端口,mem访问L1D高速缓存端口, busyinterrupt输出以及exception输入。 还有一个似乎不需要的页表浏览器端口和FPU(无论如何,eBPF中没有真正的算法)。 到目前为止,我想尝试使用这种方法做点什么,所以我不会碰到interrupt 。 另外,据我了解,有一个TileLink接口可用于非缓存的内存访问,但是现在我也不会碰它。


查询组织者


因此,我们有一个用于访问缓存的端口,但是只有一个。 同时,一个函数可以例如增加一个变量(至少可以将其变成单个原子操作),甚至可以通过加载,更新和保存它来进行不平凡的转换。 最后,一条指令可以发出几个不相关的请求。 就性能而言,这也许不是最好的主意,但是,另一方面,为什么不加载三个单词(很有可能已经在缓存中),以某种方式与组合逻辑并行处理它们(然后一拍)并保存结果。 因此,我们需要某种方案来有效地“解决”并行访问单个缓存端口的尝试。


逻辑将类似于以下内容:在特定子注入的实现的生成的开始(就RoCC而言为7位funct字段),将创建查询序列化程序的实例(使一个全局变量对我来说似乎非常有害,因为它在请求之间创建了一堆额外的依赖关系,这些依赖关系永远无法同时执行,并且最有可能浪费Fmax)。 接下来,在序列化器中注册每个创建的“保存程序” /“加载程序”。 可以这么说,在现场队列中。 在每种措施下,都会选择注册顺序中的第一个请求- 在下一项措施中会授予他许可。 自然地,这种逻辑需要适当地与测试重叠(尽管我没有太多的逻辑,所以不是验证,而是至少可以理解的最低要求)。 我使用了来自或多或少官方组件的标准PeekPokeTester来测试Chisel设计。 我已经描述过一次


结果就是这样一个怪诞的事情:


 class Serializer(isComputing: Bool, next: Bool) { def monotonic(x: Bool): Bool = { val res = WireInit(false.B) val prevRes = RegInit(false.B) prevRes := res && isComputing res := (x || prevRes) && isComputing res } private def noone(bs: Seq[Bool]): Bool = !bs.foldLeft(false.B)(_ || _) private val previousReqs = ArrayBuffer[Bool]() def nextReq(x: Bool): (Bool, Int) = { val enable = monotonic(x) val result = RegInit(false.B) val retired = RegInit(false.B) val doRetire = result && next val thisReq = enable && !retired && !doRetire val reqWon = thisReq && noone(previousReqs) when (isComputing) { when(reqWon) { result := true.B } when(doRetire) { result := false.B retired := true.B } } otherwise { result := false.B retired := false.B } previousReqs += thisReq (result, previousReqs.length - 1) } } 

请注意,在创建数字电路的过程中,Scala代码是安全执行的。 如果您仔细观察,您甚至会注意到一个ArrayBuffer ,电路的各个部分都堆叠在其中( Boolean是Scala的类型, Bool是表示带电设备的Chisel类型,而不是运行时已知的布尔)。


使用L1D缓存


使用缓存的工作主要是通过io.mem.req请求io.mem.reqio.mem.resp响应io.mem.resp 。 同时,请求端口配备了传统的readyvalid信号:第一个告诉您已准备好接受请求,第二个告诉您请求已准备好并且已经具有正确的结构,沿着前面, valid && resp响应被视为已接受。 在某些此类接口中,要求从设置为truevalid && resp的后续上升沿都对信号“无响应” fire()为方便起见,可以使用fire()方法构造此表达式)。


respresp响应端口只有一个valid符号,这是处理器在一个时钟周期内耙回答案的问题:假设它“始终准备就绪”,而fire()返回的则是valid


另外,正如我已经说过的,您无法在可怕的情况下提出请求:您不能写东西,我不知道什么,然后再读一遍,根据减去的值以后将被覆盖的内容也有些奇怪。 但是Serializer类已经理解了这一点,但是我们只给它一个信号,即当前请求已经进入缓存: next = io.mem.req.fire() 。 所有可以做的就是确保答案在“阅读器”中仅在真正出现时才更新-不早也不晚。 holdUnless有一个方便的holdUnless方法。 结果大约是以下实现:


  class Constructor extends BpfCircuitConstructor { val serializer = new Serializer(isComputing, io.mem.req.fire()) override def doMemLoad(addr: UInt, tpe: LdStType, valid: Bool): (UInt, Bool) = { val (doReq, thisTag) = serializer.nextReq(valid) when (doReq) { io.mem.req.bits.addr := addr require((1 << io.mem.req.bits.tag.getWidth) > thisTag) io.mem.req.bits.tag := thisTag.U io.mem.req.bits.cmd := M_XRD io.mem.req.bits.typ := (4 | tpe.lgsize).U io.mem.req.bits.data := 0.U io.mem.req.valid := true.B } val doResp = isComputing && serializer.monotonic(doReq && io.mem.req.fire()) && io.mem.resp.valid && io.mem.resp.bits.tag === thisTag.U && io.mem.resp.bits.cmd === M_XRD (io.mem.resp.bits.data holdUnless doResp, serializer.monotonic(doResp)) } override def doMemStore(addr: UInt, tpe: LdStType, data: UInt, valid: Bool): Bool = { val (doReq, thisTag) = serializer.nextReq(valid) when (doReq) { io.mem.req.bits.addr := addr require((1 << io.mem.req.bits.tag.getWidth) > thisTag) io.mem.req.bits.tag := thisTag.U io.mem.req.bits.cmd := M_XWR io.mem.req.bits.typ := (4 | tpe.lgsize).U io.mem.req.bits.data := data io.mem.req.valid := true.B } serializer.monotonic(doReq && io.mem.req.fire()) } override def resolveSymbol(sym: BpfLoader.Symbol): Resolved = sym match { case BpfLoader.Symbol(symName, _, size, ElfConstants.Elf64_Shdr.SHN_COMMON, false) if size <= 8 => RegisterReference(regs.getOrElseUpdate(symName, RegInit(0.U(64.W)))) } } 

将为每个生成的子指令创建此类的实例。


并非堆上的全部都是全局变量


嗯,什么是模型示例? 我想确保什么性能? 当然是AFL仪器! 它在经典版本中看起来像这样:


 #include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_branch(uint64_t tag) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; } 

如您所见,它或多或少地从__afl_area_ptr逻辑加载和保存(在它们之间为增量)一个字节,但是这里的寄存器要求起prev作用!


这就是为什么需要Resolved接口的原因:它可以包装常规内存地址或作为寄存器引用。 同时,到目前为止,我仅考虑大小为1、2、4或8字节的标量寄存器,这些标量寄存器始终以零偏移量读取,因此对于寄存器,您可以相对平静地实现调用顺序。 在这种情况下,知道必须先减去prev并用于计算索引,然后才进行重写,这非常有用。


现在仪表


在某个时候,我们有了带有RoCC接口的独立的或多或少可以使用的加速器。 现在怎么办 重新实现都一样,是否通过处理器管道? 在我看来,如果与已安装的指令并行地,简单地激活具有自动发布的实用价值函数的协处理器,则将需要更少的拐杖。 原则上,我还为此感到痛苦:我什至学会了使用SignalTap,因为调试几乎是盲目的,甚至在稍作更改后(即使更改bootrom除外-一切都很快),即使重新编译五分钟也是如此-这已经太多了。


结果,对命令解码器进行了调整,流水线稍微“变直了”,以考虑到无论解码器对原始指令说什么,突然激活的RoCC本身并不意味着对输出寄存器的写入将有很长的延迟,就像在除法运算期间那样并错过了数据缓存。


通常,指令的描述是一对([用于识别指令的模式],[配置处理器核的数据路径块的值的集合])。 例如, default (无法识别的指令)如下所示(从IDecode.scala中获取,坦白地说,在桌面Habr中看起来很丑):


 def default: List[BitPat] = // jal renf1 fence.i // val | jalr | renf2 | // | fp_val| | renx2 | | renf3 | // | | rocc| | | renx1 s_alu1 mem_val | | | wfd | // | | | br| | | | s_alu2 | imm dw alu | mem_cmd mem_type| | | | mul | // | | | | | | | | | | | | | | | | | | | | | div | fence // | | | | | | | | | | | | | | | | | | | | | | wxd | | amo // | | | | | | | | scie | | | | | | | | | | | | | | | | | dp List(N,X,X,X,X,X,X,X,X,A2_X, A1_X, IMM_X, DW_X, FN_X, N,M_X, MT_X, X,X,X,X,X,X,X,CSR.X,X,X,X,X) 

...,而Rocket核心扩展之一的典型描述是这样实现的:


 class IDecode(implicit val p: Parameters) extends DecodeConstants { val table: Array[(BitPat, List[BitPat])] = Array( BNE-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SNE, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BEQ-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SEQ, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BLT-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SLT, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BLTU-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SLTU, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BGE-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SGE, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BGEU-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SGEU, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), // ... 

事实是,在RISC-V(不仅在RocketChip中,而且在原则上在命令架构中),ISA分为强制性子集I(整数运算)和可选的M(整数乘法和除法),还定期支持A(原子)等


结果,原来的方法


  def decode(inst: UInt, table: Iterable[(BitPat, List[BitPat])]) = { val decoder = DecodeLogic(inst, default, table) val sigs = Seq(legal, fp, rocc, branch, jal, jalr, rxs2, rxs1, scie, sel_alu2, sel_alu1, sel_imm, alu_dw, alu_fn, mem, mem_cmd, mem_type, rfs1, rfs2, rfs3, wfd, mul, div, wxd, csr, fence_i, fence, amo, dp) sigs zip decoder map {case(s,d) => s := d} this } 

已被取代


相同,但是带有解码器,用于仪器化和阐明rocc激活的原因
 def decode(inst: UInt, table: Iterable[(BitPat, List[BitPat])], handlers: Seq[OpcodeHandler]) = { val decoder = DecodeLogic(inst, default, table) val sigs=Seq(legal, fp, rocc_explicit, branch, jal, jalr, rxs2, rxs1, scie, sel_alu2, sel_alu1, sel_imm, alu_dw, alu_fn, mem, mem_cmd, mem_type, rfs1, rfs2, rfs3, wfd, mul, div, wxd, csr, fence_i, fence, amo, dp) sigs zip decoder map {case(s,d) => s := d} if (handlers.isEmpty) { handler_rocc := false.B handler_rocc_funct := 0.U } else { val handlerTable: Seq[(BitPat, List[BitPat])] = handlers.map { case OpcodeHandler(pattern, funct) => pattern -> List(Y, BitPat(funct.U)) } val handlerDecoder = DecodeLogic(inst, List(N, BitPat(0.U)), handlerTable) Seq(handler_rocc, handler_rocc_funct) zip handlerDecoder map { case (s,d) => s:=d } } rocc := rocc_explicit || handler_rocc this } 

在处理器管道的变化中,最明显的也许是:


  io.rocc.exception := wb_xcpt && csr.io.status.xs.orR io.rocc.cmd.bits.status := csr.io.status io.rocc.cmd.bits.inst := new RoCCInstruction().fromBits(wb_reg_inst) + when (wb_ctrl.handler_rocc) { + io.rocc.cmd.bits.inst.opcode := 0x0b.U // custom0 + io.rocc.cmd.bits.inst.funct := wb_ctrl.handler_rocc_funct + io.rocc.cmd.bits.inst.xd := false.B + io.rocc.cmd.bits.inst.rd := 0.U + } io.rocc.cmd.bits.rs1 := wb_reg_wdata io.rocc.cmd.bits.rs2 := wb_reg_rs2 

显然,对加速器的请求的某些参数需要更正:没有响应写入寄存器,并且funct等于解码器返回的内容。 但是有一点不太明显的变化:事实是该命令并不直接传递给加速器(其中四个-哪个?),而是传递给路由器,因此您需要假装该命令的opcode == custom0 (是的,过程,而这恰恰是零加速器!)。


检查一下


实际上,本文假设将继续进行尝试,以使这种方法达到或多或少的生产水平。 切换任务时,至少必须学会保存和还原上下文(协处理器寄存器的状态)。 同时,我将检查它是否可以在温室条件下正常工作:


 #include <stdint.h> uint64_t counter; uint64_t funct1(uint64_t x, uint64_t y) { return __builtin_popcountl(x); } uint64_t funct2(uint64_t x, uint64_t y) { return (x + y) * (x - y); } uint64_t instMUL() { counter += 1; *((uint64_t *)0x81005000) = counter; return 0; } 

现在在main行中添加到bootrom/sdboot/sd.c


 #include "/path/to/freedom-u-sdk/riscv-pk/machine/encoding.h" // ... ////    -   RoCC #define STR1(x) #x #define STR(x) STR1(x) #define EXTRACT(a, size, offset) (((~(~0 << size) << offset) & a) >> offset) #define CUSTOMX_OPCODE(x) CUSTOM_##x #define CUSTOM_0 0b0001011 #define CUSTOM_1 0b0101011 #define CUSTOM_2 0b1011011 #define CUSTOM_3 0b1111011 #define CUSTOMX(X, rd, rs1, rs2, funct) \ CUSTOMX_OPCODE(X) | \ (rd << (7)) | \ (0x7 << (7+5)) | \ (rs1 << (7+5+3)) | \ (rs2 << (7+5+3+5)) | \ (EXTRACT(funct, 7, 0) << (7+5+3+5+5)) #define CUSTOMX_R_R_R(X, rd, rs1, rs2, funct) \ asm ("mv a4, %[_rs1]\n\t" \ "mv a5, %[_rs2]\n\t" \ ".word "STR(CUSTOMX(X, 15, 14, 15, funct))"\n\t" \ "mv %[_rd], a5" \ : [_rd] "=r" (rd) \ : [_rs1] "r" (rs1), [_rs2] "r" (rs2) \ : "a4", "a5"); int main(void) { // ... //  RoCC extension write_csr(mstatus, MSTATUS_XS & (MSTATUS_XS >> 1)); //   bootrom       uint64_t res; CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 1); CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 2); // ...     uint64_t x = 1; for (int i = 0; i < 123; ++i) x *= *(volatile uint8_t *)0x80000000; kputc('0' + x % 10); //   !!! // ... } 

write_csr , custom0 - custom3 . , illegal instruction, , , , . define - - , «» binutils customX RocketChip, , , .


sdboot , , .


:


 $ /hdd/trosinenko/rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "set directories bootrom" builds/zeowaa-e115/sdboot.elf Reading symbols from builds/zeowaa-e115/sdboot.elf...done. Remote debugging using :3333 0x0000000000000000 in ?? () (gdb) x/d 0x81005000 0x81005000: 123 (gdb) set variable $pc=0x10000 (gdb) c Continuing. ^C Program received signal SIGINT, Interrupt. 0x0000000000010488 in crc16_round (data=<optimized out>, crc=<optimized out>) at sd.c:151 151 crc ^= data; (gdb) x/d 0x81005000 0x81005000: 246 

funct1
 $ /hdd/trosinenko/rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "set directories bootrom" builds/zeowaa-e115/sdboot.elf Reading symbols from builds/zeowaa-e115/sdboot.elf...done. Remote debugging using :3333 0x0000000000010194 in main () at sd.c:247 247 CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 1); (gdb) set variable $a5=0 (gdb) set variable $pc=0x10194 (gdb) set variable $a4=0xaa (gdb) display/10i $pc-10 1: x/10i $pc-10 0x1018a <main+46>: sw a3,124(a3) 0x1018c <main+48>: addiw a0,a0,1110 0x10190 <main+52>: mv a4,s0 0x10192 <main+54>: mv a5,a0 => 0x10194 <main+56>: 0x2f7778b 0x10198 <main+60>: mv s0,a5 0x1019a <main+62>: lbu a5,0(a1) 0x1019e <main+66>: addiw a3,a3,-1 0x101a0 <main+68>: mul a2,a2,a5 0x101a4 <main+72>: bnez a3,0x1019a <main+62> (gdb) display/x $a5 2: /x $a5 = 0x0 (gdb) si 0x0000000000010198 247 CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 1); 1: x/10i $pc-10 0x1018e <main+50>: li a0,25 0x10190 <main+52>: mv a4,s0 0x10192 <main+54>: mv a5,a0 0x10194 <main+56>: 0x2f7778b => 0x10198 <main+60>: mv s0,a5 0x1019a <main+62>: lbu a5,0(a1) 0x1019e <main+66>: addiw a3,a3,-1 0x101a0 <main+68>: mul a2,a2,a5 0x101a4 <main+72>: bnez a3,0x1019a <main+62> 0x101a6 <main+74>: li a5,10 2: /x $a5 = 0x4 (gdb) set variable $a4=0xaabc (gdb) set variable $pc=0x10194 (gdb) si 0x0000000000010198 247 CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 1); 1: x/10i $pc-10 0x1018e <main+50>: li a0,25 0x10190 <main+52>: mv a4,s0 0x10192 <main+54>: mv a5,a0 0x10194 <main+56>: 0x2f7778b => 0x10198 <main+60>: mv s0,a5 0x1019a <main+62>: lbu a5,0(a1) 0x1019e <main+66>: addiw a3,a3,-1 0x101a0 <main+68>: mul a2,a2,a5 0x101a4 <main+72>: bnez a3,0x1019a <main+62> 0x101a6 <main+74>: li a5,10 2: /x $a5 = 0x9 

源代码

Source: https://habr.com/ru/post/zh-CN461577/


All Articles