写在前面
第一次拿到这个实验还是有点慌!之前没见过,不过还是慢慢做过来了。
这是个需要耐心的过程,请一定静下心来哦!
环境及配置
环境:Ubuntu 20.04 + GDB 调试工具
可参考配置:GDB调试工具配置(须先自行下载 Ubuntu 哈)
参考资料
MIPS 常用指令:MIPS 常用指令
MIPS寄存器说明:MIPS寄存器说明
供参考的 C 语言代码:Brief C Programs of the Bombs
队友 scy 巨巨的博客(占个坑):太奆了
phase_1
先看主函数:
...
//先看主函数
0x00400900<main>:0x00400ba4 <+676>: lw gp,16(s8)0x00400ba8 <+680>: jal 0x401fec <read_line>0x00400bac <+684>: nop0x00400bb0 <+688>: lw gp,16(s8)0x00400bb4 <+692>: sw v0,32(s8)0x00400bb8 <+696>: lw a0,32(s8)0x00400bbc <+700>: jal 0x400d6c <phase_1>
...
首先是主函数的部分,这一部分主要是为了给 phase_1 传递我们所输入的字符串。看下面的例子,首先我在 phase_1 时输入 “Helloworld!”“Hello\ world!”“Hello world!”,共 12 个字符,此时使用 GDB 进行调试:
先在 0x00400bb8 处设置一个断点,查看寄存器 $v0 的值:
可以看到,从 v0v0v0 中向后读取 12 个字符就是我们所输入的 "Helloworld!""Hello\ world!""Hello world!"。
因此,这部分的思路就是:
1. 先调用 read_line 函数去读取我们输入的字符串,并将它存入专门保存函数返回值的寄存器 v0v0v0 中;
2. 接着将 v0v0v0 中的值存到内存 (s8+32)(s8+32)(s8+32) 中去;从该内存中读取字符串保存到寄存器 a0a0a0 中(为了在 phase_1 中使用);
3. 最后跳转到 phase_1 中进行炸弹 1 的拆解。
...0x00400ba4 <+676>: lw gp,16(s8)0x00400ba8 <+680>: jal 0x401fec <read_line> //调用函数读取字符串0x00400bac <+684>: nop0x00400bb0 <+688>: lw gp,16(s8)0x00400bb4 <+692>: sw v0,32(s8) //m[$s8 + 32] = v0,将 $v0 中的值(即输入的字符串)保存到内存中0x00400bb8 <+696>: lw a0,32(s8) //a0 = m[$s8 + 32],在从内存中读出这个字符串到 $a0 中,为了在 phase_1 中使用0x00400bbc <+700>: jal 0x400d6c <phase_1> //跳转至 phase_1 中
...
知道输入的参数的传递了之后,接着看 phase_1:
0x00400d6c <+0>: addiu sp,sp,-320x00400d70 <+4>: sw ra,28(sp)0x00400d74 <+8>: sw s8,24(sp)0x00400d78 <+12>: move s8,sp0x00400d7c <+16>: sw a0,32(s8)0x00400d80 <+20>: lw a0,32(s8)0x00400d84 <+24>: lui v0,0x400x00400d88 <+28>: addiu a1,v0,100920x00400d8c <+32>: jal 0x401cf8 <strings_not_equal>0x00400d90 <+36>: nop0x00400d94 <+40>: beqz v0,0x400da4 <phase_1+56>0x00400d98 <+44>: nop0x00400d9c <+48>: jal 0x4021f0 <explode_bomb>0x00400da0 <+52>: nop0x00400da4 <+56>: move sp,s80x00400da8 <+60>: lw ra,28(sp)0x00400dac <+64>: lw s8,24(sp)0x00400db0 <+68>: addiu sp,sp,320x00400db4 <+72>: jr ra0x00400db8 <+76>: nop
首先,前四行:
这是为了在栈中开辟空间,即为 phase_1 开辟足够的空间,以便函数运行;
接下来,可以看见一个 string_not_equal 函数,用于对字符串进行比较。那么比较的是哪两个字符串呢?我们可以使用 GDB 调试来查看相关寄存器的值:
在函数 string_not_equal 调用之前,涉及到了 a0a0a0 和 a1a1a1 寄存器的处理,我们先看 a0a0a0:
再往后面输出 12 个字符:
刚好是我们输入的字符串 "Helloworld!""Hello\ world!""Hello world!",那么可以确定 a0a0a0 中存的就是输入的字符串;
接着在 0x00400d8c 设置断点之后,看 a1a1a1:
发现,我们输出 17 个字符之后,第 17 个字符就是 ‘\000’ 了,看前面 16 个字符,刚好组成:"Let′sbeginnow!""Let's\ begin\ now!""Let′s begin now!",就是内部存储的要比较的字符串啦~
结合 string_not_equal 可知,phase_1 比较的就是我们输入的字符串和 "Let′sbeginnow!""Let's\ begin\ now!""Let′s begin now!" 是否相等,并将返回值存入了专门存放返回值的寄存器 v0v0v0(相等为 1,不相等为 0)。接下来就是一个比较过程,根据注释食用哈~
0x00400d6c <+0>: addiu sp,sp,-320x00400d70 <+4>: sw ra,28(sp)0x00400d74 <+8>: sw s8,24(sp)0x00400d78 <+12>: move s8,sp //为程序段开辟栈空间0x00400d7c <+16>: sw a0,32(s8)0x00400d80 <+20>: lw a0,32(s8) //重新使输入的字符串入栈?0x00400d84 <+24>: lui v0,0x400x00400d88 <+28>: addiu a1,v0,10092 //a1 = m[0x40 + 10092],到内存中找到我们要比较的字符串0x00400d8c <+32>: jal 0x401cf8 <strings_not_equal> //进入函数比较字符串是否相等,相等则返回1,不相等返回0,这个返回存入了寄存器 $v0 中!0x00400d90 <+36>: nop0x00400d94 <+40>: beqz v0,0x400da4 <phase_1+56> //$v0 为 0 则继续(炸弹爆炸),不为 0 跳转到 <+56> 的位置,可以看到跳转后便跳过了炸弹,第一个炸弹 avoided!0x00400d98 <+44>: nop0x00400d9c <+48>: jal 0x4021f0 <explode_bomb>0x00400da0 <+52>: nop0x00400da4 <+56>: move sp,s8 //这后面就是归还栈空间,并跳回 $ra 寄存器中的地址,即return,返回主函数了!0x00400da8 <+60>: lw ra,28(sp)0x00400dac <+64>: lw s8,24(sp)0x00400db0 <+68>: addiu sp,sp,320x00400db4 <+72>: jr ra0x00400db8 <+76>: nop
炸弹一总结
爆炸点1: 比较输入字符串是否与 "Let′sbeginnow!""Let's\ begin\ now!""Let′s begin now!" 是否相等,不相等就 BOOM!BOOM!BOOM!。
phase_2
0x00400dbc <+0>: addiu sp,sp,-640x00400dc0 <+4>: sw ra,60(sp)0x00400dc4 <+8>: sw s8,56(sp)0x00400dc8 <+12>: move s8,sp0x00400dcc <+16>: lui gp,0x420x00400dd0 <+20>: addiu gp,gp,-200800x00400dd4 <+24>: sw gp,16(sp)0x00400dd8 <+28>: sw a0,64(s8)0x00400ddc <+32>: addiu v0,s8,280x00400de0 <+36>: lw a0,64(s8)0x00400de4 <+40>: move a1,v00x00400de8 <+44>: jal 0x401ba8 <read_six_numbers>0x00400dec <+48>: nop0x00400df0 <+52>: lw gp,16(s8)0x00400df4 <+56>: lw v1,28(s8)0x00400df8 <+60>: li v0,10x00400dfc <+64>: beq v1,v0,0x400e10 <phase_2+84>0x00400e00 <+68>: nop0x00400e04 <+72>: jal 0x4021f0 <explode_bomb>
首先,我们找到这道题的切入点:
0x00400de8 <+44>: jal 0x401ba8 <read_six_numbers>
read_six_numbers(),很明显,这是在告诉我们需要输入 6 个数,那我们随便输入 6 个数:2 4 7 9 15 20。(注意:若输入的不是 6 个数会直接爆炸的,这是我偶然试错试出来的!)
做个记录:炸弹1:输入的数字个数必须是 6 个,否则爆炸!
接着往下,
0x00400dfc <+64>: beq v1,v0,0x400e10 <phase_2+84>
将寄存器 v0v0v0 与寄存器 v1v1v1 进行了比较,那么我们可以使用 p $v0
和 p $v1
来查看一下两个寄存器中的值:
发现 v0v0v0 中存正好是我们输入的第一个 2,这会不会是巧合呢?我们向上查找,有这么一句:
0x00400df4 <+56>: lw v1,28(s8) //m[$s8+28] = v1
v1v1v1 中的值是从内存中来的,如果我们的猜想正确的话,继续往后读 7 个数:
前 6 个是我们输入的数,第 7 个数由于我们没有输入,因此是无效数据,这就说明我们的猜想是正确的,内存 m[m[m[s8+28]$ 中存着我们输入的第一个数,并使用了 load 访存将它放入了寄存器 v1v1v1 中。
结合着这句话:
0x00400dfc <+64>: beq v1,v0,0x400e10 <phase_2+84>
意识到是要将我们输入的第一个数与 1 作比较,相等则跳转到 phase_2+84 行去(也就避开了炸弹咯~),否则炸弹就爆炸了!
做个记录:炸弹2:第一个数必须是1,否则爆炸!
接下来循环开始,大家根据注释食用哈~
0x00400e08 <+76>: nop0x00400e0c <+80>: lw gp,16(s8)0x00400e10 <+84>: li v0,1 //循环变量,相当于 for 循环中的 i=10x00400e14 <+88>: sw v0,24(s8) //m[$s8+24] = $v0,将该循环变量入栈0x00400e18 <+92>: b 0x400ea8 <phase_2+236> //循环开始,跳转到 phase_2+236 的位置进行条件判断0x00400e1c <+96>: nop0x00400e20 <+100>: lw v0,24(s8) //$v0 = m[$s8+24],取出栈中之前存好的 i,即v0=10x00400e24 <+104>: nop0x00400e28 <+108>: addiu v0,v0,-1 //$v0--0x00400e2c <+112>: sll v0,v0,0x2 //左移操作,相当于 $v0 = $v0 * 4,把它变为int型变量的长度0x00400e30 <+116>: addiu v1,s8,24 //$s8+24 相当于到达了之前存循环变量 i 的地址0x00400e34 <+120>: addu v0,v1,v0 //$s8 + 24 + $v0 * 4,到达要取的数之前 4 个字节的位置(这一步看着有点奇怪,为什么不一步到位呢?我也是根据下一句才推出来的)0x00400e38 <+124>: lw a0,4(v0) //a0 = m[$v0+4],将我们输入的第 i 个数存入 $a0 中0x00400e3c <+128>: li v1,12 //根据后面的0x00400e40 <+132>: lw v0,24(s8) //$v0 = m[$s8+24],将之前的循环变量 i 读入0x00400e44 <+136>: nop0x00400e48 <+140>: subu v0,v1,v0 //$v0 = $v1(12) - $v0(i)0x00400e4c <+144>: lw v1,-32660(gp) //读取输入的学号0x00400e50 <+148>: sll v0,v0,0x2 //左移操作,将 $v0 变为int型变量的长度 0x00400e54 <+152>: addu v0,v1,v0 //$v0 = $v1 + $v0,到达了输入学号的倒数第 i 位的位置,具体可以使用 x $v0 进行查看0x00400e58 <+156>: lw v0,0(v0) //$v0 = m[$v0],将学号的倒数第 i 为存入 $v00x00400e5c <+160>: nop0x00400e60 <+164>: mult a0,v00x00400e64 <+168>: mflo a0 //上一句将 $a0 中的值与 $v0 中的值相乘,此句意为将结果存入 $a0 中0x00400e68 <+172>: lw v0,24(s8) //$v0 = m[$s8+24],取出之前的循环变量 i0x00400e6c <+176>: nop0x00400e70 <+180>: sll v0,v0,0x2 //将 i 变为int型变量的长度0x00400e74 <+184>: addiu v1,s8,240x00400e78 <+188>: addu v0,v1,v0 //$s8 + 24 + $v0 * 40x00400e7c <+192>: lw v0,4(v0) //这里与<+116>处类似,但要注意的是,上面的 $v0 是自减之后,那时得到的是输入的第 i 个数;在这里,$v0 = i,没有自减,因此取得的是第 i + 1 个数!//<+192>有点难以想到~0x00400e80 <+196>: nop0x00400e84 <+200>: beq a0,v0,0x400e98 <phase_2+220> //比较乘积项 a0 与 输入的第 i+1 个数 v0 是否相等//是则跳转到 phase_2+220 处咯,避开了炸弹;否则就爆炸啦!0x00400e88 <+204>: nop0x00400e8c <+208>: jal 0x4021f0 <explode_bomb>0x00400e90 <+212>: nop0x00400e94 <+216>: lw gp,16(s8) //末尾循环体0x00400e98 <+220>: lw v0,24(s8) //取得之前的循环变量 i0x00400e9c <+224>: nop0x00400ea0 <+228>: addiu v0,v0,1 //相当于 i++ 啦0x00400ea4 <+232>: sw v0,24(s8) //m[$s8+24] = v0,存入之前放置循环变量的位置0x00400ea8 <+236>: lw v0,24(s8) //第一次条件判断是从 phase_2+92 跳转下来的哈~0x00400eac <+240>: nop0x00400eb0 <+244>: slti v0,v0,6 //循环体条件判断,i < 6 ?0x00400eb4 <+248>: bnez v0,0x400e20 <phase_2+100> //为1则跳转到 phase_2+100 处咯,继续循环体;否则就往下执行了~0x00400eb8 <+252>: nop //下面就是归还空间,跳回主函数啦~//Congratulations!你坚持了下来!0x00400ebc <+256>: move sp,s80x00400ec0 <+260>: lw ra,60(sp)0x00400ec4 <+264>: lw s8,56(sp)0x00400ec8 <+268>: addiu sp,sp,640x00400ecc <+272>: jr ra0x00400ed0 <+276>: nop
做个记录:炸弹3:第 i 个输入数据与学号的倒数第 i 位相乘,结果若不与第 i + 1 个输入数据相同则爆炸!
炸弹二总结
爆炸点1: 输入的数字个数必须是 6 个,否则爆炸!
爆炸点2: 第一个数必须是1,否则爆炸!
爆炸点3: 第 i 个输入数据与学号的倒数第 i 位相乘,结果若不与第 i + 1 个输入数据相同则爆炸!
phase_3
首先,从 bomb.s 文件中 phase_3 前面的 $LC13 中可以看到输入的格式为 "%d%s%d""\%d\ \%s\ \%d""%d %s %d",即整数+字符+整数的格式。
接着往下:
0x00400ed4 <+0>: addiu sp,sp,-560x00400ed8 <+4>: sw ra,52(sp)0x00400edc <+8>: sw s8,48(sp)0x00400ee0 <+12>: move s8,sp //为程序留出空间0x00400ee4 <+16>: lui gp,0x420x00400ee8 <+20>: addiu gp,gp,-200800x00400eec <+24>: sw gp,24(sp)0x00400ef0 <+28>: sw a0,56(s8)0x00400ef4 <+32>: lw a0,56(s8)0x00400ef8 <+36>: lui v0,0x400x00400efc <+40>: addiu a1,v0,101120x00400f00 <+44>: addiu v1,s8,440x00400f04 <+48>: addiu v0,s8,400x00400f08 <+52>: addiu a2,s8,360x00400f0c <+56>: sw a2,16(sp)0x00400f10 <+60>: move a2,v10x00400f14 <+64>: move a3,v00x00400f18 <+68>: lw v0,-32636(gp) //从 bomb.s 中可以得到这里调用了 scanf() 函数0x00400f1c <+72>: nop0x00400f20 <+76>: move t9,v00x00400f24 <+80>: jalr t9 //这一步干了啥???
根据注释可以看出,以上的汇编主要是为了留出空间,调用 scanf() 输入,以及一个不知所踪的跳转(以至于在 GDB 中单步调试无法访存,根据后面盲猜是计算参数个数???)
马上就要遇见炸弹了,但还不知道 phase_3 是干嘛的,有点慌!!!!
不过往下看:
0x00400f28 <+84>: nop0x00400f2c <+88>: lw gp,24(s8)0x00400f30 <+92>: slti v0,v0,30x00400f34 <+96>: beqz v0,0x400f48 <phase_3+116>0x00400f38 <+100>: nop0x00400f3c <+104>: jal 0x4021f0 <explode_bomb>0x00400f40 <+108>: nop
似乎这个炸弹是根据比较来进行是否爆炸的,跟谁比呢?<+92> 有语句 slti v0, v0, 3
,说明是将 v0v0v0 的值与 3 进行了比较,那么 v0v0v0 又是什么呢?由于前面知道了输入应该是整数 + 字符 + 整数,因此此时查看 v0v0v0 的值是3:
嘶!!!这之前只进行了栈分配,输入,并没有出现 3 吖!栈分配无从下手,那就从输入下手!
没有输入3,却出现了 3,而我输入的是三个参数,会不会 v0v0v0 里面存的就是参数个数呢?为此,不妨改变输入试试!
我将输入改为只输入 2 个数,发现:
果然变成了 2,不妨输入 1 个参数试试,是变成了 1 的。因此,此时的比较就是判断我们输入的参数是不是小于 3 个,是则爆炸!(话说回来,这样的判断确实有点勉强,不过在猜想 + 试错的策略下确实可以解决一些问题哒!)此时小于的比较结果为 0(即参数个数 > 3,注意 beqz 和 benz区别!)就跳转,即避开了炸弹啦~
做个记录:爆炸点1:输入参数的个数若小于 3,则爆炸!
跳转过后,从 phase_3+116 看起:
0x00400f44 <+112>: lw gp,24(s8)0x00400f48 <+116>: lw v0,44(s8)0x00400f4c <+120>: nop0x00400f50 <+124>: sltiu v1,v0,80x00400f54 <+128>: beqz v1,0x401190 <phase_3+700>0x00400f58 <+132>: nop<phase_3+700>0x00401190 <+700>: li v0,1200x00401194 <+704>: sb v0,32(s8)0x00401198 <+708>: jal 0x4021f0 <explode_bomb>0x0040119c <+712>: nop
这段汇编 load 了一些数据,gpgpgp 是一个全局指针,无从下手;v0v0v0 是一个寄存器,后面使用了它来进行比较,那么我们就先看看 v0v0v0 里面存的是什么:
可以看到,$s8+36 ~ $s8+44 分别存储着我们输入的三个参数 102, ‘t’, 5。(其中字符表示为ascii码,且数据均为 16 进制)
那么,phase_3+124 的比较语句也就是为了去比较我们输入的第一个参数是否小于 8。若比较结果不小于 8,则 $v1 = 1,不发生跳转;否则跳转至 phase_3+700 的位置,炸弹爆炸!
做个记录:爆炸点2:第一个参数若大于 8 ,则炸弹爆炸!
接下来是一个根据第一个参数来进行 switch…case… 的过程,大家根据注释食用:
0x00400f5c <+136>: sll v1,v0,0x2 //将第一个输入参数 $v0 变为int型变量的长度0x00400f60 <+140>: lui v0,0x400x00400f64 <+144>: addiu v0,v0,101240x00400f68 <+148>: addu v0,v1,v00x00400f6c <+152>: lw v0,0(v0) //以上三句是为了找到 switch...case... 语句中我们应该跳转到的子句的地址,并保存在 $v0 中//可以使用 x $v0 来进行查看跳转的地址0x00400f70 <+156>: nop0x00400f74 <+160>: jr v0 //跳转到对应的 switch...case... 语句0x00400f78 <+164>: nop
我现在输入的第一个参数是 5,因此跳转到 phase_3+504 的位置。
接下来是相应的一些 case 语句,下面就来说说 switch…case… 语句中的相关操作,我们就以 case 5 为例(从上面可以看到跳转到了 <+504> 的位置哈~):
0x004010cc <+504>: li v0,116 //$v0 = 1160x004010d0 <+508>: sb v0,32(s8) //m[$s8+32]=$v0=1160x004010d4 <+512>: lw v0,-32660(gp)0x004010d8 <+516>: nop
首先,将立即数 116 存入了内存之中;
0x004010dc <+520>: lw v1,44(v0)0x004010e0 <+524>: lw v0,36(s8)0x004010e4 <+528>: nop0x004010e8 <+532>: mult v1,v00x004010ec <+536>: mflo v10x004010f0 <+540>: li v0,5130x004010f4 <+544>: beq v1,v0,0x4011e8 <phase_3+788>0x004010f8 <+548>: nop0x004010fc <+552>: jal 0x4021f0 <explode_bomb>0x00401100 <+556>: nop
在这段汇编中,我们先查看 v1v1v1 中存的是什么,根据前面的经验以及不断试错可以发现,将全局指针 $gp-32660 给到 $v0,并读取 $v0+44 的数据时,我们读取到的是输入的学号的最后一位(假设是 7)。
同时,前面说过,$s8+36 的位置存储着我们输入的第三个参数(我输入的是102),现在将它加载到了 $v0 中。
接下来,根据 $v1 = $v1 * $v0 可知,这一句是将学号的最后一位和我们输入的第三个参数相乘(这里结果位714)。
接着,将 513 存入了 $v0 寄存器中,然后将我们相乘的结果和 $v0 进行了比较。若相等,则发生跳转,也就避开炸弹啦!否则炸弹爆炸!(显然 513 ≠ 714,炸弹爆炸!)
做个记录:爆炸点3:学号的最后一位与输入的第三个数据相乘,然后与不同 case 中的数字进行比较,若不相等则爆炸!
到此已经避开 3 个炸弹啦,别急!后面还有!同时,在这里做一个内存记录:内存中 $s8 + 32 的地方存的是 116 哈,忘了请看 phase_3+504 的位置!
0x004011e8 <+788>: nop0x004011ec <+792>: b 0x4011f8 <phase_3+804>0x004011f0 <+796>: nop0x004011f4 <+800>: nop0x004011f8 <+804>: lb v0,40(s8)0x004011fc <+808>: lb v1,32(s8)0x00401200 <+812>: nop0x00401204 <+816>: beq v1,v0,0x401218 <phase_3+836>0x00401208 <+820>: nop0x0040120c <+824>: jal 0x4021f0 <explode_bomb>0x00401210 <+828>: nop0x00401214 <+832>: lw gp,24(s8)0x00401218 <+836>: move sp,s80x0040121c <+840>: lw ra,52(sp)0x00401220 <+844>: lw s8,48(sp)0x00401224 <+848>: addiu sp,sp,560x00401228 <+852>: jr ra0x0040122c <+856>: nop
经过连续跳转,我们到了 <+804> 的位置,首先 $s8+40 存着我们输入的第二个字符参数,这里我输入的是 ‘t’,将它取到 $v0 中;
接着,根据刚刚的内存分析,$s8+32 存的是 116,将它取到 $v1 中;
下面就开始比较了,由于我们输入的第二个参数是字符,因此这是一个字符的比较。走到这里,实际上,我们之前的 116 不妨去查一查,其实是字符 ‘t’!因此这里是比较 case 里面设置的字符是不是和我们输入的第二个字符参数相等。若相等,跳转到 <+836> 避开炸弹啦!否则炸弹爆炸!
做个记录:爆炸点4:每个 case 里面设置了一个字符,若我们输入的第二个字符不与这个字符相等,则炸弹爆炸!
接下来,就请看注释了哈~。case 很多,所以注释也很多,其实看懂了上面的内容,所有 case 都是一个套路了!
炸弹三总结
爆炸点1: 输入参数的个数若小于 3,则爆炸!
爆炸点2: 第一个参数若大于 8 ,则炸弹爆炸!
爆炸点3: 学号的最后一位与输入的第三个数据相乘,然后与不同 case 中的数字进行比较,若不相等则爆炸!
爆炸点4: 每个 case 里面设置了一个字符,若我们输入的第二个字符不与这个字符相等,则炸弹爆炸!
phase_4
phase_4 是一个递归,由 phase_4 之前的 $LC14 发现我们输入的形式是 %d\%d%d 即一个整数。首先我们看调用递归函数之前的部分:
先看第一个炸弹怎样才能避开呢?
0x004012bc <+0>: addiu sp,sp,-400x004012c0 <+4>: sw ra,36(sp)0x004012c4 <+8>: sw s8,32(sp)0x004012c8 <+12>: move s8,sp0x004012cc <+16>: lui gp,0x420x004012d0 <+20>: addiu gp,gp,-200800x004012d4 <+24>: sw gp,16(sp)0x004012d8 <+28>: sw a0,40(s8)0x004012dc <+32>: lw v1,40(s8)0x004012e0 <+36>: lui v0,0x400x004012e4 <+40>: addiu v0,v0,101560x004012e8 <+44>: move a0,v10x004012ec <+48>: move a1,v00x004012f0 <+52>: addiu v0,s8,240x004012f4 <+56>: move a2,v0 //前面是为了给函数申请空间0x004012f8 <+60>: lw v0,-32636(gp) //调用输入函数进行输入0x004012fc <+64>: nop0x00401300 <+68>: move t9,v00x00401304 <+72>: jalr t9 //去判断是否有输入???有输入返回1,无输入返回00x00401308 <+76>: nop0x0040130c <+80>: lw gp,16(s8)
--Type <RET> for more, q to quit, c to continue without paging--c0x00401310 <+84>: move v1,v00x00401314 <+88>: li v0,1 //由于输入一个整数,这里就是去判断是否有输入0x00401318 <+92>: bne v1,v0,0x401330 <phase_4+116> //与 1 进行比较,有输入则继续,五输入则跳转至 <+116>,炸弹爆炸!0x0040131c <+96>: nop
在 phase_3 中,类似于 jalr t9
这样的指令是去计算参数个数,但在此处似乎不太一样。无论我的输入是两个数还是两位数,$v0 都返回 1;同时我们必须输入一个数。因此在这里我认为是为了判断有无输入了~
做个记录:爆炸点1:判断有无输入,直到有输入!
接着往下看:
0x00401334 <+120>: nop0x00401338 <+124>: lw gp,16(s8)0x0040133c <+128>: nop0x00401340 <+132>: lw v0,-32660(gp)0x00401344 <+136>: nop0x00401348 <+140>: lw v0,44(v0)0x0040134c <+144>: nop
首先,我们在 phase_3 的分析中说过,全局指针 $gp-32660 之后,再访问其 +44 后的内存存储着我们输入的学号的最后一位,所以这一段意为取出输入的学号的最后一位到 $v0 中去。
下面这段汇编进行了一个与运算:
0x00401350 <+148>: andi v0,v0,0x10x00401354 <+152>: andi v0,v0,0xff0x00401358 <+156>: beqz v0,0x40139c <phase_4+224>0x0040135c <+160>: nop
前两句对学号的最后一位 $v0 进行了两个与运算以判断奇偶,举个例子:
(5)10=(00000101)2(5)_{10}=(00000101)_2(5)10=(00000101)2将它与 0x1 进行与运算∵0x1=(00000001)2∴(00000101)2&(00000001)2=(00000001)2\because 0x1=(00000001)_2 \\ \therefore (00000101)_2\ \ \& \ \ (00000001)_2=(00000001)_2∵0x1=(00000001)2∴(00000101)2 & (00000001)2=(00000001)2表示 5 是一个奇数。(这里我认为可以仅和 0x1 进行与运算就可以了,后面的再与 0xff 进行与运算没搞懂???)
然后便根据奇偶检验结果进行跳转:若为 1(表示学号最后一位为奇数),继续进行;若为 0(表示学号最后一位为偶数,发生跳转)。
假设这里我们输入的学号的最后一位为 7,那么继续进行!
往下看:
0x0040135c <+160>: nop0x00401360 <+164>: lw v0,24(s8)0x00401364 <+168>: nop0x00401368 <+172>: move a0,v00x0040136c <+176>: jal 0x401230 <func4>0x00401370 <+180>: nop0x00401374 <+184>: lw gp,16(s8)0x00401378 <+188>: move v1,v00x0040137c <+192>: li v0,80x00401380 <+196>: beq v1,v0,0x4013d0 <phase_4+276>0x00401384 <+200>: nop0x00401388 <+204>: jal 0x4021f0 <explode_bomb>0x0040138c <+208>: nop
首先,load 进了一个数,通过在 GDB 中使用 p $v0
可以看到,此时 $v0 中的数是我们输入的那个数。
接着,我们将 $v0 中的值(即56)传入了寄存器 $a0 中,然后便进入递归函数 func4 了。下面我们就去看看这个递归函数干了什么!(大家根据注释食用哈~)
0x00401230 <+0>: addiu sp,sp,-400x00401234 <+4>: sw ra,36(sp)0x00401238 <+8>: sw s8,32(sp)0x0040123c <+12>: sw s0,28(sp)0x00401240 <+16>: move s8,sp //栈空间申请0x00401244 <+20>: sw a0,40(s8) //m[$s8+40]=$a0=56,这个 $a0 相当于传入的参数,在 phase_4 跳转之前已经设置为我们输入的数 56 啦!0x00401248 <+24>: lw v0,40(s8) //$v0=m[$s8+40],将我们传入的参数给 $v00x0040124c <+28>: nop0x00401250 <+32>: slti v0,v0,2 //比较这个参数是否小于 20x00401254 <+36>: bnez v0,0x40129c <func4+108> //若小于 2,$v0=1,跳转到 <+108> 的地方去;反之则继续!//先到 <+108> 看看发生了什么哈~0x00401258 <+40>: nop0x0040125c <+44>: lw v0,40(s8) //$v0=m[$s8+40]=56,将之前存在内存中的参数给到 $v00x00401260 <+48>: nop0x00401264 <+52>: addiu v0,v0,-1 //$v0--;相当于 $v0 = 55了0x00401268 <+56>: move a0,v0 //$a0=$v0=550x0040126c <+60>: jal 0x401230 <func4> //再次执行函数,就是递归调用了//仔细看,这里传入的参数是 55,联系函数开始时是通过 $a0 传参的,这一切就说得通了//这一步相当于是递归调用 f(x-1)!0x00401270 <+64>: nop0x00401274 <+68>: move s0,v0 //$v0 是函数的返回值,因此这里就是 f(x-1) 函数执行后的返回值//若在这一步在 GDB 中进行调试,会有大量continue需要输入,因此是个递归函数hhh0x00401278 <+72>: lw v0,40(s8) //$v0=m[$s8+40]=56,同样将之前存在内存中的参数给到 $v00x0040127c <+76>: nop0x00401280 <+80>: addiu v0,v0,-2 //$v0 -= 2; $v0 自减2
--Type <RET> for more, q to quit, c to continue without paging--c0x00401284 <+84>: move a0,v0 //又是一个传参0x00401288 <+88>: jal 0x401230 <func4> //再次执行函数,相当于递归调用 f(x-2) 啦~0x0040128c <+92>: nop0x00401290 <+96>: addu v0,s0,v0 //此时 $s0 中是 f(x-1) 的返回值, $v0 是 f(x-2) 的返回值,二者相加返回给 $v00x00401294 <+100>: b 0x4012a0 <func4+112> //跳转到 <+112> 归还空间啦~0x00401298 <+104>: nop0x0040129c <+108>: li v0,1 //$v0=1,下面就是归还栈空间了。这就是递归的终止条件//if($v0 < 2) return 1;0x004012a0 <+112>: move sp,s80x004012a4 <+116>: lw ra,36(sp)0x004012a8 <+120>: lw s8,32(sp)0x004012ac <+124>: lw s0,28(sp)0x004012b0 <+128>: addiu sp,sp,400x004012b4 <+132>: jr ra0x004012b8 <+136>: nop
因此,这个递归函数实质上就是:
if (x < 2)return 1;
elsereturn f(x-1) + f(x-2);
这个递归函数就是一个求斐波那契数列的值,得到了递归函数的返回值并保存在 $v0 中,我们继续看下面的汇编:(根据注释食用哈!)
0x00401370 <+180>: nop0x00401374 <+184>: lw gp,16(s8)0x00401378 <+188>: move v1,v0 //将函数调用的返回值给到 $v10x0040137c <+192>: li v0,8 //$v0 = 80x00401380 <+196>: beq v1,v0,0x4013d0 <phase_4+276> //比较 $v1 和 $v0 中的值,即函数返回值是否等于 8,相等则跳转,避开了炸弹;否则炸弹爆炸啦!0x00401384 <+200>: nop0x00401388 <+204>: jal 0x4021f0 <explode_bomb>0x0040138c <+208>: nop0x00401390 <+212>: lw gp,16(s8)0x00401394 <+216>: b 0x4013d0 <phase_4+276>0x00401398 <+220>: nop...0x004013d0 <+276>: move sp,s8 //跳转到 <+276>,就是简单归还空间了!0x004013d4 <+280>: lw ra,36(sp)0x004013d8 <+284>: lw s8,32(sp)0x004013dc <+288>: addiu sp,sp,400x004013e0 <+292>: jr ra0x004013e4 <+296>: nop
以上是奇数的情况,偶数的情况相同,只是对递归函数返回值的要求不同,我们直接看需要比较的值:
...0x004013ac <+240>: nop0x004013b0 <+244>: lw gp,16(s8)0x004013b4 <+248>: move v1,v00x004013b8 <+252>: li v0,130x004013bc <+256>: beq v1,v0,0x4013d0 <phase_4+276>0x004013c0 <+260>: nop
...
是 13!
因此,为了避开这个炸弹,就要使我们的斐波那契数列 f(input)f(input)f(input) 的返回值等于相应的值:
Inorderavoidthebomb,then{f(input)=8,if学号最后一位是奇数f(input)=13,if学号最后一位是偶数In\ order\ avoid\ the\ bomb, then\left\{ \begin{aligned} &f(input) = 8, &if \text{学号最后一位是奇数}\\ &f(input) = 13, &if \text{学号最后一位是偶数} \end{aligned} \right.In order avoid the bomb,then{f(input)=8,f(input)=13,if学号最后一位是奇数if学号最后一位是偶数所以,奇数输入 5,偶数输入 6,成功啦!
做个记录:爆炸点2:输入n,斐波那契求值得到 f(n)。根据学号最后一位的奇偶性来判断 f(n) 是否与目标值相等。不相等则爆炸!
炸弹4总结
爆炸点1: 判断有无输入;
爆炸点2: 输入n,斐波那契求值得到 f(n)。根据学号最后一位的奇偶性来判断 f(n) 是否与目标值相等。不相等则爆炸!
phase_5
首先,说个题外话,在看这道题之前我们看怎样输入时,发现 $LC15 那儿有一个 “giants”,不是平常的 %d\%d%d 什么的。莫非…就是输入“giants”?尝试输入了一下,似乎并不是hhh
直接看汇编吧!(第一个炸弹之前)
0x004013e8 <+0>: addiu sp,sp,-720x004013ec <+4>: sw ra,68(sp)0x004013f0 <+8>: sw s8,64(sp)0x004013f4 <+12>: move s8,sp //申请空间0x004013f8 <+16>: sw a0,72(s8)0x004013fc <+20>: lw a0,72(s8)0x00401400 <+24>: jal 0x401c78 <string_length>0x00401404 <+28>: nop0x00401408 <+32>: move v1,v00x0040140c <+36>: li v0,60x00401410 <+40>: beq v1,v0,0x401420 <phase_5+56>0x00401414 <+44>: nop0x00401418 <+48>: jal 0x4021f0 <explode_bomb>0x0040141c <+52>: nop
前四句,常规申请空间操作。接下来存了又读了个啥?(我随便输入了“youbom”)在 GDB 中看看:
可以看到,$a0 里面存入的就是我们输入的字符串,
...0x004013f8 <+16>: sw a0,72(s8)0x004013fc <+20>: lw a0,72(s8)
...
这两句将它存入了 m[$s8+72]
的位置。
接下来发生了跳转:
...0x00401400 <+24>: jal 0x401c78 <string_length>
...
顾名思义,这里跳转到了计算输入字符串的长度,并将返回结果存入了专门存储返回值的寄存器 $v0 中。接下来比较了字符串的长度是否为 6,否则炸弹爆炸!(请看下面注释了)
0x00401400 <+24>: jal 0x401c78 <string_length> //字符串长度,存储在 $v0 中0x00401404 <+28>: nop0x00401408 <+32>: move v1,v0 //$v1=$v0,将长度放入 $v0 中0x0040140c <+36>: li v0,6 //$v0=60x00401410 <+40>: beq v1,v0,0x401420 <phase_5+56> //比较字符串长度是否为6,是则跳转,就避开炸弹啦;否则炸弹爆炸!0x00401414 <+44>: nop0x00401418 <+48>: jal 0x4021f0 <explode_bomb>0x0040141c <+52>: nop
做个记录:爆炸点1:输入的字符串长度若不为 6,炸弹爆炸!
接着往下,
0x00401420 <+56>: sw zero,24(s8) //m[$s8+24]=0,初始化循环变量 i = 00x00401424 <+60>: b 0x4014a8 <phase_5+192> //跳转至条件判断,下面请看 <+192> 了哈!0x00401428 <+64>: nop0x0040142c <+68>: lw v0,24(s8) //$v0=m[$s8+24],拿到之前的循环变量0x00401430 <+72>: lw v1,24(s8) //$v1=m[$s8+24],拿到之前的循环变量0x00401434 <+76>: lw a0,72(s8) //$a0=m[$s8+72],拿到之前的字符串!
--Type <RET> for more, q to quit, c to continue without paging--c0x00401438 <+80>: nop0x0040143c <+84>: addu v1,a0,v1 //到达字符串中的第 i+1 个字符,注意 i 是从 0 开始的哈!第一次拿到第 1 个字符,后面以此类推~0x00401440 <+88>: lb v1,0(v1) //拿到对应的第 i+1 个字符0x00401444 <+92>: nop0x00401448 <+96>: andi v1,v1,0xff0x0040144c <+100>: andi v1,v1,0xf //取得字符所表示的二进制数的后四位//比如:第一个字符‘y’,其ASCII码的十六进制表示为:0x79(01111001),后四位:10010x00401450 <+104>: sll v0,v0,0x2 //将 $v0 中的值变为int型变量的长度0x00401454 <+108>: addiu a0,s8,24 //到达之前存储循环变量 i 的地址0x00401458 <+112>: addu v0,a0,v0 //从这个地址向后移 4 位0x0040145c <+116>: sw v1,12(v0) //将字符的后四位存储在 m[$s8 + 24 + 12 + i * 4] 的位置
...0x004014a8 <+192>: lw v0,24(s8) //拿到之前存储的循环变量i0x004014ac <+196>: nop0x004014b0 <+200>: slti v0,v0,6 //循环条件判断,i < 6 ?0x004014b4 <+204>: bnez v0,0x40142c <phase_5+68> //不为 0 则跳转//若 i < 6, $v0 = 1,跳转至循环体部分;若 i >= 6, $v0 = 0,循环结束,继续执行。0x004014b8 <+208>: nop
以上的汇编目的就是对我们输入的字符串的每一个字符,取 ASCII 码的后四位并保存。
紧接着:
0x00401460 <+120>: lw a0,24(s8) //取得之前的循环变量 i0x00401464 <+124>: lw v0,24(s8) //取得之前的循环变量 i0x00401468 <+128>: nop0x0040146c <+132>: sll v0,v0,0x2 //将 i 变为int型长度0x00401470 <+136>: addiu v1,s8,24 //读得 i 的地址0x00401474 <+140>: addu v0,v1,v00x00401478 <+144>: lw v1,12(v0) //回想之前存字符后四位的地方,这里就是取得字符的后四位,存到 $v1 中0x0040147c <+148>: lui v0,0x410x00401480 <+152>: addiu v0,v0,125240x00401484 <+156>: addu v0,v1,v00x00401488 <+160>: lb v1,0(v0)0x0040148c <+164>: addiu v0,s8,240x00401490 <+168>: addu v0,v0,a00x00401494 <+172>: sb v1,4(v0)
看到 <+148> 时,有点懵,这是干了啥?4 句过后,又是去访问循环变量的地址。因此,我们在这一句设断点看看发生了啥。
这个 +9 提示了我们,莫非前面还有字符?我们使用 x/s $v0-9
发现:
这个地址存的就是这样一个字符串。现在我们回到那 4 句汇编:
0x00401478 <+144>: lw v1,12(v0)0x0040147c <+148>: lui v0,0x410x00401480 <+152>: addiu v0,v0,12524 //找到这个内置字符串的开头地址0x00401484 <+156>: addu v0,v1,v0 //从开头地址往后移动 $v0 位//$v0 是什么?字符的后四位。我们第一个输入的字符是 ‘y’,后四位 1001,也就是 9。//我们在第一次使用 x/s $v0 输出的时候,输出就是从第 9 位开始的!//说明我们字符的后四位就是为了在这个内置字符串中找到相应的字符!0x00401488 <+160>: lb v1,0(v0) //为了验证上面的猜想,我们在这里输出 $v1 试试
发现就是 ‘b’。那么基本思路就有了:首先根据我们输入的字符串中的每个字符取 ASCII 码的后四位;再根据这后四位再内置字符串中找到对应的字符(举个栗子:见下面的表格);接着…(往后看吧)
‘y’ | ‘o’ | ‘u’ | ‘b’ | ‘o’ | ‘m’ | |
---|---|---|---|---|---|---|
后四位 | 1001 | 1111 | 0101 | 0010 | 1111 | 1101 |
十进制 | 9 | 15 | 5 | 2 | 15 | 13 |
“isrveawhobpnutfg” | ‘b’ | ‘g’ | ‘a’ | ‘r’ | ‘g’ | ‘t’ |
有了这个字符串:接着往下:
0x00401488 <+160>: lb v1,0(v0)0x0040148c <+164>: addiu v0,s8,240x00401490 <+168>: addu v0,v0,a00x00401494 <+172>: sb v1,4(v0) //将它们存入 m[$s8 + 28 + i]的位置
Going on:
0x004014b8 <+208>: nop0x004014bc <+212>: sb zero,34(s8) //字符串的尾部置空 '\0'0x004014c0 <+216>: addiu v0,s8,280x004014c4 <+220>: move a0,v0 //这两句到达之前存经过映射后的字符串('bgargt')的位置0x004014c8 <+224>: lui v0,0x400x004014cc <+228>: addiu a1,v0,10160 //另一个比较的字符串,GDB 查看查看0x004014d0 <+232>: jal 0x401cf8 <strings_not_equal>0x004014d4 <+236>: nop
居然是 “giants”,之前差点当作输入!破案了,就是要通过映射将输入映射为 “giants” !
做个记录:爆炸点2:通过内置字符串映射,若结果为“giants”,则通过;否则炸弹爆炸!
下面就看注释了哈!
0x004014d8 <+240>: beqz v0,0x4014e8 <phase_5+256> //比较结果//注意!string_NOT_equal,若相等返回的是 0,而不是 10x004014dc <+244>: nop0x004014e0 <+248>: jal 0x4021f0 <explode_bomb>0x004014e4 <+252>: nop0x004014e8 <+256>: move sp,s8 //归还空间,拆弹结束!0x004014ec <+260>: lw ra,68(sp)0x004014f0 <+264>: lw s8,64(sp)0x004014f4 <+268>: addiu sp,sp,720x004014f8 <+272>: jr ra0x004014fc <+276>: nop
炸弹五总结
爆炸点1: 输入的字符串长度若不为 6,炸弹爆炸!
爆炸点2: 通过内置字符串映射,若结果为“giants”,则通过;否则炸弹爆炸!
phase_6
0x00401500 <+0>: addiu sp,sp,-960x00401504 <+4>: sw ra,92(sp)0x00401508 <+8>: sw s8,88(sp)0x0040150c <+12>: move s8,sp0x00401510 <+16>: lui gp,0x420x00401514 <+20>: addiu gp,gp,-200800x00401518 <+24>: sw gp,16(sp)0x0040151c <+28>: sw a0,96(s8)0x00401520 <+32>: lui v0,0x410x00401524 <+36>: addiu v0,v0,125920x00401528 <+40>: sw v0,32(s8)0x0040152c <+44>: addiu v0,s8,360x00401530 <+48>: lw a0,96(s8)0x00401534 <+52>: move a1,v00x00401538 <+56>: jal 0x401ba8 <read_six_numbers>
前面就是申请程序空间,然后 read_six_numbers() 读取 6 个数。
接下来就是循环开始了:
0x0040153c <+60>: nop0x00401540 <+64>: lw gp,16(s8)0x00401544 <+68>: sw zero,28(s8) //m[$s8+28]=0,设置循环变量 i = 00x00401548 <+72>: b 0x40163c <phase_6+316> //跳转到第一重循环的条件判断,看<+316>的位置哈0x0040154c <+76>: nop
--Type <RET> for more, q to quit, c to continue without paging--c0x00401550 <+80>: lw v0,28(s8) //取得循环变量 i0x00401554 <+84>: nop0x00401558 <+88>: sll v0,v0,0x2 //将 i 变成int型长度0x0040155c <+92>: addiu v1,s8,240x00401560 <+96>: addu v0,v1,v00x00401564 <+100>: lw v0,12(v0) //取得输入的第 i 个数,可以在 GDB 中 ‘p $v0’ 查看0x00401568 <+104>: nop0x0040156c <+108>: slti v0,v0,7 //条件判断,第 i 个数是否小于 7,input[i] < 7?0x00401570 <+112>: beqz v0,0x40159c <phase_6+156> //若大于 7,则跳转到<+156>,炸弹爆炸;否则继续0x00401574 <+116>: nop0x00401578 <+120>: lw v0,28(s8) //取得之前的循环变量 i0x0040157c <+124>: nop0x00401580 <+128>: sll v0,v0,0x2 //将 i 变成int型长度0x00401584 <+132>: addiu v1,s8,240x00401588 <+136>: addu v0,v1,v00x0040158c <+140>: lw v0,12(v0) //取得输入的第 i 个数,可以在 GDB 中 ‘p $v0’ 查看0x00401590 <+144>: nop0x00401594 <+148>: bgtz v0,0x4015a8 <phase_6+168> //判断第 i 个数是否大于 0,若是,则发生跳转,避开了炸弹;否则炸弹爆炸!0x00401598 <+152>: nop0x0040159c <+156>: jal 0x4021f0 <explode_bomb>0x004015a0 <+160>: nop0x004015a4 <+164>: lw gp,16(s8)0x004015a8 <+168>: lw v0,28(s8) //取得之前的循环变量 i0x004015ac <+172>: nop0x004015b0 <+176>: addiu v0,v0,1 //第二重循环变量的设定,j = i + 10x004015b4 <+180>: sw v0,24(s8) //m[$s8+24]=v0,将第二重循环的循环变量进行存储0x004015b8 <+184>: b 0x401618 <phase_6+280> //第二重循环的条件判断,下面看<+280>哈0x004015bc <+188>: nop0x004015c0 <+192>: lw v0,28(s8) //取得第一重循环变量 i0x004015c4 <+196>: nop0x004015c8 <+200>: sll v0,v0,0x2 //将 i 变成int型长度0x004015cc <+204>: addiu v1,s8,240x004015d0 <+208>: addu v0,v1,v00x004015d4 <+212>: lw v1,12(v0) //取得输入的第 i 个数字0x004015d8 <+216>: lw v0,24(s8) //取得第二重循环变量 j0x004015dc <+220>: nop0x004015e0 <+224>: sll v0,v0,0x20x004015e4 <+228>: addiu a0,s8,240x004015e8 <+232>: addu v0,a0,v00x004015ec <+236>: lw v0,12(v0) //取得第 j 个数字0x004015f0 <+240>: nop0x004015f4 <+244>: bne v1,v0,0x401608 <phase_6+264> //比较 $v0 和 $v1 的值,若不相等则跳转,避开了炸弹;不相等则炸弹爆炸!0x004015f8 <+248>: nop0x004015fc <+252>: jal 0x4021f0 <explode_bomb>0x00401600 <+256>: nop0x00401604 <+260>: lw gp,16(s8)0x00401608 <+264>: lw v0,24(s8) //取得第二重循环变量 j0x0040160c <+268>: nop0x00401610 <+272>: addiu v0,v0,1 //j++0x00401614 <+276>: sw v0,24(s8)0x00401618 <+280>: lw v0,24(s8) //第二重循环的条件判断0x0040161c <+284>: nop0x00401620 <+288>: slti v0,v0,6 //条件判断,i < 6 ?0x00401624 <+292>: bnez v0,0x4015c0 <phase_6+192> //若 < 6,则 $v0 为1,再次执行循环题;反之,则继续往下执行啦~0x00401628 <+296>: nop0x0040162c <+300>: lw v0,28(s8)0x00401630 <+304>: nop0x00401634 <+308>: addiu v0,v0,10x00401638 <+312>: sw v0,28(s8)0x0040163c <+316>: lw v0,28(s8) //取得之前的循环变量0x00401640 <+320>: nop0x00401644 <+324>: slti v0,v0,6 //第一重循环条件判断,i < 6 ?0x00401648 <+328>: bnez v0,0x401550 <phase_6+80>0x0040164c <+332>: nop
做个记录:爆炸点1:若输入的数 < 0 或者 > 7,则炸弹爆炸!
做个记录:爆炸点2:若第 i 个数和它后面的某个数相等,则炸弹爆炸!
在第一个部分,进行了一个二重循环。其中第一重循环判断输入的数是不是在 [0,7][0, 7][0,7] 的范围内,第二重循环判断第 i 个数是否和它之后的某个数相等;
phase_6 中基本都是循环,所以接下来第二个循环开始啦:
0x0040164c <+332>: nop0x00401650 <+336>: sw zero,28(s8) //设置第一重循环变量 i = 00x00401654 <+340>: b 0x4016f8 <phase_6+504> //第一重循环条件判断,下面看<+504>部分哈!0x00401658 <+344>: nop0x0040165c <+348>: lui v0,0x410x00401660 <+352>: addiu v0,v0,125920x00401664 <+356>: sw v0,32(s8)
看到<+352>的时候会有些疑惑,究竟存储了个什么。我们在 GDB 中进行调试看看:
是一个 node1 标识,盲猜是一个节点,不妨先放在这儿,等会儿回来推一下
0x0040164c <+332>: nop0x00401650 <+336>: sw zero,28(s8) //设置第一重循环变量 i = 00x00401654 <+340>: b 0x4016f8 <phase_6+504> //第一重循环条件判断,下面看<+504>部分哈!0x00401658 <+344>: nop0x0040165c <+348>: lui v0,0x410x00401660 <+352>: addiu v0,v0,125920x00401664 <+356>: sw v0,32(s8)0x00401668 <+360>: li v0,1 //设置第二重循环变量 j = 10x0040166c <+364>: sw v0,24(s8) //m[$s8+24]=$v00x00401670 <+368>: b 0x40169c <phase_6+412> //第二重循环条件判断,下面看 <+412> 吧0x00401674 <+372>: nop0x00401678 <+376>: lw v0,32(s8) //找到一个位置0x0040167c <+380>: nop0x00401680 <+384>: lw v0,8(v0) //将它向后移0x00401684 <+388>: nop0x00401688 <+392>: sw v0,32(s8) //保存移动之后的位置0x0040168c <+396>: lw v0,24(s8) //取得第二重循环变量 j0x00401690 <+400>: nop0x00401694 <+404>: addiu v0,v0,1 //j++0x00401698 <+408>: sw v0,24(s8)0x0040169c <+412>: lw v0,28(s8) //获得第一重循环变量 i0x004016a0 <+416>: nop0x004016a4 <+420>: sll v0,v0,0x2 //将 i 变成int型长度0x004016a8 <+424>: addiu v1,s8,240x004016ac <+428>: addu v0,v1,v00x004016b0 <+432>: lw v1,12(v0) //取得输入的第 i 个数0x004016b4 <+436>: lw v0,24(s8) //取得第二重循环变量 j0x004016b8 <+440>: nop0x004016bc <+444>: slt v0,v0,v1 //判断 j 是否小于输入的第 i 个数。若是,则 1 ,集训循环;若不是,则 0,结束循环向下执行0x004016c0 <+448>: bnez v0,0x401678 <phase_6+376>0x004016c4 <+452>: nop
在执行到 <+388> 的位置时,我们每次查看 $v0 中的值,可以发现:
根据这些标识,可以确定,这是一个链表类型的题目。
那么我们的第二重循环代表什么呢?仔细回想一下可以发现,就是这段C语言代码:
for (int j = 1; j < a[i]; ++j)node = node->next
意思就是根据我们输入的数 s,去找到第 s 个节点。
那么找到了我们要干些什么?接着往下看:
0x004016c4 <+452>: nop0x004016c8 <+456>: lw v0,28(s8) //第一重循环变量0x004016cc <+460>: nop0x004016d0 <+464>: sll v0,v0,0x20x004016d4 <+468>: addiu v1,s8,240x004016d8 <+472>: addu v0,v1,v00x004016dc <+476>: lw v1,32(s8) //$v1=m[$s8+32],取了一个数是什么呢?可以在 GDB 中看一看(下面有截图哈!),可以看到 $v1 存的就是对应节点的值0x004016e0 <+480>: nop0x004016e4 <+484>: sw v1,36(v0) //m[$v0+36]=$v1,将这个值存到内存中。//由于这是一个循环,因此存的地址是连续的。因此可以看作将节点的值存到一个数组里面去了,假设这个数组就是newList哈~0x004016e8 <+488>: lw v0,28(s8) //取得第一重循环变量 i0x004016ec <+492>: nop0x004016f0 <+496>: addiu v0,v0,1 //++i0x004016f4 <+500>: sw v0,28(s8) //保存循环变量0x004016f8 <+504>: lw v0,28(s8) //取得第一重循环变量0x004016fc <+508>: nop0x00401700 <+512>: slti v0,v0,6 //第一重循环条件判断,i < 6 ? 若不小于,则继续循环;若小于则结束循环往下执行啦!0x00401704 <+516>: bnez v0,0x40165c <phase_6+348>0x00401708 <+520>: nop
在 <+476> 对 $v1 进行查看时发现:
node3 中的值是 0x12d,由于我第一个输入为 3,所以刚好也满足前面的推断。接下来把所有节点的值都列出来啦:
节点 | 值 |
---|---|
node1 | 0x0fd |
node2 | 0x2d5 |
node3 | 0x12d |
node4 | 0x3e5 |
node5 | 0x0d4 |
node6 | 0x1b0 |
接下来:
0x0040170c <+524>: lw v0,60(s8)0x00401710 <+528>: nop0x00401714 <+532>: sw v0,32(s8) //到达 newList[0]0x00401718 <+536>: li v0,1 //设置循环变量 i = 10x0040171c <+540>: sw v0,28(s8) //m[$s8+28]=$v0,存储循环变量0x00401720 <+544>: b 0x40177c <phase_6+636> //循环条件判断,下面看<+636>哈0x00401724 <+548>: nop0x00401728 <+552>: lw v0,28(s8) //获得循环变量 i0x0040172c <+556>: nop0x00401730 <+560>: sll v0,v0,0x2 //将 i 变成int型长度0x00401734 <+564>: addiu v1,s8,240x00401738 <+568>: addu v0,v1,v00x0040173c <+572>: lw v1,36(v0) //得到 newList[i],第一次中这个是第二个节点哦!0x00401740 <+576>: lw v0,32(s8) //第一次中这是第一个节点node0x00401744 <+580>: nop0x00401748 <+584>: sw v1,8(v0) //node->next=newList[i]0x0040174c <+588>: lw v0,28(s8) //取得循环变量 i0x00401750 <+592>: nop0x00401754 <+596>: sll v0,v0,0x2 //将 i 变成int型长度0x00401758 <+600>: addiu v1,s8,240x0040175c <+604>: addu v0,v1,v00x00401760 <+608>: lw v0,36(v0) //node = node->next0x00401764 <+612>: nop0x00401768 <+616>: sw v0,32(s8) //m[$s8+32]=$v0,存储当前节点0x0040176c <+620>: lw v0,28(s8) //获得循环变量 i0x00401770 <+624>: nop0x00401774 <+628>: addiu v0,v0,1 //++i0x00401778 <+632>: sw v0,28(s8) //保存循环变量0x0040177c <+636>: lw v0,28(s8) //获得循环变量0x00401780 <+640>: nop0x00401784 <+644>: slti v0,v0,6 //条件判断,i < 6 ?0x00401788 <+648>: bnez v0,0x401728 <phase_6+552> //若 < 6,则跳转;否则继续往下执行0x0040178c <+652>: nop
这一段汇编的意思就是:在之前我们已经按照我们输入的顺序,将原链表上的节点保存在了数组的对应位置上。此时我们要做的就是将链表按照在数组中的顺序进行一个重新连接。
这两部合二为一,即按照我们输入的顺序重构了链表!
接着往下看:
0x00401790 <+656>: lw v0,32(s8) //经过上面的连接,现在node处于最后一个节点0x00401794 <+660>: nop0x00401798 <+664>: sw zero,8(v0) //node->next = null0x0040179c <+668>: lw v0,60(s8)0x004017a0 <+672>: nop0x004017a4 <+676>: sw v0,32(s8) //重置node至链表的firstNode0x004017a8 <+680>: sw zero,28(s8) //设置循环变量 i = 00x004017ac <+684>: b 0x401878 <phase_6+888> //条件判断0x004017b0 <+688>: nop0x004017b4 <+692>: lw v0,-32660(gp)0x004017b8 <+696>: nop0x004017bc <+700>: lw v0,44(v0) //获得学号的最后一位0x004017c0 <+704>: nop0x004017c4 <+708>: andi v0,v0,0x10x004017c8 <+712>: andi v0,v0,0xff //老规矩,判断奇偶0x004017cc <+716>: beqz v0,0x401818 <phase_6+792> //奇数 1,不跳转;偶数 0,跳转0x004017d0 <+720>: nop0x004017d4 <+724>: lw v0,32(s8) //找到前一个节点 node0x004017d8 <+728>: nop0x004017dc <+732>: lw v1,0(v0) //获得前一个节点的值0x004017e0 <+736>: lw v0,32(s8) //重置node0x004017e4 <+740>: nop0x004017e8 <+744>: lw v0,8(v0) //node = node->next0x004017ec <+748>: nop0x004017f0 <+752>: lw v0,0(v0) //获得后一个节点的值0x004017f4 <+756>: nop0x004017f8 <+760>: slt v0,v1,v0 //比较,升序则 $v0 = 1,反之 $v0 = 00x004017fc <+764>: beqz v0,0x401854 <phase_6+852> //升序则不跳转,炸弹爆炸;反之避开炸弹0x00401800 <+768>: nop0x00401804 <+772>: jal 0x4021f0 <explode_bomb>0x00401808 <+776>: nop0x0040180c <+780>: lw gp,16(s8)0x00401810 <+784>: b 0x401854 <phase_6+852>0x00401814 <+788>: nop0x00401818 <+792>: lw v0,32(s8)0x0040181c <+796>: nop0x00401820 <+800>: lw v1,0(v0) //$v1 是前一个节点0x00401824 <+804>: lw v0,32(s8)0x00401828 <+808>: nop0x0040182c <+812>: lw v0,8(v0)0x00401830 <+816>: nop0x00401834 <+820>: lw v0,0(v0) //$v0 是后一个节点0x00401838 <+824>: nop0x0040183c <+828>: slt v0,v0,v1 //降序则 1,反之则 00x00401840 <+832>: beqz v0,0x401854 <phase_6+852> //降序则爆炸;反之避开炸弹;0x00401844 <+836>: nop0x00401848 <+840>: jal 0x4021f0 <explode_bomb>0x0040184c <+844>: nop0x00401850 <+848>: lw gp,16(s8)0x00401854 <+852>: lw v0,32(s8) //更新 node0x00401858 <+856>: nop0x0040185c <+860>: lw v0,8(v0) //node = node->next0x00401860 <+864>: nop0x00401864 <+868>: sw v0,32(s8) //保存当前节点地址0x00401868 <+872>: lw v0,28(s8) //获得循环变量 i0x0040186c <+876>: nop0x00401870 <+880>: addiu v0,v0,1 //++i0x00401874 <+884>: sw v0,28(s8) //保存循环变量0x00401878 <+888>: lw v0,28(s8) //获得循环变量0x0040187c <+892>: nop0x00401880 <+896>: slti v0,v0,5 //条件判断,i < 5 ?0x00401884 <+900>: bnez v0,0x4017b4 <phase_6+692> //是则跳转,反之结束循环0x00401888 <+904>: nop0x0040188c <+908>: move sp,s8 //归还空间0x00401890 <+912>: lw ra,92(sp)0x00401894 <+916>: lw s8,88(sp)0x00401898 <+920>: addiu sp,sp,960x0040189c <+924>: jr ra0x004018a0 <+928>: nop
做个记录:爆炸点3:学号最后一位若是奇数,节点值排序后是升序,则爆炸!若是偶数,节点值排序后是降序,则爆炸!
炸弹六总结
爆炸点1: 若输入的数 < 0 或者 > 7,则炸弹爆炸!
爆炸点2: 若第 i 个数和它后面的某个数相等,则炸弹爆炸!
爆炸点3: 学号最后一位若是奇数,节点值排序后是升序,则爆炸!若是偶数,节点值排序后是降序,则爆炸!(现在是2021-11-21 1:29)
secret_phase
拆完这 6 个炸弹,如果能够理解前面的汇编语句,算是一种成功。但听说这之中还有一个隐藏炸弹,我们现在来看看。
我们首先对phase_defused进行单步调试发现,在这里有一个输出为 %d%s\%d\ \%s%d %s 的形式,接下来就是 string_not_equal 的比较了,那么很显然后面比较的字符串就是通过这一步来进行输入的。
再回想我们整个拆弹过程,能按照 %d%s\%d\ \%s%d %s 的格式输入的似乎只有 phase_4,虽然那时只是输入了一个整数,但是确实是离这个输入最近的。因此,我们再 phase_4 的输入时后面再输入一个字符串 “helloworld”,接着在 string_not_equal 的地方进行查看:
...0x004022e0 <+124>: nop0x004022e4 <+128>: addiu v0,s8,240x004022e8 <+132>: move a0,v00x004022ec <+136>: lui v0,0x400x004022f0 <+140>: addiu a1,v0,104160x004022f4 <+144>: jal 0x401cf8 <strings_not_equal> //出现了一个字符串的比较。一个炸弹已经拆解的函数,为什么要比较字符串呢?我们在 GDB 中进行查看0x004022f8 <+148>: nop0x004022fc <+152>: lw gp,16(s8)0x00402300 <+156>: bnez v0,0x402354 <phase_defused+240>0x00402304 <+160>: nop0x00402308 <+164>: lui v0,0x400x0040230c <+168>: addiu a0,v0,104320x00402310 <+172>: lw v0,-32712(gp)0x00402314 <+176>: nop0x00402318 <+180>: move t9,v00x0040231c <+184>: jalr t90x00402320 <+188>: nop0x00402324 <+192>: lw gp,16(s8)0x00402328 <+196>: lui v0,0x400x0040232c <+200>: addiu a0,v0,104720x00402330 <+204>: lw v0,-32712(gp)0x00402334 <+208>: nop0x00402338 <+212>: move t9,v00x0040233c <+216>: jalr t90x00402340 <+220>: nop0x00402344 <+224>: lw gp,16(s8)0x00402348 <+228>: jal 0x401990 <secret_phase>0x0040234c <+232>: nop
可以看到:$a0 中存储着我们之前输入的字符串,而 $a1 中存的是一个“austinpowers”,相等返回 1,继续向下执行,也就进入 secret_phase 啦!
接下来,大家就根据注释食用吧!
0x00401990 <+0>: addiu sp,sp,-400x00401994 <+4>: sw ra,36(sp)0x00401998 <+8>: sw s8,32(sp)0x0040199c <+12>: move s8,sp0x004019a0 <+16>: lui gp,0x420x004019a4 <+20>: addiu gp,gp,-200800x004019a8 <+24>: sw gp,16(sp)0x004019ac <+28>: jal 0x401fec <read_line>0x004019b0 <+32>: nop0x004019b4 <+36>: lw gp,16(s8)0x004019b8 <+40>: sw v0,28(s8) //存储输入的数据(这里我输入的是 10)0x004019bc <+44>: lw v0,28(s8) //load 保存输入的数据0x004019c0 <+48>: nop0x004019c4 <+52>: move a0,v00x004019c8 <+56>: move a1,zero0x004019cc <+60>: li a2,100x004019d0 <+64>: lw v0,-32656(gp)0x004019d4 <+68>: nop0x004019d8 <+72>: move t9,v00x004019dc <+76>: jalr t90x004019e0 <+80>: nop
--Type <RET> for more, q to quit, c to continue without paging--c0x004019e4 <+84>: lw gp,16(s8)0x004019e8 <+88>: sw v0,24(s8) //保存输入的数据(10)0x004019ec <+92>: lw v0,24(s8) //load 输入的数据0x004019f0 <+96>: nop0x004019f4 <+100>: addiu v0,v0,-1 //$v0--(9)0x004019f8 <+104>: sltiu v0,v0,1001 //条件判断,$v0 < 1001?0x004019fc <+108>: bnez v0,0x401a10 <secret_phase+128> //是则跳转,反之炸弹爆炸!0x00401a00 <+112>: nop0x00401a04 <+116>: jal 0x4021f0 <explode_bomb>0x00401a08 <+120>: nop0x00401a0c <+124>: lw gp,16(s8)0x00401a10 <+128>: lui v0,0x410x00401a14 <+132>: addiu a0,v0,12676 //$a0 = 0x240x00401a18 <+136>: lw a1,24(s8) //输入的数据(10)0x00401a1c <+140>: jal 0x4018a4 <fun7> //跳转至 fun7 啦
做个记录:爆炸点1:输入数据若 > 1001,则爆炸!
这里发生了一个跳转,那么接下来我们看看 fun7 干了啥:
0x004018a4 <+0>: addiu sp,sp,-320x004018a8 <+4>: sw ra,28(sp)0x004018ac <+8>: sw s8,24(sp)0x004018b0 <+12>: move s8,sp0x004018b4 <+16>: sw a0,32(s8) //$a0=0x24,第二次:$a0=0x080x004018b8 <+20>: sw a1,36(s8) //$a1=10,第二次:$a1=100x004018bc <+24>: lw v0,32(s8) //$v0=0x24,第二次:$v0=0x080x004018c0 <+28>: nop0x004018c4 <+32>: bnez v0,0x4018d8 <fun7+52> //$a0=0x24,不为 0,所以跳转至 <+52>0x004018c8 <+36>: nop0x004018cc <+40>: li v0,-10x004018d0 <+44>: b 0x401978 <fun7+212>0x004018d4 <+48>: nop0x004018d8 <+52>: lw v0,32(s8) //$v0=0x24(n1),第二次$v0=0x080x004018dc <+56>: nop0x004018e0 <+60>: lw v1,0(v0) //$v1=36,第二次 $v1=80x004018e4 <+64>: lw v0,36(s8) //$v0=10,第二次 $v0=100x004018e8 <+68>: nop0x004018ec <+72>: slt v0,v0,v1 //$v0 < $v1 ? 若是返回 1,不跳转;反之则跳转0x004018f0 <+76>: beqz v0,0x401924 <fun7+128>0x004018f4 <+80>: nop
--Type <RET> for more, q to quit, c to continue without paging--c0x004018f8 <+84>: lw v0,32(s8) //$v0 = 0x24(n1)0x004018fc <+88>: nop0x00401900 <+92>: lw v0,4(v0) //$v0 = 0x08(n21)0x00401904 <+96>: nop0x00401908 <+100>: move a0,v0 //$a0 = 0x08(n21)0x0040190c <+104>: lw a1,36(s8) //$a1 = 100x00401910 <+108>: jal 0x4018a4 <fun7>0x00401914 <+112>: nop0x00401918 <+116>: sll v0,v0,0x1 //左移 1 位,末尾补 10x0040191c <+120>: b 0x401978 <fun7+212>0x00401920 <+124>: nop0x00401924 <+128>: lw v0,32(s8) //第二次:$v0=0x080x00401928 <+132>: nop0x0040192c <+136>: lw v1,0(v0) //第二次:$v1=80x00401930 <+140>: lw v0,36(s8) //第二次:$v0=100x00401934 <+144>: nop0x00401938 <+148>: slt v0,v1,v0 //$v1 < $v0 ? 若是,则不跳转,反之跳转0x0040193c <+152>: beqz v0,0x401974 <fun7+208>0x00401940 <+156>: nop0x00401944 <+160>: lw v0,32(s8) //第二次:$v0=0x080x00401948 <+164>: nop0x0040194c <+168>: lw v0,8(v0) //第二次:$v0=0x160x00401950 <+172>: nop0x00401954 <+176>: move a0,v0 //$a0=0x160x00401958 <+180>: lw a1,36(s8) //$a1=100x0040195c <+184>: jal 0x4018a4 <fun7>0x00401960 <+188>: nop0x00401964 <+192>: sll v0,v0,0x10x00401968 <+196>: addiu v0,v0,1 //左移 1 位,末尾补 10x0040196c <+200>: b 0x401978 <fun7+212>0x00401970 <+204>: nop0x00401974 <+208>: move v0,zero0x00401978 <+212>: move sp,s80x0040197c <+216>: lw ra,28(sp)0x00401980 <+220>: lw s8,24(sp)0x00401984 <+224>: addiu sp,sp,320x00401988 <+228>: jr ra0x0040198c <+232>: nop
那么,这个函数就相当于是一个二叉搜索树,根据 $v0 和 $v1 的大小关系来确定下一节点是左孩子还有右孩子。同时,没执行结束一个函数都会将 $v0 向左移动一位,末位补 1.
下面来看这个返回值在 secret_phase 中的作用:
0x00401a20 <+144>: nop0x00401a24 <+148>: lw gp,16(s8)0x00401a28 <+152>: move v1,v0 //$v1=$v0,即将返回值给 $v10x00401a2c <+156>: li v0,7 //$v0 = 70x00401a30 <+160>: beq v1,v0,0x401a44 <secret_phase+180> //$v1 = 7 ? 相等则跳转,避开炸弹;反之炸弹爆炸!0x00401a34 <+164>: nop0x00401a38 <+168>: jal 0x4021f0 <explode_bomb>0x00401a3c <+172>: nop0x00401a40 <+176>: lw gp,16(s8)0x00401a44 <+180>: lui v0,0x40
按照之前所说,每一次函数的执行,都会将 $v0 左移 1 位,末尾补 1,那么
000→001→011→111000 \rightarrow 001 \rightarrow 011 \rightarrow 111000→001→011→111因此,我们需要做的就是控制函数的执行次数是 3 次。即二叉搜索树的搜索次数是 3 次。
做个记录:爆炸点2:二叉搜索数的搜索次数为 3 次,否则爆炸
在 GDB 中进行查看,很容易得到这棵二叉搜索树,我们给出其节点值(作图如下):
隐藏炸弹总结
做个记录:爆炸点1: 输入数据若 > 1001,则爆炸!
做个记录:爆炸点2: 二叉搜索数的搜索次数为 3 次,否则爆炸!
总结
写完文章,CSDN 已经开始卡顿了hhh,不妨静下心来一个一个看这些炸弹,跟着思路走一走。相信真正理解了这些汇编,这个实验才算真正达到了目的吧!
有什么问题烦请告知哈!