【跟我学RISC-V】(二)RISC-V的基础知识学习与汇编练习

写在前面:

这篇文章是跟我学RISC-V的第二期,是第一期的延续,第一期主要是带大家了解一下什么是RISC-V,是比较大体、宽泛的概念。这一期主要是讲一些基础知识,然后进行RISC-V汇编语言与c语言的编程。在第一期里我们搭建了好几个环境,你可以任意选一个你喜欢的RISC-V环境(能够执行RV机器码的平台),然后进行代码编写、编译、汇编、链接、运行、观察现象的这一过程。同样地,在这一篇里我也会拿x86的知识与RISC-V进行对比,这样也可以促进对两种指令集的学习。

一、RISC-V指令集的基础信息

1、RISC-V的通用寄存器

在第一期里我讲过,无论是RV32还是RV64,它的通用寄存器的数量都是32个。32真是一个好数字,刚好是2的5次方,实际上伯克利大学的研究员在设计RV的时候就非常讲究,这么做的好处是颇多的,也体现了RISC-V指令集的特色,这个我们在学习之后再讨论这个问题。

这32个寄存器分别是x0 x1 x2 ... x31这样去编号,但是就单纯的这样去写汇编的话,是非常不方便的,因此每一个寄存器又有自己的别名,这个别名就代表了这个寄存器的含义,以及函数调用时候的规则。也就是说,你在实际汇编编程的时候,既可以使用编号名,也可以使用别名,实际上使用别名更好,这样能够把寄存器的含义和在这里的作用绑定起来,别人看你的代码就知道你要做什么了。

寄存器名别名作用在函数调用过程中的维护
x0zero零寄存器,永远是0不需要维护
x1rareturn address在函数调用时存放返回地址caller
x2spstack pointer栈指针寄存器callee
x3gpglobal pointer全局寄存器(用于联接器松弛优化)经常使用基于gp的寻址模式来访问全局变量和静态数据,从而提高访问速度和效率caller/不需要保存
x4tpthread pointer线程寄存器(保存pcb的地址)与线程相关
x5t0temporaries临时寄存器,相当于c语言的临时用一下变量,callee可能会改变他们的值,caller根据实际情况看是否要保存caller
x6t1
x7t2
x8s0saved保存寄存器,在函数调用过程中必须保存的寄存器callee
x9s1
x10a0

argumeng参数寄存器,在函数调用过程中传递参数和返回值。同时,a0和a1又会在函数返回时的传递返回值。

caller
x11a1
x12a2
x13a3
x14a4
x15a5
x16a6
x17s7
x18

s2

saved保存寄存器,在函数调用过程中必须保存的寄存器callee
x19s3
x20s4
x21s5
x22s6
x23s7
x24s8
x25s9
x26s10
x27s11
x28t3临时寄存器,总共有7个临时寄存器caller
x29t4
x30t5
x31t6

上图描述了32个通用寄存器在编程中作用的约定,特别是c编程时候的默认调用约定。其实这些寄存器本身来说想咋用,但是如果这样的话,你写一套使用寄存器的风格,他也有一套自己的风格,这样的话我写的函数你就没法调用了,因为寄存器安排不同,这样就非常麻烦,根本不利于开发。于是RISC-V指令集在设计之处,就把这些寄存器的作用和安排都规定好了,别名也取好了。你不需要自己想一套函数调用的法则,你只需要遵守约定就好。这样,我写的函数,你也可以直接调用,而不需要考虑参数保存在哪个寄存器,因为这都已经规定好了。

比如说A函数调用的B函数,那么caller就是A函数,callee就是B函数。

这个寄存器的别名是很有用的,你不需要去记忆x寄存器名到别名的映射,你只需要记住别名中前缀的含义,你在汇编语言编程的时候就知道该使用哪一个寄存器来保存什么信息了。

如果你是第一次看见这个表格,你可能会感觉很抽象,不过只要编程练习一下,那么也就不抽象了。不过想进行RISC-V汇编语言的编程,光是知道通用寄存器还是不够的,你还得知道一些指令,所以我在这里先分析一下RV的寄存器和x86的不同之处。

我们都知道,x86是CISC,而RISC-V在RISC,这二者在寄存器的安排上就有非常大的不同。在x86架构中,有一种说法是“寄存器较弱的体系结构”,意思就是x86架构的通用寄存器的数量是非常少的。在实模式下,也就ax,bx,cx,dx,bp,sp,si,di, 就是搞来搞去就这么几个寄存器,并且比如bx,bp还要拿来作为offset偏移量寻址、cx还要拿来作为循环次数的保存、sp还是指向栈顶。总结来说就是能够程序员使用的通用寄存器的数量是在是太少太少了,我在大一的时候学习8086汇编就比较难受,寄存器满打满算就这么几个,一下子就用掉了,总感觉不太够用(你可以看看我之前的blog)。进入IA-32e的长模式感觉就好多了,通用寄存器又加上了r8 ~ r15 ,Intel终于是不挤牙膏了。而对于RISC-V而言,有整整32个通用寄存器,其中临时寄存器的数量就有7个,相比x86真是太爽了,随便拿一个就能临时保存一下我计算过程中的数据(你可以认为是打草稿)。对程序员来说,这太方便、舒服了。

还有一些区别是,在x86中(保护模式),函数调用时候参数非常依赖于内存。也就是参数都是保存在栈里的,保存在栈里问题倒是不大,就是读写内存的速度相比读写寄存器的数据差距太大了。在RISC-V有中专门的a系列的寄存器可以用户保存函数调用时候的参数,a0 ~ a7 整整8个寄存器呢!基本上来说,你一个函数的参数也很少会超过8个,当然如果超过了那还是要保存在栈里。总的来说,参数保存在寄存器里那速度是快了好几倍。(当然在Intel IA-32e中也是使用寄存器保存参数了,Intel在多年的迭代过程中算是学聪明了,而RISC-V是一开始就这么聪明,这就是后发的优势)

还有一点就是在RISC-V体系结构中,专门可以拿出一个寄存器tp, 来存放指向当前进程task_struct的指针,用于加快访问速度。而像x86这样的体系结构(通用寄存器数量不多)就只能在内存栈顶创建thread_info结构,通过计算偏移量间接的查找task_struct(也就是pcb)。也就是x86体系中每一次调用current去查找当前进程的pcb都需要访问多次内存,还要通过偏移量去找到task_struct的地址,这转来转去速度就会变慢。而RISC-V则直接通过tp寄存器直接就能找到pcb ,那访问寄存器的速度快很多,并且也不需要通过偏移量去寻址。这又是一大优势。

还有就是在x86中通用寄存器是可拆解的,比如IA-32e的rax寄存器是64位的,你可以拆解它。rax、eax、ax、ah、al, 从8位到16位到32位到64位,是可以拆分的。这都是Intel为了兼容性而设计的这么一套东西,因为早期的8088是8位的CPU,8086是16位的,老奔腾是32位的,酷睿又是64位的,它为了兼容就用这种方法,你即便进入了长模式,仍然可以使用al寄存器。但是在RISC-V中,RV32寄存器的大小就是32位的,你不可能说拆成hx和hl,没有这样的用法。所以说RISC-V指令集里面,你在使用load系列指令的时候,把一个不到32位的数值放置到32位的寄存器,会进行符号扩展或者零扩展,而不是直接把这个数直接放到寄存器里,这样会损失符号的。

在上面表格中有一个非常特殊的寄存器x0 zero寄存器,它类似于LInux里的/dev/null这个设备,你往里面写入任何数据都没用,再怎么写都是0,写入任何数都会被丢弃掉;如果你把这个寄存器的值给读出来,也还是0。你可能觉得这个寄存器好像没啥用啊,难道我就不能用立即数0去替代这个x0寄存器吗?实际上,这个x0 寄存器是非常有用的,有很多地方都会用到它。特别是伪指令在转换成汇编指令的时候,会经常用到zero寄存器,这个我会在后面讲到。


2、RISC-V的指令格式与特点

(一)RISC-V指令的特点

RISC-V的每条指令宽度都是32位(固定的4B),如果有c扩展使用指令压缩后会变成2B ,这个我们先不提。RV的指令格式如图所示分为6种。

  1. R-type:寄存器与寄存器算术指令,这里的R就是register寄存器的意思;
  2. I-type:寄存器与立即数算术指令或者加载指令;
  3. S-type:存储指令(和上面的加载指令刚好是反义词);
  4. B-type:条件跳转指令;
  5. U-type:长立即数操作指令;
  6. J-type:无条件跳转指令。

大家从图里可以清晰地看到:无论是什么类型的指令,确实都是4B的,并且共同点就是opcode操作码都在低7位。操作码这个概念相信学过计算机组成原理的都知道。

  • 足够的编码空间:使用7位操作码可以提供128种不同的可能值,这允许定义多种不同的基本操作和指令格式。对于一个旨在可扩展和支持多种扩展模块(如整数、浮点、原子操作等)的现代处理器架构来说,这一点非常重要。

  • 简化解码:RISC-V的指令长度固定为32位,这使得硬件能够更加简单和高效地解码指令。opcode位于指令的最低7位,硬件可以快速地读取这7位并确定如何进一步解析整个指令,这对提高指令解码速度和处理器整体性能至关重要。

  • 支持指令格式多样性:RISC-V使用不同的指令格式(如R、I、S、B、U、J格式)来支持不同类型的操作。这些格式有不同的字段组合和长度,opcode的7位设计帮助区分这些格式,并指导如何解析随后的字段。

  • 扩展性:RISC-V架构被设计为可扩展的,以支持新的功能和指令集扩展。7位opcode为未来可能的指令集扩展留出了空间,使得可以轻松加入新的操作码而不会干扰现有的指令解码逻辑。

在图中的rd就是目的寄存器,rs就是源寄存器。这个概念类似于x86中的rdi和rsi。大家可以总结出来,rd要么是1个要么是0个(有的指令是不用把值输出到目的寄存器的),rs最多支持两个,就是最多放两个源寄存器进来。无论是rd还是rs,它所占用的位数都是5位。这个事情我们之前提到过,因为寄存器总共就32个,2的5次方等于32,那么设置成5位这样,是非常巧妙的。

图片中还经常出现imme,这个就是立即数的意思,immediately.

还有就是占用3位的funct3与占用7位的funct7,就是说单纯的opcode还不足以确定这条指令究竟是哪一条指令。而是要opcode和funct功能码,这二者一起才能共同决定这条指令对应的具体的汇编指令手动反汇编的时候要用到。

实际上对照这张表格,你就很容易做到反汇编了。拿到一个4B的16进制数,你先把他转换成32位二进制数,然后对照opcode先确定是什么类型,确定好之后再根据具体的funct(如果存在)就能确定是哪一条指令了。确定指令之后,再通过rs,rd推出对应的寄存器号,有立即数的话把立即数也带进去。这样,一整条汇编指令就出现了。


(二)RISC-V每条指令详解

接下来,我要对每一条指令进行说明,大家耐心看一看吧。为了让现象更加明显,我使用c语言内联汇编的方法,把指令执行后的现象给展示出来,方便大家查看,那么大家如果能够跟着实践一遍这样更好。这里我还没有讲到c语言内联汇编的东西,不过有编程基础的应该能够看懂asm语句,我会在c代码后面讲述这么做的目的。如果你先前没有c语言内嵌汇编的基础,你可以先看第二节编程理论。

①加载指令

加载指令load就是把数据从内存加载到寄存器的这一过程。

指令格式数据位宽说明
lb rd,offset(rs)8把rs寄存器里的值指向的地址作为基地址,在偏移offset的地址处,加载1B的数据经过符号扩展之后放入到rd寄存器里面
lbu rd,offset(rs)8作为无符号加载,经过零扩展放入到寄存器rd
lh rd,offset(rs)16符号扩展加载2B
lhu rd,offset(rs)16零扩展加载2B
lw rd,offset(rs)32符号扩展加载4B
lwu rd,offset(rs)32零扩展加载4B
ld rd,offset(rs)64直接加载到rd寄存器里,不用扩展了
lui rd,imme64把立即数imme左移12位,然后符号扩展,再把结果写入到rd寄存器(这里的u是upper的意思,不是unsigned的意思)

RISC-V的指令都挺有规律的,l就是load加载的意思,代表数据从内存加载到寄存器;b是byte的意思,表示1个字节;h是halfword的意思,表示半字,2个字节;w表示word,一个字,4个字节;d表示double word表示双字,就是8个字节。跟在b/h/w/d后面的u是unsigned的意思,表示这是无符号数,不存在符号扩展;直接跟在l后面的是u ,表示这是upper,需要左移。记忆是比较容易的。

我们先进行一些区分:

#include <stdio.h>int main(void)
{long rd = 0;char rs[3];rs[0] = 'a';rs[1] = 'b';rs[2] = 'c';asm volatile("lb %0,1(%1)    \n\t":"=r"(rd):"r"(rs));printf("%c\n",rd);return 0;
}

这是一段非常简单的c语言内联汇编的代码,意思就是把rs作为地址传入到寄存器里,再通过lb指令把rs指向的地址作为基地址,偏移了1B的地址里面取出来1B,把这个数据经过符号扩展放入寄存器里,在输出到rd变量。我们打印rd变量,确实是字符b.由于字符b是一个正数,因此符号扩展之后值就是本身。


lb.c

#include <stdio.h>int main(void)
{long rd = 0;char rs = -20;asm volatile("lb %0,0(%1)    \n\t":"=r"(rd):"r"(&rs));printf("%d\n",rd);return 0;
}

lbu.c

#include <stdio.h>int main(void)
{long rd = 0;char rs = -20;asm volatile("lbu %0,0(%1)    \n\t":"=r"(rd):"r"(&rs));printf("%d\n",rd);return 0;
}

可以看见,即便rs变量的值是-20,如果你使用的是lbu指令,那么就会进行零扩展,符号位就无效了。

从以上这个例子我们不难看出:符号扩展是计算机系统中把小字节转换成大字节的规则之一,它会将符号扩展到所需要的位数。

比如一个1字节的数0x8A,它的最高位也就是第7位是1,那么就需要进行符号扩展,高字节使用1来填充。如果扩展到64位,那么它的值就是0xffff ffff ffff ff8a

而零扩展的就是当成无符号来处理,既然是无符号数,高字节部分使用0来填充。

还有一点要注意的是,符号扩展是小字节往大字节扩展的时候进行的,而ld这一条指令,它本身就是从内存加载一个64位的数到寄存器,没有从小字节到大字节的过程,因此是不需要符号扩展的。


我再测试一下lui指令:

lui.c

#include <stdio.h>int main(void)
{long rd = 0;asm volatile("lui %0,0xff    \n\t":"+r"(rd));printf("rd = %lx\n",rd);return 0;
}

确实是左移了12位,1个16进制的0代表2进制的4位。你也许会很困惑,为什么要左移12位?为什么不是左移13位?为什么不是干脆不左移?

  1. 寻址能力的扩展

lui 指令将 20 位立即数置于寄存器的高 20 位。这样做的目的是允许程序能够引用位于较高地址范围内的内存地址或数据。考虑到 RISC-V 的寄存器是 32 位的,这种设计使得使用 lui 加上一个后续的加法或其他指令(比如 addi),可以访问整个 32 位地址空间。

  1. 高效的常数加载

通过将立即数左移 12 位,lui 指令可以快速地设置寄存器中的高位,这对设置大的常数值非常有效。如果需要加载的立即数不仅仅是高位,可以通过随后的 addi(Add Immediate)等指令来设置剩余的低 12 位。

  1. 指令编码的简化

在 RISC-V 的指令格式中,立即数字段(imm字段)经常被复用以适应不同类型的指令。lui 指令的设计使得指令的立即数字段直接对应于寄存器的高 20 位,从而简化了指令的解码和执行过程。

  1. 支持编译器优化

这种左移 12 位的设计也有助于编译器生成更优化的代码,尤其是在进行全局地址或大范围数据定位的时候。编译器可以更容易地生成用于初始化大数组或访问静态变量的代码。

总之就是,在RISC-V中一条指令总共就4B,能够分配给立即数imme的部分是很有限的,为了能够寻址到“高地址的地方”,于是很多指令都是具有upper的性质,即把其中的立即数左移12位,然后低于12位的部分你可以使用add系列的指令加上来,这样你的寻址能力大大提升,不用再受限于4B指令有限的imme位数能够表示的最大值了。此时你可能会觉得这也太麻烦了,我寻址一下难道还要把一个完成的地址给拆分成高位和低12位,这样组合成地址吗?实际上,你可以手动这样去组合、去拼凑,因为精简指令集本身就是多条指令的组合才能完成一个功能的,而不像x86那样,一条MOV指令打天下。当然,RISC-V的设计者为了程序员方便,它提供了大量的“伪指令”,你使用伪指令之后,伪指令会再拆分成真正的RISC-V汇编指令。有了这些伪指令,编程是不会太麻烦了。

在这个例子你,你可以使用一条伪指令叫做li,这个li就可以把一个立即数放进寄存器里。

li.c

#include <stdio.h>int main(void)
{long rd = 0;asm volatile("li %0,0xff    \n\t":"+r"(rd));printf("rd = %lx\n",rd);return 0;
}

不过你的记得,这是一条伪指令,它不是真正的RISC-V汇编指令,它是多条指令的组合。


②存储指令

存储指令就是加载指令的反义词 --把数据从寄存器移动到内存里。只是它更加简单了,没有符号扩展,直接移动数据即可。

指令位宽说明
sb rs2,offset(rs1)8把rs2寄存器的低8位的值存储到以rs1寄存器的值为基地址,offset为偏移量的地址处。
sh rs2,offset(rs1)16低16位
sw rs2,offset(rs1)32低32位
sd rs2,offset(rs1)64整个rs2寄存器的值

这个存储指令就是store,把寄存器的值往内存里存,对应的指令类型是S-type.

大家其实也发现了,这个store指令系列对于load来说,简单太多了,没有什么又是u啊又是i的,就是非常单纯的把寄存器值的一部分或者整个寄存器的值,放置到指定的内存地址里面去。这里不需要什么符号扩展、零扩展的。

#include <stdio.h>int main(void)
{char rs[3] = {0};asm volatile("li t0,'b'  \n\t""sb t0,1(%0)    \n\t"::"r"(rs):"t0","memory");printf("rs[1] = %c\n",rs[1]);return 0;
}

注意这里我们在扩展内联汇编里直接使用到了寄存器t0,因此在损坏部分要把它写进去,这样在asm嵌入的代码块执行结束的时候会把t0原先的值给恢复回去。


③算术指令

算术指令相对来说是比较重要、用到的场景也是比较多的。

指令指令格式说明
addadd rd,rs1,rs2把rs1寄存器的值和rs2寄存器的值相加,并把加法的结果放到rd寄存器里
addiadd rd,rs,imme把rs寄存器的值和立即数imme相加,把结果放到rd寄存器里
addwaddw rd,rs1,rs2截取rs1和rs2寄存器的低32位,相加后把结果进行符号扩展并放到rd寄存器里
addiwaddiw rd,rs,imme截取rs寄存器的低23位并与imme立即数相加,把结果进行符号扩展并放到rd寄存器里
subsub rd,rs1,rs2把rs1寄存器里的值减去rs2寄存器里的值,把结果放到rd寄存器里
subwsubw rd,rs1,rs2把rs1寄存器的低32位减去rs2寄存器的低32位,把结果放到rd寄存器里

这个看起来比较简单,实践起来也不复杂。

add.c

#include <stdio.h>int main(void)
{long rs1 = 20;long rs2 = 30;long rd = 0;asm volatile("add %0,%1,%2   \n\t":"=r"(rd):"r"(rs1),"r"(rs2));printf("rd = %d\n",rd);return 0;
}

sub.c

#include <stdio.h>int main(void)
{long rs1 = 20;long rs2 = 30;long rd = 0;asm volatile("sub %0,%1,%2   \n\t":"=r"(rd):"r"(rs1),"r"(rs2));printf("rd = %d\n",rd);return 0;
}

怎么样,这样的汇编风格写起来,相比x86来说是不是简单太多了。


到目前为止,我们已经学习了load加载指令和store存储指令,这些都是真汇编指令,但有的时候,这些指令用起来会不太方便,毕竟不像x86那样一个MOV就能够达到目的。因此,对伪指令的学习也是非常重要的。

程序计数器(Program Counter,PC)是用来指示下一条指令的地址。为了保证CPU能够正确地执行程序的指令代码。就会使用一套PC寄存器来存储这个地址,那么硬件上就只需要把PC指针指向的地址里面的数据当作是代码,然后由指令领取单元IFU把指令送入预译码器并进行预译码。在这里面我们可以看到这个PC寄存器的重要作用,不同指令集给出的PC实现方式也不太一样。比如在x86架构中是使用CS:IP这一对寄存器来指定代码段的位置。而在RISC-V中简化了这一过程,它单纯使用PC寄存器来指定下一条指令的地址。这个PC寄存器,我们不能去读它的位置,但是可以用别的指令去相对PC寄存器进行寻址。

auipc rd,imme

auipc指令就是这么一条,通过PC寄存器进行相对寻址的指令。它的英文名是Add upper immediate to PC.

其中有upper,也就是说这里面的imme立即数也是要左移12位的,这里和上面是一样的。因此它只能寻址到与4KB对齐的地址,如果一个地址是在4KB内存块的内部,则auipc寻址不到它。不过我们也有相应的伪指令可以很方便地去寻址。这个auipc指令,我们用到的其实不太多,程序员用到的更多的是基于它的伪指令,当然这些基于它的伪指令展开还是auipc.

伪指令指令组合说明
la rd,symbol

auipc rd,delta[31:12]+delta[11]

addi rd,rd,delta[11:0]

加载符号的绝对地址
la rd,symbol

auipc rd,delta[31:12]+delta[11]

l{b,h,w,d} rd,rd,delta[11:0]

加载符号的绝对地址
lla rd ,symbol

auipc rd,delta[31:12]+delta[11]

addi rd,rd,delta[11:0]

记载符号的本地地址
l{b,h,w,d} rd,symbol

auipc rd,delta[31:12]+delta[11]

l{b,h,w,d} rd,rd,delta[11:0](rd)

把符号内容加载到寄存器里
s{b,h,w,d} rd,symbol,rt

auipc rd,delta[31:12]+delta[11]

s{b,h,w,d} rd,rd,delta[11:0](rt)

存储内容到符号中,其中rt为临时寄存器register tmp
li rd,imne根据实际情况展开为不同的汇编指令加载立即数imme到指定的寄存器

指令里面的a就代表了auipc指令。由于这个偏移的特性,你可以在这些伪指令展开成的汇编指令里经常看到delta这样的字眼。如果你自己去计算这些偏移量的话,实在是不方便,因此才提供这些个伪指令,来帮助程序员进行编程。大家在RISC-V汇编语言编程的时候要把这些伪指令给利用起来。

la.c

#include <stdio.h>void print_hello(){printf("Hello World!\n");
}int main(void)
{unsigned long address;asm volatile("la %0,print_hello  \n\t":"=r"(address));printf("the function of print_hello is %p\n",address);return 0;
}



二、RISC-V指令集的编程理论

我们到目前为止已经学习和体验了单条的RISC-V汇编指令的作用,但是实际编程是复杂且困难的。因此我想先从c语言内嵌汇编语言讲起,这个c语言是简单、直接的,大家学起来也很方便。

c语言内嵌汇编有两种形式:基础内嵌汇编和扩展内嵌汇编。在x86架构上这两种内嵌汇编的形式语法还不太一样,而在RISC-V中,这两种形式的语法是几乎一样的,节省了学习成本。

基础内嵌汇编就是在c语言汇编成汇编语言的时候单纯的把asm语句里的RISC-V汇编给插入进去,或者说是嵌入进去。

扩展内嵌汇编是在基础内嵌汇编的基础上,允许带上输入输出参数,也就是把c语言的变量给输入到汇编语言里,把汇编语言里输出的结果返回给c语言,让这两种语法完全不同的编程语言进行交互。当然,c语言汇编之后它的本质还是汇编语言,你可以把c语言当作是对汇编语言的封装。这二者的目的都是最终生成机器码。

基础内嵌汇编:

asm ("汇编指令")

基本内嵌汇编提供了一种简单的方法来嵌入裸汇编代码。在这种模式下,编译器对嵌入的汇编代码本身不做优化,因为它没有足够的信息来理解这些汇编指令的具体作用。编译器仅将这些汇编代码作为黑盒插入到生成的机器代码中。

我直接说扩展内联内嵌汇编吧。

asm 修饰词("汇编指令    \n\t""汇编指令    \n\t":输出部分:输入部分:损坏部分
);

扩展内嵌汇编允许你详细说明汇编指令与C程序中的变量之间的关系,包括输入输出约束和副作用。这种详细的信息使得编译器能够更好地理解汇编代码的意图,因此在保持语义正确的前提下,编译器可以对这些汇编代码进行优化,如重排指令、删除冗余代码等。

asm这个关键字是GNU的一个扩展。汇编指令就是我们前面讲到过的一条条的汇编指令。

  • GCC会把汇编代码块当成一个字符串
  • GCC不会解析和分析汇编代码块
  • 如果你在asm语句里要写多条汇编代码,你得像我这样用\n\t隔开来

修饰词主要有如下几个:

  • volatile:确保这部分代码在编译时不会被优化掉,从而保证程序的正确执行
  • inline:告诉GCC,把汇编代码编译成尽可能短的代码
  • goto:复杂的控制流,在内嵌汇编代码里跳转到c语言的标签处。

每条指令加上双引号,在指令末尾来几个空格或者Tab然后加上\n\t.最好是把指令给对齐,这样能够美观一点,不会看起来太乱。


三、RISC-V指令集的编程实践

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/6598.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

FBA头程空运发货流程详解|携手天图通逊,开启高效跨境物流之旅

在众多头程发货方式中&#xff0c;空运以其速度快、时效高的特点&#xff0c;成为许多卖家特别是急需快速补货或应对市场变化的友好选择&#xff0c;那FBA头程空运的发货流程是怎样的呢? 1、发货准备 在开始空运之前&#xff0c;首先需要进行发货准备。这包括将货物送达指定的…

DETR类型检测网络实验2---优化测试

补全reference_point Anchor-DETR提出用预定义的参考点生成query_pos; DBA-DETR提出预定义参考信息由(x,y)增至(x,y,w,h) 那么在3D检测任务中是否可以把预定义参考信息补全为(x,y,z,l,w,h,sint,cost),而query_pos都是使用xy两个维度(因为是bev网络). (这种方法在Sparse-DETR中…

轻松应对数据恢复挑战:雷神笔记本,不同情况不同策略

在数字化时代&#xff0c;数据无疑是我们生活中不可或缺的一部分。无论是重要的工作文件、珍贵的家庭照片&#xff0c;还是回忆满满的视频&#xff0c;一旦丢失&#xff0c;都可能给我们的生活带来诸多不便。雷神笔记本作为市场上备受欢迎的电脑品牌&#xff0c;用户在使用过程…

Adobe-Premiere-CEP 扩展 入门-视频剪辑-去气口插件-Silence Remover

短视频&#xff0c;这两年比较火&#xff0c;不要再问为什么用Premiere&#xff0c;非常难用&#xff0c;为什么不用某影&#xff0c;某些国内软件非常接地气简单&#xff0c;又例如某音资深的视频短编辑就很好用了。。。 Premiere二次开发调试难&#xff0c;不如自己搞个cons…

perl:用 MIDI::Simple 生成midi文件,用 pygame 播放 mid文件

在 csdn.net 下载 strawberry-perl-5.32.1.1-64bit.zip 解压安装在 D:\Strawberry\ 运行 cpan install MIDI::Simple D:\Strawberry\c\bin\gmake.exe test -- OK Running make install for CONKLIN/MIDI-Perl-0.84.tar.gz Installing D:\Strawberry\perl\site\lib\MIDI.pm I…

kubebuilder(6)webhook

operator中的webhook也是很重要的一块功能。也是相对比较独立的模块&#xff0c;所以放在后面讲。 webhook是一个callback&#xff0c;注册到k8s的api-server上。当某个特定的时间发生时&#xff0c;api server就会查询注册的webhook&#xff0c;并根据一些逻辑确认转发消息给…

【ARM Cortex-M3指南】3:Cortex-M3基础

文章目录 三、Cortex-M3基础3.1 寄存器3.1.1 通用目的寄存器 R0~R73.1.2 通用目的寄存器 R8~R123.1.3 栈指针 R133.1.4 链接寄存器 R143.1.5 程序计数器 R15 3.2 特殊寄存器3.2.1 程序状态寄存器3.2.2 PRIMASK、FAULTMASK和BASEPRI寄存器3.2.3 控制寄存器 3.3 操作模式3.4 异常…

使用FPGA实现串-并型乘法器

介绍 其实我们知道&#xff0c;用FPGA实现乘法器并不是一件很简单的事&#xff0c;而且在FPGA中也有乘法器的IP核可以直接调用&#xff0c;我这里完全就是为了熟悉一些FPGA的语法然后写了这样一个电路。 串-并型乘法器模块 从字面上看&#xff0c;串-并乘法器就是其中一个乘数…

Nodejs process.nextTick() 使用详解

还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#xff0c;webgl&#xff0c;ech…

OpenCV 为轮廓创建边界框和圆(62)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:OpenCV检测凸包(61) 下一篇 :OpenCV如何为等值线创建边界旋转框和椭圆(62) ​ 目标 在本教程中&#xff0c;您将学习如何&#xff1a; 使用 OpenCV 函数 cv::boundingRect使用 OpenCV 函数 cv::mi…

数据库(MySQL)—— 事务

数据库&#xff08;MySQL&#xff09;—— 事务 什么是事务事务操作未控制事务测试异常情况 控制事务一查看/设置事务提交方式&#xff1a;提交事务回滚事务 控制事务二开启事务提交事务回滚事务 并发事务问题脏读&#xff08;Dirty Read&#xff09;不可重复读&#xff08;Non…

数据结构练习题---环形链表详解

链表成环&#xff0c;在力扣中有这样的两道题目 https://leetcode.cn/problems/linked-list-cycle/ https://leetcode.cn/problems/linked-list-cycle-ii/description/ 这道题的经典解法是利用快慢指针&#xff0c;如果链表是一个环形链表&#xff0c;那么快指针(fast)和慢指…

关于MS-DOS时代的回忆

目录 一、MS-DOS是什么&#xff1f; 二、MS-DOS的主要功能有哪些&#xff1f; 三、MS-DOS的怎么运行的&#xff1f; 四、微软开源MS-DOS源代码 五、高手与漂亮女同学 一、MS-DOS是什么&#xff1f; MS-DOS&#xff08;Microsoft Disk Operating System&#xff09;是微软公…

工作问题记录React(持续更新中)

一、backdrop-filter:blur(20px); 毛玻璃效果&#xff0c;在安卓机上有兼容问题&#xff0c;添加兼容前缀也无效&#xff1b; 解决方案&#xff1a;让设计师调整渐变&#xff0c;不要使用该属性! 复制代码 background: radial-gradient(33% 33% at 100% 5%, #e9e5e5 0%, rgba…

C++类定义时成员变量初始化

在C11中允许在类定义时对成员变量初始化。 class A { public:A() { }void show(){cout << "m_a " << m_a << endl;cout << "m_b " << m_b << endl;} private:int m_a 10;//类定义时初始化int m_b; //没有初始化…

Microsoft 365 for Mac(Office 365)v16.84正式激活版

office 365 for mac包括Word、Excel、PowerPoint、Outlook、OneNote、OneDrive和Teams的更新。Office提供了跨应用程序的功能&#xff0c;帮助用户在更短的时间内创建令人惊叹的内容&#xff0c;您可以在这里创作、沟通、协作并完成重要工作。 Microsoft 365 for Mac(Office 36…

Delta lake with Java--liquid clustering

网上说liquid clustering还是实验阶段&#xff0c;python和scala有对应的函数&#xff0c;java没有&#xff0c;只能用sql语句来建表&#xff0c;尝试了两天&#xff0c;遇到很奇怪的情况&#xff0c;先上代码&#xff1a; import io.delta.tables.DeltaTable; import org.apa…

HTML_CSS学习:浮动

一、浮动简介 相关代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>浮动_简介</title><style>div{width: 600px;height: 400px;background-color: #1c80d9;}img{float:…

智慧旅游引领旅游行业创新发展:借助智能科技的力量,实现旅游资源的优化配置和高效利用,推动旅游行业的转型升级和可持续发展

目录 一、引言 二、智慧旅游的定义与特点 1、信息化程度高 2、智能化服务丰富 3、互动性强 4、个性化服务突出 5、可持续性发展 三、智慧旅游在旅游行业创新发展中的作用 &#xff08;一&#xff09;优化旅游资源配置 &#xff08;二&#xff09;提升旅游服务质量 &…

VMware虚拟机下载安装教程【超详细】

推荐大佬文章&#xff1a;VMware下载安装教程(超详细)-CSDN博客 目录 一、VMware下载 二、VMware安装 一、VMware下载 1、进入VMware官网 2、点击“Products”&#xff0c;向下滑动 --> 选择“Workstation Pro” 3、向下滑动&#xff0c;找到并选择“Download VMware Wo…