CS:APP 第三章练习题

    技术2024-12-19  11

    3.10.5 支持变长栈帧

    练习题3.49

    这道题挺难的,综合的知识点也挺多,特此梳理一下

    push %rbp movq %rsp, %rbp

    1.%rbp是被调用者保存寄存器,为了防止被修改,先提前保存 2.这个步骤是设置帧指针,代码将%rbp之前的值保存到栈中。如图,帧指针跟栈指针之间的这块内存构成了栈帧。

    3.设置%rbp的核心思想 (因为后面的for循环要不断修改i的值,所以要获取i的地址) 这里直接用%rbp - 8 就可以获取到i的地址

    其实直接用%rsp来计算i地址应该也是可以的,书上说最近的GCC版本放弃了使用帧指针。


    subq $16, %rsp

    栈指针-16,这是一个分配内存的操作,为i分配8字节,剩余8字节未被使用。 这里开始卡住了,我一直纳闷为什么不直接放寄存器里。

    p[0] = &i;

    主要是因为这里需要引用i的地址。

    翻回P170

    3.7.4 栈上的局部存储

    局部数据优先保存到寄存器中,但是以下情况保存到内存中。

    寄存器不足够存放所有本地数据,比如函数参数超过6个对一个局部变量使用地址运算符’&’,因此必须为它产生一个地址某些局部变量是数组或结构,因此必须能够通过数组或结构引用访问到

    局部变量存储无非就是放入寄存器跟栈中,如果放入%rax这样的寄存器,在过程调用的时候值可能被覆盖,因为寄存器是过程调用共享的资源,因此寄存器的地址就没什么实际意义,所以要将带有"&"的值存到栈中。


    leaq 22(, %rdi, 8), %rax andq $-16, %rax

    这里的话是 8n+22 & 0x-16 0x-16 -> 111111…10000(2)

    也就是把8n+22向下舍入最接近的16的倍数,n是奇数时,结果是8n+8,n是偶数时,结果是8n+16

    第8-10行计算(s2+7)/8 * 8,代表将s2向上舍入到最近的8的倍数

    这也是为什么n = 5, s=2065的情况下, e1 = 1, 刚好舍入到2064 然后再减去8*5 也就是p的内存大小得到2024. n为奇数,之前讲过,奇数舍入的结果是8n+8 e1分配了1,因此e2分配7

    这里需要理解数组和对齐的概念,保证了指针数组的内存大小总是为8的倍数。


    家庭作业

    3.58

    这道题我第二次看的时候算错了,我是按寄存器来算的;

    subq %rdx, %rsi imulq %rsi, %rdi

    执行到第二行%rsi = y - z; 我还是把它当y来计算,以至于后面都错了

    %rsi = y - z; %rdi = x * y; //=> incorrect %rdi = x * (y - z) //=> correct

    正确答案

    long decode2(long x,long y,long z) { y = y - z; x = x * y; y <<= 63; y >>= 63; return y ^ x; }

    如果y-z是偶数 即000...0 -> 000...0 y-z是奇数 即000...1 -> 111...1

    3.59

    typedef __int128 int_t; void store_prod(int128_t *dest, int64_t x, int64_t y) { *dest = x * (int128_t) y; }

    做题的时候让我困惑的地方

    对cpto 这个指令不是很了解,一直很困惑只有一次sarq指令,也就只进行一次算术右移对于右移进行扩展的概念不理解

    其实就是将高位放在一个另外一个寄存器里。 假设x是四位表示的补码 原本%rsi 1111表示的是 -1 扩展以后 1111 1111还是 -1 可以根据书上的公式计算 x = 2 4 ∗ x h ​ + x l x=2 ^4 ∗x h ​ +x l x=24xh+xl

    2 4 ∗ ( − 1 ) + 15 = − 1 2^4*(-1) + 15 = -1 24(1)+15=1

    注意1111 1111 高位代表的是-1 ,低位代表的是15 因为符号位扩展到高位了。

    store_prod: movq %rdx, %rax # %rax = y cqto # convert q to o,4字符号拓展到8,假如y的符号位为1,那么%rdx所有位都是1(此时值是-1),否则,%rdx全为0(此时值是0).%rdx = yh movq %rsi, %rcx # %rcx = x sarq $63, %rcx # 将%rcx向右移63,%rdx的含义一样,二进制位要么全是1,要么是0,%rcx = xh. imulq %rax, %rcx # %rcx = y * xh imulq %rsi, %rdx # %rdx = x * yh addq %rdx, %rcx # %rcx = y * xh + x * yh,计算了第二项 mulq %rsi # 无符号计算 xl*yl,并将xl*yl的128位结果的高位放在%rdx,低位放在%rax,计算了第三项. addq %rcx, %rdx # 将第二项计算结果加到%rdx movq %rax, (%rdi) # 将%rax的值放到dest的低位 movq %rdx, 8(%rdi)# 将%rdx的值放到dest的高位 ret

    参考的这篇答案,上面对6-8行解释的很详细。 https://blog.csdn.net/anlian523/article/details/84679282

    第九行无符号计算 xl*yl,这里实在是不懂汇编怎么运算的。 参考了王爽老师的汇编语言P199。 movb 100,al movb 10,bl mul bl

    这里计算的是10*100 结果放入寄存器bl中 这里计算的应该是bl和bl上一行的值

    对应题目6-9行 %rax -> y %rsi-> x

    imulq %rax<-, %rcx imulq %rsi<-, %rdx addq %rdx, %rcx mulq %rsi # %rax * %rsi 无符号计算 xl*yl

    我一直以为计算的是 %rdx 和 %rsi 的值保存到%rsi,但是对应上面的第二行已经修改%rdx的值为x*yh。

    小结:

    cqto指令mulq指令符号扩展符号位保存到高位寄存器无符号和有符号数乘法(相同位级表示,最后的结果相同)

    3.60 - 3.61

    还是这篇答案,讲的很清楚 https://blog.csdn.net/anlian523/article/details/84679282

    3.62

    这道题上面的答案是有问题的,不过也可能是印刷错误导致的答案不一样。

    long switch3(long *p1, long *p2, mode_t action) { long result = 0; switch(action) { case MODE_A: result = *p2; *p2 = *p1; break; case MODE_B: *p1 = *p1 + *p2; // result = *p1; 这里应该是 *p1 = result; break; case MODE_C: *p1 = 59; result = *p2; break; case MODE_D: *p1 = *p2; result = 27; break; case MODE_E: result = 27; break; default: result = 12; break; } return result; }

    3.64

    二维数组 A [ S ] [ T ] A[S][T] A[S][T] 对应等式3.1 & A [ i ] [ j ] = X a + L ( T ∗ i + j ) \&A[ i ][ j ] = Xa + L(T * i + j) &A[i][j]=Xa+L(Ti+j)

    Xa是数组首位元素的地址 L是数组单位长度,如int : L = 4 T是一行的元素的个数 这时候i代表一行

    三维数组 A [ R ] [ S ] [ T ] A[R][S][T] A[R][S][T]

    相当于多个二维数组表格堆在一起 A [ R ] [ S ] [ T ] − > A [ R + 1 ] [ S ] [ T ] A[R][S][T] ->A[R + 1][S][T] A[R][S][T]>A[R+1][S][T] 一个表格到另一个表格相同坐标位置相差距离为 S*T & A [ i ] [ j ] [ k ] = X a + L ( S T ∗ i + T ∗ j + R ) \&A[i][j][k] = Xa+L(ST*i + T*j+R) &A[i][j][k]=Xa+L(STi+Tj+R) 这时候i代表一个表格

    3.67

    # long eval(long x, long y, long z) # x in %rdi, y in %rsi, z in %rdx eval: subq $104, %rsp movq %rdx, 24(%rsp) leaq 24(%rsp), %rax movq %rdi, (%rsp) movq %rsi, 8(%rsp) movq %rax, 16(%rsp) leaq 64(%rsp), %rdi //这道题的关键,将栈指针偏移64位的地址存入%rdi call process movq 72(%rsp), %rax addq 64(%rsp), %rax addq 80(%rsp), %rax addq $104, %rsp ret

    call process之前

    # strB process(strA s) # s in %rdi process: movq %rdi, %rax movq 24(%rsp), %rdx movq (%rdx), %rdx movq 16(%rsp), %rcx movq %rcx, (%rdi) <-64(%rsp) 地址存入y movq 8(%rsp), %rcx movq %rcx, 8(%rdi) <-72(%rsp) 地址存入x movq %rdx, 16(%rdi) <-80(%rsp) 地址存入z ret

    E. 实际上就是通过%rsp+偏移量来传值 F.作为函数参数和函数结果的结构体,都是使用指针传值的。

    3.69

    这道题没做出来主要是因为

    typedef struct { int first; a_struct a[CNT]; //=> 这里是一个结构数组 int last; } b_struct;

    如果不了解结构数组的话,后面的汇编就提取不出有效信息。

    C primer plus的例子

    struct book { char title[100]; char author[100]; float value; } # struct book 相当于一种数据类型 # 声明一本书 -> int fir_book; # 声明struct book 类型的一本书 -> struct book fir_book; # 如果声明多本书 # struct book sec_book; # struct book thr_book; ... # 这样显然太麻烦,也不方便查找,使用结构数组可以解决这个问题 # struct book library[100]; # 声明一个能容纳一百个struct book类型值的数组 # 数组的大小取决于结构的大小 test: mov 0x120(%rsi), %ecx add (%rsi), %ecx lea (%rdi,%rdi,4), %rax # %rax = i*5 lea (%rsi,%rax,8), %rax # %rax = bp+i*40

    从这两行就能够提取到信息 a_struct的大小为40字节。

    test: mov 0x120(%rsi), %ecx # %ecx = bp -> first add (%rsi), %ecx lea (%rdi,%rdi,4), %rax lea (%rsi,%rax,8), %rax

    注意这里的偏移量是0x120,需要转成十进制计算,我就是直接用120 - 8 (int first 字节对齐) -> 112 所以怎么都算不出结果。 实际上应该是288-8 -> 280 / 40 (a_struct) -> CNT = 7

    # void test(long i, b_struct *bp) # i in %rdi, bp in %rsi test: mov 0x120(%rsi), %ecx # bp+288 匹配bp->last add (%rsi), %ecx # bp->first + bp->last lea (%rdi,%rdi,4), %rax # %rax = i*5 lea (%rsi,%rax,8), %rax # %rax = bp+i*40 # ap = &bp->a[i] = bp+8+i*40, +8意味着从bp开始的第1个八字节里面只有int,且a_struct大小必为8字节或更大,若为4字节,就不是+8而是+4了 # 因为是i*40,所以a_struct大小为40字节 # 此句很明显取出了一个数,再结合倒数第二条指令mov %rcx, 0x10(%rax,%rdx,8),所以%rdx为ap->idx # 而且在结构体a_struct中,第一个成员为整数类型的idx mov 0x8(%rax), %rdx movslq %ecx, %rcx # mov时符号拓展成48字节 # 先看0x10(%rax,)部分,是bp+16+i*40,比ap多了8字节,这里是a_struct数组成员的开始地址,也说明了idx大小为8字节 # 再看(,%rdx,8)部分,是idx*8,所以说明了a_struct数组成员的大小为8字节 # 合起来看就是bp+8+i*40+8 +idx*8,第二个+8跳过了a_struct的整数成员idx mov %rcx, 0x10(%rax,%rdx,8)

    让我困惑很久的是这两句

    mov 0x8(%rax), %rdx mov %rcx, 0x10(%rax,%rdx,8) // 第一行 %rdx = ap = &bp->a[i] = bp+8+i*40

    %rdx保存的是ap指针的地址,指向第i个结构数组,看第二句。

    (,%rdx,8)

    一直想不明白为什么要用ap的地址*8

    先看前半部分

    mov %rcx, 0x10(%rax,) 16 + &bp + 40i 16怎么来的? 8字节是int first的偏移量 还有8字节应该就是idx的偏移量,这样就可以得到数组的起始位置

    数组位置计算公式: & a [ i ] = X a + C ∗ i \&a[i] = Xa + C*i &a[i]=Xa+Ci Xa是起始位置,C是数组每个值的长度。 刚好对应上 (0x10 + %rax) + 8 * %rdx

    所以可以得出数组的每个值长度为8字节,又是有符号数,只能是long型. 至于为什么是 8 * %rdi(&ap) 只有一种可能,就是刚好idx这个值在a_struct的最初始位置。让我困惑了很久… 之前得出a_struct的字节长度为40

    typedef struct { long idx; long x[4]; } a_struct
    Processed: 0.020, SQL: 9