首页 > 编程语言 >一起学RISC-V汇编第10讲之汇编器语法

一起学RISC-V汇编第10讲之汇编器语法

时间:2024-10-07 22:02:14浏览次数:11  
标签:10 汇编器 字节 汇编 RISC add 指令 table

目录
了解了RISC-V的基础指令集以及ABI接口,我们就可以动手写汇编程序了,编写汇编程序有两种常用的方式:汇编源程序内嵌汇编

  • 汇编源程序:

    即:手写汇编,汇编源程序作为汇编器的输入,一般以.s 或 .S 作为文件扩展名,程序由汇编器指令(Assembler Directive,与架构无关)和汇编指令(Instruction,与指令集相关)两部分构成。

    .S 与 .s 的区别:

    .s文件只包含和CPU架构相关的汇编指令、和汇编相关的汇编指令、注释等,而.S 可以包含预编译,可以简单理解为:.S = .s + 预编译,所以.S 中可以使用#include,#ifdef等预编译语句, .S 文件经过cc1预处理器处理后得到.s文件,.s 文件可以由汇编器链接器来汇编链接生成最终的目标文件,

  • 内嵌汇编

    即:允许在高级语言(c或c++)中嵌入汇编语言,从而实现汇编语言和高级语言混合编程。

这里只讲汇编器语法,主要参考《汇编语言编程基础-基于LoongArch》,下一讲再讲述内嵌汇编。

1 常用的汇编器指令

汇编器指令与汇编指令不同:

汇编指令:就是前面讲的各种运算,逻辑,移位,比较,跳转等指令,与架构相关;

汇编器指令:是用于指导汇编器怎么工作的指令(如汇编器怎么定义变量和常量,汇编指令在目标文件中如何存放),与架构无关,所以可能ARM,RISCV等架构面向AS汇编器的汇编器指令是相同的。

汇编器语法同样需要常量、变量、函数等,所以就从这里讲起:

1.1 定义字符串变量

定义类似于C语言中的字符串 char str[10] = "hello";

.globl str          # 指定符号str为全局变量
.data               # 在data段,所以str并不是常量字符串
.align 3            # 定义2^3 即8字节对齐
.type str,@object   # 类型为对象
.size str,10        # 大小为10字节
str:                # 字符串符号
.asciz "hello"      # 内容

1.2 定义整数变量

定义类似于C语言中的整数 static int var = 20;

.local var          # 指定变量为局部类型static,可省略
.data               # 在data段
.align 2            # 定义2^2 即4字节对齐
.type var,@object   # 类型为对象
.size var,4         # 大小为4字节
var:                # 变量符号
.word 20            # 值

1.3 定义一个函数

我们可以写一个测试用例,测试汇编是否与C语言等价,见后面例子。

/*
add.S

等价于如下C语言:
int add(int a, int b)
{
 return a + b;
}
*/

# 汇编指令

.text                # 代码一般放在text段
.align 2             # 定义2^2 即4字节对齐
.globl add           # 定义作用域
.type add,@function  # 类型为函数

add:                 # 函数名
add a0, a0, a1       
ret
.size add,.-add      # 函数大小

以上使用的指令依次说明如下:

  1. 全局变量与局部变量

    指令 使用示例 描述
    .globl (兼容.global) .globl symbol_name symbol_name为全局变量或全局函数
    .local .local symbol_name symbol_name为局部变量或局部函数

    在汇编语言中,.globl.local 是用于定义符号的指令,用于控制符号的作用域。

    • .globl: 用于声明一个全局符号,表示该符号可以在整个程序中被访问和使用,其他文件中可以通过链接器访问这个符号。
    • .local: 用于声明一个局部符号,表示该符号只能在当前模块或函数内部使用,不能被其他文件访问。

    简而言之,.globl 声明的符号可以被整个程序中的其他文件访问,而 .local 声明的符号只能在当前模块或函数内部使用。

  2. 段指令

    指令 使用示例 描述
    .section .section .text
    .section .data
    .section .rodata
    .section .bss
    定义内存段,还可以自定义段
    .text .text 代码段
    .data .data 数据段
    .rodata .rodata 常量区,用来存const修饰的常量或字符串常量
    .bss .bss 未被初始化数据段
  3. 对齐方式

    指令 使用示例 描述
    .align .align 3 2^3 即8字节对齐
    .p2align .p2align 3 2的n次幂字节对齐,2^3 即8字节对齐
    .balign .balign 3 3字节对齐

    汇编指令.align expr 用于指定符号的对齐方式,不同架构下.align expr 意思不同,比如x86中, .align 3表示3字节对齐,而在其它一些平台可能表示8(2^3)字节对齐,为了避免歧义,所以引入了.p2align与.balign两条汇编指令,它们的行为是确定的,.p2align 3 表示8字节(2^3)对齐,而 .balign 4 在任何架构中都表示4字节对齐。

  4. 类型对象

    指令 使用示例 描述
    .type .type my_function, @function
    .type var, @object
    定义my_function为函数,定义var为变量
  5. 设置符号大小

    指令 使用示例 描述
    .size .size var, 10
    .size my_function, .-my_function
    用于设置符号(包括变量和函数)的大小:
    设置变量大小时接一个正整数,如:.size var, 10;
    当设置函数大小时,常为“. - my_function ” 动态计算大小,
    因为手动计算函数大小不方便,伪指令可能会被展开。
  6. 定义字符串

    指令 使用示例 描述
    .string .string “hello” 定义字符串
    .ascii .string “hello\0” 定义字符串,.ascii在字符串末尾不会自动追加'\0'
    .asciz .asciz “hello” 定义字符串, .asciz在字符串末尾自动追加'\0'

    以上三种方式都可以用于定义一个字符串,且可不带参数或者多个字符串(逗点分开),用于把汇编好的字符串存入连续的地址。

  7. 定义数据

    汇编指令中,用于定义数据类型的汇编指令有:

    指令 使用示例 描述
    .byte .byte 10 定义字节8bit数 10
    .half .half 100 定义半字16bit
    .word .word 10000 定义字32bit
    .long .long 10000 .word 相同
    .8byte .8byte 100000000 用于分配8字节的内存空间
    .dword .dword 100000000 .long 相同,用于分配4字节的内存空间并初始化
    .quad .quad 10000000000 用于分配8字节的内存空间
    .float .float 3.14 分配4字节的内存空间,使用单精度浮点来初始化
    .double .double 3.14 分配8字节的内存空间,使用双精度浮点来初始化

2 其它汇编器指令

2.1 条件编译与文件引用

GNU汇编器提供如:.set/.if/.else/.endif/.ifdef/.ifndef/.ifeq等条件编译指令,但我们也可以直接在.S 中使用C语言的#ifdef、#else、#endif、#define等预处理指令。

同样,文件引用可以.include "file"来实现,也可以直接在.S 中使用C语言的#include等预处理指令,常用的指令如下表:

GNU汇编 等价C语言预处理指令(需使用.S) 功能
.set FLAG, 0 #define FLAG 0 设置常量
.if FLAG==1
.else
.endif
#if FLAG==1
#else
#endif
条件编译
.ifdef symbol
.ifndef symbol
#ifdef symbol
#ifndef symbol
是否定义符号symbol
.include "file" #include "file" 引用文件

2.2 宏定义

汇编器指令.macro name args .endm 功能类似c语言中的宏定义功能,其中name为宏名称,args为参数,可以为多个参数,参数之间可以使用空格或者逗号分隔,也可以为0个参数,以.endm结尾。如下例表示插入a条nop指令的宏:

.text
.macro inset_nop a  # 宏名称为inset_nop,参数为a
    .rept \a        # 宏内使用参数加上反斜杠\
     nop
    .endr
.endm

其中:宏定义体中使用参数时格式为\参数,.rept \a 和 .endr 用于将内部的语句展开a次。

如汇编某处插入10条nop指令,可以这么调用:

inset_nop 10

2.3 循环展开

上例已经展示了循环展开的使用,汇编器指令“.rept count”和“.endr”可用于将其内部的语句循环展开count次,

.globl table          # 指定数组为全局变量
.data                 # 在data段,所以str并不是常量字符串
.align 2              # 定义2^2 即4字节对齐
.type table,@object   # 类型为对象
.size table,10        # 大小为4字节
table:                # 数组
.word
.rept 10
.word 200
.endr

2.4 本地标签和程序跳转

为了方便程序的编写,汇编器指令中提供一种本地标签(Local Label)用于逻辑跳转。本地标签有如下两种方式:

  • 可采用编号(可以为数字、字母、特殊字符或其组合)加冒号“:”的格式

  • 标签“Nb”和“Nf”(其中“N”是数字),这是 GNU 程序集的智能扩展。它可以“向前”搜索“f”,“向后”搜索“b”,找到标签N。

    1: j 1f  # 向后跳转到第三条(即1:j 2f)位置
    2: j 1b  # 向前跳转到第一条(即1:j 1f)位置
    1: j 2f  # 向后跳转到第四条(即2:j 1b)位置
    2: j 1b  # 向前跳转到第三条(即1:j 2f)位置
    

    等价于:

    label1: j label3
    label2: j label1
    label3: j label4
    label4: j label3
    

2 汇编源程序例子

举一个汇编源程序的例子:

## add.s

# 函数add_asm
.text
.align 2
.globl add_asm
.type add_asm,@function
add_asm:
add a0, a0, a1
ret
.size add_asm,.-add_asm


# 字符串变量,有FLAG控制,FLAG=1时str="hello flag1",否则str="hello flag0"
.set FLAG,1
.globl str
.data
.align 2
.type str,@object
.size str,12
.if FLAG == 1
str:
.asciz "hello flag1"
.else
str:
.asciz "hello flag0"
.endif

# 数组table,相当于int table[10];
.globl table          # 指定数组为全局变量
.data                 # 在data段,所以str并不是常量字符串
.align 2              # 定义2^2 即4字节对齐
.type table,@object   # 类型为对象
.size table,40        # 大小为40字节
table:                # 数组
.word
.rept 10
.word 200
.endr

测试代码:

// main.c

#include <stdio.h>
#include <stdlib.h>

extern char *str;
extern int table[10];
extern int add_asm(int a, int b);

int add_c(int a, int b)
{
    return a + b;
}

int main(void)
{
    int a = 10;
    int b = 21;

    int res_c, res_asm;

    res_c = add_c(a, b);
    res_asm = add_asm(a, b);

    printf("res_c = %d res_asm = %d\r\n", res_c, res_asm);

    printf("str is %s\r\n", &str);

    for (int i = 0; i < 10; i++) {
        printf("table[%d] = %d\r\n", i, table[i]);
    }
    return 0;
}

编译:

riscv64-unknown-linux-gnu-gcc -O2 -static add.S main.c -o demo_asm

拷贝到risc-v linux环境,执行, log如下:

res_c = 31 res_asm = 31
str is hello flag1
table[0] = 200
table[1] = 200
table[2] = 200
table[3] = 200
table[4] = 200
table[5] = 200
table[6] = 200
table[7] = 200
table[8] = 200
table[9] = 200

示例2:rv32上的打印helloword代码

.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                 # 装入返回值 0
ret                     # 返回
.size main,.-main

.section .rodata         # 指示符:进入只读数据节
.balign 4                # 指示符:将数据按 4 字节对齐
string1:                 # 第一个字符串符号
.string "Hello, %s!\n"   # 指示符:以空字符结尾的字符串
string2:                 # 第二个字符串符号
.string "world"          # 指示符:以空字符结尾的字符串

参考:

  1. 汇编(一):risc-v汇编语法 - 知乎 (zhihu.com)

  2. riscv-asm-manual/riscv-asm.md at master · riscv-non-isa/riscv-asm-manual · GitHub

  3. linux/include/linux/linkage.h

标签:10,汇编器,字节,汇编,RISC,add,指令,table
From: https://www.cnblogs.com/sureZ-learning/p/18450736

相关文章

  • 一起学RISC-V汇编第11讲之内嵌汇编
    目录1内嵌汇编示例2内嵌汇编样式2.1模版关键字2.2汇编指令列表2.3输出操作数2.4输入操作数2.5破坏描述部分3内嵌汇编使用示例内嵌汇编(InlineAssembly),允许在高级语言(c或c++)中嵌入汇编语言,从而实现汇编语言和高级语言混合编程。我之前的一篇学习笔记讲过内嵌汇编,见risc......
  • 10.7 ~ 10.13
    10.7国庆最后一天。大家今天都开学,就我们不开,赢!上午模拟赛。T1看着就是那种很签的题,想了一会发现不会,先把\(O(n^4)\)的暴力写了;然后开始看\(c_{i,j}\le400\)的\(80\%\),想了个枚举颜色+两行的写法,直接开写;写完之后突然发现复杂度是\(n^3c\)的,这不和暴力一个复杂......
  • 一起学RISC-V汇编第9讲之RISC-V ABI之寄存器使用约定
    目录1RISC-V寄存器使用约定2Caller-saved与Callee-saved2.1对比几种不同的寄存器保存方式2.2为什么要分caller-saved与callee-saved?2.3caller-saved与callee-saved寄存器的灵活使用寄存器使用约定告诉我们函数调用时通过哪些寄存器传递参数、通过哪些寄存器保存返回值、......
  • 2024.10.05 刷题记录
    2024.10.05刷题记录P7597「EZEC-8」猜树加强版不难发现\(u\)的儿子的条件是在\(u\)的子树内且深度比\(u\)恰好大\(1\)。每次询问子树内的所有节点深度或许可以解决此题,但询问次数达到了\(n^2\)。在\(u\)的子树内,如果知道所属其他儿子的子树的节点,知道属于\(u\)......
  • 10.4 - ?? 改题纪要
    10.4-??改题纪要还是决定写一起。等写完了就会把问号改了冲刺CSP联训模拟2以后见了计数先想容斥!!!T1挤压按位拆开,每两位一块考虑,枚举每个数是否选,\(O(nlog^2n)\)的。T2工地难题考虑恰好比较难做,先前缀和变成至少。发现\(0\)个数一定,其将序列分成若干小段,每段有......
  • 代码源Csp-S模拟赛Day10-11赛后总结
    代码源Csp-S模拟赛Day10-11赛后总结Day10赛时T1赛时感觉很难维护时间以及多个精灵同时到达同一格子的情况,后来想了一种做法:对于每个格子最早被遍历到的时间我们的处理是容易的,你考虑我们可以对每行每列都建一棵线段树(数据范围保证了\(rc\leq5e5\),所以总空间大致是一个\(4rc......
  • SS241007B. 逆序对(inverse)
    SS241007B.逆序对(inverse)题意给你一个长度为\(n\)的排列,你可以选择一对\((i,j)\),交换\(p_i,p_j\),或者不操作,使减少的逆序对个数最多,求最多减少几个逆序对。思路首先考虑交换\(i,j\)会对哪些部分的逆序对数量产生影响。不难发现,如果我们画一个二维平面,上面有\(n\)个点......
  • 20241007
    sequence我们会发现,我们每次删的一定是长度最短的那个,所以我们可以最开始按照长的排一下序,然后用线段树维护每一个区间中还有几个数,每次加上答案后在两个端点打上标记即可#include<bits/stdc++.h>#define_1(__int128)1usingnamespacestd;usingll=longlong;vo......
  • 2024.10.7 鲜花
    【UNR#3】百鸽笼花の塔君が持ってきた漫画くれた知らない名前のお花今日はまだ来ないかな?初めての感情知ってしまった窓に飾った絵画をなぞってひとりで宇宙を旅してそれだけでいいはずだったのに君の手を握ってしまったら孤独を知らないこの街にはもう二度と帰ってく......
  • 2024/10/07 模拟赛总结
    \(20+55+25+0=100\),压线拿到小饼干!#A.A可以发现\(u_i=A,v_i=B,w_i=C\)至少有一个成立,将这些点抽象到三位空间中。则原长方体一定被一个从\((1,1,1)\)出发的长方体打穿,但是似乎重叠部分比较难实现对于从底打到顶的长方体,可以用后缀\(\max\)解决,然后原长方体就变成了阶梯......