chap2_solutions
2.1
没有subi
这条指令!用addi
代替!
2.3
- 数组下标i-j:临时变量用临时寄存器x30存储
- 下标需要通过slli左移3乘以8后才能使用!
- A[8]的下标是8,store的时候也是用64(x11),即8*8
==2.4==
- load指令中:带括号的0(x30)是取x30这个地址上存储的值,不带括号的x30是取x30这个寄存器存储的值;
store指令中:0(x30)表示把值存到x30所存储的这个地址上去
- 因为偏移量不能用寄存器表示,所以直接把存储A[0]的x10加上i*8后的值
![[Pasted image 20250108204125.png|500]]
2.5
- 对于16进制数(0x…),大小端法都是一个字节存“两个数字”
- “大佐”:大端法–高字节即0x最左边的两个数字被存到低地址
![[Pasted image 20250108200537.png|800]]
2.7
如果x30中寄存着一个数组A的地址&A,那么x30
表示&A这个地址值,0(x30)
表示x30所存的地址所存储的值A[0]。
2.8
可以多看看第3、4行
2.10
add
和sub
的结果不溢出的条件是结果>$2^{63}-1$或<$-2^{63}$
其中63是因为寄存器是64位的,如果寄存器是32位,则改为$2^{31}-1$或$-2^{63}$- 第一个x5+x6得到的数跃出上界了,无法用64位表示,所以overflow
- 第二个x5-x6只是得到负值而已,并没有出下界,所以no overflow
- 算16进制数的加减法时一定要一个一个地数,比如0x8+0xd=0x5:
8-9-a-b-c-d-e-==f-0-1==-2-3-4-5,一共加了13个1,最后得到0x5;
重点是:f+1≠1,而是等于0!
同理,0x8-0xd=0xb,可以自己算一下
2.11
add
和sub
的结果不溢出的条件是结果>$2^{63}-1$或<$-2^{63}$
其中63是因为寄存器是64位的,如果寄存器是32位,则改为$2^{31}-1$或$-2^{63}$
![[Pasted image 20250108212514.png|500]]
2.12-2.15
根据机器编码写出指令类型
2.16
- 寄存器个数的变化会怎样影响opcode?没明白
- 寄存器个数的变化会导致rs1、rs2、rd的位数变化,比如寄存器增加到128=$2^{7}$个,那么这三个字段的位数就变为7,才能完整地表示每个寄存器;
2.17
- slli x7, x5, 4
即16进制数0x00000000AAAAAAAA 左移4位 ,结果不是0x0000AAAAAAAA0000,而是0x0000000AAAAAAA==0== - srli x7, x5, 3
a. 看清是r还是l,左移还是右移
b. 右移3位,那么0x00000000AAAAAAAA就变成了0x0000000015555555 - andi x7, x7, 0xFEF
看清是and不是add!是按位与 - RISC-V 的
andi
指令设计为只对最低 12 位进行操作,而寄存器的高位保持不变
![[Pasted image 20250109021615.png]]
如图,andi指令是从x7中提取出其最低的12位,与12位立即数相与,而不管x7的其他位是什么;
2.18
- 不能直接写
andi x5, x5, 0x000000000001F800
,要通过移位得到右边这个imm
![[Pasted image 20250109022003.png]]
2.19
- 按位取反的指令:==xori x5, x6, -1==
即把x6的值按位取反后存到x5里。
-1=0xffff…,所有位都是1,把任何数与-1进行异或,都会得到该数按位取反的结果。
2.22
问:PC=0x2000 0000,为什么jal和beq指令能跳转到的地址范围分别是:
[0x1ff00000, 0x200FFFFE]
[0x1FFFF000, 0x20000ffe]
解答:
![[Pasted image 20250109034506.png]]
- J型指令只有jal一个
- jalr是I型指令,因为它的操作数不是label而是imm,PC计算方法是PC+imm
- B型指令和J型指令的操作数都是label,这个label会被机器自动计算出一个offset,PC计算方法是PC+offset
![[Pasted image 20250109034737.png|500]]
- B型指令的立即数只有==12位==,故offset范围为==[-2^11, 2^11 - 1]==
- J型指令的立即数有20位,故offset范围为
[-2^19, 2^19 - 1]
PC可跳转地址的范围就是PC+offset的值的范围
[!NOTE] 重点
B、J型的立即数左移1位
- 分支指令(如
beq
)和跳转指令(如jal
)的偏移量是以字节为单位的地址左移 1 位(即乘以 2),这是因为偏移量字段只需要指定偶地址(每条指令 4 字节对齐),所以在指令格式中省略了最低有效位。故而虽然B、J型的imm分别为12、20位,但实际上应该在它们后面再加个0,能够表示13、21位的offset; - 这就解释了为什么20位的offset最小能表示-2^19即0x00080000,但是0x1ff00000+0x00080000并不等于0x20000000——因为0x00080000还需要再左移一位变成0x00100000才是真正的offset!而0x00100000+0x1ff00000=0x20000000
- 同理,20位的offset最大能表示-2^19即0x000effff,但需要左移1位变成0x000ffffe,才是真正的offset!
2.23
UJ型就是J型、SB型就是B型
为什么要写成先-1、再+1的形式?
答:如果写成这样1 2
loop: bgt x29, x0, exit // 如果 x29 > 0,则跳出循环 addi x29, x29, -1 // 否则将 x29 减 1
那么-1后,将直接退出循环,无法进行下一次判断。
所以只能这样写:(其实前两句是一个循环,第三句addi是循环体外的)
1
2
3
4
5
6
loop:
addi x29, x29, -1 // Subtract 1 from x29
bgt x29, x0, loop // Continue if x29 not negative
addi x29, x29, 1
//当值减为0时跳出循环,此时x29的值实际上比预期少了 1,因此将它加回去,恢复成正确的值。
2.25
JAL
指令的第一个操作数(即目标寄存器rd
)通常是用来存储返回地址的。如果你将rd
设置为x0
,对x0
的任何写操作都会被忽略。因此,当JAL
的目标寄存器设置为x0
时,返回地址不会被保存,此时JAL
的行为就变成了一个单纯的无条件跳转,类似于J
指令。![[Pasted image 20250109050703.png 300]] - 一个循环框架的4条基本语句:
for(int i-0;i<a;i++)
1 2 3 4 5 6 7
Loop: addi x7, x0, 0 //初始化i=0 bge x7, x5, Exit //若i>=a则跳出循环 ... addi x7, x7, 1 //i++ jal x0, Loop Exit:
- riscv不会自动回到循环体,所以要==手动添加一个”无条件跳转“==即
jal x0, Loop
- 第二条B型指令一般写的是”反向条件“,即退出循环的条件,跳转到Exit标签而不是Loop
- riscv不会自动回到循环体,所以要==手动添加一个”无条件跳转“==即
- 特别注意蓝色的两句
x30用来存储&D[4*j]这个地址,而==寄存器是不会随着循环而清空的!==
所以每次j+1,x30所存储的地址即&D[4*j]就要变成&D[4*(j+1)],而这个”+1“要先*4,然后还要*8(因为一个数据占8字节),所以x30所存储的地址值每次循环要加32!这个地址值会保持到下次循环,然后sd x31,0(x30)
中的目标地址0(x30)实际已经是上次sd的地址+32后的地址了!
![[Pasted image 20250109052443.png|350]]
2.27
- 注意能识别出:数组下标、循环
riscv:
addi x6, x0, 0
addi x29, x0, 100
LOOP:
lw x7, 0(x10)
add x5, x5, x7
addi x10, x10, 8
addi x6, x6, 1
blt x6, x29, LOOP
翻译为:
int i;
for (i = 0; i < 100; i++) {
result += MemArray[i];
}
return result;
2.29
- 递归–>栈
- x10, x11是函数的 参数、返回值 寄存器,而本题中n既是参数又是返回值
- x1是返回地址寄存器,x2是栈指针寄存器
- 对于递归函数,一般有两个值需要压栈保存:返回地址x1、参数x10(或x11)
- 递归调用时,只需要更改参数寄存器x10/x11的值,然后直接跳转jal x1, func就行(PC+4会存到x1里)
- 递归调用后,x10/x11的值变成了递归调用得到的返回值,如fib(n-1),为了保存这个返回值,一般会将它压栈保存
- 在函数的结尾别忘了恢复返回地址
ld x1, 0(x2)
- 别忘了弹栈清理数
![[Pasted image 20250109091714.png|500]]
2.31
- x12~x15都是参数寄存器,但不具有返回值寄存器的功能!
![[Pasted image 20250109094104.png|500]]
2.32
尾调用优化的原理
在一般的函数调用中,每次调用都会分配一个新的栈帧,用来保存调用者的局部变量、返回地址等信息。递归调用会不断增加栈的深度,最终可能导致栈溢出。
尾调用优化的原理是:如果函数调用是尾调用,则不需要再保留当前函数的栈帧,可以直接复用当前的栈帧来执行被调用的函数。这样,即使递归调用的次数很多,也不会增加栈的深度,从而避免栈溢出。- 条件:函数调用在当前函数的最后一步执行,且调用后不再需要保留当前函数的状态
- 尾递归是尾调用的一种特殊形式,即函数自己在最后一步调用自己
![[Pasted image 20250109094244.png]]
2.35
注意lb只加载 从目标地址开始的那1个字节 的数据,也就是8位数据,譬如目标地址是0(x7),则0x1122334455667788只有0x88或0x11会被加载到目标地址中(16进制的一位数字代表4位二进制数,两位16进制数就是一个字节);而若目标地址是1(x7)
如果是大端法,则0x”最左端的字节“也就是0x11被存在地址最低的地方,即0(x7),所以
lb x6, 0(x7)
加载到x6的值就是0x11;如果是小端法则反之,为0x88
查漏
- 28, 30, 33, 34没有看,有空可以看看