网站Logo 网友马大帅的博客

RISC-V汇编语言

ughostx
24
2025-12-18

汇编器

在 Unix 系统中,这一步的输入是以.s 为后缀的文件,比如 foo.s;在 MS-DOS 中则是.ASM,最终输出可执行可链接文件(以.o为后缀)。汇编程序的开头是一些汇编指示符(assemble directives)。它们是汇编器的命令,具有告诉汇编器代码和数据的位置、指定程序中使用的特定代码和数据常量等作用。常见的指示符有:

⚫ .text:进入代码段。
⚫ .align 2:后续代码按 22 字节对齐。
⚫ .globl main:声明全局符号“main”。
⚫ .section .rodata:进入只读数据段
⚫ .balign 4:数据段按 4 字节对齐。
⚫ .string “Hello, %s!\n”:创建空字符结尾的字符串。
⚫ .string “world”:创建空字符结尾的字符串。

risc-v示例的hello world 汇编代码:

.text   指示符:进入代码段
.align 2    指示符:按 2^2 字节对齐代码
.globl main 指示符:声明全局符号 main
main:   main 开始标记
    addi sp,sp,-16  分配栈帧
    sw ra,12(sp)    存储返回地址
    lui a0,%hi(string1) 计算 string1 的地址
    addi a0,a0,%lo(string1)
    lui a1,%hi(string2) 计算 string2 的地址
    addi a1,a1,%lo(string2)
    call printf 调用 printf 函数
    lw ra,12(sp)    恢复返回地址
    addi sp,sp,16   释放栈帧
    li a0,0 读取返回值
    ret 返回
    .section .rodata    指示符:进入只读数据段
    .balign 4   指示符:按 4 字节对齐数据
string1:    第一个字符串标记
    .string "Hello, %s!\n"  指示符:空字符结尾的字符串
string2:    第二个字符串标记
    .string "world" 指示符:空字符结尾的字符串

链接器

链接器允许各个文件独立地进行编译和汇编,这样在改动部分文件时,不需要重新编译全部源代码。链接器把新的目标代码和已经存在的机器语言模块(如函数库)等“拼接”起来。链接器这个名字源于它的功能之一,即编辑所有对象文件的跳转并链接指令(jump andlink)中的链接部分。它其实是链接编辑器(link editor)的简称。在 Unix 系统中,链接器的输入文件有.o 后缀,输出 a.out 文件;在 MS-DOS 中输入文件后缀为.OBJ 或.LIB,输出.EXE 文件。

下图展示了一个典型的 RISC-V 程序分配给代码和数据的内存区域,链接器需要调整对象文件的指令中程序和数据的地址,使之与图中地址相符。如果输入文件中的是与位置无关的代码(PIC),链接器的工作量会有所降低。PIC 中所有的指令转移和文件内的数据访问都不受代码位置的影响。

image-20251203161632228.png

链接器检查程序的 ABI 是否和库匹配。尽管编译器本身可能支持多种 ABI 和 ISA 扩展的组合,但机器上可能只安装了特定的几种库。因此,一种常见的错误是在缺少合适的库的情况下链接程序。在这种情况下,链接器不会直接产生有用的诊断信息,它会尝试进行链接,然后提示不兼容。这种错误常常在从一台计算机上编译另一台计算机上运行的程序(交叉编译)时发生。

risc-v示例的hello world 链接结果(可以看到有了详细的执行地址,调用地址等):

image-20251203170812044.png

静态链接和动态链接

  • 静态链接:程序运行之前所有库都进行了链接和加载,库无法更新,且占内存

  • 动态链接:外部的函数在第一次被调用时才会加载和链接。后续调用使用快速链接,通过存根函数在内存函数表中查找实际的函数地址并跳转。

加载器

加载器的作用是把这个程序加载到内存中,并跳转到它开始的地址。如今的“加载器”就是操作系统。换句话说,加载 a.out 是操作系统众多的任务之一。动态链接程序的加载稍微有些复杂。操作系统不直接运行程序,而是运行一个动态链接器,再由动态链接器开始运行程序,并负责处理所有外部函数的第一次调用,把它们加载到内存中,并且修改程序,填入正确的调用地址。

指令类型

  • 乘法和除法指令:RV32M 向 RV32I 中添加了整数乘法和除法指令

image-20251212144604793.png

浮点运算: RV32F (单精度浮点)和 RV32D(双精度浮点)

image-20251212145225308.png

  • 原子指令: RV32A。内存原子操作(AMO)和加载保留/条件存储

    • AMO 指令对内存中的操作数执行一个原子操作,并将目标寄存器设置为操作前的内存值。原子表示内存读写之间的过程不会被打断,内存值也不会被其它处理器修改。

    • 加载保留和条件存储保证了它们两条指令之间的操作的原子性。加载保留读取一个内存字,存入目标寄存器中,并留下这个字的保留记录。而如果条件存储的目标地址上存在保留记录,它就把字存入这个地址。如果存入成功,它向目标寄存器中写入 0;否则写入一个非0 的错误代码。

image-20251212145551206.png

  • 压缩指令:RV32C。压缩的指令只对汇编器和链接器可见,程序员能感知到的就是程序size变小了

image-20251212150411110.png

动物装饰