《深入理解计算机系统》Bomb Lab实验解析

Bomb Lab

简介

这是CMU15213课程的第二个实验,也是十分经典的一个实验,世界上用CSAPP当教科书的高校一般都会保留这个实验,实验要求是给一个用C语言编写的可执行文件bomb,你可以看到它主函数的C语言代码,除此之外,一概不知,实验分为六个阶段,每个阶段需要输入一串字符,有点像破译密码,如果六次输入的密码都是对的,炸弹解除,否则炸弹会爆炸(退出,并打印爆炸信息),如果是CMU的学生,每个人获得的炸弹是不一样的(高超的 反作弊技巧),每次爆炸会扣0.5分,扣满20分为止。不过我们作为自学党,没有这个限制,用的是自学用bomb,随便炸,根本不虚,不过为了达成良好的学习效果,还是尽可能减少爆炸次数。

C语言bomb源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <stdio.h>
#include <stdlib.h>
#include "support.h"
#include "phases.h"

/*
* Note to self: Remember to erase this file so my victims will have no
* idea what is going on, and so they will all blow up in a
* spectaculary fiendish explosion. -- Dr. Evil
*/

FILE *infile;

int main(int argc, char *argv[])
{
char *input;

/* Note to self: remember to port this bomb to Windows and put a
* fantastic GUI on it. */

/* When run with no arguments, the bomb reads its input lines
* from standard input. */
if (argc == 1) {
infile = stdin;
}

/* When run with one argument <file>, the bomb reads from <file>
* until EOF, and then switches to standard input. Thus, as you
* defuse each phase, you can add its defusing string to <file> and
* avoid having to retype it. */
else if (argc == 2) {
if (!(infile = fopen(argv[1], "r"))) {
printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
exit(8);
}
}

/* You can't call the bomb with more than 1 command line argument. */
else {
printf("Usage: %s [<input_file>]\n", argv[0]);
exit(8);
}

/* Do all sorts of secret stuff that makes the bomb harder to defuse. */
initialize_bomb();

printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");

/* Hmm... Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
* Let me know how they did it. */
printf("Phase 1 defused. How about the next one?\n");

/* The second phase is harder. No one will ever figure out
* how to defuse this... */
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");

/* I guess this is too easy so far. Some more complex code will
* confuse people. */
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");

/* Oh yeah? Well, how good is your math? Try on this saucy problem! */
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");

/* Round and 'round in memory we go, where we stop, the bomb blows! */
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");

/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard. */
input = read_line();
phase_6(input);
phase_defused();

/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */

return 0;
}

解决方案

根据C语言的bomb源码,我们发现main函数提供了两种读取数据的形式,从文件读取所有数据,或者一行一行从标准输入,也就是键盘,读入数据,分为六个阶段,对应phase_1到phase_6这6个函数,不过很遗憾,我们所能看到的源码只有这些了,这些phase函数具体用来干嘛的,怎么写的,我们一概不知。

根据题目的要求以及提示,我们可以将bomb可执行文件反汇编,对汇编语言代码进行逆向分析,而gdb是我们调试汇编语言的有效工具。

gdb命令

就个人做这个lab的经验而言,这个lab本身就是一个利用gdb反汇编的一个实践操作,以下gdb的指令经常用到,值得牢记,记不住也没关系,就个人经验而言,每次忘了,都去查资料,查多了,也就记住了

命令 功能
gdb filename 开始调试
run 开始运行
run 1 2 3 开始运行,并且传入参数1,2,3
kill 停止运行
quit 退出gdb
break sum 在sum函数的开头设置断点
break *0x8048c3 在0x8048c3的地址处设置断点
delete 1 删除断点1
clear sum 删除在sum函数入口的断点
stepi 运行一条指令
stepi 4 运行4条指令
continue 运行到下一个断点
disas sum 反汇编sum函数
disas 0X12345 反汇编入口在0x12345的函数
print /x /d /t $rax 将rax里的内容以16进制,10进制,2进制的形式输出
print 0x888 输出0x888的十进制形式
print *(int*)0x123456 将0x123456地址所存储的内容以数字形式输出
print (char*)0x123456 输出存储在0x123456的字符串
x/w $rsp 解析在rsp所指向位置的word
x/2w $rsp 解析在rsp所指向位置的两个word
x/2wd $rsp 解析在rsp所指向位置的word,以十进制形式输出
info registers 寄存器信息
info functions 函数信息
info stack 栈信息

以上是我个人在整个实验过程中,用的比较频繁的命令,如果还有别的需要可以自行搜索文档或者根据以上命令进行推断,比如x/3w $rsp 就是连续输出三个word。

x86-64寄存器

牢记下图,事半功倍:

实验解析

phase_1

反汇编phase_1

1
2
3
4
5
6
7
8
0x0000000000400ee0 <+0>:	sub    $0x8,%rsp
0x0000000000400ee4 <+4>: mov $0x402400,%esi
0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal>
0x0000000000400eee <+14>: test %eax,%eax
0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23>
0x0000000000400ef2 <+18>: callq 0x40143a <explode_bomb>
0x0000000000400ef7 <+23>: add $0x8,%rsp
0x0000000000400efb <+27>: retq

phase_1十分简单粗暴,调用了strings_not_equal函数直接判断两个字符串是否相等,第二个字符串起始地址直接存储在%esi里,作为第二个参数传入,观察第二行我们可以发现用来比较的字符串存在0x402400里,直接用(char*)0x402400解析,得到字符串“Border relations with Canada have never been better.”即为第一步正确答案。

phase_2

来到第二阶段,首先依然是反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
0x0000000000400efc <+0>:	push   %rbp
0x0000000000400efd <+1>: push %rbx
0x0000000000400efe <+2>: sub $0x28,%rsp
0x0000000000400f02 <+6>: mov %rsp,%rsi
0x0000000000400f05 <+9>: callq 0x40145c <read_six_numbers> ;读取6个数字,存入栈
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp)
0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52> ;第一个数字必须是1
0x0000000000400f10 <+20>: callq 0x40143a <explode_bomb>
0x0000000000400f15 <+25>: jmp 0x400f30 <phase_2+52>
0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax;上一个数字的地址
0x0000000000400f1a <+30>: add %eax,%eax;上一个数字*2
0x0000000000400f1c <+32>: cmp %eax,(%rbx); an = 2 * an-1
0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41>
0x0000000000400f20 <+36>: callq 0x40143a <explode_bomb>
0x0000000000400f25 <+41>: add $0x4,%rbx
0x0000000000400f29 <+45>: cmp %rbp,%rbx
0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27>
0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64>
0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx;读取下一个数字的地址
0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp
;第六个数字的位置
0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27>
0x0000000000400f3c <+64>: add $0x28,%rsp
0x0000000000400f40 <+68>: pop %rbx
0x0000000000400f41 <+69>: pop %rbp
0x0000000000400f42 <+70>: retq

根据第五行,调用了read_six_numbers这个函数,显然这次要求的输入是6个数字,根据第八行和第九行,第一个数字必须是1,否则会爆炸,我们观察到如果第一个数字是1,跳转到+52,也就是lea 0x4(%rsp),%rbx,%rsp是栈指针的地址,每个int的数据长度为4个bytes,这句话的意思就是说读取下一个数字的地址,存入%rbx里。

(当然,read_six_numbers如果不放心,可以将其反汇编):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x000000000040145c <+0>:	sub    $0x18,%rsp
0x0000000000401460 <+4>: mov %rsi,%rdx
0x0000000000401463 <+7>: lea 0x4(%rsi),%rcx
0x0000000000401467 <+11>: lea 0x14(%rsi),%rax
0x000000000040146b <+15>: mov %rax,0x8(%rsp)
0x0000000000401470 <+20>: lea 0x10(%rsi),%rax
0x0000000000401474 <+24>: mov %rax,(%rsp)
0x0000000000401478 <+28>: lea 0xc(%rsi),%r9
0x000000000040147c <+32>: lea 0x8(%rsi),%r8
0x0000000000401480 <+36>: mov $0x4025c3,%esi; "%d %d %d %d %d %d"
0x0000000000401485 <+41>: mov $0x0,%eax
0x000000000040148a <+46>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x000000000040148f <+51>: cmp $0x5,%eax
0x0000000000401492 <+54>: jg 0x401499 <read_six_numbers+61>
0x0000000000401494 <+56>: callq 0x40143a <explode_bomb>
0x0000000000401499 <+61>: add $0x18,%rsp
0x000000000040149d <+65>: retq

下一行有个奇怪的数值0x18,十进制为24,24/4=6正好是6个数字,这一行的目的就是设置一个结束点,放在%rbp中,然后回到+27.

仔细分析27行,不难发现,这段程序是在循环判断一个数组是否为公比为2的等比数列,如果不是则引爆炸弹,由于第一个数字是1,我们不难得出答案:

1 2 4 8 16 32

phase_3

第三阶段,老规矩先看看phase_3是个啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
0x0000000000400f43 <+0>:	sub    $0x18,%rsp
0x0000000000400f47 <+4>: lea 0xc(%rsp),%rcx
0x0000000000400f4c <+9>: lea 0x8(%rsp),%rdx
0x0000000000400f51 <+14>: mov $0x4025cf,%esi;格式字符串
0x0000000000400f56 <+19>: mov $0x0,%eax
0x0000000000400f5b <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>;其实就是sscanf,第二个参数为格式字符串
0x0000000000400f60 <+29>: cmp $0x1,%eax
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>; sscanf的返回值应该大于1,否则会触发炸弹
0x0000000000400f65 <+34>: callq 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp)
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106>;第一个数如果大于7则触发炸弹
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax
0x0000000000400f75 <+50>: jmpq *0x402470(,%rax,8);分支选择
0x0000000000400f7c <+57>: mov $0xcf,%eax
0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123>
0x0000000000400f83 <+64>: mov $0x2c3,%eax
0x0000000000400f88 <+69>: jmp 0x400fbe <phase_3+123>
0x0000000000400f8a <+71>: mov $0x100,%eax
0x0000000000400f8f <+76>: jmp 0x400fbe <phase_3+123>
0x0000000000400f91 <+78>: mov $0x185,%eax
0x0000000000400f96 <+83>: jmp 0x400fbe <phase_3+123>
0x0000000000400f98 <+85>: mov $0xce,%eax
0x0000000000400f9d <+90>: jmp 0x400fbe <phase_3+123>
0x0000000000400f9f <+92>: mov $0x2aa,%eax
0x0000000000400fa4 <+97>: jmp 0x400fbe <phase_3+123>
0x0000000000400fa6 <+99>: mov $0x147,%eax
0x0000000000400fab <+104>: jmp 0x400fbe <phase_3+123>
0x0000000000400fad <+106>: callq 0x40143a <explode_bomb>
0x0000000000400fb2 <+111>: mov $0x0,%eax
0x0000000000400fb7 <+116>: jmp 0x400fbe <phase_3+123>
0x0000000000400fb9 <+118>: mov $0x137,%eax
0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax;获取第二个读入的数字
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>; 第二个数必须等于分支中所获得的立即数
0x0000000000400fc4 <+129>: callq 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add $0x18,%rsp
0x0000000000400fcd <+138>: retq

这段代码一上来调用了sscanf函数,通过查文档发现,这个函数是用来解析字符串里的数字,按照规定格式存到另一个字符串里,并返回所解析的数字的个数,第二个参数就是解析格式,print (char*)0x4025cf,发现格式字符串为“%d %d”,也就是两个int数字,根据+32我们也可以确认如果数字个数(返回值)<=1 也会引爆炸弹,直接gg,接下来+39读取第一个数字,联系下面几行不难得出,第一个数字必须<=7,否则也会引爆炸弹,再往下+50,发现跳转语句,根据第一个数字跳转到不同的行数,获取一个值放入%eax里,要求第二个数必须等于放入的值

这题多解,一个比较好的方法,是带入一个数字,然后逐步运行,看看跳转到哪个条件分支,改条件分支给了%eax什么样的数字,然后让第二个数字等于它即可,当然也可以直接算,以下皆为正确答案:

1
2
3
4
5
6
7
8
0 207
1 311
2 707
3 256
4 389
5 206
6 682
7 327

phase_4

反汇编phase_4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x40100c <phase_4>      sub    $0x18,%rsp
0x401010 <phase_4+4> lea 0xc(%rsp),%rcx # 存第二个数字
0x401015 <phase_4+9> lea 0x8(%rsp),%rdx # 存第一个数字
0x40101a <phase_4+14> mov $0x4025cf,%esi # "%d %d"
0x40101f <phase_4+19> mov $0x0,%eax
0x401024 <phase_4+24> callq 0x400bf0 <__isoc99_sscanf@plt>
0x401029 <phase_4+29> cmp $0x2,%eax
0x40102c <phase_4+32> jne 0x401035 <phase_4+41> # 必须是两个数字
0x40102e <phase_4+34> cmpl $0xe,0x8(%rsp)
0x401033 <phase_4+39> jbe 0x40103a <phase_4+46> # 第一个数字必须 <= 14, >= 0(无符号)
0x401035 <phase_4+41> callq 0x40143a <explode_bomb>
0x40103a <phase_4+46> mov $0xe,%edx # func4 第三个参数为 14
0x40103f <phase_4+51> mov $0x0,%esi # 第二个参数为 0
0x401044 <phase_4+56> mov 0x8(%rsp),%edi # 第一个参数为我们输入的第一个数字
0x401048 <phase_4+60> callq 0x400fce <func4>
0x40104d <phase_4+65> test %eax,%eax
0x40104f <phase_4+67> jne 0x401058 <phase_4+76> # 测试 func4 的返回值,如果不为零,则引爆炸弹
0x401051 <phase_4+69> cmpl $0x0,0xc(%rsp)
0x401056 <phase_4+74> je 0x40105d <phase_4+81> # 测试我们输入的第二个数字,如果为 0,则过关
0x401058 <phase_4+76> callq 0x40143a <explode_bomb>
0x40105d <phase_4+81> add $0x18,%rsp
0x401061 <phase_4+85> retq

这道题,也是要求读取两个数字,第一个数字要让func4返回0,第二个数字是0,反汇编func4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;这个函数有三个参数,不妨设置为val,i,j
0x0000000000400fce <+0>: sub $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax
0x0000000000400fd4 <+6>: sub %esi,%eax; eax = j - i
0x0000000000400fd6 <+8>: mov %eax,%ecx
0x0000000000400fd8 <+10>: shr $0x1f,%ecx
0x0000000000400fdb <+13>: add %ecx,%eax ;判断eax的符号
0x0000000000400fdd <+15>: sar %eax ;eax /= 2
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx ;val = eax + i = (j + i) / 2
0x0000000000400fe2 <+20>: cmp %edi,%ecx ; 比较输入和 val
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> ;如果输入小于或等于 val,则跳转
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx
0x0000000000400fe9 <+27>: callq 0x400fce <func4>
0x0000000000400fee <+32>: add %eax,%eax
0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57>
0x0000000000400ff2 <+36>: mov $0x0,%eax
0x0000000000400ff7 <+41>: cmp %edi,%ecx
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57>
0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi
0x0000000000400ffe <+48>: callq 0x400fce <func4>
0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax
0x0000000000401007 <+57>: add $0x8,%rsp
0x000000000040100b <+61>: retq

这个函数有点绕,大概是做一个二分,具体的内容写在注释里,自己可以用笔模拟下这个函数在干嘛,可以推断出C语言代码大致如下

1
2
3
4
5
6
7
8
9
int func4(int n, int i, int j) {
int val = (j + i) / 2;
if (val <= n) {
if (val >= n) return 0;
else return 2 * func4(n, val+1, j) + 1;
} else {
return 2 * func4(n, i, val-1);
}
}

共有四组可能的答案让这个函数返回0:

1
2
3
4
0 0
1 0
3 0
7 0

这题还是有一定难度的

phase_5

……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
0x0000000000401062 <+0>:	push   %rbx
0x0000000000401063 <+1>: sub $0x20,%rsp
0x0000000000401067 <+5>: mov %rdi,%rbx
0x000000000040106a <+8>: mov %fs:0x28,%rax ;防护代码,设置一个canary,可以不管它
0x0000000000401073 <+17>: mov %rax,0x18(%rsp)
0x0000000000401078 <+22>: xor %eax,%eax
0x000000000040107a <+24>: callq 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax;要求获取一个长度为6的字符串
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112>
0x0000000000401084 <+34>: callq 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx
0x000000000040108f <+45>: mov %cl,(%rsp)
0x0000000000401092 <+48>: mov (%rsp),%rdx
0x0000000000401096 <+52>: and $0xf,%edx;获取当前字符的后四位
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx;某个地方存了个字符串,每次获取第%rdx个字符,%rdx从输入所获得的字符的后四位
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1);保存截取的字符
0x00000000004010a4 <+66>: add $0x1,%rax;每次读取一个字符,类似++i
0x00000000004010a8 <+70>: cmp $0x6,%rax
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>;读取6次
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp)
0x00000000004010b3 <+81>: mov $0x40245e,%esi;一个字符串
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi
0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal>;字符串相等判断
0x00000000004010c2 <+96>: test %eax,%eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119>;相等则过关
0x00000000004010c6 <+100>: callq 0x40143a <explode_bomb>
0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1)
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov $0x0,%eax
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41>
0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax
0x00000000004010de <+124>: xor %fs:0x28,%rax
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140>
0x00000000004010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt>
0x00000000004010ee <+140>: add $0x20,%rsp
0x00000000004010f2 <+144>: pop %rbx
0x00000000004010f3 <+145>: retq

这个程序比较复杂,重要语句注释写在上面的代码里,意思是读取一个长度为6的字符串,对于每个字符截取后四位数字,用来作为index,获取另一个字符串里对应的字符,并保存起来,产生一个新的长度为6的字符串,要求等于另一个字符串。

解析可以看到,用来被截取的字符串str1为“maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?”(我们发现作者在嘲讽我们:) ),用来对比的字符串str2为“flyers”,我们发现flyers6个字符分别出现在str1的第9位,第15位,第14位,第5位,第6位,第7位,输入的字符串后四位的二进制只要分别表示9,15,14,5,6,7即可,翻查ASCII表,可以得到结果:

1
ionefg

这题更像密码破解了

phase_6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
0x00000000004010f4 <+0>:	push   %r14
0x00000000004010f6 <+2>: push %r13
0x00000000004010f8 <+4>: push %r12
0x00000000004010fa <+6>: push %rbp
0x00000000004010fb <+7>: push %rbx
0x00000000004010fc <+8>: sub $0x50,%rsp
0x0000000000401100 <+12>: mov %rsp,%r13
0x0000000000401103 <+15>: mov %rsp,%rsi
0x0000000000401106 <+18>: callq 0x40145c <read_six_numbers> ;读6个数字,老朋友了
0x000000000040110b <+23>: mov %rsp,%r14
0x000000000040110e <+26>: mov $0x0,%r12d
0x0000000000401114 <+32>: mov %r13,%rbp
0x0000000000401117 <+35>: mov 0x0(%r13),%eax
0x000000000040111b <+39>: sub $0x1,%eax
0x000000000040111e <+42>: cmp $0x5,%eax ;-1然后与5比较
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52>
0x0000000000401123 <+47>: callq 0x40143a <explode_bomb>
0x0000000000401128 <+52>: add $0x1,%r12d
0x000000000040112c <+56>: cmp $0x6,%r12d ;六次循环,每个数字要满足小于等于6
0x0000000000401130 <+60>: je 0x401153 <phase_6+95>
0x0000000000401132 <+62>: mov %r12d,%ebx
0x0000000000401135 <+65>: movslq %ebx,%rax
0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax
0x000000000040113b <+71>: cmp %eax,0x0(%rbp)
0x000000000040113e <+74>: jne 0x401145 <phase_6+81> ;每个数字不相等
0x0000000000401140 <+76>: callq 0x40143a <explode_bomb>
0x0000000000401145 <+81>: add $0x1,%ebx
0x0000000000401148 <+84>: cmp $0x5,%ebx
0x000000000040114b <+87>: jle 0x401135 <phase_6+65>
0x000000000040114d <+89>: add $0x4,%r13
0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32>
0x0000000000401153 <+95>: lea 0x18(%rsp),%rsi
0x0000000000401158 <+100>: mov %r14,%rax
0x000000000040115b <+103>: mov $0x7,%ecx
0x0000000000401160 <+108>: mov %ecx,%edx
0x0000000000401162 <+110>: sub (%rax),%edx;与7求差
0x0000000000401164 <+112>: mov %edx,(%rax); 结果放入原来的位置
0x0000000000401166 <+114>: add $0x4,%rax
0x000000000040116a <+118>: cmp %rsi,%rax
0x000000000040116d <+121>: jne 0x401160 <phase_6+108>;每个数字与7的差,循环
0x000000000040116f <+123>: mov $0x0,%esi
0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163>
0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx; next指针
0x000000000040117a <+134>: add $0x1,%eax
0x000000000040117d <+137>: cmp %ecx,%eax
0x000000000040117f <+139>: jne 0x401176 <phase_6+130>
0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148>
0x0000000000401183 <+143>: mov $0x6032d0,%edx
0x0000000000401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2)
0x000000000040118d <+153>: add $0x4,%rsi
0x0000000000401191 <+157>: cmp $0x18,%rsi
0x0000000000401195 <+161>: je 0x4011ab <phase_6+183>
0x0000000000401197 <+163>: mov (%rsp,%rsi,1),%ecx; 重新安排链表
0x000000000040119a <+166>: cmp $0x1,%ecx
0x000000000040119d <+169>: jle 0x401183 <phase_6+143>
0x000000000040119f <+171>: mov $0x1,%eax
0x00000000004011a4 <+176>: mov $0x6032d0,%edx
0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130>
0x00000000004011ab <+183>: mov 0x20(%rsp),%rbx
0x00000000004011b0 <+188>: lea 0x28(%rsp),%rax
0x00000000004011b5 <+193>: lea 0x50(%rsp),%rsi
0x00000000004011ba <+198>: mov %rbx,%rcx
0x00000000004011bd <+201>: mov (%rax),%rdx
0x00000000004011c0 <+204>: mov %rdx,0x8(%rcx)
0x00000000004011c4 <+208>: add $0x8,%rax
0x00000000004011c8 <+212>: cmp %rsi,%rax
0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222>
0x00000000004011cd <+217>: mov %rdx,%rcx
0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201>
0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx)
0x00000000004011da <+230>: mov $0x5,%ebp
0x00000000004011df <+235>: mov 0x8(%rbx),%rax
0x00000000004011e3 <+239>: mov (%rax),%eax
0x00000000004011e5 <+241>: cmp %eax,(%rbx)
0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250>;链表每个节点所对应的值要求严格递减
0x00000000004011e9 <+245>: callq 0x40143a <explode_bomb>
0x00000000004011ee <+250>: mov 0x8(%rbx),%rbx
0x00000000004011f2 <+254>: sub $0x1,%ebp
0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235>
0x00000000004011f7 <+259>: add $0x50,%rsp
0x00000000004011fb <+263>: pop %rbx
0x00000000004011fc <+264>: pop %rbp
0x00000000004011fd <+265>: pop %r12
0x00000000004011ff <+267>: pop %r13
0x0000000000401201 <+269>: pop %r14
0x0000000000401203 <+271>: retq

这是最后一道题,也是最难的一题,Prof在上课的时候也强调了hard for best students,仔细观察下这个题,我们发现,phase_6基本上还是在本函数内跳来跳去,一上来先要读取6个数字,要求满足一定的条件,跟之前所做过的2类似。

这个代码非常长,我们可以把它划分为几个部分,每个部分完成不同的功能,即不同的限制条件,第一部分是+23-+60,要求每个数小于等于6,第二部分+60-+95,是两个循环,用来判断数字两两之间不相等,第三部分是+108-128,将读来的6个数与7求差,再存入原来的地方,第四部分130-181,这个部分有一些复杂,相当于是在栈上存了些地址,可以发现+130 的代码为mov 0x8(%rdx),%rdx,这其实就是链表的形式,每次读取next地址,这段代码的意思是,用第三部分与7求出来的差值重新安排链表节点,1对应第一个节点,2对应第二个节点,依次类推;第五部分+183-257,用来判断重新构造出的链表里的数字满足严格递减。

我们利用gdb命令: x/24w 0x6032d0(每个节点占用了4个w)发现序号1-6的节点对应的数字为:332,168,924,691,477,413,为了使其严格递减,序号应该是:3,4,5,6,1,2,考虑到我们之前用7对传入的数字作差,为了得到这个序列,传入的数字应该是,4,3,2,1,6,5这就是我们最终的结果。

总结下,这个函数是传入一个互不相等的,由6个数组成的数组,然后每个数与7作差,根据作差的结果,每个数对应原来链表的第一个节点,重新排序一个链表,要求这个链表的数值降序排列

这一题核心在于分步处理,以及观察出那个链表的存在,这十分具有挑战性,我个人在这部分上卡了很久。

总结

CMU之所以是计算机排名第一的院校,果真有其道理,大牛愿意花时间精心设计作业,寓教于乐,从这个bomblab上可见一斑,每个bomb难度层层递增,cover了教材和课堂上所讲的重要内容,这个作业完成后,我对于汇编语言的了解与认知来到了一个新的层次,解决每个bomb都能给我带来一种成功解谜的成就感,希望中国以后也能出现这种愿意在教学和作业设计上花精力的大牛吧。