# 第 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 <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 的完整声明。假设这个结构中只有字段 idx 和 x,并且这两个字段保存的都是有符号值。