# 第 3 章家庭作业:3.64~3.69

家庭作业 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 产生下面的汇编代码:

long store_ele(long i, long j, long k, long *dest)
i in %rdi, j in %rsi, k in %rdx, dest in %rcx
store_ele:
    leaq    (%rsi,%rsi,2), %rax
    leaq    (%rsi,%rax,4), %rax
    movq    %rdi, %rsi
    salq    $6, %rsi
    addq    %rsi, %rdi
    addq    %rax, %rdi
    addq    %rdi, %rdx
    movq    A(,%rdx,8), %rax
    movq    %rax, (%rcx)
    movl    $3640, %eax
    ret

A. 将等式(3.1)从二维扩展到三维,提供数组元素 A[i][j][k] 的位置的公式。

B. 运用你的逆向工程技术,根据汇编代码,确定 R、S 和 T 的值。

家庭作业 3.65(★)下面的代码转置一个 M×M 矩阵的元素,这里 M 是一个用 #define 定义的常数:

void transpose(long A[M][M]) {
    long i, j;
    for (i = 0; i < M; i++)
        for (j = 0; j < i; j++) {
            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 的值是多少?

家庭作业 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 (i = 0; i < NR(n); i++)
        result += A[i][j];
    return result;
}

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

long sum_col(long n, long A[NR(n)][NC(n)], long j)
n in %rdi, A in %rsi, j in %rdx
sum_col:
    leaq    1(,%rdi,4), %r8
    leaq    (%rdi,%rdi,2), %rax
    movq    %rax, %rdi
    testq   %rax, %rax
    jle     .L4
    salq    $3, %r8
    leaq    (%rsi,%rdx,8), %rcx
    movl    $0, %eax
    movl    $0, %edx
.L3:
    addq    (%rcx), %rax
    addq    $1, %rdx
    addq    %r8, %rcx
    cmpq    %rdi, %rdx
    jne     .L3
    rep; ret
.L4:
    movl    $0, %eax
    ret

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

家庭作业 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;
    return 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);
    return r.u[0] + r.u[1] + r.q;
}

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

strB process(strA s)
process:
    movq    %rdi, %rax
    movq    24(%rsp), %rdx
    movq    (%rdx), %rdx
    movq    16(%rsp), %rcx
    movq    %rcx, (%rdi)
    movq    8(%rsp), %rcx
    movq    %rcx, 8(%rdi)
    movq    %rdx, 16(%rdi)
    ret

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
    call    process
    movq    72(%rsp), %rax
    addq    64(%rsp), %rax
    addq    80(%rsp), %rax
    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. 就如何传递作为函数参数的结构以及如何返回作为函数结果的结构值,你可以看出什么通用的原则?

家庭作业 3.68(★★★)在下面的代码中,A 和 B 是用 #define 定义的常数:

typedef struct {
    int x[A][B]; /* Unknown constants A and 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 产生下面的代码:

void setVal(str1 *p, str2 *q)
p in %rdi, q in %rsi
setVal:
    movslq  8(%rsi), %rax
    addq    32(%rsi), %rax
    movq    %rax, 184(%rdi)
    ret

A 和 B 的值是多少?(答案是唯一的。)

家庭作业 3.69(★★★)你负责维护一个大型的 C 程序,遇到下面的代码:

typedef struct {
    int first;
    a_struct a[CNT];
    int last;
} b_struct;

void test(long i, b_struct *bp)
{
    int n = bp->first + bp->last;
    a_struct *ap = &bp->a[i];
    ap->x[ap->idx] = n;
}

编译时常数 CNT 和结构 a_struct 的声明是在一个你没有访问权限的文件中。幸好,你有代码的 .o 版本,可以用 OBJDUMP 程序来反汇编这些文件,得到下面的反汇编代码:

void test(long i, b_struct *bp)
i in %rdi, bp in %rsi
0000000000000000 &lt;test>:
   0: 8b 8e 20 01 00 00     mov    0x120(%rsi),%ecx
   6: 03 0e                 add    (%rsi),%ecx
   8: 48 8d 04 bf           lea    (%rdi,%rdi,4),%rax
   c: 48 8d 04 c6           lea    (%rsi,%rax,8),%rax
  10: 48 8b 50 08           mov    0x8(%rax),%rdx
  14: 48 63 c9              movslq %ecx,%rcx
  17: 48 89 4c d0 10        mov    %rcx,0x10(%rax,%rdx,8)
  1c: c3                    retq

运用你的逆向工程技术,推断出下列内容:

A. CNT 的值。

B. 结构 a_struct 的完整声明。假设这个结构中只有字段 idxx,并且这两个字段保存的都是有符号值。