跳到主要内容

汇编语言编程

本章介绍 RISC-V 汇编语言编程,包括汇编程序结构、伪指令、常用编程模式等内容。

汇编程序结构

基本结构

一个完整的 RISC-V 汇编程序通常包含以下部分:

# 数据段
.section .data
message: .asciz "Hello, World!\n"
buffer: .space 100

# 代码段
.section .text
.globl _start

_start:
# 程序入口
la a0, message # 加载字符串地址
li a7, 64 # write 系统调用号
li a1, 14 # 字符串长度
li a2, 1 # 文件描述符(stdout)
ecall # 系统调用

li a7, 93 # exit 系统调用号
li a0, 0 # 退出码
ecall # 系统调用

段定义

RISC-V 汇编程序使用段(section)来组织代码和数据:

段名用途
.text代码段,存放可执行指令
.data数据段,存放已初始化的全局变量
.rodata只读数据段,存放常量
.bss未初始化数据段,存放未初始化的全局变量
.stack栈段

段声明伪指令

.section .text    # 声明代码段
.section .data # 声明数据段
.section .rodata # 声明只读数据段
.section .bss # 声明未初始化数据段

数据定义伪指令

基本数据定义

# 字节
.byte 0x12 # 定义一个字节
.byte 1, 2, 3, 4 # 定义多个字节

# 半字(16 位)
.half 0x1234 # 定义一个半字
.half 100, 200 # 定义多个半字

# 字(32 位)
.word 0x12345678 # 定义一个字
.word 100, 200, 300 # 定义多个字

# 双字(64 位)
.dword 0x123456789ABCDEF0 # 定义一个双字

字符串定义

.asciz  "Hello, World!\n"   # 以 null 结尾的字符串
.ascii "Hello" # 不以 null 结尾的字符串
.string "Hello" # 等同于 .asciz

空间分配

.space  100      # 分配 100 字节空间,不初始化
.skip 50 # 跳过 50 字节,等同于 .space
.zero 20 # 分配 20 字节并初始化为 0

对齐

.align  2        # 4 字节对齐(2^2 = 4)
.align 4 # 16 字节对齐(2^4 = 16)
.balign 4 # 4 字节对齐
.p2align 3 # 8 字节对齐(2^3 = 8)

符号定义

# 定义全局符号
.globl main
.globl _start

# 定义局部符号
.local helper_func

# 定义等价符号
.equ BUFFER_SIZE, 100
.set MAX_VALUE, 255

# 使用等价符号
buffer: .space BUFFER_SIZE

常用伪指令

加载伪指令

li - 加载立即数

li  a0, 100          # 加载立即数
li a0, 0x12345678 # 加载 32 位立即数

展开为:

# 小立即数
addi a0, x0, 100

# 大立即数
lui a0, 0x12345
addi a0, a0, 0x678

la - 加载地址

la  a0, message      # 加载符号地址
la a1, buffer # 加载缓冲区地址

展开为:

auipc a0, %pcrel_hi(message)
addi a0, a0, %pcrel_lo(message)

移动伪指令

mv  a0, a1           # 寄存器移动,展开为 addi a0, a1, 0

比较伪指令

seqz  a0, a1         # a0 = (a1 == 0) ? 1 : 0
snez a0, a1 # a0 = (a1 != 0) ? 1 : 0
sltz a0, a1 # a0 = (a1 < 0) ? 1 : 0
sgtz a0, a1 # a0 = (a1 > 0) ? 1 : 0

取反伪指令

not  a0, a1          # 按位取反,展开为 xori a0, a1, -1
neg a0, a1 # 取负,展开为 sub a0, x0, a1

跳转伪指令

j    label           # 无条件跳转,展开为 jal x0, label
jr ra # 跳转到寄存器地址,展开为 jalr x0, 0(ra)
ret # 返回,展开为 jalr x0, 0(ra)
call function # 调用函数,展开为 auipc + jalr
tail function # 尾调用,展开为 auipc + jalr x0

函数调用

函数定义

# 函数定义
function_name:
# 函数序言:保存寄存器,分配栈空间
addi sp, sp, -16
sw ra, 12(sp)
sw s0, 8(sp)

# 函数体
# ...

# 函数尾声:恢复寄存器,释放栈空间
lw ra, 12(sp)
lw s0, 8(sp)
addi sp, sp, 16
ret

函数调用示例

# 调用函数:int add(int a, int b)
li a0, 10 # 第一个参数
li a1, 20 # 第二个参数
call add # 调用函数
# 返回值在 a0 中

# 函数定义
add:
add a0, a0, a1 # a0 = a0 + a1
ret # 返回

栈帧管理

# 复杂函数的栈帧
complex_func:
addi sp, sp, -32 # 分配栈空间
sw ra, 28(sp) # 保存返回地址
sw s0, 24(sp) # 保存帧指针
addi s0, sp, 32 # 设置帧指针

# 保存被调用者保存寄存器
sw s1, 20(sp)
sw s2, 16(sp)

# 函数体
# ...

# 恢复被调用者保存寄存器
lw s2, 16(sp)
lw s1, 20(sp)

# 恢复帧指针和返回地址
lw s0, 24(sp)
lw ra, 28(sp)
addi sp, sp, 32 # 释放栈空间
ret

控制结构

条件分支

if-else 结构

// C 代码
if (a > b) {
max = a;
} else {
max = b;
}
# RISC-V 汇编
ble a0, a1, else # 如果 a <= b,跳转到 else
mv a2, a0 # max = a
j end
else:
mv a2, a1 # max = b
end:

if-else if-else 结构

// C 代码
if (a == 0) {
result = 0;
} else if (a == 1) {
result = 1;
} else {
result = -1;
}
# RISC-V 汇编
bnez a0, check_1 # 如果 a != 0,检查下一个条件
li a1, 0 # result = 0
j end
check_1:
li t0, 1
bne a0, t0, else # 如果 a != 1,跳转到 else
li a1, 1 # result = 1
j end
else:
li a1, -1 # result = -1
end:

循环结构

while 循环

// C 代码
int sum = 0;
int i = 0;
while (i < 10) {
sum += i;
i++;
}
# RISC-V 汇编
li a0, 0 # sum = 0
li a1, 0 # i = 0
li t0, 10 # 循环上限
loop:
bge a1, t0, end # 如果 i >= 10,退出循环
add a0, a0, a1 # sum += i
addi a1, a1, 1 # i++
j loop
end:

for 循环

// C 代码
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
# RISC-V 汇编
li a0, 0 # sum = 0
li a1, 0 # i = 0
li t0, 10 # 循环上限
for_loop:
bge a1, t0, for_end # 条件判断
add a0, a0, a1 # 循环体
addi a1, a1, 1 # 迭代
j for_loop
for_end:

do-while 循环

// C 代码
int i = 0;
do {
i++;
} while (i < 10);
# RISC-V 汇编
li a0, 0 # i = 0
li t0, 10 # 循环上限
do_loop:
addi a0, a0, 1 # i++
blt a0, t0, do_loop # 如果 i < 10,继续循环

数组操作

数组访问

// C 代码
int arr[10];
arr[i] = value;
# RISC-V 汇编
la t0, arr # 加载数组基地址
slli t1, a0, 2 # 计算偏移:i * 4
add t0, t0, t1 # 计算元素地址
sw a1, 0(t0) # 存储值

数组遍历

// C 代码
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += arr[i];
}
# RISC-V 汇编
la t0, arr # 数组基地址
li a0, 0 # sum = 0
li a1, 0 # i = 0
li t1, 10 # 循环上限
array_loop:
bge a1, t1, array_end
slli t2, a1, 2 # 偏移
add t2, t0, t2 # 元素地址
lw t3, 0(t2) # 加载元素
add a0, a0, t3 # sum += arr[i]
addi a1, a1, 1 # i++
j array_loop
array_end:

结构体操作

结构体定义

// C 代码
struct Point {
int x;
int y;
};
# RISC-V 汇编
# 偏移量定义
.equ POINT_X, 0
.equ POINT_Y, 4
.equ POINT_SIZE, 8

# 访问成员
la t0, point # 加载结构体地址
lw a0, POINT_X(t0) # 读取 x
lw a1, POINT_Y(t0) # 读取 y

结构体数组

// C 代码
struct Point points[10];
points[i].x = 10;
# RISC-V 汇编
la t0, points # 数组基地址
li t1, POINT_SIZE # 结构体大小
mul t2, a0, t1 # 计算偏移
add t0, t0, t2 # 结构体地址
li t3, 10
sw t3, POINT_X(t0) # 设置 x 成员

内存操作

内存复制

# 复制 n 字节从 src 到 dst
# a0: dst, a1: src, a2: n
memcpy:
beqz a2, memcpy_end # 如果 n == 0,结束
memcpy_loop:
lb t0, 0(a1) # 读取字节
sb t0, 0(a0) # 写入字节
addi a0, a0, 1 # dst++
addi a1, a1, 1 # src++
addi a2, a2, -1 # n--
bnez a2, memcpy_loop
memcpy_end:
ret

内存清零

# 将 n 字节内存清零
# a0: addr, a1: n
memset_zero:
beqz a1, memset_end
memset_loop:
sb x0, 0(a0) # 写入 0
addi a0, a0, 1
addi a1, a1, -1
bnez a1, memset_loop
memset_end:
ret

系统调用

Linux 系统调用

RISC-V Linux 使用 ecall 指令进行系统调用:

# 系统调用号在 a7,参数在 a0-a5,返回值在 a0
li a7, syscall_number
ecall

常用系统调用

系统调用号名称参数描述
57closea0=fd关闭文件
62lseeka0=fd, a1=offset, a2=whence文件定位
63reada0=fd, a1=buf, a2=count读取文件
64writea0=fd, a1=buf, a2=count写入文件
93exita0=status退出程序
214brka0=addr调整堆

系统调用示例

输出字符串

la   a0, message      # 字符串地址
li a1, 14 # 字符串长度
li a2, 1 # stdout
li a7, 64 # write
ecall

读取输入

la   a0, buffer       # 缓冲区地址
li a1, 100 # 缓冲区大小
li a2, 0 # stdin
li a7, 63 # read
ecall

退出程序

li   a0, 0            # 退出码
li a7, 93 # exit
ecall

完整示例

Hello World

.section .data
message:
.asciz "Hello, World!\n"

.section .text
.globl _start

_start:
la a0, message
li a1, 14
li a2, 1
li a7, 64
ecall

li a0, 0
li a7, 93
ecall

阶乘计算

# 计算 n 的阶乘
# 输入:a0 = n
# 输出:a0 = n!
factorial:
li t0, 1 # result = 1
fact_loop:
beqz a0, fact_end # 如果 n == 0,结束
mul t0, t0, a0 # result *= n
addi a0, a0, -1 # n--
j fact_loop
fact_end:
mv a0, t0 # 返回结果
ret

斐波那契数列

# 计算第 n 个斐波那契数
# 输入:a0 = n
# 输出:a0 = fib(n)
fibonacci:
li t0, 0 # fib(0) = 0
li t1, 1 # fib(1) = 1
beqz a0, fib_ret_0 # 如果 n == 0
li t2, 1
beq a0, t2, fib_ret_1 # 如果 n == 1

fib_loop:
add t2, t0, t1 # fib(n) = fib(n-1) + fib(n-2)
mv t0, t1 # 移动
mv t1, t2
addi a0, a0, -1
li t3, 1
bgt a0, t3, fib_loop

mv a0, t2
ret

fib_ret_0:
mv a0, t0
ret

fib_ret_1:
mv a0, t1
ret

小结

本章介绍了 RISC-V 汇编语言编程:

  • 汇编程序结构和段定义
  • 数据定义伪指令
  • 常用伪指令
  • 函数调用和栈帧管理
  • 控制结构实现
  • 数组和结构体操作
  • 系统调用

掌握汇编语言编程对于理解底层系统工作原理和优化关键代码非常重要。下一章将介绍调用约定。