课题05:RISC-V汇编代码美化器

难度:低 | 类型:项目实战 | 源文件scratchv/backend/asm_beautifier.py | 行数:~535 状态:✅ 已完成


概述

开发独立工具,读取编译器生成的.s汇编文件,输出格式整洁、带注释、对齐良好的版本。该工具自动对RISC-V汇编代码进行三项处理:列对齐(标签、操作码、操作数分别对齐到固定宽度)、语义注释(为每条指令自动生成人类可读的解释)、分段标记(识别代码段/数据段并插入分隔标题),大幅降低汇编代码的阅读门槛。


理解背景

是什么?

汇编美化器(asm_beautifier.py)把机器生成的"裸"RISC-V 汇编代码变成人类友好的格式。它做三件事:

  1. 列对齐:标签、操作码、操作数分别对齐到固定宽度列
  2. 语义注释:给每条指令自动加上"这句在做什么"的注释
  3. 分段标记:识别代码段/数据段,插入分隔标题

效果对比:

# 美化前(机器生成的原始汇编)
main:
addi sp,sp,-32
sw ra,28(sp)
li a5,3
li a4,5
add a5,a5,a4

# 美化后(人类友好)
# --- Function: main ---
main:   addi  sp, sp, -32     # sp = sp + -32
        sw    ra, 28(sp)      # MEM[sp + 28] = ra
        li    a5, 3           # a5 = 3
        li    a4, 5           # a4 = 5
        add   a5, a5, a4      # a5 = a5 + a4

为什么?

初学 RISC-V 汇编时,你经常需要对照手册才能看懂 addi sp, sp, -32 在干什么。美化器把每条指令翻译成人类可读的公式,大大降低阅读门槛。

对编译器开发者来说,美化后的汇编更容易: - 人工检查代码生成是否正确 - 对比优化前后的汇编差异 - 教学演示时让学生一眼看懂


核心概念

1. 汇编行的结构

一条 RISC-V 汇编行可以拆成 4 个部分:

[label:]  [opcode]  [operands]  [# comment]
  标签      操作码     操作数        注释

例如:main: add a0, a1, a2 # a0 = a1 + a2

2. 语义注释模板

美化器内置了 80+ 条 RISC-V 指令的"翻译模板"。例如:

指令 模板 效果
add rd, rs1, rs2 {rd} = {rs1} + {rs2} a0 = a1 + a2
lw rd, imm(rs1) {rd} = MEM[{rs1} + {imm}] a0 = MEM[sp + 16]
beq rs1, rs2, label if {rs1} == {rs2} goto {label} if a0 == a1 goto .L1
jal rd, imm {rd} = PC+4; goto {imm} ra = PC+4; goto 0x100
li rd, imm {rd} = {imm} a0 = 42
mv rd, rs1 {rd} = {rs1} a0 = a1

3. 函数/段检测

美化器自动识别: - 段切换.text → 代码段,.data → 数据段,.bss → 未初始化数据段 - 函数入口:不以 .Lloop 开头的标签视为函数


详细任务
  1. 解析汇编行,识别标签、指令、操作数、注释。
  2. 对齐字段:标签左对齐,指令助记符占固定宽度(如8字符),操作数左对齐。
  3. 为每条指令自动添加注释:如addi x1, x0, 5 # x1 = x0 + 5。提供指令注释模板库。
  4. 添加段注释:.text.data、函数入口前插入分隔线和描述。
  5. 支持命令行参数:输入文件、输出文件、是否添加注释、是否对齐。
  6. 输出美化后的汇编文件。

交付产物
  • Python脚本asm_beautifier.py
  • 示例输入输出文件
  • 文档:使用方法、自定义注释模板

代码走读

使用美化器

Step 1: 从 Python 调用

from scratchv.backend.asm_beautifier import beautify_asm

raw_asm = """
main:
addi sp,sp,-32
sw ra,28(sp)
li a5,3
add a5,a5,a5
"""

pretty = beautify_asm(raw_asm)
print(pretty)

Step 2: 从命令行使用

# 美化汇编文件并输出到终端
python -m scratchv.backend.asm_beautifier output.s

# 保存到文件
python -m scratchv.backend.asm_beautifier output.s -o output_pretty.s

# 关闭对齐
python -m scratchv.backend.asm_beautifier output.s --no-align

# 关闭自动注释
python -m scratchv.backend.asm_beautifier output.s --no-comments

Step 3: 编译 CNN 模型并美化汇编

# 先生成汇编
python scratchv/standalone/onnx_to_riscv_standalone.py models/graph/cnn.onnx \
    -o /tmp/cnn.bin --asm /tmp/cnn_raw.s

# 再美化
python -m scratchv.backend.asm_beautifier /tmp/cnn_raw.s -o /tmp/cnn_pretty.s

# 对比看看效果
head -50 /tmp/cnn_pretty.s

核心函数:beautify_asm()
def beautify_asm(asm_text: str, align: bool = True,
                 add_comments: bool = True) -> str:
    # 1. 按行解析 → 每行拆成 {label, opcode, operands, comment}
    lines = asm_text.strip().split("\n")
    parsed_lines = [_parse_line(ln) for ln in lines]

    # 2. 第一遍扫描:计算每列的最大宽度
    max_label = max(len(p["label"]) for p in parsed_lines)
    max_opcode = max(len(p["opcode"]) for p in parsed_lines)
    # ... clamp 宽度避免溢出 ...

    # 3. 第二遍扫描:按对齐宽度格式化,加注释
    for p in parsed_lines:
        formatted = _format_line(p, align, max_label, max_opcode, ...)
        output_lines.append(formatted)

    return "\n".join(output_lines)

注释生成逻辑:_gen_comment()
def _gen_comment(opcode: str, operands: list[str]) -> str:
    template = _INST_COMMENTS.get(opcode)  # 查模板表
    if template is None:
        return ""  # 未知指令不加注释
    # 替换模板变量:{rd}→实际寄存器, {rs1}→实际寄存器, ...
    return template.format(rd=operands[0], rs1=operands[1], ...)

关键细节
  • 内存操作数处理lw a0, 16(sp) 需要把 16 提取为 immsp 提取为 rs1
  • 分支跳转指令:目标标签通常是最后一个操作数(不是第一个)

动手练习

练习 1: 美化一段手写汇编

创建 test.s

.text
main:
addi sp,sp,-16
li a0,10
li a1,20
add a0,a0,a1
sw a0,0(sp)
lw a0,0(sp)
ret

然后运行美化器观察效果。

练习 2: 添加新的指令模板

_INST_COMMENTS 字典中添加一条新指令的注释模板。比如添加 max(ScratchV 伪指令)的模板。

练习 3: 对比美化前后的可读性

make bench-cnn 生成汇编,分别看美化前后的效果。哪种更容易理解?


常见坑
说明
操作数顺序混淆 RISC-V 中 add rd, rs1, rs2sw rs2, imm(rs1) 的操作数"角色"不同,美化器需要区分
伪指令 limvcall 等不是真正的 RISC-V 指令,是汇编器的"语法糖",模板需要单独处理
列宽 clamp 标签列宽最大 30、操作码最大 12、操作数最大 40,太长会被截断避免排版溢出
浮点指令 fadd.sfmul.s 等浮点指令注释模板已内置但较少使用(ScratchV 目前主要用整数路径)

进阶阅读

12周每周目标
  • W1:学习RISC-V基础指令集,阅读项目生成的.s文件样例,分析格式问题。
  • W2:编写正则表达式,从一行汇编中提取标签、指令、操作数(逗号分割)、注释。
  • W3:实现字段对齐:设定指令助记符宽度8,操作数宽度20,左对齐或右对齐。
  • W4:输出对齐后的汇编行,保留空行和纯注释。生成第一版美化脚本。
  • W5:构建指令注释字典,为常见指令(add, sub, lw, sw, beq, jal)撰写人类可读解释模板。
  • W6:实现自动注释生成:根据指令操作数填充模板中的寄存器名(如x1ra可选)。
  • W7:在代码段前添加段注释:.text前加# ===== CODE SECTION =====.data前加类似标记。
  • W8:处理伪指令(li, mv)的特殊注释,确保注释不会过长。
  • W9:添加命令行参数(argparse):输入文件、输出文件、--no-comments--no-align
  • W10:美化错误处理:遇到无法解析的行原样输出并警告,支持批量处理多个文件。
  • W11:测试至少10个不同的汇编文件(包括错误格式),对比美化前后可读性,编写测试脚本。
  • W12:撰写文档(安装依赖、使用示例、正则表达式规则),准备演示。