CSAPP实验二—— bomb lab实验
实验前准备
在linux操作系统上安装GDB调试器。由于我是在Ubuntu虚拟机上进行实验,所以只需要在终端输入如下安装指令即可快速安装。
sudo apt install gdb
在可执行程序所在的目录下进入终端,输入gdb bomb
即可启动调试过程(bomb是可执行程序名称),可以输入quit
结束GDB调试。
第一部分(phase_1)
使用disas phase_1
反汇编phase_1函数
0x0000000000400ee0 <+0>: sub $0x8,%rsp
0x0000000000400ee4 <+4>: mov $0x402400,%esi //修改%esi的值作为参数传入后面函数
0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal>
0x0000000000400eee <+14>: test %eax,%eax #修改条件码
0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23> //根据条件码进行跳转,如果%eax寄存器里存放的是0则跳过爆炸的程序
0x0000000000400ef2 <+18>: callq 0x40143a <explode_bomb>
0x0000000000400ef7 <+23>: add $0x8,%rsp
0x0000000000400efb <+27>: retq
发现第一部分还是很简单的,基本操作就是调用strings_not_equal
函数,根据返回值来决定是否引爆炸弹。至于strings_not_equal
函数,从名字就可以看出来是实现比较两字符串是否相等的功能,通过disas strings_not_equal
也可以验证一下猜想。
比较的两个字符串一个是用户输入的字符串,一个是地址存放在%esi寄存器里的内存中的值,也就是地址为0x402400
的内存值。
可以使用x/s 0x402400
来查看对应的字符串,得到以下内容:
0x402400: “Border relations with Canada have never been better.”
所以只要输入的字符串与之完全一样,则可以通过,进入下一关。
第二部分(phase_2)
使用disas phase_2
反汇编phase_2函数
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>
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp) //将第一个输入与1比较
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 //eax寄存器存放上一个输入数字的值
0x0000000000400f1a <+30>: add %eax,%eax //上一个输入数字*2
0x0000000000400f1c <+32>: cmp %eax,(%rbx) //当前数字与上一个数字*2比较
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 //0x18=24,控制循环的终点
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
函数的汇编代码,可以知道其完成的是读取用户输入的六个整数,每个数字占用四个字节大小空间,所以最终循环的终点为栈指针偏移4*6=0x18=24的位置。
这部分代码实现的是一个循环,将输入的六个数字依次与其前一位的两倍进行比较。当且仅当第一个数字为1,后一位依次是前一位的两倍的情况下,才不会引起爆炸。所以得到第二问的答案为
1 2 4 8 16 32
第三部分(phase_3)
前两部分内容都算简单,下面来看第三块
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>
0x0000000000400f60 <+29>: cmp $0x1,%eax
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>
0x0000000000400f65 <+34>: callq 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp) //第一个输入与7比较
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106> //用ja而不是jg,暗示这一部分可能和switch语句有关
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax
0x0000000000400f75 <+50>: jmpq *0x402470(,%rax,8) //根据第一个输入的值来进行jmp的间接跳转
0x0000000000400f7c <+57>: mov $0xcf,%eax //第一个输入为0时跳转到这里
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 //第二个输入与对应的%eax里的值比较大小,只有等于才不会触发爆炸程序
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>
0x0000000000400fc4 <+129>: callq 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add $0x18,%rsp
0x0000000000400fcd <+138>: retq
这一部分卡我时间最长的是不知道第7行的%eax里函数的返回值为多少。如果对__isoc99_sscanf@plt
函数过程进行分析感觉有点困难。这里我用了一个稍微讨巧一点的办法,和第二问的read_six_numbers
函数里进行类比。因为在read_six_numbers
函数里也调用了__isoc99_sscanf@plt
函数,看看那里的%eax里存放的返回值会不会对我们有一定的提示。
0x000000000040148a <+46>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x000000000040148f <+51>: cmp $0x5,%eax
0x0000000000401492 <+54>: jg 0x401499 <read_six_numbers+61>
上图展示的是read_six_numbers
函数反汇编得到的部分代码。第二问是要求输入六个数字,而这里是%eax里存放的值>5则跳转,所以可以猜想%eax存放的是输入的参数个数。
感觉有点牵强的话,也可以动手验证一下,设置好断点后,一条指令一条指令单步调试。用i r
指令来查看寄存器里的值。发现猜想应该是没问题的,并且输入的参数应该是整数类型。
从jmpq *0x402470(,%rax,8)
这条指令往下,都是和对应跳转的位置有关。跳转到哪一行与第一个输入的数值有关,所以这一部分考察的应该是switch语句的相关知识。第一个输入与7进行无符号数比较,则可以断定第一个输入只可以为0,1,2,3,4,5,6。再分别计算0x402470(,%rax,8)的地址,就可以得到对应的跳转位置。
第一个输入值 | 跳转位置 |
---|---|
0 | 0x400f7c |
1 | 0x400fb9 |
2 | 0x400f83 |
3 | 0x400f8a |
4 | 0x400f91 |
5 | 0x400f98 |
6 | 0x400f9f |
第二个输入会根据第一个输入与相应的数值比较,只有相等才可以通过。最终可以计算第三部分的答案如下
0 207(或1 311,或2 707,或3 256,或4 389,或5 206,或6 682)
第四部分(phase_4)
phase_4反汇编得到的代码如下
0x000000000040100c <+0>: sub $0x18,%rsp
0x0000000000401010 <+4>: lea 0xc(%rsp),%rcx
0x0000000000401015 <+9>: lea 0x8(%rsp),%rdx
0x000000000040101a <+14>: mov $0x4025cf,%esi
0x000000000040101f <+19>: mov $0x0,%eax
0x0000000000401024 <+24>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000401029 <+29>: cmp $0x2,%eax //输入为两个数值
0x000000000040102c <+32>: jne 0x401035 <phase_4+41>
0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp)
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46> //第一个输入数值要小于等于14
0x0000000000401035 <+41>: callq 0x40143a <explode_bomb>
0x000000000040103a <+46>: mov $0xe,%edx
0x000000000040103f <+51>: mov $0x0,%esi
0x0000000000401044 <+56>: mov 0x8(%rsp),%edi
0x0000000000401048 <+60>: callq 0x400fce <func4> //调用func4函数,也是这一块的核心函数
0x000000000040104d <+65>: test %eax,%eax
0x000000000040104f <+67>: jne 0x401058 <phase_4+76> //%eax必须为0
0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp)
0x0000000000401056 <+74>: je 0x40105d <phase_4+81> //第二个输入要等于0
0x0000000000401058 <+76>: callq 0x40143a <explode_bomb>
0x000000000040105d <+81>: add $0x18,%rsp
0x0000000000401061 <+85>: retq
通过分析发现,在phase_4
函数中还调用了func4
函数,为了进一步确定第一个输入的数值,需要我们来查看func4
的汇编代码。如下:
0x0000000000400fce <+0>: sub $0x8,%rsp
0x0000000000400fd2 <+4>: mov %edx,%eax
0x0000000000400fd4 <+6>: sub %esi,%eax //%eax=%edx-%esi
0x0000000000400fd6 <+8>: mov %eax,%ecx
0x0000000000400fd8 <+10>: shr $0x1f,%ecx //逻辑右移31位,所以相当于保留符号位
0x0000000000400fdb <+13>: add %ecx,%eax //负数需要加一个偏置1,正数没有变化
0x0000000000400fdd <+15>: sar %eax //%eax等于(%edx-%esi)/2
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx //%ecx为(%edx+%esi)/2
0x0000000000400fe2 <+20>: cmp %edi,%ecx //与%edi也就是第一个输入比较
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36>
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx //如果大了则%edx(上界)变成均值-1,进行递归
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 //与%edi也就是第一个输入比较
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57>
0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi //如果小了则%esi(下界)变成均值+1,进行递归
0x0000000000400ffe <+48>: callq 0x400fce <func4>
0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax //注意这里会变化%eax的值
0x0000000000401007 <+57>: add $0x8,%rsp
0x000000000040100b <+61>: retq
第四部分其实想得到答案很容易,根据分析,是比较第一个输入和%edx与%esi两个寄存器的值的均值,类似二分法折半查找。最直接的答案是第一次进入func4就直接出来,也就是第一个输入正好等于均值。由于在调用之前%edx=14
,%esi=0
,所以第一个输入为7时,可以递归一次直接返回到phase_4
,这时%eax返回值也为0,不会引爆炸弹。
如果想进一步分析的话,可以看到无论函数内部实现如何,我们只希望返回值%eax必须为0就可以了。而%eax一般都是置为0的,第20行的lea 0x1(%rax,%rax,1),%eax
会对其产生影响,所以让每次递归都是上界变小就会保证%eax一定为0。
用数据会更直观一点,这里只考虑非负数的情况,假设每次递归都是上界减小,也就是均值大于第一个输入的值,直到最终结束递归。
递归次数 | %edx | %esi | %ecx | %eax | %edi(第一个输入) |
---|---|---|---|---|---|
1 | 14 | 0 | 7 | 7 | 7(一次直接返回) |
2 | 6 | 0 | 3 | 3 | 3(两次后返回) |
3 | 2 | 0 | 1 | 1 | 1(三次后返回) |
4 | 0 | 0 | 0 | 0 | 0(四次后返回) |
所以答案如下
7 0(或3 0,或1 0,或0 0…)
第五部分(phase_5)
第五部分还是蛮有意思的,里面的构思比较巧妙。首先看一下汇编代码:
0x0000000000401062 <+0>: push %rbx
0x0000000000401063 <+1>: sub $0x20,%rsp
0x0000000000401067 <+5>: mov %rdi,%rbx //%rdi和%rbx存放着输入内容的内存地址
0x000000000040106a <+8>: mov %fs:0x28,%rax //金丝雀,缓冲区的保护机制
0x0000000000401073 <+17>: mov %rax,0x18(%rsp) //检查潜在的缓冲区溢出风险
0x0000000000401078 <+22>: xor %eax,%eax
0x000000000040107a <+24>: callq 0x40131b <string_length>
0x000000000040107f <+29>: cmp $0x6,%eax
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112> //输入字符串长度为6
0x0000000000401084 <+34>: callq 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx //每一个字符的ascii码数值传递给%ecx
0x000000000040108f <+45>: mov %cl,(%rsp)
0x0000000000401092 <+48>: mov (%rsp),%rdx
0x0000000000401096 <+52>: and $0xf,%edx //仅保留%edx最后四位
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx //将0x4024b0加上%edx对应的内存位置的内容给%edx
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) //得到的值进行存放
0x00000000004010a4 <+66>: add $0x1,%rax
0x00000000004010a8 <+70>: cmp $0x6,%rax //循环的终点
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp) //超过6的后续用0填充
0x00000000004010b3 <+81>: mov $0x40245e,%esi
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi
0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal> //前面循环新得到的字符串和0x40245e存放的字符串进行比对
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
一开始拿到这一题直接看到了callq 0x401338 <strings_not_equal>
,所以想当然地直接先找到0x40245e
对应内存位置存放的字符串“flyers"试试看,结果发现爆炸了。
于是就静下心来看了看,发现比较的两个字符串其中一个是内存地址为0x40245e
的字符串,但另一个并不是输入,而是输入映射后得到的新的六个字符。也就是当你输入"flyers",结果并不会是”flyers"。具体来说,在第一次循环时,%rbx存放的是"f"的ascii码0x66,%edx中保存的是该ascii码的最后4位,也就是0x6。再根据movzbl 0x4024b0(%rdx),%edx
这句话,此时%edx得到的值为"r",不是一开始的"f"。自然也就爆炸了。所以该问题还是得找到映射关系。
有了这个思路,就可以尝试查看内存0x4024b0
存放了什么映射关系,使用x/s 0x4024b0
查看得到结果如下:
“maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?”
后面的那句话简直了,顿时觉得实验特别好玩。前面的那些小写字母得顺序就是对应的映射关系。依次找到“f", “l”, “y”, “e”, “r”, "s"对应的位置是“9”,“15”,“14”,“5”,“6”,“7”。所以只需要ascii码最后四位值等于这些的字符就可以解开。我依次选取了“0x69”,“0x6f”,“0x6e”,“0x65”,“0x66”,“0x67”。也就是如下字符,第五问就解决了。
ionefg
第六部分(phase_6)
第六部分确实挺有难度,花的时间也是前五问的总和。其实现的功能可以进行拆分,一个部分一个部分去看会让自己的思路更清晰一点。
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 //%r12d用来计数,外部循环初始化起点i
0x0000000000401114 <+32>: mov %r13,%rbp //%rbp更新
0x0000000000401117 <+35>: mov 0x0(%r13),%eax //当前输入数字
0x000000000040111b <+39>: sub $0x1,%eax //当前输入数字-1
0x000000000040111e <+42>: cmp $0x5,%eax
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52> //需要输入的每个数字小于等于6(减1小于等于5)
0x0000000000401123 <+47>: callq 0x40143a <explode_bomb>
0x0000000000401128 <+52>: add $0x1,%r12d
0x000000000040112c <+56>: cmp $0x6,%r12d //外部循环终点
0x0000000000401130 <+60>: je 0x401153 <phase_6+95>
0x0000000000401132 <+62>: mov %r12d,%ebx //内部循环起点j为外部循环的当前循环i
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 //%r13更新,后面再传递给%rbp
0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32>
/*前面做的任务是判断输入的六个数字都小于等于6,且两两不相同*/
0x0000000000401153 <+95>: lea 0x18(%rsp),%rsi //%rsi对应于循环终点
0x0000000000401158 <+100>: mov %r14,%rax //这里%r14存放的值是%rsp,作为循环起点
0x000000000040115b <+103>: mov $0x7,%ecx
0x0000000000401160 <+108>: mov %ecx,%edx
0x0000000000401162 <+110>: sub (%rax),%edx
0x0000000000401164 <+112>: mov %edx,(%rax) //输入的每个数字x[i]都变成了7-x[i]
0x0000000000401166 <+114>: add $0x4,%rax //指向下一个数字,判断是否结束循环
0x000000000040116a <+118>: cmp %rsi,%rax
0x000000000040116d <+121>: jne 0x401160 <phase_6+108>
/*前面做的任务是将输入的每个数字x[i]都变成了7-x[i]*/
0x000000000040116f <+123>: mov $0x0,%esi
0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163>
0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx //不停修改%rdx的值,修改次数与输入数字的值有关
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) //每个占8个字节
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>
/*前面做的任务是修改(%rsp+32)到(%rsp+80)的内存值(8字节为单位),根据输入的六个数字来决定*/
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>
/*让之前(%rsp+32)到(%rsp+80)的六块内存区域,前一块内存区域后八字节存放指向下一块内存区域的指针,链表结构*/
0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx) //链表尾节点的指针指向0
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
第一块是由两个循环嵌套组成的。外层循环依次遍历输入的第一个数字到第六个数字。外层循环首先判断输入的数字是否不超过6,紧接着内存循环遍历当前输入的数字到末尾剩下的数字,实现的功能是判断输入的六个数字两两不相等。
第二块比较简单,用一个寄存器%edx依次保存7与输入的数字的差值,再将差值覆盖原来输入的数字。
第三块是根据前两块实现后得到的新的6个数字进行操作。在内存0x6032d0到0x60332f中存放着6个大小为16字节的结构体变量。每个结构体变量的后8个字节都存放着一个指针,指向下一个结构体变量的内存位置。这部分代码是根据新的6个数字的数值来重新给这几个结构体排序,将这6个结构体变量的初始地址按照排好的顺序依次存放在栈指针偏移0x20~0x50的位置。
第四块是修改6个结构体变量中存放的指针值,按照重新排好的顺序让前一个指针指向后一个指针,所以这里基本可以确定第三第四块实现的是重新排列一个链表节点的功能。
第五块是验证新排列的六个结构体变量是否是按照从大到小的顺序来排列的,如果验证满足,则炸弹就拆除了。
分析后发现:node3.value>node4.value>node5.value>node6.value>node1.value>node2.value
所以第二块得到的新的六个数字为:3,4,5,6,1,2
所以一开始的输入的六个数字为:4 3 2 1 6 5
总结
bomb lab实验总共花了将近两天的时间,做完实验感觉对课程的理解加深了。不过看汇编语言还是挺累的,后面看的有点烦(lol)。实验的设计很有意思,不愧为cmu,真希望国内一些大学的课程实验也改成类似的形式。