Post

chap2_solutions

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


  • addsub的结果不溢出的条件是结果>$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

addsub的结果不溢出的条件是结果>$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.png300]]
  • 一个循环框架的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:
    
    1. riscv不会自动回到循环体,所以要==手动添加一个”无条件跳转“==即jal x0, Loop
    2. 第二条B型指令一般写的是”反向条件“,即退出循环的条件,跳转到Exit标签而不是Loop
  • 特别注意蓝色的两句
    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没有看,有空可以看看
This post is licensed under CC BY 4.0 by the author.