本篇博客是《深入理解计算机系统》实验记录的第二篇,也是非常有名的炸弹实验。首先吐槽一下学校的这个实验代码不更新的问题,教材和上课讲授的均是x86-64位汇编语言,结果实验给的还是第二版配套的32位汇编语言,这两者之间有不少的差别,为此还查阅了一些关于32位汇编的资料自己学习(例如32位的参数传递规则和64位不同),这无疑增加了学习成本。
接下来进入正题,我领取的代码包是第19号,即bomb19。后来发现不同人领取的代码包实验题目虽不同,但解决思路大体相同,答案也是不一样的。不知为何,我感觉我的实验包好像难度可能还大一些,这可能是错觉。
无论如何,这几个phase全靠自己拿头磕出来的,花了两天多才拆完6个炸弹,第一天拆了5个,第二天边上课边拆剩下的1个(最后一个有点难哦)。现在把整个过程写出来,作为对这段经历的记录。
Ahead Of Work
这些事情是完成这个实验后总结的心得体会:
1.使用
objdump -d bomb > objdump.s
将可执行文件bomb反汇编出来,我们的工作需要阅读这些汇编代码,使用一个好用的阅读器阅读会事半功倍。
2.了解32位汇编语言传参规则,与64位不同,32位传参全部使用栈来完成,同时还需要了解enter和leave指令。
3.关于GDB的基本用法,这里有一个课程官网上提供的关于GDB的reference,建议配套使用。
4.拆炸弹的最好办法是画出栈帧图,求解过程要大胆假设,小心求证。
5.阅读bomb.c可知,所有phase的调用全部都是先由read_line读入你的input,然后作为唯一参数将input传送入phase。
6.下面分析中涉及大量栈帧结构,第N号栈字表示从栈帧基址指针开始计数(向地址递减方向)的第N个字,简记为#N。
Phase1
第一个phase比较简单,阅读代码后,这里给出栈帧图示:
如果阅读phase_1的汇编代码,可以很容易画出上面的栈帧图。注意phase_1的参数构造区,这两个参数将被传递进一个名为strings_not_equal的函数,从名字上可以看出这个函数用于判断两个字符串是否相等。你可以去再阅读一下它的细节,但是从这里我们已经可以大胆地作出假设,那个magic number(0x8049958)中存放着要对比的字符串。在GDB下打印出来:
(gdb) break phase_1
Breakpoint 1 at 0x8048b86
(gdb) x/s 0x8049958
0x8049958: "For NASA, space is still a high priority."
这句For NASA, space is still a high priority.,就是最终第一题的解。实验时的截图如下:
phase2
第二个炸弹稍微复杂一些,阅读反汇编可以知道函数中调用了一个名为read_six_numbers的函数使用sscanf从输入的字符串input中解析出6个数字。画出栈帧示意图如下:
图中的蓝线表示了将对应单元的地址放置到对应的栈帧。看图中右边关于read_six_numbers函数的栈帧,从名字可以猜测出来这个函数期待从我们输入的字符串中读取6个整数。根据IA32的传参规则,我们可以知道读取的6个整型数字将会被放置回phase_2的栈帧中,作为局部变量参与后面的运算。为什么说是6个整型数字呢,因为read_six_numbers向sscanf传递的第一个参数,也就是0x8049c7d这个东西(看上去很奇怪是吧),其实这指向了一个模式字符串。如果打印出来,可以看到:
好了,知道了这个函数的作用,所以我们知道我们一定要输入6个整型数字,少于6个就会引爆炸弹,这是对sscanf返回值的检测得出的。现在返回phase_2,看看它要对这六个数字做什么:
8048bbc: 8b 45 e4 mov -0x1c(%ebp),%eax ;第7号栈字的内容送入%eax
8048bbf: 83 f8 01 cmp $0x1,%eax ;cmp %eax : 1
8048bc2: 74 05 je 8048bc9 <phase_2+0x25> ;相等则跳转,否则引爆炸弹
8048bc4: e8 99 0a 00 00 call 8049662 <explode_bomb>
8048bc9: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%ebp) ;1送入第1号栈字
8048bd0: eb 22 jmp 8048bf4 <phase_2+0x50>
8048bd2: 8b 45 fc mov -0x4(%ebp),%eax ;%eax = 第1号栈字中的内容
8048bd5: 8b 4c 85 e4 mov -0x1c(%ebp,%eax,4),%ecx ;%ecx = *(%ebp + 4 * %eax - 28)
8048bd9: 8b 45 fc mov -0x4(%ebp),%eax ;%eax = 1号栈字中的内容
8048bdc: 48 dec %eax ;%eax = %eax - 1
8048bdd: 8b 54 85 e4 mov -0x1c(%ebp,%eax,4),%edx ;%edx = *(%ebp + 4 * %eax - 28)
8048be1: 8b 45 fc mov -0x4(%ebp),%eax ;%eax = 1号栈字中的内容
8048be4: 40 inc %eax ;%eax = %eax + 1
8048be5: 0f af c2 imul %edx,%eax ;%eax = %eax * %edx
8048be8: 39 c1 cmp %eax,%ecx ;cmp %ecx : %eax
8048bea: 74 05 je 8048bf1 <phase_2+0x4d> ;相等跳至8048bf1,否则引爆炸弹
8048bec: e8 71 0a 00 00 call 8049662 <explode_bomb>
8048bf1: ff 45 fc incl -0x4(%ebp) ;递增1号栈字
8048bf4: 83 7d fc 05 cmpl $0x5,-0x4(%ebp) ;cmp 第1号栈字:5
8048bf8: 7e d8 jle 8048bd2 <phase_2+0x2e> ;相等或小于,jmp 8048bd2
上来就进行了一次判定,要判定#7是否为1,否则引爆炸弹。所以这使得我们输入的第一个数一定要是1,接下来的动作可以列个表格(汇编代码注释已给出,不再逐行解读):
这里%eax是作为循环计数变量的,循环次数为5.
%eax | %ecx | %edx | cmp |
---|---|---|---|
1 | #6 | #7 | #6:2#7 |
2 | #5 | #6 | #5:3#6 |
3 | #4 | #5 | #4:4#5 |
4 | #3 | #4 | #3:5#4 |
5 | #2 | #3 | #2:6#3 |
上面执行的比较操作要求两个比较数字完全相等,否则就会引爆炸弹。
因为#7确定是1,所以可以推出来:
#6 = 2 (#7 * 2)
#5 = 6 (#6 * 3)
#4 = 24 (#5 * 4)
#3 = 120 (#4 * 5)
#2 = 720 (#3 * 6)
所以要输入的序列就是
1 2 6 24 120 720
phase3
首先给出汇编代码:
08048bfc <phase_3>:
8048bfc: 55 push %ebp
8048bfd: 89 e5 mov %esp,%ebp
8048bff: 83 ec 28 sub $0x28,%esp
8048c02: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp) ;赋值
8048c09: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp) ;赋值 x2
8048c10: 8d 45 f0 lea -0x10(%ebp),%eax ;第4个栈字的地址交给%eax
8048c13: 89 44 24 0c mov %eax,0xc(%esp) ;第4个栈字的地址交给#7
8048c17: 8d 45 f4 lea -0xc(%ebp),%eax ;第3个栈字的地址交给%eax
8048c1a: 89 44 24 08 mov %eax,0x8(%esp) ;第3个栈字的地址交给#8
8048c1e: c7 44 24 04 82 99 04 movl $0x8049982,0x4(%esp) ;magic number->#9
8048c25: 08
8048c26: 8b 45 08 mov 0x8(%ebp),%eax
8048c29: 89 04 24 mov %eax,(%esp) ;input放到栈顶
8048c2c: e8 37 fc ff ff call 8048868 <sscanf@plt> ;读取一些东东
8048c31: 89 45 fc mov %eax,-0x4(%ebp) ;返回值放在#1
8048c34: 83 7d fc 01 cmpl $0x1,-0x4(%ebp) ;cmp ret:1
8048c38: 7f 05 jg 8048c3f <phase_3+0x43> ;大于1跳转,否则引爆炸弹
8048c3a: e8 23 0a 00 00 call 8049662 <explode_bomb>
8048c3f: 8b 45 f4 mov -0xc(%ebp),%eax ;将#3中的东东放到%eax
8048c42: 89 45 ec mov %eax,-0x14(%ebp) ;将%eax放到#5
8048c45: 83 7d ec 07 cmpl $0x7,-0x14(%ebp) ;cmp #5:7
8048c49: 77 54 ja 8048c9f <phase_3+0xa3> ;#5 > 7, jmp 0x8048c9f, 引爆炸弹
8048c4b: 8b 55 ec mov -0x14(%ebp),%edx ;#5放到%edx
8048c4e: 8b 04 95 88 99 04 08 mov 0x8049988(,%edx,4),%eax ;(0x8049988 + 4 * #5)放到%eax
8048c55: ff e0 jmp *%eax ;间接跳转
8048c57: c7 45 f8 52 02 00 00 movl $0x252,-0x8(%ebp) ;给#2赋值为0x252
8048c5e: eb 44 jmp 8048ca4 <phase_3+0xa8>
8048c60: c7 45 f8 0e 02 00 00 movl $0x20e,-0x8(%ebp) ;给#2赋值为0x20e
8048c67: eb 3b jmp 8048ca4 <phase_3+0xa8>
8048c69: c7 45 f8 5a 01 00 00 movl $0x15a,-0x8(%ebp) ;给#2赋值为0x15a
8048c70: eb 32 jmp 8048ca4 <phase_3+0xa8>
8048c72: c7 45 f8 b9 01 00 00 movl $0x1b9,-0x8(%ebp) ;给#2赋值为0x1b9
8048c79: eb 29 jmp 8048ca4 <phase_3+0xa8>
8048c7b: c7 45 f8 6b 01 00 00 movl $0x16b,-0x8(%ebp) ;给#2赋值为0x16b
8048c82: eb 20 jmp 8048ca4 <phase_3+0xa8>
8048c84: c7 45 f8 2a 01 00 00 movl $0x12a,-0x8(%ebp) ;给#2赋值为0x12a
8048c8b: eb 17 jmp 8048ca4 <phase_3+0xa8>
8048c8d: c7 45 f8 28 03 00 00 movl $0x328,-0x8(%ebp) ;给#2赋值为0x328
8048c94: eb 0e jmp 8048ca4 <phase_3+0xa8>
8048c96: c7 45 f8 e8 00 00 00 movl $0xe8,-0x8(%ebp) ;给#2赋值为0xe8
8048c9d: eb 05 jmp 8048ca4 <phase_3+0xa8>
8048c9f: e8 be 09 00 00 call 8049662 <explode_bomb> ;引爆炸弹
8048ca4: 8b 45 f0 mov -0x10(%ebp),%eax ;#4的值放到%eax
8048ca7: 39 45 f8 cmp %eax,-0x8(%ebp) ;cmp #2:#4,相等,安全退出,否则引爆炸弹
8048caa: 74 05 je 8048cb1 <phase_3+0xb5>
8048cac: e8 b1 09 00 00 call 8049662 <explode_bomb>
8048cb1: c9 leave
8048cb2: c3 ret
这道题栈帧结构不难,但是它有些信息是比较隐秘的,需要用gdb在执行中去获取,以下是phase_3的栈帧结构。
栈帧示意图中的蓝线表示将对应栈帧的地址放置到对应的单元,phase_3会调用sscanf函数从你的输入中读取两个整数,魔数0x8049982指向sscanf从你输入的字符串中解析数据时用到的格式字符串。
解析之后的返回值根据汇编代码可知会放置到#1,汇编代码会测试是否读取了大于一个值,如果你输入的数字只有一个,那么会引爆炸弹,所以需要输入两个数字,这和之前的分析是相吻合的。随后汇编代码会检查输入的第一个数字是否大于7,大于7则引爆炸弹,所以我们第一个输入的数字不可以大于7。
接下来是重点,我们输入的数字会作为一个偏移量去索引一个跳表(jump table)结构,因为这里使用了间接跳转指令。为了知道这个跳表的具体内容,我们必须用gdb去打印并调试,0x8049988这个显得很突兀的地址就是跳表的基址。
8048c4e: 8b 04 95 88 99 04 08 mov 0x8049988(,%edx,4),%eax ;(0x8049988 + 4 * #5)放到%eax
8048c55: ff e0 jmp *%eax ;间接跳转
使用gdb打印0x8049988,可见跳表的内容:
这8个地址分别对应到phase_3下面的一系列赋值语句(0-7共8种情况,正好一一对应),随后汇编代码会对比赋值语句赋值给#2的数字和你输入的第二个数字是否相等,不等则引爆炸弹。
所以这题已经豁然开朗了,你必须根据自己的第一个输入来确定跳转地址,从而得到第二个输入。
所以本题解不唯一,我使用的是:
4 363
这组解。
phase4
这道题涉及到了递归函数,这道题的汇编代码对应的栈帧结构如下所示,为了更好地理解递归函数的作用,我们假设输入的数字是3,这样可以在比较好的了解递归函数的作用同时不至于使栈帧结构过长。
首先给出添加注释的phase_4汇编代码:
8048ce2: 55 push %ebp
8048ce3: 89 e5 mov %esp,%ebp
8048ce5: 83 ec 28 sub $0x28,%esp
8048ce8: 8d 45 f4 lea -0xc(%ebp),%eax ;#3的地址放在%eax
8048ceb: 89 44 24 08 mov %eax,0x8(%esp) ;%eax放在#8
8048cef: c7 44 24 04 a8 99 04 movl $0x80499a8,0x4(%esp) ;magic number->#9
8048cf6: 08
8048cf7: 8b 45 08 mov 0x8(%ebp),%eax ;input放到%eax
8048cfa: 89 04 24 mov %eax,(%esp) ;input放到栈顶
8048cfd: e8 66 fb ff ff call 8048868 <sscanf@plt> ;读取一些东东
8048d02: 89 45 fc mov %eax,-0x4(%ebp) ;sscanf返回值放在#1
8048d05: 83 7d fc 01 cmpl $0x1,-0x4(%ebp) ;cmp #1:1
8048d09: 75 07 jne 8048d12 <phase_4+0x30> ;#1不等于1,引爆炸弹
8048d0b: 8b 45 f4 mov -0xc(%ebp),%eax ;#3中的值(输入值)放入%eax
8048d0e: 85 c0 test %eax,%eax ;测试一下输入的数据
8048d10: 7f 05 jg 8048d17 <phase_4+0x35> ;大于0,jmp 8048d17,否则引爆炸弹
8048d12: e8 4b 09 00 00 call 8049662 <explode_bomb>
8048d17: 8b 45 f4 mov -0xc(%ebp),%eax ;#3中的值放入%eax
8048d1a: 89 04 24 mov %eax,(%esp) ;解析后的输入值放入栈顶
8048d1d: e8 91 ff ff ff call 8048cb3 <func4>
8048d22: 89 45 f8 mov %eax,-0x8(%ebp) ;func4的返回值放回#2
8048d25: 81 7d f8 d0 02 00 00 cmpl $0x2d0,-0x8(%ebp) ;cmp #2:720
8048d2c: 74 05 je 8048d33 <phase_4+0x51> ;相等安全退出,否则引爆炸弹
8048d2e: e8 2f 09 00 00 call 8049662 <explode_bomb>
8048d33: c9 leave
8048d34: c3 ret
简而言之,phase_4首先使用sscanf函数从输入的字符串中解析出一个数字,随后判断这个数字是否大于0。如果输入的字符串中包含了除了一个数字之外的其他内容,或者输入的是一个负数,那么都会引爆炸弹。随后,phase_4以输入的数值为参数调用func4。整体的栈帧结构如下(假设输入的是3,蓝线表示的是将对应单元的地址送入另一单元):
func4的汇编代码含注释如下:
8048cb3: 55 push %ebp
8048cb4: 89 e5 mov %esp,%ebp
8048cb6: 83 ec 08 sub $0x8,%esp
8048cb9: 83 7d 08 01 cmpl $0x1,0x8(%ebp) ;cmp input:1
8048cbd: 7f 09 jg 8048cc8 <func4+0x15> ;如果输入大于1,jmp 8048cc8
8048cbf: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%ebp) ;输入小于等于,#1放1
8048cc6: eb 15 jmp 8048cdd <func4+0x2a> ;
8048cc8: 8b 45 08 mov 0x8(%ebp),%eax ;input放到%eax中
8048ccb: 48 dec %eax ;递减%eax
8048ccc: 89 04 24 mov %eax,(%esp) ;%eax放到栈顶
8048ccf: e8 df ff ff ff call 8048cb3 <func4> ;递归调用func4
8048cd4: 89 c2 mov %eax,%edx ;递归调用的返回值放回%edx
8048cd6: 0f af 55 08 imul 0x8(%ebp),%edx ;%edx = %edx * 当前函数的input
8048cda: 89 55 fc mov %edx,-0x4(%ebp) ;%edx放到#1
8048cdd: 8b 45 fc mov -0x4(%ebp),%eax ;#1放到%eax,返回
8048ce0: c9 leave
8048ce1: c3 ret
参照栈帧结构逐行跟踪func4的执行过程,可以发现这个递归函数func4在计算输入参数的阶乘。
现在回到phase_4,可以看到phase_4将func4的返回结果与720进行比较,相等则正常退出。
那么事实上这个程序就是想让我们输入一个数,它的阶乘等于720,很容易得到这个数字是6。
所以这道题输入的是
6
phase5
这个题有一定的难度,首先给出这道题的汇编代码:
8048d35: 55 push %ebp
8048d36: 89 e5 mov %esp,%ebp
8048d38: 83 ec 38 sub $0x38,%esp
8048d3b: 8d 45 e8 lea -0x18(%ebp),%eax ;#6的地址放在%eax
8048d3e: 89 44 24 0c mov %eax,0xc(%esp) ;%eax放置在#11
8048d42: 8d 45 ec lea -0x14(%ebp),%eax ;#5的地址放在%eax
8048d45: 89 44 24 08 mov %eax,0x8(%esp) ;%eax放置在#12
8048d49: c7 44 24 04 82 99 04 movl $0x8049982,0x4(%esp) ;magic number->#13.
8048d50: 08
8048d51: 8b 45 08 mov 0x8(%ebp),%eax ;input放置到%eax
8048d54: 89 04 24 mov %eax,(%esp) ;%eax放置到栈顶
8048d57: e8 0c fb ff ff call 8048868 <sscanf@plt> ;读取一些东东
8048d5c: 89 45 fc mov %eax,-0x4(%ebp) ;sscanf返回值放置到#1
8048d5f: 83 7d fc 01 cmpl $0x1,-0x4(%ebp) ;cmp #1:1
8048d63: 7f 05 jg 8048d6a <phase_5+0x35> ;如果#1大于1,跳转到8048d6a,否则引爆炸弹
8048d65: e8 f8 08 00 00 call 8049662 <explode_bomb>
8048d6a: 8b 45 ec mov -0x14(%ebp),%eax ;#5(input1)的值放置到%eax
8048d6d: 83 e0 0f and $0xf,%eax ;只保留input1的低四位
8048d70: 89 45 ec mov %eax,-0x14(%ebp) ;把低四位放回到#5
8048d73: 8b 45 ec mov -0x14(%ebp),%eax ;#5的值放置到%eax
8048d76: 89 45 f8 mov %eax,-0x8(%ebp) ;%eax的值放置到#2
8048d79: c7 45 f0 00 00 00 00 movl $0x0,-0x10(%ebp) ;0放置到#4
8048d80: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp) ;0放置在#3
8048d87: eb 16 jmp 8048d9f <phase_5+0x6a> ;跳转到8048d9f
8048d89: ff 45 f0 incl -0x10(%ebp) ;递增#4
8048d8c: 8b 45 ec mov -0x14(%ebp),%eax ;#5的值放置到%eax
8048d8f: 8b 04 85 c0 a5 04 08 mov 0x804a5c0(,%eax,4),%eax ;%eax = *(0x804a5c0 + 4 * %eax)
8048d96: 89 45 ec mov %eax,-0x14(%ebp) ;%eax放置到#5
8048d99: 8b 45 ec mov -0x14(%ebp),%eax ;#5放置到%eax
8048d9c: 01 45 f4 add %eax,-0xc(%ebp) ;#3 = #3 + %eax
8048d9f: 8b 45 ec mov -0x14(%ebp),%eax ;#5的值放置到%eax
8048da2: 83 f8 0f cmp $0xf,%eax ;cmp %eax:15
8048da5: 75 e2 jne 8048d89 <phase_5+0x54> ;%eax不等于15,跳转至8048d89
8048da7: 83 7d f0 0b cmpl $0xb,-0x10(%ebp) ;cmp #4:11
8048dab: 75 08 jne 8048db5 <phase_5+0x80> ;如果#4不等于11,引爆炸弹
8048dad: 8b 45 e8 mov -0x18(%ebp),%eax ;#6的值放置在%eax
8048db0: 39 45 f4 cmp %eax,-0xc(%ebp) ;cmp #3:%eax
8048db3: 74 05 je 8048dba <phase_5+0x85> ;如果%eax等于#3,正常离开否则引爆炸弹
8048db5: e8 a8 08 00 00 call 8049662 <explode_bomb>
8048dba: c9 leave
8048dbb: c3 ret
对应的栈帧结构为:
大致描述一下这段代码做的事情,首先这段代码调用sscanf从输入的字符串中解析出2个整数,如果输入的整数个数不满足要求就会引爆炸弹。然后这段代码用输入的第一个数字对15取余,随后就是一个非常tricky的过程:
这段代码在索引一个数组,这个数组开始于内存地址0x804a5c0,用gdb去打印一下:
这个数组非常重要,下面我换一种表现方式,左上角是偏移量:
后面的代码实际上完成的事情是这样的,它把你输入的第一个数取低四位,也就是对15取余(这里我们可以看出来此题的答案不唯一)。我们开始使用这个数来索引数组中的元素,之后再以索引到的数字作为偏移量再次索引,这样重复11次之后要求恰好索引到15这个数字,然后输入的第二个数字对应的是这个索引过程中所经历节点数值之和,这两个条件有一个不满足就会引爆炸弹。这两个条件是:
1.从(输入的第一个数字mod15)开始索引数组,索引11次后到达15
2.路径中通过的数字加和等于第二个输入数字
现在就用到了上述的数组表格,我们采用逆推法,可以在格子中模拟索引过程如下:
15 -> 6 -> 14 -> 2 -> 1 -> 10 -> 0 -> 8 -> 4 -> 9 -> 13 -> 11
上面恰好是从15这个数字逆向推11次得到的结果,可以发现最终停留在了11,那么11就是我们输入的第一个数字。
这个过程中的路径加和为:
13 + 9 + 4 + 8 + 0 + 10 + 1 + 2 + 14 + 6 + 15 = 82
所以这道题的最终答案是:
11 82
phase6
最后一个题相对较难,我在拆这个炸弹的过程中耗费了不少时间,而且在最终解决的过程做了合理的联想。
这道题不准备给出栈帧结构,因为实际在解题的过程中发现,栈帧结构对于求解本题的作用不大。
求解本题的关键是搞懂这个fun6是在做什么,这个函数的反汇编如下:
08048dbc <fun6>:
8048dbc: 55 push %ebp
8048dbd: 89 e5 mov %esp,%ebp
8048dbf: 83 ec 10 sub $0x10,%esp
8048dc2: 8b 45 08 mov 0x8(%ebp),%eax ;地址放置到%eax
8048dc5: 89 45 f0 mov %eax,-0x10(%ebp) ;%eax放置到#4
8048dc8: 8b 45 08 mov 0x8(%ebp),%eax ;地址放置到%eax
8048dcb: 89 45 f0 mov %eax,-0x10(%ebp) ;%eax放置到#4
8048dce: 8b 45 08 mov 0x8(%ebp),%eax ;地址放置到%eax
8048dd1: 8b 40 08 mov 0x8(%eax),%eax ;%eax = *(地址+8)
8048dd4: 89 45 f4 mov %eax,-0xc(%ebp) ;%eax放置到#3 如果*(地址+8)是0,那么直接返回#4,否则
8048dd7: 8b 45 f0 mov -0x10(%ebp),%eax ;#4(地址)放置到%eax
8048dda: c7 40 08 00 00 00 00 movl $0x0,0x8(%eax) ;*(地址+8) = 0
8048de1: eb 62 jmp 8048e45 <fun6+0x89> ;jmp 8048e45
8048de3: 8b 45 f0 mov -0x10(%ebp),%eax ;#4放置到%eax
8048de6: 89 45 fc mov %eax,-0x4(%ebp) ;%eax放置到#1
8048de9: 8b 45 f0 mov -0x10(%ebp),%eax ;#4放置到%eax
8048dec: 89 45 f8 mov %eax,-0x8(%ebp) ;%eax放置到#2
8048def: eb 0f jmp 8048e00 <fun6+0x44> ;jmp 8048e00
8048df1: 8b 45 fc mov -0x4(%ebp),%eax ;#1放置到%eax
8048df4: 89 45 f8 mov %eax,-0x8(%ebp) ;%eax放置到#2
8048df7: 8b 45 fc mov -0x4(%ebp),%eax ;#1放置到%eax
8048dfa: 8b 40 08 mov 0x8(%eax),%eax ;*(%eax + 8)放置到%eax
8048dfd: 89 45 fc mov %eax,-0x4(%ebp) ;%eax放置到#1
8048e00: 83 7d fc 00 cmpl $0x0,-0x4(%ebp) ;cmp #1:0 其实就是判断指针是否非空
8048e04: 74 0e je 8048e14 <fun6+0x58> ;相等则jmp 8048e14
8048e06: 8b 45 fc mov -0x4(%ebp),%eax ;#1放置到%eax
8048e09: 8b 10 mov (%eax),%edx ;*(%eax)放置到%edx
8048e0b: 8b 45 f4 mov -0xc(%ebp),%eax ;#3放置到%eax
8048e0e: 8b 00 mov (%eax),%eax ;*(%eax)放置到%eax
8048e10: 39 c2 cmp %eax,%edx ;cmp %edx:%eax
8048e12: 7f dd jg 8048df1 <fun6+0x35> ;if %edx > %eax, jmp 8048df1
8048e14: 8b 45 f8 mov -0x8(%ebp),%eax ;#2放置到%eax
8048e17: 3b 45 fc cmp -0x4(%ebp),%eax ;cmp %eax:#1
8048e1a: 74 0b je 8048e27 <fun6+0x6b> ;相等则jmp 8048e27
8048e1c: 8b 55 f8 mov -0x8(%ebp),%edx ;#2放置到%edx
8048e1f: 8b 45 f4 mov -0xc(%ebp),%eax ;#3放置到%eax
8048e22: 89 42 08 mov %eax,0x8(%edx) ;%eax放置到*(%edx+8)
8048e25: eb 06 jmp 8048e2d <fun6+0x71> ;jmp 8048e2d
8048e27: 8b 45 f4 mov -0xc(%ebp),%eax ;#3放置到%eax
8048e2a: 89 45 f0 mov %eax,-0x10(%ebp) ;%eax放置到#4,那么这时候#4放置的是*(地址+8)
8048e2d: 8b 45 f4 mov -0xc(%ebp),%eax ;#3放置到%eax
8048e30: 8b 40 08 mov 0x8(%eax),%eax ;*(%eax + 8)放置到%eax
8048e33: 89 45 f8 mov %eax,-0x8(%ebp) ;%eax放置到#2
8048e36: 8b 55 f4 mov -0xc(%ebp),%edx ;#3放置到%edx
8048e39: 8b 45 fc mov -0x4(%ebp),%eax ;#1放置到%eax
8048e3c: 89 42 08 mov %eax,0x8(%edx) ;%eax放置到*(%edx + 8)
8048e3f: 8b 45 f8 mov -0x8(%ebp),%eax ;#2放置到%eax
8048e42: 89 45 f4 mov %eax,-0xc(%ebp) ;%eax放置到#3
8048e45: 83 7d f4 00 cmpl $0x0,-0xc(%ebp) ;cmp #3:0
8048e49: 75 98 jne 8048de3 <fun6+0x27> ;#3不是0则跳到8048de3
8048e4b: 8b 45 f0 mov -0x10(%ebp),%eax ;#3是0,#4放置到%eax,返回
8048e4e: c9 leave
8048e4f: c3 ret
很长而且也不容易理顺,我在求解时给出了类C的伪代码形式,如下所示(和前面的约定相同,这里的#N表示的是对应函数栈帧的第N个栈字):
start:
#1 = #4;
#2 = #4;
while(#1 && *(#1) > *(#3))
#2 = #1;
#1 = *(#1 + 8);
if(#1 != #2)
*(#2 + 8) = #3;
else
#4 = #3;
#2 = *(#3 + 8);
*(#3 + 8) = #1;
#3 = #2;
if(#3) goto start;
这就是上述汇编代码对应的类C语言形式,乍一看还是难以理解。这时候需要加一些联想,这里面反复出现的*(#N + 8)让我联想到了链表结构连续向后索引的时候的动作,因此这道题很有可能和链表有关。一旦想到这道题可能和链表有关之后,就立即去着手解读和模拟上面这份代码,你会发现它执行的实际上是一个链表的插入排序算法(从大到小),这个算法以你输入的数字构造一个节点,随后遍历一个在内存中已经存在的一个链表,以插入排序的形式将其与你输入的链表节点完全合并。
当函数从fun6返回至phase6时,返回值为指向链表头部的指针,随后phase6的代码会进行5次索引,它要求你输入的数值在链表中排序后正好处在第5位,phase6的汇编代码注释如下所示:
08048e50 <phase_6>:
8048e50: 55 push %ebp
8048e51: 89 e5 mov %esp,%ebp
8048e53: 83 ec 18 sub $0x18,%esp
8048e56: c7 45 f8 6c a6 04 08 movl $0x804a66c,-0x8(%ebp) ;magic number->#2
8048e5d: 8b 45 08 mov 0x8(%ebp),%eax
8048e60: 89 04 24 mov %eax,(%esp) ;input放置到栈顶
8048e63: e8 f0 f9 ff ff call 8048858 <atoi@plt> ;字符串转化为整数
8048e68: 89 c2 mov %eax,%edx ;返回值放置到%edx
8048e6a: 8b 45 f8 mov -0x8(%ebp),%eax ;#2->%eax
8048e6d: 89 10 mov %edx,(%eax) ;返回值放置到*(#2)
8048e6f: 8b 45 f8 mov -0x8(%ebp),%eax ;#2->%eax
8048e72: 89 04 24 mov %eax,(%esp) ;%eax放置到栈顶
8048e75: e8 42 ff ff ff call 8048dbc <fun6>
8048e7a: 89 45 f8 mov %eax,-0x8(%ebp) ;fun6返回值放置到#2
8048e7d: 8b 45 f8 mov -0x8(%ebp),%eax ;#2放置到%eax
8048e80: 89 45 fc mov %eax,-0x4(%ebp) ;%eax放置到#1
8048e83: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%ebp) ;1放置到#3
8048e8a: eb 0c jmp 8048e98 <phase_6+0x48> ;jmp 8048e98
8048e8c: 8b 45 fc mov -0x4(%ebp),%eax ;#1放置到%eax
8048e8f: 8b 40 08 mov 0x8(%eax),%eax ;%eax = *(%eax + 8)
8048e92: 89 45 fc mov %eax,-0x4(%ebp) ;%eax放置到#1
8048e95: ff 45 f4 incl -0xc(%ebp) ;递增#3
8048e98: 83 7d f4 05 cmpl $0x5,-0xc(%ebp) ;cmp #3:5
8048e9c: 7e ee jle 8048e8c <phase_6+0x3c> ;if #3<= 5, jmp 8048e8c
8048e9e: 8b 45 fc mov -0x4(%ebp),%eax ;否则#1放置到%eax
8048ea1: 8b 10 mov (%eax),%edx ;%edx = *(%eax)
8048ea3: a1 6c a6 04 08 mov 0x804a66c,%eax ;*(0x804a66c) -> %eax,注意这不是立即数!!!
8048ea8: 39 c2 cmp %eax,%edx ;cmp %edx:%eax
8048eaa: 74 05 je 8048eb1 <phase_6+0x61> ;相等则正常离开,否则引爆炸弹
8048eac: e8 b1 07 00 00 call 8049662 <explode_bomb>
8048eb1: c9 leave
8048eb2: c3 ret
所以当务之急是找出这个原本在内存中的链表的详细信息,我使用gdb一个个遍历了这个链表的所有结点,汇总如下:
地址 | val(decimal) | next |
---|---|---|
0x804a660 | 652 | 0x804a654 |
0x804a654 | 557 | 0x804a648 |
0x804a648 | 802 | 0x804a63c |
0x804a63c | 134 | 0x804a630 |
0x804a630 | 826 | 0x804a624 |
0x804a624 | 895 | 0x804a618 |
0x804a618 | 438 | 0x804a60c |
0x804a60c | 309 | 0x804a600 |
0x804a600 | 693 | 0(end of list) |
所以简单排一下序,这时候我们得到的链表是这样的:
895 -> 826 -> 802 ->693 -> [652 -> 557] -> 438 -> 309 -> 308
我们要想让我们输入的节点排序后位于此链表的第六个节点,那么需要让它的值介于[557, 652]之间,因此此题答案不唯一,我选择的值是:
558
感想
bomb lab在完成6个炸弹的拆除之后来到了终点,其实这还不是真正的终点,因为你可以看到在完成的反汇编代码文件中还有(secret_phase)这个隐藏的秘密阶段。考虑到时间问题,我没有继续完成和考虑这个秘密阶段。总的来说,做这个实验还是非常的有收获,它给了所有计算机专业的从业者和学生一个接触、阅读和调试机器级代码的机会,如果可以认真做这个实验,将会对计算机偏底层的逻辑表示方式有一个更加深入的理解。