汇编器
在 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 中所有的指令转移和文件内的数据访问都不受代码位置的影响。

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

静态链接和动态链接
静态链接:程序运行之前所有库都进行了链接和加载,库无法更新,且占内存
动态链接:外部的函数在第一次被调用时才会加载和链接。后续调用使用快速链接,通过存根函数在内存函数表中查找实际的函数地址并跳转。
加载器
加载器的作用是把这个程序加载到内存中,并跳转到它开始的地址。如今的“加载器”就是操作系统。换句话说,加载 a.out 是操作系统众多的任务之一。动态链接程序的加载稍微有些复杂。操作系统不直接运行程序,而是运行一个动态链接器,再由动态链接器开始运行程序,并负责处理所有外部函数的第一次调用,把它们加载到内存中,并且修改程序,填入正确的调用地址。
指令类型
乘法和除法指令:RV32M 向 RV32I 中添加了整数乘法和除法指令

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

原子指令: RV32A。内存原子操作(AMO)和加载保留/条件存储
AMO 指令对内存中的操作数执行一个原子操作,并将目标寄存器设置为操作前的内存值。原子表示内存读写之间的过程不会被打断,内存值也不会被其它处理器修改。
加载保留和条件存储保证了它们两条指令之间的操作的原子性。加载保留读取一个内存字,存入目标寄存器中,并留下这个字的保留记录。而如果条件存储的目标地址上存在保留记录,它就把字存入这个地址。如果存入成功,它向目标寄存器中写入 0;否则写入一个非0 的错误代码。

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