ch-5 汇编

295 阅读8分钟

1.汇编语句组成

image-20210811114637151
  • label:标签
  • operation:真正的语句
  • comment:注释
image-20210811115004059

_first:

# First RISC-V Assemble Sample

.macro do_nothing	# directive (指示/伪操作:以.开头,通知汇编器如何控制代码的产生,不产生实际的指令)
	nop		# pseudo-instruction
	nop		# pseudo-instruction
.endm			# directive [.macro 到 .endm 之间是定义宏]

	.text		# directive [.text指示把这里的写到汇编的.text节]
	.global _start	# directive  [.global类似于标识全局变量,让外部可见]
_start: 		# Label
	li x6, 5	# pseudo-instruction (伪指令:一条伪指令对应多条实际的指令 [方便编写代码效率] )
	li x7, 4	# pseudo-instruction
	add x5, x6, x7	# instruction
	do_nothing	# Calling macro
stop:	j stop		# statement in one line

	.end		# End of file (告诉汇编器程序在这结束,.end后的指令不会执行)

2.RISC-V 汇编指令总览

image-20210811133603663 image-20210811134639403 image-20210811135457791

image-20210811135727148

3.RISC-V 指令详解

1.算术运算指令

ADD加法

image-20210811140218613

SUB减法

image-20210811150825711

ADDI 加立即数

image-20210811151326334 image-20210811151536969

符号扩展,指的是:原来立即数的12位,最高位是1的话,扩展成32位时,前面的全部为1,这样就保证了数值相同

image-20210811152045022

addi的立即数只有12位,如果需要给寄存器赋值给大的数怎么办?

  • 如上..

这个设置高20位的命令是LUI,如下介绍

LUI(给寄存器的低12位前的高位赋值)(rd=立即数<<12)

image-20210811152336380

这个命令让数左移12位,所以低12位是0.

看一个低12位是正数的情况

image-20210811152527114

这个简单,看一个低12位是负数的情况

image-20210811152836647 image-20210811152852935

所以可以看到,给寄存器赋值还是一个略微复杂的操作,所以提供了一个易用的li命令

LI(给寄存器赋值)

image-20210811153108017

相关伪指令

image-20210811151904583

AUIPC(rd=立即数<<12+PC)

Add Upper Immediate Programing Counter

image-20210811175648177

经常用于构造相对地址.

LA(LoadAddress)

image-20210811175943416

LA:是用一个地址赋值给寄存器,通常用auipc和其他指令结合,相对寻址去得到一个地址

image-20210811180218700

可以看到,通常la是用一个lable去赋值给寄存器

image-20210811181951140
  • 执行完第一个语句后,t0=0x80000000,也就是这个lable的地址,即当前时刻PC的值
  • 第二个语句等于没执行一样
  • 第三个指令是跳转到t0=0x80000000的绝对地址,也就是回到第一个语句去执行...
image-20210811182855476

我把上面的第一个语句的_start变成stop,看看是啥情况:

image-20210811183030806

可以看到,这里的la x5,stop变成了两个语句

  1. auipc t0,0x0,是将t0 = PC
  2. addi t0,t0,12,是将t0 += 12,12是相对地址

所以,la x5,stop ,是将stop地址用一种相对寻址的方式得到一个绝对地址

总结

image-20210811183718601

2.逻辑运算指令

image-20210811190436042

RISC-V中提供了AND,OR,XOR,没有提供NOT

NOT是一个伪指令:是RD = RS XOR 0b111111111111.... ,这样就是把0的变成1,把1的变成0

3.移位运算指令

逻辑移位(用0补齐)(SLL,SRL,SLLI,SRLI)

image-20210811190821287

算术移位(没有算术右移,只有算术左移,右移时用符号位补齐)(SRA)(SRAI)

image-20210811192015072

比如1000-1000右移1位 ->> 1100-0100

127+正数1>>127+26+正数1/2 =127/2+正数1/2-1*2^7 + 正 数1 -->> -1*2^7+2^6+正数1/2\ = -1*2^7/2 + 正数1/2

所以可见,负数算术右移动时,负数位的值变成原来的一半,正数位的值变成原来的一半

4.内存读写指令

image-20210811192134489

Load和Store是站在CPU的角度,所以读是读到寄存器,写是从寄存器写到内存.

内存读到寄存器(LB,LBU,LH,LHU,LW)

image-20210811192304938

Word是4字节

  • LB:Byte一个字节 (有符号->所以是符号扩展)

  • LBU:Byte一个字节(无符号->所以是0扩展)

  • LH:Half Word 两个字节

  • LHU:Half Word 两个字节(无符号)

  • LW:Word 四个字节

为什么四字节的没有一个LWU?因为把一个寄存器直接塞满了,不用扩展了...

寄存器写到内存(SB,SH,SW)

image-20210811193033479

  • SB:Byte 1字节
  • SH:Half Word 2字节
  • SW:Word 4字节

store不区分有无符号,是因为CPU直接把寄存器的这个低位直接赋值给内存

image-20210811193628992

5.条件分支指令(BEQ等等)

image-20210812100215131

这个图片写的很清楚了.

立即数*2的原因是,由于地址要对齐,2字节对齐,所以地址的最低一位永远是0,这样就浪费了一个比特,所以 *2

image-20210812101443280

6.无条件跳转指令(JAL,JALR,J,JR)

image-20210812102220803

JAL实际上是做了两件事

  1. 将当前指令的下一条指令的地址写入RD (作为返回地址)
  2. 跳转到lable的地址去执行

通常是调用子函数的时候,跳转到子函数,并把当前下一条指令作为返回地址写入一个寄存器,子函数return的时候就跳转到这个返回地址去执行即可。

image-20210812102825704

int a = 1;
int b = 1;
void sum()
{
    a = a + b;
} 
void _start()
{
    sum();
}

image-20210812103313134

这段代码还是很有趣的,_start中,用jal将返回地址保存到x5,然后跳转到sum去执行。 _sum中,完成a=a+b的任务后,用jalr跳转到x5寄存器存着的地址中

image-20210812103854567

x0寄存器读和写永远都保持0,所以可以做这样的伪指令

7.寻址模式总结

image-20210812104408571

4.汇编函数调用约定

4.1 函数调用过程概述

image-20210812133607785

image-20210812133650443

4.2 汇编编程时为何需要制定函数调用约定

image-20210812133736012

  • Caller:调用者
  • Callee:被调用者

同一个人写的汇编,或者编译过来的汇编,那是没有问题的,就可以指定某个寄存器存返回地址,指定一些确定的东西来存参数,这样就可以成功调用。

但是,很多情况下,不同的函数是不同的人写的,或者是不同的.c文件单独编译的,这样就一定需要一些公共的约定去遵守

4.3 函数调用过程中有关寄存器的编程约定

image-20210812134111535

4.3.1 有关寄存器的编程约定

image-20210812134523127

  • x1 = ra(return address) , 存放函数的返回地址,调用者保存这个
  • x2 = sp(stack pointer),存放栈指针,被调用者(子函数)保存这个 .
  • x5x7,x28x31 = t0~t6,t表示temporary临时,存放临时寄存器,调用者保存,之所以叫临时寄存器,就是因为他是调用者保存的,所以被调用者(子函数)可以随便改这些寄存器,而调用者如果要用到这些寄存器,就需要先保存到栈中,再调用子函数,再pop
  • x8,x9,x18~x27 = s0~s11,s表示saved保存,存放保存寄存器,被调用者保存。所以,如果子函数会修改这些寄存器,那么需要开始的时候先入栈,结束的时候pop.

小小的总结:临时寄存器和保存寄存器的临时和保存是针对子函数来说的,看子函数是不用维护,还是要保存.

  • x10,x11 = a0,a1,a表示argument,存放参数寄存器,用于在函数调用过程中保存第一个和第 二个参数,以及在函数返回时传递返回值。 [调用者保存]
  • x12~x17 = a2~a7,a表示argument,存放参数寄存器,如果函数调用时需要传递更多的参数, 则可以用这些寄存器,但注意用于传递参数的寄存器最多只有 8 个 (a0 ~ a7),如果还有更多的参数则要利用栈。
image-20210813133102455
  • x3 - gp - global pointer

  • x4 - tp - thread pointer

  • s0 - fp - frame pointer

4.3.2 函数跳转和返回指令的编程约定

image-20210812135810457

  • 要存返回地址的时候,用x1 (ra)存返回地址
  • ret:jalr x0,0(x1),即跳转到x1的地址中去执行

image-20210812142718585

4.3.3 实现被调用函数的编程约定

image-20210812143035740

  1. 函数执行体前:

    1. 减少sp的值,即开辟了一个栈帧
    2. 将saved寄存器(callee保存)的值先压到栈中
    3. 由于ra是caller保存的,所以如果还会调用其他的函数,那么需要将ra寄存器的值保存到栈中
  2. 函数执行体

  3. 函数执行体后:

    1. 从栈中恢复saved寄存器
    2. 如果中间调用过别的函数,那么就说明事先ra保存再栈中了,那么就要将ra给pop
    3. 增加sp的值,恢复到本函数之前的状态
    4. 调用ret返回

例子1:cc_leaf (尾调用):

void _start()
{
    square(3);
}

int square(int num)
{
    return num * num
}

可以看到,尾调用的特点是,子函数中不会保存RA了.

例子2:cc_nested

void _start()
{
    // calling nested routine
    aa_bb(3, 4);
}
int aa_bb(int a, int b)
{
    return square(a) + square(b);
}
int square(int num)
{
    return num * num;
}

汇编:

看vmware-asm

5.汇编与C混合编程

5.1 汇编调用C函数

image-20210812163745834

image-20210812163814770

test.c

/*
 * a0 --> a
 * a1 --> b
 * c  <-- a0
 */
int foo(int a, int b)
{
	int c = a + b;
	return c;
}

test.s

# ASM call C

	.text			# Define beginning of text section
	.global	_start		# Define entry _start
	.global	foo		# foo is a C function defined in test.c
	# .global foo 相当于c的extern的作用,告诉他外面定义了foo了

_start:
	la sp, stack_end	# prepare stack for calling functions

	# RISC-V uses a0 ~ a7 to transfer parameters
	li a0, 1	# 参数1 = 1
	li a1, 2	# 参数2 = 2
	call foo	# 调用foo函数
	
	# RISC-V uses a0 & a1 to transfer return value
	# check value of a0

stop:
	j stop			# Infinite loop to stop execution

	nop			# just for demo effect

stack_start:
	.rept 10
	.word 0
	.endr
stack_end:

	.end			# End of file

5.2 C函数中嵌入汇编

image-20210812164332374

  • asm 后可以跟volatile也可以不跟 [如果跟:不允许编译器优化]
int foo(int a,int b)
{
    int c;
    asm volatile(
    	"add %0,%1,%2"	//汇编指令
        :"=r"(c)	//输出操作数说明
        :"r"(a),"r"(b)	//输入操作数说明
    )	
    return c
}
  • r:表示与寄存器关联起来

  • m(这里没有用这个): 表示与栈内存关联起来

  • 0,1,2:是按照写的顺序去指代,先是c,再a,再b

实际具体选什么寄存器,往往是留给编译器自己去选择,我们需要告诉他把c和add得到的寄存器关联起来,把a和b与相加的两个寄存器关联起来.