第 3 章家庭作业:3.58~3.63
第 3 章家庭作业:3.58~3.63
家庭作业 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 代码。
家庭作业 3.59(★★)下面的代码计算两个 64 位有符号值 x 和 y 的 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=264 · xh+xl 和 y=264 · yh+yl,这里 xh、xl、yh 和 yl 都是 64 位值。类似地,128 位的乘积可以写成 p=264 · ph+pl,这里 ph 和 pl 是 64 位值。请解释这段代码是如何用 xh、xl、yh 和 yl 来计算 ph 和 pl 的。
家庭作业 3.60(★★)考虑下面的汇编代码:
long loop(long x, int n)
x in %rdi, n in %esi
loop:
movl %esi, %ecx
movl $1, %edx
movl $0, %eax
jmp .L2
.L3:
movq %rdi, %r8
andq %rdx, %r8
orq %r8, %rax
salq %cl, %rdx
.L2:
testq %rdx, %rdx
jne .L3
rep; 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 代码中所有缺失的部分。
家庭作业 3.61(★★)在 3.6.6 节,我们查看了下面的代码,作为使用条件数据传送的一种选择:
long cread(long *xp) {
return (xp ? *xp : 0);
}我们给出了使用条件传送指令的一个尝试实现,但是认为它是不合法的,因为它试图从一个空地址读数据。
写一个 C 函数 cread_alt,它与 cread 有一样的行为,除了它可以被编译成使用条件数据传送。当编译时,产生的代码应该使用条件传送指令而不是某种跳转指令。
家庭作业 3.62(★★)下面的代码给出了一个开关语句中根据枚举类型值进行分支选择的例子。回忆一下,C 语言中枚举类型只是一种引入一组与整数值相对应的名字的方法。默认情况下,值是从 0 向上依次赋给名字的。在我们的代码中,省略了与各种情况标号相对应的动作。
/* Enumerated type creates set of constants numbered 0 and upward */
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:
case MODE_B:
case MODE_C:
case MODE_D:
case MODE_E:
default:
}
return result;
}产生的实现各个动作的汇编代码部分如图 3-52 所示。注释指明了参数位置、寄存器值,以及各个跳转目的的情况标号。
p1 in %rdi, p2 in %rsi, action in %edx
.L8: MODE_E
movl $27, %eax
ret
.L3: MODE_A
movq (%rsi), %rax
movq (%rdi), %rdx
movq %rdx, (%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
图 3-52 家庭作业 3.62 的汇编代码。这段代码实现了 switch 语句的各个分支
填写 C 代码中缺失的部分。代码包括落入其他情况的情况,试着重建这个情况。
家庭作业 3.63(★★)这个程序给你一个机会,从反汇编机器代码逆向工程一个 switch 语句。在下面这个过程中,去掉了 switch 语句的主体:
long switch_prob(long x, long n) {
long result = x;
switch(n) {
/* Fill in code here */
}
return result;
}图 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 代码填写开关语句的主体,使它的行为与机器代码一致。
long switch_prob(long x, long n)
x in %rdi, n in %rsi
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 00 lea 0x0(,%rdi,8),%rax
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
图 3-53 家庭作业 3.63 的反汇编代码