在前面的四个部分中,已为使用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位寄存器。 提出了一种原始的递归算法:
- 我们从上下文中开始,在该上下文中,指令的操作数位于
r1
和r2
中,其余为零,都有效(更确切地说,有效性等于协处理器指令的“就绪”) - 如果看到算术逻辑指令,则从上下文中提取其操作数寄存器,调用自身作为列表的尾部,并在上下文中将输出操作数替换为一对
(data1 op data2, valid1 && valid2)
- 如果遇到分支,我们只需递归地构建两个分支:如果分支发生,否则
- 如果遇到加载或保存到内存的问题,我们就会以某种方式摆脱困境:我们执行已传输的回调,并假设在执行该指令期间一旦
valid
语句无法被调用的不变性。 保存操作的有效性由我们与globalValid
标志进行AND运算,该标志必须在返回控件之前进行设置。 同时,我们必须沿线进行读写,以正确处理增量和其他修改。
因此,将尽可能并行地而不是逐步地执行操作。 同时,请您注意,对内存特定字节的所有操作自然应该完全排序,否则结果是不可预测的,UB。 即 *addr += 1
这是正常现象,直到读取完成(直到现在我们仍然不知道要写什么),才会真正开始写入,但是*addr += 1; return *addr;
*addr += 1; return *addr;
我通常会安全地给出零或类似的值。 也许值得调试(也许它隐藏了一些更棘手的问题),但是无论如何,这种吸引力本身就是一个主意,因为您必须跟踪工作已经完成的内存地址,但是我有一个愿望valid
可能,请静态验证值。 这正是固定大小的全局变量将要执行的操作。
结果是一个抽象类BpfCircuitConstructor
,它没有实现的方法doMemLoad
, doMemStore
和resolveSymbol
:
trait BpfCircuitConstructor {
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高速缓存端口, busy
和interrupt
输出以及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.req
和io.mem.resp
响应io.mem.resp
。 同时,请求端口配备了传统的ready
和valid
信号:第一个告诉您已准备好接受请求,第二个告诉您请求已准备好并且已经具有正确的结构,沿着前面, valid && resp
响应被视为已接受。 在某些此类接口中,要求从设置为true
到valid && resp
的后续上升沿都对信号“无响应” fire()
为方便起见,可以使用fire()
方法构造此表达式)。
resp
, resp
响应端口只有一个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] =
...,而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"
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
源代码