更新于:2024-04-09T23:17:58+08:00
深入理解计算机系统——二进制炸弹实验
实验简介
二进制炸弹是一个作为目标代码文件的程序。运行时,它提示用户输入若干个不同的字符串。如果其中一个不正确,炸弹就会“爆炸”,打印出一条错误信息。用户必须通过对程序的反汇编和逆向工程来求出这六个字符串,解决这些炸弹。该实验的主要目的是,深入理解汇编语言,并学习使用 gdb 调试器。
在本次实验中,我们需要拆解七个炸弹(其中一个为隐藏炸弹)。
实验预备
下载完实验材料后,有两个文件值得关注:bomb
和 bomb.c
。bomb
就是要“拆解”的目标文件代码,而 bomb.c
只是辅助理解的部分源代码文件。首先,对 bomb
进行逆向:
1 objdump -d bomb >> bomb.s
得到反汇编文件 bomb.s
,然后,在该文件中找到相关函数的反汇编代码,开始拆解炸弹!
拆弹
phase_1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 phase_1: push %ebp mov %esp,%ebp sub $0x8,%esp movl $0x8049948,0x4(%esp) mov 0x8(%ebp),%eax mov %eax,(%esp) call <strings_not_equal> test %eax,%eax je <phase_1+0x22> call <explode_bomb> leave ret
比较字符串,若相等就不会爆炸。找到目标字符串。
gdb找到:
成功!
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 phase_2: push %ebp ; set up stack size==0x28 mov %esp,%ebp sub $0x28,%esp lea -0x1c(%ebp),%eax ; eax=ebp-0x1c mov %eax,0x4(%esp) ; ebp-0x1c=(esp+0x4) mov 0x8(%ebp),%eax ; (ebp+0x8)=(esp) mov %eax,(%esp) call <read_six_numbers> ; read_six_numbers() needs 2 para movl $0x1,-0x4(%ebp) ; loop begin i=0x1 jmp <phase_2+0x3f> mov -0x4(%ebp),%eax ; eax=i mov -0x1c(%ebp,%eax,4),%edx ;edx=(ebp-0x1c+4*eax) mov -0x4(%ebp),%eax dec %eax ; eax=i-1 mov -0x1c(%ebp,%eax,4),%eax ; eax=(ebp-0x1c+4*eax) add $0x5,%eax ; eax+=0x5 cmp %eax,%edx ; is equal? explode if not je <phase_2+0x3c> call <explode_bomb> incl -0x4(%ebp) ; (ebp-0x4)++ cmpl $0x5,-0x4(%ebp) ; loop unit i==5 jle <phase_2+0x21> leave ret
仔细阅读后发现,程序先调用了 read_six_numbers
的函数,而在之前,程序准备了一个参数放在 %esp+4
处,该参数的意义需要进一步阅读下面的代码才能知晓。之后,程序就进入了一个循环,用于不断测试条件,如果条件不符,炸弹就会爆炸!显然,该循环就是关键,这个炸弹就是考验汇编语言的循环部分了。经整理,循环用 C 代码表示如下:
1 2 3 4 5 6 7 *a = &(ebp-0x1c );for (int i = 1 ; i <= 5 ; i++) { edx=a[i]; eax=a[i-1 ]+5 ; if (eax != edx) explode_bomb(); }
现在,明白 read_six_numbers
函数的参数 ebp-0x1c
是一个数组地址。大致可以猜到,只要输入 6 个数字,相邻两个差为 5 即可拆解炸弹。
等等,并不是任意的差为 5 的等差数列都满足要求!对首项,read_six_numbers
函数也有要求:
1 2 3 4 5 6 7 8 9 10 read_six_numbers: ; .... mov 0x8(%ebp),%eax mov %eax,(%esp) call <sscanf@plt> mov %eax,-0xc(%ebp) cmpl $0x5,-0xc(%ebp) ; compare with (ebp-0xc) 5 jg <read_six_numbers+0x62> call <explode_bomb> add $0x30,%esp
看起来,输入的首项必须比 5 大,那么首项就是 6 吧 :smile:。
验证一下:
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 phase_3: ; ... movl $0x0,-0x8(%ebp) movl $0x0,-0x4(%ebp) lea -0x10(%ebp),%eax mov %eax,0xc(%esp) ; (esp+0xc)=ebp-0x10 lea -0xc(%ebp),%eax mov %eax,0x8(%esp) ; (esp+0x8)=ebp-0xc movl $0x8049972,0x4(%esp) ; (esp+0x4)=?? mov 0x8(%ebp),%eax mov %eax,(%esp) call <sscanf@plt> mov %eax,-0x4(%ebp) cmpl $0x1,-0x4(%ebp) ; eax > 1, if not explode! jg <phase_3+0x43> call <explode_bomb> mov -0xc(%ebp),%eax ; eax > 7 will explode! mov %eax,-0x14(%ebp) ; (ebp-0xc)->(ebp-0x14) cmpl $0x7,-0x14(%ebp) ja <phase_3+0x95> ; jmp to call <explode_bomb>
注意到,程序调用了函数 sscanf
。该函数的意义是,从第一个参数的字符串中读取变量的值。就本次调用而言,函数共准备了四个参数,分别位于%esp
%esp+0x4
、 %esp+0x8
和 %esp+0xc
。其中,%esp
就是我们输入的行字符串,这 0x8049972
表示的是:
看来是需要输入两个整数。那么,最后的两个参数就一定是输入的整数了,%esp+0x8
是第一个整数地址,%esp+0xc
应该是第二个整数地址。sscanf
函数完成后,读入的两个整数就位于 ebp-0xc
和 ebp-0x10
中。
然后对这两个整数做简单判断:要求第一个整数要大于 1 且小于等于 7,否则炸弹会爆炸。接下来,看函数的第二部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ;... mov -0x14(%ebp),%edx ; edx=(ebp-0x14) mov 0x8049978(,%edx,4),%eax ; eax=(edx*4+0x8049978) jmp *%eax ; a switch table addl $0x108,-0x8(%ebp) subl $0x1e5,-0x8(%ebp) addl $0x19a,-0x8(%ebp) subl $0x35f,-0x8(%ebp) addl $0x239,-0x8(%ebp) subl $0x38e,-0x8(%ebp) addl $0x38e,-0x8(%ebp) subl $0xe3,-0x8(%ebp) jmp <phase_3+0x9a> call <explode_bomb> mov -0xc(%ebp),%eax ; eax=(ebp-0xc) cmp $0x5,%eax ; eax > 5 explode! jg <phase_3+0xaa> mov -0x10(%ebp),%eax ; eax=(ebp-0x10) cmp %eax,-0x8(%ebp) ; compare eax with (ebp-0x8) je <phase_3+0xaf> call <explode_bomb> leave ret
第二句汇编又出现了一个奇怪的数字 0x8049978
,在gdb 调试程序中,打开一看,就会发现其中的奥秘:
在0x8049978
处,存放了后面 8 条语句的地址!显然,这就是一个跳转表,PC 接下来该指向哪里,完全取决于 %edx
,也即输入的第一个数字的值。如果第一个数为 2 ,那么程序最终计算 %ebp-0x8
处的值就是 0xff91
(当然不是计算得到的,通过gdb调试器得出的结果)。于是输入的数值可以为 2 和 -111。
经验证,第三颗炸弹已被拆除,感兴趣的话,也可以尝试一下其他数值。不过注意到,程序最后规定,第一个数字不能大于 5 !
phase_4
第四颗炸弹:前面和第三颗炸弹一样,调用了 sscanf
函数。那么这次,sscanf
函数使用了什么字符串呢?看起来只要输入一个整数就行:
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 phase_4: push %ebp mov %esp,%ebp sub $0x28,%esp lea -0xc(%ebp),%eax mov %eax,0x8(%esp) ; (esp+0x8)=(ebp-0xc) movl $0x8049998,0x4(%esp) ;(esp+0x4)=?? mov 0x8(%ebp),%eax mov %eax,(%esp) call <sscanf@plt> mov %eax,-0x4(%ebp) cmpl $0x1,-0x4(%ebp) ; eax!= 0x1 explode! jne <phase_4+0x30> mov -0xc(%ebp),%eax test %eax,%eax ; eax=(ebp-0xc) jg <phase_4+0x35> ; eax & eax > 0, if not explode! call <explode_bomb> mov -0xc(%ebp),%eax mov %eax,(%esp) call <func4> mov %eax,-0x8(%ebp) cmpl $0x37,-0x8(%ebp) je <phase_4+0x4e> ; 0x37==eax if not explode! call <explode_bomb> leave ret
经gdb调试后,%ebp-0xc
是输入的整数,必须大于 0 ,而 sscanf
函数的返回值必须为 1,否则炸弹会引爆。然后,程序就开始调用 func4
函数了。注意到,func4
函数的返回值必须等于 0x37
否则炸弹会爆炸。那么 func4
函数是怎么计算的呢?
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 func4: push %ebp mov %esp,%ebp push %ebx sub $0x8,%esp cmpl $0x1,0x8(%ebp) ; (ebp+0x8)>0x1? jg <func4+0x16> movl $0x1,-0x8(%ebp) ; (ebp-0x8)=0x1 jmp <func4+0x37> mov 0x8(%ebp),%eax ; eax=(ebp+0x8) dec %eax mov %eax,(%esp) ; esp=eax-1 call <func4> mov %eax,%ebx mov 0x8(%ebp),%eax sub $0x2,%eax mov %eax,(%esp) call <func4> add %eax,%ebx ; ebx += eax mov %ebx,-0x8(%ebp) ; (ebp-0x8)=ebx mov -0x8(%ebp),%eax ; eax=ebx add $0x8,%esp pop %ebx pop %ebp ret
仔细阅读后,整理成如下 C 代码:看起来就是一个斐波那契数列 :joy:。结合 func4
函数的返回值必须是 0x37
,那么很容易想到,问题是让我们求出,斐波那契数列的哪一项是 0x37
。
1 2 3 4 5 6 7 8 9 int func4 (int n) { if (n > 1 ) { sum = func4(n-1 )+func4(n-2 ); return sum; } else { eax=0x1 return eax; } }
嗯,第 9 项,输入 9 就可以拆除炸弹了。
phase_5
与炸弹四一样,phase_5 阶段一开始也调用了 sscanf
函数,用 gdb 调试,找到要求输入的字符串:需要给出两个整数。
1 2 3 4 5 6 7 8 9 10 11 12 phase_5: push %ebp mov %esp,%ebp sub $0x38,%esp lea -0x18(%ebp),%eax mov %eax,0xc(%esp) ; (%esp+0xc)=&%ebp-0x18 lea -0x14(%ebp),%eax mov %eax,0x8(%esp) ; (%esp+0x8)=&%ebp-0x14 movl $0x8049972,0x4(%esp) ; (%esp+0x4)=?? mov 0x8(%ebp),%eax mov %eax,(%esp) call 8048868 <sscanf@plt>
当输入了两个整数后(已经很熟悉了, %esp+0x8
和 %esp+0xc
指向了两个输入整数的地址,而%ebp-x14
和 %ebp-0x18
存放了整数值),接下来看看炸弹怎么对这两个整数进行操作。
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 mov -0x14(%ebp),%eax and $0xf,%eax mov %eax,-0x14(%ebp) ; first int & 0xf, write back mov -0x14(%ebp),%eax mov %eax,-0x8(%ebp) movl $0x0,-0x10(%ebp) movl $0x0,-0xc(%ebp) jmp <phase_5+0x6a> incl -0x10(%ebp) ; loop i=(%ebp-0x10) mov -0x14(%ebp),%eax mov 0x804a5c0(,%eax,4),%eax ; 0x804a5c0 ?? mov %eax,-0x14(%ebp) ; write back mov -0x14(%ebp),%eax add %eax,-0xc(%ebp) ; accumulate (%ebp-0xc) mov -0x14(%ebp),%eax cmp $0xf,%eax ;; %eax == 0xf ? ne to loop jne 8048d80 <phase_5+0x54> cmpl $0xb,-0x10(%ebp) jne 8048dac <phase_5+0x80> ; i must be 0xb, or explode! mov -0x18(%ebp),%eax cmp %eax,-0xc(%ebp) je 8048db1 <phase_5+0x85> ; second int == (%ebp-0xc). or explode! call 8049656 <explode_bomb> leave ret
仔细阅读汇编代码,初步得出结论:输入的第一个整数会作为一个数组的索引,在一个循环中不断累加,得到的累加值会与输入的第二个整数比较。炸弹不爆炸的条件是:循环次数必须为 11 次(0x1-0xb),且第二个整数的值与累加值相等。
首先,来看一下位于 0x804a5c0
的数组吧:
由于要求循环次数必须为 11 次,而终止循环的条件是 %eax == 0xf
,也就是说取到数组中的 0xf
才能结束循环,那我们从后往前推,要取到 0xf
,必须取到 0x6
(因为 0xf
的索引是 6),取到 0x6
,必须先取到 0xe
……
于是有:0xf -> 0x6 -> 0xe -> 0x2 -> 0x1 -> 0xa -> 0x0 -> 0x8 -> 0x4 -> 0x9 -> 0xd -> 0xb。累加起来值为(不算0xb)82。于是,输入 11 和 82 就可以拆解炸弹。
phase_6
最后一个炸弹,继续前进。注意到,phase_6 先调用了 atoi
函数,看来是要把某个字符串变为整数。
1 2 3 4 5 6 7 8 phase_6: push %ebp mov %esp,%ebp sub $0x18,%esp movl $0x804a66c,-0x8(%ebp) ; 0x804a66c ??? mov 0x8(%ebp),%eax ; %ebp+0x8 ?? mov %eax,(%esp) call <atoi@plt>
首先,还是要用 gdb 调试器看看 0x804a66c
和传入的参数 %ebp+0x8
是什么东西。
得出的信息有点少(主要是看不懂这个字符串),而且 0x804a66c
处的字符串为空。随后,程序很快就调用了 fun6
函数:
1 2 3 4 5 6 7 call <atoi@plt> mov %eax,%edx mov -0x8(%ebp),%eax mov %edx,(%eax) ; ((%ebp-0x8)) = %edx mov -0x8(%ebp),%eax mov %eax,(%esp) ; %eax as arg call 8048db3 <fun6>
fun6
函数完成后,会进入一个循环,从下面的汇编代码可以看到,循环中不断重复的一条有效指令就是 (%ebp-0x4)=((%ebp-0x4)+0x8)
。从这点上看,可以理解为 (%ebp-0x4)
是一个指针,而这种循环更像是对链表的遍历。后来,根据我的不断尝试,发现该炸弹确实是对一个链表进行操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 mov %eax,-0x8(%ebp) mov -0x8(%ebp),%eax ; fun6 return value is %eax mov %eax,-0x4(%ebp) ; (%ebp-0x4)=%eax movl $0x1,-0xc(%ebp) ; (%ebp-0xc)=1 i=1 jmp 8048e8f <phase_6+0x48> ; loop1 mov -0x4(%ebp),%eax mov 0x8(%eax),%eax mov %eax,-0x4(%ebp) ; (%ebp-0x4)=((%ebp-0x4)+0x8) incl -0xc(%ebp) ; i++ cmpl $0x4,-0xc(%ebp) jle 8048e83 <phase_6+0x3c> ; (%ebp-0xc) <= 4 to loop mov -0x4(%ebp),%eax mov (%eax),%edx ; %edx=((%ebp-0x4)) =[0x804a60c] mov 0x804a66c,%eax cmp %eax,%edx ; %edx == 0x804a66c ?? je 8048ea8 <phase_6+0x61> ; if not eq, explode! call 8049656 <explode_bomb> leave ret
fun6
函数非常复杂,但仔细观察发现,该函数运行后产生的结果与我们给的输入无关。那么就有一个技巧,可以用gdb调试器在 fun6
函数返回后设置断点,观察其返回值 %eax==0x804a618
。然后,上述汇编代码的循环会执行 5 次,我使用 gdb 调试器将整个链表的值都打印在下图中。0x804a618
对应的是 node7,而上面汇编代码中,要求 %edx==[0x804a66c]
,其对应的是 node0,经过多次实验,我发现,node0 为用户输入的值(见下图,输入 5 时)。
那么问题来了,应该输入什么才能拆解炸弹呢?顺着程序过一遍,一开始 %eax=0x804a618
,指向了 node7,然后根据 (%ebp-0x4)=((%ebp-0x4)+0x8)
,链接到下一个节点,一共执行 5 次,先后是 node3、node5、node9、node8 ,遍历后, %ebp-0x4
中的值应该为 0x804a60c
(见下图),当从中取出的值也为 0x20e
时,才能拆解炸弹。
如下图,已经成功拆解完 6 个炸弹。
secret_phase
一切还没结束,从汇编代码看,我们还剩一个隐藏炸弹没有拆解掉。首先,我们需要找到隐藏炸弹的输入入口。否则,gdb 调试器是帮不了我们的。全局搜索一下,发现在 phase_defused
函数下找到了 call secret_phase
语句。
经分析,确认是要在拆解完所有炸弹后,才有机会见到这枚隐藏炸弹。而如何输入密语也是非常考验我们的水平的!因此,先要细细研究 phase_defused
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 phase_defused: ; ... jne 80496fe <phase_defused+0x7e> mov $0x804a990,%eax ; 0x804a990 ??? mov %eax,%edx lea -0x54(%ebp),%eax ; arg : (%ebp-0x54) mov %eax,0xc(%esp) lea -0x58(%ebp),%eax ; arg : (%ebp-0x58) mov %eax,0x8(%esp) movl $0x8049e38,0x4(%esp) ; 0x8049e38 ?? mov %edx,(%esp) call 8048868 <sscanf@plt> ; sscanf mov %eax,-0x4(%ebp) cmpl $0x2,-0x4(%ebp) jne 80496f2 <phase_defused+0x72> movl $0x8049e3e,0x4(%esp) ; 0x8049e3e ?? lea -0x54(%ebp),%eax mov %eax,(%esp) call 804908f <strings_not_equal> test %eax,%eax ; to see if string is equal jne 80496f2 <phase_defused+0x72> ; ...
重点是 sscanf
函数及其参数,第一个参数是 %edx
,指向 0x804a990
,第二个参数是 0x8049e38
,其实是一个字符串(见下图), 第三个参数和第四个参数分别是 %ebp-0x58
和 %ebp-0x54
。从后半部分代码看出,要求输入的字符串必须是 austinpowers,且 sscanf
函数返回值为 2,意思是整数和字符串必须都有对应的输入。
再来看看 0x804a990
(见下图),很奇怪的是,它只有一个 “9”,这样的话,sscanf
函数的返回值就只能是 1 了,我们就没法打开隐藏关卡了。还有,这个 <input_string>
是什么?
看起来,要先解决 <input_string>
的问题,不过,一切来的很轻松(见下图)。看来,我们之前拆炸弹的每一个输入都以 80 字节对齐的方式存放于此!这样看来,就是要在第 4 个炸弹拆解时,后面加上字符串 austinpowers, 就可以发现隐藏炸弹了!
终于,我们来到了隐藏炸弹,现在看看这一炸弹的具体代码。
1 2 3 4 5 6 7 8 9 secret_phase: push %ebp mov %esp,%ebp sub $0x18,%esp call <read_line> mov %eax,-0xc(%ebp) mov -0xc(%ebp),%eax mov %eax,(%esp) call <atoi@plt>
看起来,需要读入一行,并且读入的这一行会使用 atoi
函数转为数字。要求数字必须为正数,且小于 0x3e9
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 mov %eax,-0x8(%ebp) cmpl $0x0,-0x8(%ebp) jle <secret_phase+0x2b> ; %eax <= 0 explode cmpl $0x3e9,-0x8(%ebp) jle <secret_phase+0x30> ; %eax <= 0x3e9 call 8049656 <explode_bomb> mov -0x8(%ebp),%eax mov %eax,0x4(%esp) ; (%esp+0x4)=int movl $0x804a720,(%esp) ; 0x804a720 ??? call 8048eaa <fun7> mov %eax,-0x4(%ebp) ; return val=eax cmpl $0x3,-0x4(%ebp) ; %eax==0x3, or explode je <secret_phase+0x51> call <explode_bomb> movl $0x804999c,(%esp) call 80487c8 <puts@plt> call 8049680 <phase_defused> leave ret
重点是 fun7
函数,其参数有两个,一个是我们输入的数字,另一个是立即数 0x804a720
,这个立即数不应当是一个“数字”,更有可能是一个指针。由上面的代码可知,返回值应该为3,否则就会爆炸。接下来,仔细研究一下 fun7
的代码。
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 fun7: push %ebp mov %esp,%ebp sub $0xc,%esp cmpl $0x0,0x8(%ebp) ; (%ebp+0x8)=0x804a720 jne <fun7+0x15> movl $0xffffffff,-0x4(%ebp) ; low 2 word is 0 then (%ebp-0x4)=-1 jmp 8048f13 <fun7+0x69> mov 0x8(%ebp),%eax mov (%eax),%eax cmp 0xc(%ebp),%eax ; %eax=((%ebp+0x8)) <= (%ebp+0xc) jle <fun7+0x3b> mov 0x8(%ebp),%eax mov 0x4(%eax),%edx ; %edx = ((%ebp+0x8)+0x4) mov 0xc(%ebp),%eax mov %eax,0x4(%esp) ; (%esp+0x4)=(%ebp+0xc) mov %edx,(%esp) call 8048eaa <fun7> add %eax,%eax mov %eax,-0x4(%ebp) jmp 8048f13 <fun7+0x69> mov 0x8(%ebp),%eax ; mov (%eax),%eax cmp 0xc(%ebp),%eax ; %eax=((%ebp+0x8)) != (%ebp+0xc) jne 8048ef8 <fun7+0x4e> movl $0x0,-0x4(%ebp) jmp 8048f13 <fun7+0x69> mov 0x8(%ebp),%eax mov 0x8(%eax),%edx ; %edx = ((%ebp+0x8)+0x8) mov 0xc(%ebp),%eax ; mov %eax,0x4(%esp) ; (%esp+0x4)=(%ebp+0xc) mov %edx,(%esp) call 8048eaa <fun7> ; call fun7 add %eax,%eax inc %eax mov %eax,-0x4(%ebp) mov -0x4(%ebp),%eax leave ret
转化为 C 代码,可能比较方便理解,可见,还是递归函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int fun7 (int *a, int b) { if (*a & 0x0ffff == 0 ) { return -1 ; } else { if (*a <= b) { if (*a != b) { a+=8 byte; int ret = fun7(a, b); ret += ret; ret++; return ret; } else return 0 ; } else { a+=4 byte; int ret = fun7(a, b); ret += ret; return ret; } } }
现在,需要让 fun7
函数返回值为3,应当如何设置a 和 b 的值呢?不难想到,返回值为3,要求最内部的函数返回值为0,然后再返回1,再返回3,但首先,需要知道 0x804a720
的值。经过多次尝试,求出如下数据:
根据之前的推算,需要调用三层 fun7
函数,第一次需走入 *a<b
的分支,第二次也是,第三次需要走入 *a==b
的分支,这样返回的值才能为3。倒推回去,第一次指针为 0x804a720
,第二次需要 +8,指针地址为 0x804a708
,然后再加8,为 0x804a6d8
,此时指针指向的值是 0x6b
。要求输入的值与 0x6b
相等,才能返回3,因此,拆解该炸弹的答案是 107。
最后的最后,解答成功!
总结
回头看这几颗炸弹的拆解过程,每颗炸弹难度递增,考点不一但都极具代表性。第一阶段考察了字符串比较,需要我们熟练使用 gdb 调试器。第二阶段考察了汇编语言的循环部分,能否理解循环非常关键。第三阶段考察了汇编语言的 switch
语句的实现。第四阶段考察了递归函数调用,第五阶段考察了数组索引以及循环结构,第六阶段则是关于链表的遍历。最后的秘密阶段更是融合了多个考点,做完之后很有成就感!