一、前篇练习

 上一篇留下的负数加法可以看到在寄存器中的表现就是如此了,x7是-2,x5是-1。

这里不多说,大家复习补码的概念即可。

来看看减法:sub

# Substract
# Format:
#	SUB RD, RS1, RS2
# Description:
#	The contents of RS2 is subtracted from the contents of RS1 and the result
#	is placed in RD.

	.text			# Define beginning of text section
	.global	_start		# Define entry _start

_start:
	li x6, -1		# x6 = -1
	li x7, -2		# x7 = -2
	sub x5, x6, x7		# x5 = x6 - x7

stop:
	j stop			# Infinite loop to stop execution

	.end			# End of file

使用make code 反汇编:

尝试将b3 05 95 00 (低地址到高地址)反汇编: 

实际指令:00 95 05 b3 =》0000 0000 1001 0101 0000 0101 1011 0011

0000 000/0 1001 /0101 0/000/ 0101 1/011 0011
funct7/rs2/rs1/funct3/rd/opcode

对照表可以看出是加法操作的寄存器为x9,x10,x11,x11=x9+x10。

二、新指令ADDI

其实就是要介绍一下Itype的指令。Rtype是操作三个寄存器,而Itype仅有两个寄存器,而有一个12位的立即数(相当于没有了funct7和rs2)。

这样可以减少一个寄存器的操作,也就是可以直接把数据放在指令里直接用,但是因为只有12位的关系,所以表达范围是很有限的,除去符号位,也就是在[-2048,2047]之内。在参与计算时也会被符号拓展成32位(这里针对的是RV32I)。值得注意的是RISCV ISA并没有提供SUBI指令

基于算术运算指令实现的其他伪指令

NEG:negative,取反运算,NEG RD RS等价于SUB RD X0 RS (0-RS 放到RD里。)

MV:move,移动,ADDI RD 0 RS (把RS加0放到RD里。)

NOP:什么也不做,也就是对 ADDI x0 x0 0,把0+x0放到x0里。x0是只读且只为0。

那么addi赋值范围有限,12位的数据不能满足我们的赋值要求怎么办。对于32位寄存器,我们可以自定义一个指令先赋值高20位再通过addi来赋值低12位。

LUI命令(load upper immediate)

事实上就是一个Utype,负责给出rs1(这里为rd)的高20位。 

opcode:0110111

例子:lui x5 0x12345 ,这之后x5的高20位就是0001 0010 0011 0100 0101低12位为0。

那么具体来看怎么使用这一指令配合addi完成对寄存器大数的加载。

你可能以为就是一个负责高位,一个负责低位的关系,事实上不是,举两个例子就知道了:

如果要载入的是12345678,那么lui命令加载0x12345 addi再加上 0x678就可以了

12345 678
lui x5 0x12345
0001 0010 0011 0100 0101 
addi x5 0x678 x5
0110 0111 1000
0000 0000 0000 0000 0000 0110 0111 1000

最终得到
0001 0010 0011 0100 0101 0110 0111 1000

看起来很顺利对不对,来看看后面一个例子:

12345 fff
lui x5 0x12345
0001 0010 0011 0100 0101 0000 0000 0000
addi x5 0xfff x5
0110 0111 1000
1111 1111 1111 1111 1111 1111 1111 1111

最终得到
     
(0001) 0001 0010 0011 0100 0100 1111 1111 1111
溢出不算:得到什么?
原因是符号拓展。

最后为(1) 12344FFF,可以看到和我们需要的完全不同,这是因为实际上我们这里addi加的FFF是一个负数,经过符号拓展就和我们想象的完全不同了,也就是说,类似于这种原理:一个最大是100就溢出为0的系统中,一个数字减去1,等于加上99,比如2-1=1,而加上99相当于加上100再减1,对于系统来说100相当于溢出的0,也就是得到的结果101与01相同。

 所以我们要得到12345fff这个数字,不能简单的直接addi x5 0xfff x5,而我们要的fff代表的实际上是一个正值的一部分,那么我们根据刚刚的逆向思维,如果加上一个fff会被符号拓展之后当成一种别样的减法的话(加fff会导致前面的符号拓展1....1使得前面溢出。),我们只要把高位先加上1,再减去刚刚我们要加的那个数字距离溢出的差值,也就是如果我们要加的是fff或者ffe,那么我们高位就给出12346,再减去1或者2就可以达到我们的目的了。

来整理一下:

因为加上一个带有符号拓展为
1111 1111 1111 1111 1111 xxxx xxxx xxxx 数
虽然最后低位被补全,但是高位发生了溢出
(相当于一个最大两位数的系统,比如8加上99就会变为107,得到07,
相当于做了一个减法,减去的是99和最大溢出上限的差距 1 。)

所以我们要构造一个具有 
xxxx xxxx xxxx xxxx xxxxx 1xxx xxxx xxxx的数字,
就会出现问题,那么我们怎么办呢,同样的我们也利用了所谓溢出的原理,
既然加99使其溢出相当于减1.那么我们提前在高位加上1,
也就是12345 fff的组合不用 12345和fff组合出来,而是,12346 与 -1 组合。  

lui x5 12346000

addi x5 -1 

最后来一个理解:高20位可能由于符号拓展导致溢出,
那么我们就使用先在高20位加上1,再在低位减去后面的数和这12位溢出的差距。
事实上就是预判了溢出提前做好了对高位的补偿。

事实上原因就是因为addi是一个兼具了减法功能的加法,即带有符号拓展。

 其实有点复杂,在有些时候甚至难以理解,所以我们有一个伪指令:li(load immediate)。汇编器会根据实际情况生成正确的真实指令。

# Load Immediate
# Format:
#	LI RD, IMM
# Description:
#	The immediate value (which can be any 32-bit value) is copied into RD.
#	LI is a pseudoinstruction, and is assembled differently depending on 
#	the actual value present.
#
#	If the immediate value is in the range of -2,048 .. +2,047, then it can
#	be assembled identically to: 
#
#	ADDI RD, x0, IMM
#
#	If the immediate value is not within the range of -2,048 .. +2,047 but 
#	is within the range of a 32-bit number (i.e., -2,147,483,648 .. +2,147,483,647) 
#	then it can be assembled using this two-instruction sequence:
#
#	LUI RD, Upper-20
#	ADDI RD, RD, Lower-12
#
#	where "Upper-20" represents the uppermost 20 bits of the value 
#	and "Lower-12" represents the least significant 12-bits of the value.
#	Note that, due to the immediate operand to the addi has its 
#	most-significant-bit set to 1 then it will have the effect of 
#	subtracting 1 from the operand in the lui instruction.

	.text			# Define beginning of text section
	.global	_start		# Define entry _start

_start:
	# imm is in the range of [-2,048, +2,047]
	li x5, 0x80

	addi x5, x0, 0x80

	# imm is NOT in the range of [-2,048, +2,047]
	# and the most-significant-bit of "lower-12" is 0
	li x6, 0x12345001

	lui x6, 0x12345
	addi x6, x6, 0x001

	# imm is NOT in the range of [-2,048, +2,047]
	# and the most-significant-bit of "lower-12" is 1
	li x7, 0x12345FFF	

	lui x7, 0x12346
	addi x7, x7, -1

stop:
	j stop			# Infinite loop to stop execution

	.end			# End of file

这就是伪指令的意义。

 

 对比一下生成的机器码可以发现,li在对不同数字的处理时生成不同的机器猫,和我们预料的完全一致。可以看到li生成的和我们预料的相同,导致会出现两次相同机器码。

三、 AUIPC构造地址

我们说了算数运算加减和构造数字。那么一个地址也是32位,我们也需要在寄存器内构造地址的时候怎么办呢?auipc就是:

我认为这是一条真指令,但是chatgpt给我翻译为伪指令。一笑处之罢了。总之,auipc的作用就是给pc高20位加立即数读结果存到指定寄存器。这里立即数为0的话就可以直接读出pc的值。

伪指令也是有的,la,就是load address命令,就和li一样:

 事实上就是给rd赋值label的地址。

la x5, _start
jr x5

jr就是跳转到x5内存储的地址。

四、逻辑运算指令

and,or,xor.andi.ori,xori。

与,或,异或,立即与,立即或,立即异或。

and rd, rs1,rs2  #R类

andi rd,rs1,imm  #I类

其他的类似。

另外没有取反指令,只有伪指令,利用的是异或的操作

not指令:相当于xor rd, rs1, -1。-1是全1,进行异或,就会导致1全部变为0,0全部变为1.达到取反效果,至于为什么不用全0,全零是保持本来样子的意思。异或的意思就是和我一样就为0,那么0始终为0,和或的区别是不能两个1同时出现罢了。

五、移位运算指令

这是很多学过计组的学生都头疼的问题,我们先讲个简单的。

逻辑移位运算:

sll,srl,slli,srli。

sll rd, rs1,rs2  #R类 rd=rs1<<rs2

slli rd,rs1,imm  #I类 rd=rs1<<imm

srl rd, rs1,rs2  #R类 rd=rs1>>rs2

srli rd,rs1,imm  #I类 rd=rs1>>imm

逻辑左移和逻辑右移补的都是0。

下一类就是算术移位,算术移位只有右移,这和计组可能不太一样。补全的是符号位,也就是00100000会被补全为00010000,而10010000会被补全为11001000。

举个例子-2:原码为1000 0010 补码:11111110,右移之后:11111111;-1。

64位指令集还有其他移位指令,这里暂时不提。

六、内存读写指令

看手册看手册:

l代表load bhwd分别为1字节,2字节,4字节,8字节,即byte,half,word,double之意。u的意思就是作为无符号数拓展,lw为什么没有写lwu,其实是有的,但是对32位RV32I来说没有,对RV64I就有了。为什么?可以思考一下。

 

 

 

 请注意我并没有把所有命令都列出,你要明白的是指令的名字和类型的含义而不是希望在这里找到一本手册。

总结作用呢就是从外部内存取数到寄存器。

lb rd,imm(rs1)

这一条命令就是根据rs1给出的地址加上imm给出的偏移量形成地址来找到内存,然后就把一个字节的数据进行符号拓展读取到rd。其他的命令可以自己理解一下,不多赘述。又因为读取imm12位那么最多就是相对于rs存的地址有[-2048,2047]的偏移。

我们不仅要读内存也要写内存,类似,但是我们不用考虑符号了,因为在存入内存时是不需要进行拓展的。

SB,SH,SW

64位还有: 

 

 举个例子:

sb rs2,imm(rs1)

从rs2中拿取低8位存到地址rs1+imm的内存里。注意内存只要byte对齐就行。

下一节就把汇编过完。

更多推荐

【RISC-V操作系统】从零开始写一个操作系统(七)RISCV汇编语言编程