CSAPP 3.58-3.68

题目 *3.58

一个函数的原型为:

long decode2(long x, long y, long z);

GCC 产生如下汇编代码:

decode2:
    subq    %rdx, %rsi
    imulq   %rsi, %rdi
    movq    %rsi, %rax
    salq    $63, %rax
    sarq    $63, %rax
    xorq    %rdi, %rax
    ret

参数 x、y 和 z 通过寄存器 %rdi、%rsi 和 %rdx 传递。代码将返回值存放在寄存器 %rax 中。写出等价于上述汇编代码的 decode2 的 C 代码。

不算复杂的题,逐行翻译即可,不过由于我的设备上long是4字节的,无法完成移位63的操作,需要把原型换成long long

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

汇编后发现没有movq %rsi %rax的一步,补了一个中间变量后没变化,似乎被优化掉了,那就干脆保持原样吧

subq	%rdx, %rsi
imulq	%rdi, %rsi
salq	$63, %rsi
sarq	$63, %rsi
orq	%rdi, %rsi
setne	%al
movzbl	%al, %eax
ret

对y变量只保留了最后一位,然后异或,大概是一种掩码操作

题目 **3.59

下面的代码计算两个 64 位有符号值的 128 位乘积,并将结果存在内存中:

typedef __int128 int128_t;

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

GCC 产生下面的汇编代码来实现计算:

store_prod:
    movq    %rdx, %rax
    cqto
    movq    %rsi, %rcx
    sarq    $63, %rcx
    imulq   %rax, %rcx
    imulq   %rsi, %rdx
    addq    %rdx, %rcx
    mulq    %rsi
    addq    %rcx, %rdx
    movq    %rax, (%rdi)
    movq    %rdx, 8(%rdi)
    ret

为了满足在 64 位机器上实现 128 位运算所需的多精度计算,这段代码用了三个乘法。描述用来计算乘积的算法,对汇编代码加注释,说明它是如何实现你的算法的。

提示: 在把参数 x 和 y 扩展到 128 位时,它们可以重写为 $x = 2^{64} \cdot x_h + x_l$ 和 $y = 2^{64} \cdot y_h + y_l$,这里 $x_h$、$x_l$、$y_h$ 和 $y_l$ 都是 64 位值。类似地,128 位的乘积可以写成 $p = 2^{64} \cdot p_h + p_l$,这里 $p_h$ 和 $p_l$ 是 64 位值。请解释这段代码是如何用 $x_h$、$x_l$、$y_h$ 和 $y_l$ 来计算 $p_h$ 和 $p_l$ 的。

x和y都是有符号的,所以拓展到128位就是把符号位算数左移64位,其余不变。
默认的128位处理是把低64位放rax,高64位放rdx,但是遇到了负数补码就乱了

$$p = 2^{128} \cdot x_h y_h + 2^{64} \cdot (x_h y_l + x_l y_h) + x_l y_l$$

其中 $2^{128} \cdot x_h y_h$ 直接忽略

store_prod:
    movq    %rdx, %rax    ; rax = y 
    cqto                  ; 将rax符号扩展到rdx,rdx = y_h
    movq    %rsi, %rcx    ; rcx = x 
    sarq    $63, %rcx     ; 算术右移63位提取符号,rcx = x_h
    imulq   %rax, %rcx    ; rcx = y * x_h (即 y_l * x_h)
    imulq   %rsi, %rdx    ; rdx = x * y_h (即 x_l * y_h)
    addq    %rdx, %rcx    ; rcx = y_l * x_h + x_l * y_h
    mulq    %rsi          ; rax:rdx = y_l * x_l (无符号乘法)
    addq    %rcx, %rdx    ; rdx += rcx,得到最终的高64位
    movq    %rax, (%rdi)  ; 存储低64位到dest[0]
    movq    %rdx, 8(%rdi) ; 存储高64位到dest[1]
    ret                   

总结一下重点,cqto(Convert Quadword to Octoword)指令,直接完成四字到八字的转换,高位放在rdx里
如果要自己完成,移位要用算数右移,因为负数存的是补码

题目 **3.60

考虑以下的汇编代码:
long loop(long x, int n)

loop:
  movl %esi %ecx ; ecx = n
  movl $1 %edx
  movl $0 %eax
  jmp .L2
.L3:
  movq %rdi , %r8
  andq %rdx , %r8
  org %r8 , %rax
  salq %cl , %rdx
.L2: 
  testq %rdx %rdx
  jne .L3
  req; ret

以上代码是汇编以下整体形式的C代码产生的:

long loop(long x,int n){
  long result = _____;
  long mask;
  for(mask = __ ; mask __ ; mask = ___){
      result |= ___;
  } 
  return result;
}

你的任务是填写这个C代码中缺失的部分,得到一个程序等价于产生的汇编代码。回想一下,这个函数的结果是在寄存器%rax中返回的。
你会发现以下工作很有帮助:检查循环之前、之中和之后的汇编代码,形成一个寄存器和程序变量之间一致的映射。

  • A.哪个寄存器保存着程序值x、n、result和mask、
  • B.result和mask的初始值是什么?
  • C.mask的测试条件是什么?
  • D.mask是如何被修改的?
  • E.result是如何被修改的
  • F.填写这段C代码中所有空缺部分

既然题目给了引导,那就逐步解答:

  • A. 按照约定,x在rdi,n在esi和ecx;L2的循环开始值是看rdx是否为0,所以mask在rdx;L3里的或运算太明显了,result在rax
  • B. loop标签里有两个立即数赋值,result = 0 , mask = 1
  • C. mask要为0,按位与之后才能把0标志位设置为1,而jne判断标准就是0标志位,为1说明相等,不跳转,返回
  • D. 看一眼循环中rdx的变化,把x放到r8,在把mask和r8按位与但是这里结果在r8,没有改变mask。改变在第10行,把rdx算术左移了cl的值位,为什么是cl?
  • E. result = result | (mask & x)
  • F. 开始猜
long loop(long x,int n){
    long result = 0;
    long mask;
    for(mask = 1 ; mask != 0 ; mask = mask << n ){
        result |= (mask & x);
    }
    return result;
}
int main(){
  loop(100,50);
}
gcc -S 3.60.cpp -o 3.60.s -O1
.LFB0:
	.cfi_startproc
	endbr64
	movl	%esi, %ecx
	movl	$1, %eax
	movl	$0, %edx
.L2:
	movq	%rdi, %r8
	andq	%rax, %r8
	orq	%r8, %rdx
	salq	%cl, %rax
	testq	%rax, %rax
	jne	.L2
	movq	%rdx, %rax
	ret

呃,所有rax和rdx都反了,然后末尾多了一行movq %rdx, %rax,没差,一遍过

此题告诉我们jne的上一行不一定是cmp,也可以是test

题目 **3.61

在3.6.6节,我们查看了下面的代码,作为使用条件数据传送的一种选择:

long cread(long *xp){
  return (xp ? *xp : 0);
}

我们给出了使用条件传送指令的一个尝试实现,但认为它是不合法的,因为它试图从一个空地址读数据。
写一个C函数cread_alt,它与cread有一样的的行为,除了它可以被编译成使用条件数据传送。当编译时,产生的代码应该使用条件传送指令而不是某种跳转指令。

时间有点久,复习一下条件数据传送和控制条件转移

  • 控制条件转移就是各种跳转,跑起来还要分支预测,预测错了还有惩罚
  • 数据传送就是预测什么我全都要,然后判断完了选一个,只在受限情况下更优

那回到本题,条件传送的思想是走完所有分支,但是前提是分支里可以无副作用且不会出错,本题里就不满足这一前提,所以编译器的行为不是数据传送

先试试引入中间变量:

long cread_alt(long *xp){
  long t = 0;
  if (xp) t = *xp;
  return t;
}

int main(){
  long *p ;
  cread_alt(p);
}

失败,又试了几种方案,比如引入中间指针,但是转到汇编都是跳转,去网上找了几版"答案",给的都是
return (!xp ? 0 : *xp);

#include <stdio.h>
#include <assert.h>

long cread(long *xp) {
  return (xp ? *xp : 0);
}

long cread_alt(long *xp) {
  return (!xp ? 0 : *xp);
}

int main(int argc, char* argv[]) {
  long a = 0;
  assert(cread(&a) == cread_alt(&a));
  assert(cread(NULL) == cread_alt(NULL));
  return 0;
}

然而汇编完还是条件跳转,就很无语,编译器任性是吧,先搁置吧,贴一下我的环境:

gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04)

题目 **3.62

下面的代码给出了一个switch语句中根据枚举类型值进行分支选择的例子。回忆一下,C语言中枚
举类型只是一种引入一组与整数值相对应的名字的方法。默认情况下,值是从0向上依次赋给名
字的。在我们的代码中,省略了与各种情况标号相对应的动作。

typedef enum {MODE_A, MODE_B, MODE_C, MODE_D, MODE_E} mode_t

long switch3(long *p1, long *p2, mode_t action){
  long result = 0;
  swtich(action){
      case MODE_A:

      case MODE_B:

      case MODE_C:

      case MODE_D:

      case MODE_E:
      
      default:

  }
  return result;
}

产生的实现各个动作的汇编代码部分如图 3-52 所示。注释指明了参数位置,寄存器值,
各个跳转目的的情况标号。

>.L8            ;MODE_E
  movl $27, %eax
  ret
>.L3            ;MODE_A
  movq  (%rsi), %rax
  movq  (%rdi), rdx
  movq  %rdi, (%rsi)
  ret
>.L5:           ;MODE_B
  movq  (%rdi), %rax
  addq  (%rsi), %rax
  movq  %rax, (%rdi)
  ret
>.L6:           ;MODE_C
  movq  $59, (%rdi)
  movq  (%rsi), %rax
  ret
>.L7            ;MODE_D
  movq  (%rsi), %rax
  movq  %rax , (%rdi)
  movl  $27 , %eax
  ret
>.L9:           ;default
  movl $12, %eax
  ret

填写C代码中缺失的部分。代码包括落入其他情况的情况,试着重建这个情况。

看着没有什么难度

#include<stdio.h>

typedef enum {MODE_A, MODE_B, MODE_C, MODE_D, MODE_E} mode_t;

long switch3(long *p1, long *p2, mode_t action){
    long result = 0;
   switch(action){
        case MODE_A:{
          long tmp = *p2;
          long tmp2 = *p1;
          *p2 = *p1;
          return tmp;
        }
        case MODE_B:{
          long tmp = *p2;
          tmp += *p1;
          *p1 = tmp;
          return tmp;
        }
        case MODE_C:{
          *p1 = 59;
          return *p2;
        }

        case MODE_D:{
          long tmp = *p2;
          *p1 = tmp;
          return 27;
        }
        case MODE_E:
          return 27;
        default:
          return 12;
    }
    return result;
}

int main(){
    long a = 100l;
    long b = 200l;
    long* p1 = &a;
    long* p2 = &b;
    switch3(p1, p2, MODE_A);
    return 0;
}
gcc -S 3.62.cpp -o 3.62.s -O1

.L3:
	movl	$27, %eax
	ret
.L8:
	movq	(%rsi), %rax
	movq	(%rdi), %rdx
	movq	%rdx, (%rsi)
	ret
.L7:
	movq	(%rdi), %rax
	addq	(%rsi), %rax
	movq	%rax, (%rdi)
	ret
.L6:
	movq	$59, (%rdi)
	movq	(%rsi), %rax
	ret
.L5:
	movq	(%rsi), %rax
	movq	%rax, (%rdi)
	movl	$27, %eax
	ret
.L9:
	movl	$12, %eax
	ret

呃,确实没有什么难度。唯一有意思的地方在case MODE_B,最开始我写的是

case MODE_B:{
     long tmp = *p1;
     tmp += *p2;
     *p1 = tmp;
     return tmp;
    }

看似和汇编对应的更严谨,先把第一个参数拿出来,再加上第二个参数,然而结果却是rdi和rsi位置颠倒,需要反过来才能严格对应上(虽然并不影响结果)

题目 **3.63

这个程序给你一个机会,从反汇编机器代码逆向工程一个switch 语句。在下面这个过程中,去掉了
switch 语句的主体:

long switch_prob(long x, long n){
  long result = x;
  switch(n){
      /*Fill in code here*/
  }
}

图3-53给出了这个过程的反汇编机器代码。
跳转表驻留在内存的不同区域中。可以从第5行的间接跳转看出来,跳转表的起始地址为0x4006f8 。用调试器 GDB, 我们可以用命令 x/6gx 0x4006f8 来检查组成跳转表的6个8字节字的内存。 GDB 打印出下面的内容:

(gdb)x/6gx 0x4006f8
0x4006f8:      0x00000000004005a1      0x00000000004005c3
0x400708:      0x00000000004005a1      0x00000000004005aa
0x400718:      0x00000000004005b2      0x00000000004005bf

用C代码填写switch语句的主体,使它的行为与机器代码一致

0000000000400590 <switch_prob>:
  400590:  48 83 ee 3c             sub    $0x3c,%rsi
  400594:  48 83 fe 05             cmp    $0x5,%rsi
  400598:  77 29                   ja     4005c3 <switch_prob+0x33>
  40059a:  ff 24 f5 f8 06 40 00    jmpq   *0x4006f8(,%rsi,8)
  4005a1:  48 8d 04 fd 00 00 00    lea    0x0(,%rdi,8),%rax
  4005a8:  00
  4005a9:  c3                      retq
  4005aa:  48 89 f8                mov    %rdi,%rax
  4005ad:  48 c1 f8 03             sar    $0x3,%rax
  4005b1:  c3                      retq
  4005b2:  48 89 f8                mov    %rdi,%rax            
  4005b5:  48 c1 e0 04             shl    $0x4,%rax
  4005b9:  48 29 f8                sub    %rdi,%rax
  4005bc:  48 89 c7                mov    %rax,%rdi
  4005bf:  48 0f af ff             imul   %rdi,%rdi
  4005c3:  48 8d 47 4b             lea    0x4b(%rdi),%rax
  4005c7:  c3                      retq 

又是讨厌AT&T的一天,这样下去看intel快觉得别扭了

在400594进行了比较,后面的ja是default,jmpq就是跳转到跳转表,典型的switch语句格式。
很多条件分支没有break……真是

long switch_prob(long x, long n){
    long result = x;
    switch(n){
        case 60:
        case 62:
            result = 8 * x;
            break;
        case 63:
            result = x >> 3;
            break;
        case 64:
            result = (result << 4 ) - x;
        case 65:
            result *= result;
        case 61:
        default:
          result += 75;
          break;
    }
    return result;
}

int main(){
  switch_prob(2,3);
}
    subq	$60, %rsi
	cmpq	$5, %rsi
	ja	.L2
	leaq	.L4(%rip), %rdx
	movslq	(%rdx,%rsi,4), %rax
	addq	%rdx, %rax
	notrack jmp	*%rax
.L4:
	.long	.L7-.L4
	.long	.L2-.L4
	.long	.L7-.L4
	.long	.L6-.L4
	.long	.L5-.L4
	.long	.L3-.L4
	.text
.L7:
	leaq	0(,%rdi,8), %rax
	ret
.L6:
	movq	%rdi, %rax
	sarq	$3, %rax
	ret
.L5:
	movq	%rdi, %rax
	salq	$4, %rax
	subq	%rdi, %rax
	movq	%rax, %rdi
.L3:
	imulq	%rdi, %rdi
.L2:
	leaq	75(%rdi), %rax
	ret

嗯……欧亨利的条件数据传送。

题目 ***3.64

考虑下面的源代码,这里R、S和T都是用#define声明的常数:

long A[R][S][T];
long store_ele(long i, long j, long k, long *dest){
  *dest = A[i][j][k];
  return sizeof(A);
}

在编译这个程序中,GCC产生下面的汇编代码:

1store_ele:
2  leaq    (%rsi,%rsi,2), %rax ;rax = 3j
3  leaq    (%rsi,%rax,4), %rax ;rax = 13j
4  movq    %rdi, %rsi          ;rsi = i
5  salq    $6, %rsi            ;rsi = 2^6 i
6  addq    %rsi, %rdi          ;rdi = 65i
7  addq    %rax, %rdi          ;rdi = 65i + 13j
8  addq    %rdi, %rdx          ;rdx = 65i + 13j + k
9  movq    A(,%rdx,8), %rax    ;rax = A + 8*rdx
10 movq   %rax, (%rcx)         ;*dest = rax
11 movl   $3640, %eax
12 ret
  • A.将等式(3.1)从二维扩展到三维,根据数组元素A[i][j][k]的位置的公式
  • B.运用你的逆向工程技术,根据汇编代码,确定R、S和T的值

对于一个数组 T $D[R][C] $;中$D[i][j]$的内存地址,有等式3.1:

&$ D[i][j] = x_0 + L(C \cdot i + j)$

其中L是数据类型T以字节为单位的大小

看起来不难,

  • A:

&$ A[i][j][k] = x_0 + L(S \cdot T \cdot i + T \cdot j + k)$

  • B:
    三维数组A的大小明着告诉了,是3640字节,一个long算8字节的话(第9行可确定),RST之积就是455。接下来就又是数偏移了,解答过程写在注释里了

根据得出来的$rdx = 65i + 13j + k$就可以确定另一个参数是$\frac{455}{65} = 7$,所以 R = 7 S = 5 T = 13

很清晰,应该不必验证了

题目 *3.65

下面的代码转置一个 $M \times M$矩阵的元素,这里$M$是一个用#define定义的常数:

void transpose(long A[M][M]){
  long i, j;
  for (int i = 0; i < M; i++){
      long t = A[i][j];
      A[i][j] = A[j][i];
      A[j][i] = t;
  }
}

当用优化等级-O1编译时,GCC为这个函数的内循环产生下面的代码:

.L6
  movq (%rdx), %rcx
  movq (%rax), %rsi
  movq %rsi, (%rdx)
  movq %rcx, (%rax)
  addq $8, %rdx
  addq $120, %rax
  cmpq %rdi, %rax
  jne .L6

我们可以看到GCC把数组索引转换成了指针代码

  • A. 哪个寄存器保存着指向数组元素A[i][j]的指针?
  • B. 哪个寄存器保存着指向数组元素A[j][i]的指针?
  • C. $M$的值是多少

既然要表示索引,那每次循环都要增加,两个立即数,8和120简直明示答案了,$M$= 15,rdx跟着j走,rax跟着i,这样指向A[i][j]的指针就是rdx,A[j][i]的就是rax,秒了

检验一下$M$

#define M 15
void transpose(long A[M][M]){
    long i, j;
    for (int i = 0; i < M; i++){
        long t = A[i][j];
        A[i][j] = A[j][i];
        A[j][i] = t;
    }
}

int main(){}
.L2:
	movq	(%rax), %rdx
	movq	(%rdi), %rcx
	movq	%rcx, (%rax)
	movq	%rdx, (%rdi)
	addq	$120, %rax
	addq	$8, %rdi
	cmpq	%rsi, %rax
	jne	.L2

寄存器的使用完全不一样,但无所谓,120对了就说明$M$没错

题目 *3.66

考虑下面的源代码,这里$NR$和$NC$是用#define定义的宏表达式,计算用参数$n$表示的矩阵$A$的维度。这段代码计算矩阵的第j列的元素之和

long sum_col(long n, long A[NR(n)][NC(n)], long j){
  long i;
  long result = 0;
  for (int i = 0 ;i < NR(n) ; i++){
      result += A[i][j];
  return result;
  }
}

编译这个程序,GCC产生下面的汇编代码:

sum_col:
  leaq    1(,%rdi,4), %r8     ;r8 = 4n + 1
  leaq    (%rdi,%rdi,2), %rax;rax = 3n
  movq    %rax, %rdi          ;rdi = 3n
  testq   %rax, %rax          ;按位与rax,设置符号位
  jle .L4                     ;如果小于等于,也就是0标志位为1 或 符号位为1,也就是rax <= 0,就跳转到L4
  salq    $3, %r8             ;r8 *= 2^3 ; r8 = 8(4n + 1)
  leaq    (%rsi,%rdx,8), %rcx ;rcx = rsi + 8 * rdx 
  movl    $0, %eax;           ;eax = 0
  movl    $0, %edx;           ;edx = 0
.L3:
  addq    (%rcx), %rax        ;rax = (rcx)
  addq    $1, %rdx            ;rdx += 1
  addq    %r8, %rcx           ;rcx += r8
  cmpq    %rdi, %rdx          ;
  jne     .L3                 如果rdi和rdx不相等就回到L3
  req; ret
.L4:
  movl    $0,%eax             
  ret                         ;return 0

运用你的逆向工程技术,确定$NR$和$NC$的定义

看L4会发现直接return 0了,也就是说3n <= 0就返回0,那NR(n)就是3n了

循环主体是L3,循环条件时rdx和rdi不相等,rdx每次加一,是索引i,那rdi就是NR(n),往上找rdi的值,和刚才的猜测符合

接下来确定RC(x),其确定了索引前移的步长,又是找索引,r8 = 8(4n + 1),8是一个long的字节数,那RC(x)就是4n+1呗

#include <stdio.h>

#define NR(n) (3 * (n))
#define NC(n) (4 * (n) + 1)

long sum_col(long n, long A[NR(n)][NC(n)], long j) {
    long i;
    long result = 0;
    for (i = 0; i < NR(n); i++) {
        result += A[i][j];
    }
    return result;
}

int main() {} 

测试时出了意外,二维数组作为参数时,大小不支持由一个不确定的表达式来表示

gcc -S 3.66.c -o 3.66.s -O1 -std=c99

这样就可以了

sum_col:
.LFB13:
	.cfi_startproc
	endbr64
	leaq	1(,%rdi,4), %r8
	testq	%rdi, %rdi
	jle	.L4
	salq	$3, %r8
	leaq	(%rsi,%rdx,8), %rdx
	leaq	(%rdi,%rdi,2), %rsi
	movl	$0, %ecx
	movl	$0, %eax
.L3:
	addq	(%rdx), %rcx
	addq	$1, %rax
	addq	%r8, %rdx
	cmpq	%rsi, %rax
	jne	.L3
.L1:
	movq	%rcx, %rax
	ret
.L4:
	movl	$0, %ecx
	jmp	.L1

寄存器不一样,计算顺序也不太一样,但是逻辑等价,过

题目**3.67

这个作业要查看GCC为参数和返回值中有结构的函数产生的代码,由此可看到这些语言特性通常是如何实现的。
下面的C代码中有一个函数process,它用结构作为参数和返回值,还有一个函数eval,它调用process:

typedef struct{
  long a[2];
  long *p;
}strA;

typedef struct{
  long u[2];
  long q;
}strB;

strB process(strA s){
  strB r;
  r.u[0] = s.a[1];
  r.u[1] = s.a[0];
  r.q = *s.p;
  retrun r;
}

long eval(long x, long y, long z){
  strA s;
  s.a[0] = x;
  s.a[1] = y;
  s.p = &z;
  strB r= process(s);
  retrun r.u[0] + r.u[1] + r.q;
}

GCC为这两个函数产生下面的代码:

process: 
  movq    %rdi, %rax          ;rax = rdi
  movq    24(%rsp), %rdx      ;rdx = &z
  movq    (%rdx), %rdx        ;rdx = *(&z) = q
  movq    16(%rsp), %rcx      ;rcx = s.a[1]
  movq    %rcx, (%rdi)        ;*rdi = rcx = s.a[1] 
  movq    8(%rsp), %rcx       ;rcx = s.a[0]
  movq    %rcx, 8(%rdi)       ;*rdi + 8 = s.a[0]
  movq    %rdx, 16(%rdi)      ;*rdi + 16 = rdx = q
  ret

eval:
  subq    $104, %rsp          ;rsp回退13*8,开始存参数
  movq    %rdx, 24(%rsp)      ;s.p = z
  leaq    24(%rsp), %rax      ;rax = &z
  movq    %rdi, (%rsp)        ;s.a[0] = x
  movq    %rsi, 8(%rsp)       ;s.a[1] = y
  movq    %rax, 16(%rsp)      ;s.p = &z
  leaq    64(%rsp), %rdi      ;首地址存在rsp + 64
  call    process             ;调用函数,传入了rdi,也就是结构体首地址,这里rsp - 8
  movq    72(%rsp), %rax      ;局部变量
  addq    64(%rsp), %rax      
  addq    80(%rsp), %rax      ;r.u[0] + r.u[1] + r.q
  addq    $104, %rsp          ;恢复栈指针
  ret    
  • A.从eval函数的第2行我们可以看到,它在栈上分配了104个字节。画出eval的栈帧,给出它在调用process前储存在栈上的值
  • B.eval调用process时传递了什么值?
  • C.process的代码是如何访问结构参数s的元素的?
  • D.process的代码是如何设置结果结构r的字段的?
  • E.完成eval的栈帧图,给出在从process返回后eval是如何访问结构r的元素的
  • F.就如何传递作为函数参数的结构以及如何返回作为函数结果的结构值,你可以看出什么通用的原则?

好久没见到rsp了,前面的题基本都是几个寄存器就解决了。正常来说函数的参数按照一定约定存在寄存器里,而此题显然是把参数压入栈了,但是process函数偏偏还在用rdi

逐步分析流程(题目汇编代码的注释)

C.借助栈指针rsp与一定的偏移量
D.rdi是指针,存在其指向的地址里,然后返回
E.借助rdi,eval调用process时把rdi设置成rsp+64,返回后,借助rsp访问原本存在rdi里的值
F.函数作为参数:把结构体内容压入栈,再传入指针放到寄存器,借助指针访问栈;返回作为结果的结构值:借助指针,返回指向结构体的指针在rax里

栈帧略

题目***3.68

在下面的代码中,$A$和$B$都是用#define定义的常数:

typedef struct{
  int x[A][B];
  long y;
}str1;

typedef struct{
  char array[B];
  int t;
  short s[A];
  long u;
}str2;

void setVal(str1 *p, str2 *q){
  long v1 = q->t;
  long v2 = q->u;
  p->y = v1 + v2;
}

GCC为setVal产生下面的代码:

setVal:
  movslq  8(%rsi), %rax
  addq    32(%rsi), %rax
  movq    %rax, 184(%rdi)
  ret

很简短的一道题,复杂度也不高,但是确实三颗星

开猜,B = 8,A = 8 ————8和32都碰上了,但是184没有
184 = 4 * 46, 46 = 2 * 23,如果把A和B分别设置成2和23,184字节的偏移正确但是8和32两个偏移不对

尝试2,23时意外碰上了32这个偏移,仔细一看原来是对齐,提醒我们此题的数字是因为要求的四字节对齐产生的

考虑对齐的话:
$
43 \leq A \cdot B \leq 46 ,
$
$
5 \leq B \leq 8,
$
$
29 \leq B + 5 + 2A \leq 32
$

题目说只有唯一解,开猜:

#define A 9
#define B 5

typedef struct{
    int x[A][B];
    long y;
}str1;

typedef struct{
    char array[B];
    int t;
    short s[A];
    long u;
}str2;

void setVal(str1 *p, str2 *q){
    long v1 = q->t;
    long v2 = q->u;
    p->y = v1 + v2;
}

int main(){}
.LFB0:
	.cfi_startproc
	endbr64
	movslq	8(%rsi), %rax
	addq	32(%rsi), %rax
	movq	%rax, 184(%rdi)
	ret

一致,得解