CSAPP3.70-3.75

题目***3.70

考虑下面的联合声明:

union ele{
  struct{
      long *p;
      long y;
  }e1;
  struct{
      long x;
      union ele *next;
  }e2;
};

这个声明说明联合中可以嵌套结构
下面的函数(省略了一些表达式)对一个链表进行操作,链表是以上述联合作为元素的

void proc(union ele *up){
  up->___=*(___)-___;
}

A.下列字段的偏移量是多少(以字节为单位)

  • e1.p ___
  • e1.y ___
  • e2.x ___
  • e2.next ___

B.这个结构总共需要多少个字节
C.编译器为proc产生下面的汇编代码:

proc:
  movq    8(%rdi), %rax   ;rax = next
  movq    (%rax), %rdx    ;rdx = next->p
  movq    (%rdx), %rdx    ;rdx = *p
  subq    8(%rax), %rdx   ;rdx - next->y
  movq    %rdx, (%rdi)    ;*rdi = rdx
  ret  

在这些信息的基础上,填写proc代码中缺失的表达式。**提示:**有些联合引用的解释可以有歧义。当你清楚引用指引到哪里的时候,就能够澄清这些歧义。只有一个答案,不需要进行强制类型转换,且不违法任何类型限制

A.p和x都是第一个元素,偏移量为0,y和next偏移量都是8
B.16字节
C.rax有一个解引用操作,所以是next,next继续解引用,可能是p或x,最后的结果在(rdi)
第一次尝试:

up->e2.x=*(up->e2.next->e1.p) - up->e1.y;

movq	8(%rdi), %rdx
movq	(%rdx), %rax
movq	(%rax), %rax
subq	%rdx, %rax
movq	%rax, (%rdi)
ret

subq行少了一个8字节偏移

up->e2.x=*(up->e2.next->e1.p) - up->e2.next->e1.y;

movq	8(%rdi), %rdx
movq	(%rdx), %rax
movq	(%rax), %rax
subq	8(%rdx), %rax
movq	%rax, (%rdi)
ret 

只有rdx和rax的使用是反的,完成

题目*3.71

写一个函数good_echo,它从标准输入读取一行,再把它写到标准输出。你的实现应该对任意长度的输入行都能工作。
可以使用库函数fgets,但是你必须确保即使当输入行要求比你已经为缓存区分配的更多空间时,你的函数也能正确地工作。
你的代码还应该检查错误条件,要在遇到错误时返回。参考标准I/O函数的定义文档[45,61]。

去看一下fgets的声明:

char *fgets(char *s, int size, FILE *stream);
s:存储读入数据的缓冲区地址。
size:要读取的字符数。
stream:要读取的流。

那么题目要求就是从stdin读一个可以非常大的内容,类比ostep里做过的动态分配堆来实现,再加上C的错误检查:

#include <stdio.h>
#include <stdlib.h>

typedef struct{
  char* p;
  size_t size;
}buff;

int good_echo(buff *buff, size_t size){
  if (buff == NULL) {
    return -1;
  }
  
  if(buff->size < size){
    char* new_p = (char*) realloc(buff->p, size);// 超出缓冲区,重新分配内存
    if (new_p == NULL) {
      return -1;
    }
    buff->p = new_p;
    buff->size = size;
  }
  
  if (fgets(buff->p, size, stdin) == NULL) {
    return -1;
  }

  if (printf("%s", buff->p) < 0) {
    return -1;
  }
  
  return 0; 
}

int main(){}

题目**3.72

图3-54a给出了一个函数的代码,该函数类似于函数vfunct(图3-34a)。我们用vfunct来说明过帧指针在管理变长栈帧中的使用情况。
这里的新函数aframe调用库函数alloca为局部数组p分配空间。alloca类似于更常用的malloc,区别在于它在运行时栈上分配空间。
当正在执行的过程返回时,该空间会自动释放

图3-54b给出了部分的汇编代码,建立栈指针,为局部变量i和p分配空间。非常类似于vframe对应的代码。在此使用与练习题3.49中同样的表示法:栈指针在第四行设置为值$s_1$,在第七行设置为值$s_2$。
数组$p$的起始地址在第9行被设置为值$p$。$s_2$和$p$之间可能有额外的空间$e_2$,数组$p$结尾和$s_1$之间可能有额外的空间$e_1$

  • A.用数学语言解释计算$s_2$的逻辑。
  • B.用数学语言解释计算$p$的逻辑。
  • C.确定使$e_1$的值最小和最大的$n$和$s_1$的值。
  • D.这段代码为$s_2$和$p$的值保证了怎样的对齐属性?
#include<alloca.h>

long aframe(long n, long idx, long *q){
  long i;
  long **p = alloca(n * sizeof(long *));
  p[0] = &i;
  for (i = 1 ; i < n; i++)
      p[i] = q;
  return *p[idx];
}
aframe:
  pushq   %rbp
  movq    %rsp, %rbp
  subq    $16, %rsp           ;Allocate space for i (%rsp = s_1)
  leaq    30(,%rdi,8), %rax
  andq    $-16, %rax
  subq    %rax, %rsp          ;ALlocate space for array p (%rsp = s_2)
  leaq    15(%rsp), %r8
  andq    $-16, %r8           ;Set %r8 to &p[0] 
  ...

rbp入栈;保存rsp到rbp;rbp减16,留给两个局部变量i和p的;rax = 30 + 8n;rax与-16做与运算,也就是把后四位清0,这是为了对齐吗?;rsp再减rax,为数组留位置;
把此时的rsp后15字节的值移到r8,再把r8后四位清零,r8保存着数组首地址

  • A. $s_2 = 起始rsp - 16 - (30 + 8n)$(十六字节对齐),更具体地说,n为奇数,30就留24,n为偶数,30就只留16
  • B. $ p = 15 + s_2 $(十六字节对齐)
  • C.$e1$在p末尾和s1之前,s1位置固定,p的结尾是15+s2+8n,其中s2已经对齐了,为s1 - (8n + 16/24),整理得:

$p_e = (15 + s_1 - 16)$ mod 16 (n even)
$p_e = (15 + s_1 - 24)$ mod 16 (n odd)

越分析越晕,这题真两星难度吗?选择暴力破解:

//模拟函数
void stimulate(long rbp, long n){
  long rsp = rbp;
  rsp -= 16;
  long s1 = rsp;
  
  // 汇编代码:leaq 30(,%rdi,8), %rax
  long rax = 30 + 8 * n;
  
  // 汇编代码:andq $-16, %rax 
  rax &= (-16);
  
  // 汇编代码:subq %rax, %rsp
  rsp -= rax;
  long s2 = rsp;
  
  // 汇编代码:leaq 15(%rsp), %r8
  long r8 = 15 + rsp;
  
  // 汇编代码:andq $-16, %r8 
  long p = r8 & (-16);
  
  // 计算e1 = s1 - (p + n*8)
  long e1 = s1 - (p + n * 8);
  
  printf("rbp%%16=%2ld, n=%2ld, s1=%4ld, s2=%4ld, p=%4ld, e1=%3ld\n", 
         rbp % 16, n, s1, s2, p, e1);
}
//放到main函数里
  long min_e1 = 1000, max_e1 = -1000;
  long min_rbp = 0, min_n = 0, max_rbp = 0, max_n = 0;
  
  // 测试不同的rbp对齐情况 (rbp % 16 = 0到15)
  for(long rbp_mod = 0; rbp_mod < 16; rbp_mod++){
    long rbp = 1000 + rbp_mod;  // 基准值加上余数
    
    // 测试不同的n值 (1到20)
    for(long n = 1; n <= 20; n++){
      long rsp = rbp;
      rsp -= 16;
      long s1 = rsp;
      
      long rax = 30 + 8 * n;
      rax &= (-16);
      rsp -= rax;
      long s2 = rsp;
      
      long r8 = 15 + rsp;
      long p = r8 & (-16);
      
      long e1 = s1 - (p + n * 8);
      
      printf("rbp%%16=%2ld, n=%2ld, s1=%4ld, s2=%4ld, p=%4ld, e1=%3ld\n", 
             rbp_mod, n, s1, s2, p, e1);
      
      // 更新最小值和最大值
      if (e1 < min_e1) {
        min_e1 = e1;
        min_rbp = rbp_mod;
        min_n = n;
      }
      if (e1 > max_e1) {
        max_e1 = e1;
        max_rbp = rbp_mod;
        max_n = n;
      }
    }
    printf("\n");
  }

得到的结果是:

最小值情况 (rbp%16=9, n=2):
  30 + 8*n = 46
  rax = (30 + 8*n) & (-16) = 32
  s2 = s1 - rax = 993 - 32 = 961
  p = (s2 + 15) & (-16) = 976
  e1 = s1 - p - 8*n = 993 - 976 - 16 = 1

最大值情况 (rbp%16=8, n=1):
  30 + 8*n = 38
  rax = (30 + 8*n) & (-16) = 32
  s2 = s1 - rax = 992 - 32 = 960
  p = (s2 + 15) & (-16) = 960
  e1 = s1 - p - 8*n = 992 - 960 - 8 = 24

具体为啥留给读者了……

  • D. 16字节对齐

题目*3.73

用汇编代码写出匹配图3-51中函数find_range行为的函数。你的代码必须只包含一个浮点比较指令,并用条件分支指令来生成正确的结果。在$2^{32}$种可能的参数值上测试你的代码。网格旁注ASM:EASM描述了如何在C程序中嵌入汇编代码

typedef enum {NEG, ZERO, POS, OTHER} range_t;

range_t find_range(float x){
  int result;
  if(x < 0) 
      result = NEG;
  else if(x == 0)  
      result = ZERO;
  else if(x > 0)
      result = POS;
  else 
      result = OTHER;
  return result; 
}
find_range:
 vxorps  %xmm1, %xmm1, %xmm1          ;Set %xmm1 = 0
 vucomiss       %xmm0, %xmm1          ;Compare 0:x
 ja   .L5                             ;if > goto neg
 vucomiss       %xmm1, %xmm0          ;Compare x:0
 jp   .L8                             ;if NaN , goto posornan 
 movl         $1, %eax                ;result = ZERO
 je   .L3                             ;If =,goto done
.L8:                                  ;posornan:
  vucomiss        .LCO(%rip), %xmm0   ;compare x:0        
  setbe   %al                         ;Set reuslt = NaN ? 1 : 0
  movzbl  %al, %eax                   ;Zero-extend
  addl    $2, %eax                    ;result += 2 (POS for > , OTHER for NaN)
  ret
.L5:
  movl    $0, %eax                    ;result = NEG
.L3:
  rep;ret                             ;return

题目要求只比较一次,而上文中的代码比较了两次。思路是根据标志位区分小于等于大于0和NaN,比一次足够

find_range:
    vxorps  %xmm1, %xmm1, %xmm1     ; %xmm1 = 0.0不变
    vucomiss %xmm1, %xmm0           ; 比较 x 和 0.0
    
    movl    $0, %eax                 
    ja      .L_done                  ; x > 0.0 时返回0
    movl    $1, %eax                 ; 
    je      .L_done                  ; x == 0.0 时返回1
    movl    $3, %eax                 ;
    jp      .L_done                  ; 如果是 NaN 返回3
    movl    $2, %eax                 ; 否则返回2
    
.L_done:
    ret

题目**3.74

用汇编代码写出匹配图3-51中函数find_range行为的函数。你的代码必须只包含一个浮点比较指令,并用条件传送指令来生成正确的结果。你可能会想要用指令cmovq(如果设置了偶校验位传送)。在$2^{32}$种可能的参数值上测试你的代码。网格旁注ASM:EASM描述了如何在C程序中嵌入汇编代码

思路是类似的,使用条件传送指令重构:

find_range:
    vxorps  %xmm1, %xmm1, %xmm1     ; %xmm1 = 0.0
    vucomiss %xmm1, %xmm0           ; 比较 x 和 0.0,设置标志位
    
    movl    $0, %eax                 ; NEG
    movl    $1, %edx                 ; ZERO
    movl    $2, %ecx                 ; POS  
    movl    $3, %r8d                 ; OTHER (NaN)
    
    cmove   %edx, %eax               ; 如果 ZF=1 (x==0), 传送 ZERO
    cmova   %ecx, %eax               ; 如果 CF=0&&ZF=0 (x>0), 传送 POS
    cmovp   %r8d, %eax               ; 如果 PF=1 (NaN), 传送 OTHER
    
    ret

下一步是穷举所有可能测试验证,顺便尝试一下在C里内联汇编代码

#include <cstdint>
#include <stdio.h>
#include <math.h>
#include <float.h>

typedef enum {NEG, ZERO, POS, OTHER} range_t;

range_t find_range(float x) {
    range_t result;
    asm volatile (
        "vxorps  %%xmm1, %%xmm1, %%xmm1\n\t"
        "vucomiss %%xmm1, %%xmm0\n\t"
        "movl    $0, %%eax\n\t"          // NEG
        "movl    $1, %%edx\n\t"          // ZERO
        "movl    $2, %%ecx\n\t"          // POS
        "movl    $3, %%ebx\n\t"          // OTHER
        "cmove   %%edx, %%eax\n\t"       // x == 0
        "cmova   %%ecx, %%eax\n\t"       // x > 0
        "cmovp   %%ebx, %%eax\n\t"       // NaN
        : "=a" (result)
        : "x" (x)
        : "edx", "ecx", "ebx", "xmm1"
    );
    return result;
}

range_t find_range_stdlib(float x) {
    // 首先检查是否为NaN
    if (isnan(x)) {
        return OTHER;
    }

    // 检查是否为零(包括+0.0和-0.0)
    if (x == 0.0f) {
        return ZERO;
    }

    // 检查正负
    if (x > 0.0f) {
        return POS;
    } else {
        return NEG;
    }
}

int main() {
    for (uint64_t i = 0; i < (1ULL << 32); i++) {
        uint32_t bits = (uint32_t)i;
        float x = *(float*)&bits;

        range_t expected = find_range(x);
        range_t actual = find_range_stdlib(x);
        if (expected != actual) {
          printf("出现错误");
          return 0;
        }
    }
    printf("和参考函数完全一致");
    return 0;
}

过了大概十几秒后输出:

auceptin_fang@Auceptin:~/CSAPP/chapter3$ ./3.74
和参考函数完全一致

没问题

题目*3.75

ISO C99包括了支持复数的拓展。任何浮点数类型都可以用关键字complex修饰。这里有一些使用复数数据的示例函数,调用了一些关联的库函数

#include<complex.h>

double c_imag(double complex x){
  return cimag(x);
}

double c_real(double complex x){
  return creal(x);
}

double complex c_sub(double complex x, double complex y){
  return x - y;
}

编译时,GCC为这些函数产生如下的代码:

c_imag:
  movapd  %xmm1, %xmm0
  ret
c_real:
  rep; ret
c_sub:
  subsd   %xmm2, %xmm0
  subsd   %xmm3, %xmm1
  ret 

根据这些例子,回答下列问题:
A.如何向函数传递复数参数?

B.如何从函数返回复数值

复数分为实部(real)和虚部(imag),分别加减。c_imag将xmm1移动到了xmm0,说明参数在xmm1,返回值在xmm0,而c_real直接返回,说明参数就在xmm0

再看c_sub,运算内容就是实部和虚部分别相减,于是推断偶数号浮点数寄存器存参数的实部,奇数存参数的虚部,而返回值是xmm0,xmm1

总结一下就是,传递参数,复数的实部通过偶数号寄存器传递、虚部通过奇数号寄存器传递;返回值,实部在xmm0,虚部在xmm1