第一次拿到这个实验还是有点慌!之前没见过,不过还是慢慢做过来了。
这是个需要耐心的过程,请一定静下心来哦!
环境:Ubuntu 20.04 + GDB 调试工具
可参考配置:GDB调试工具配置(须先自行下载 Ubuntu 哈)
MIPS 常用指令:MIPS 常用指令
MIPS寄存器说明:MIPS寄存器说明
供参考的 C 语言代码:Brief C Programs of the Bombs
队友 scy 巨巨的博客(占个坑):太奆了
先看主函数:
...
//先看主函数
0x00400900<main>:
0x00400ba4 <+676>: lw gp,16(s8)
0x00400ba8 <+680>: jal 0x401fec <read_line>
0x00400bac <+684>: nop
0x00400bb0 <+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 时输入 “ H e l l o w o r l d ! ” “Hello\ world!” “Hello world!”,共 12 个字符,此时使用 GDB 进行调试:
先在 0x00400bb8 处设置一个断点,查看寄存器 $v0 的值:
可以看到,从 v 0 v0 v0 中向后读取 12 个字符就是我们所输入的 " H e l l o w o r l d ! " "Hello\ world!" "Hello world!"。
因此,这部分的思路就是:
1. 先调用 read_line 函数去读取我们输入的字符串,并将它存入专门保存函数返回值的寄存器 v 0 v0 v0 中;
2. 接着将 v 0 v0 v0 中的值存到内存 ( s 8 + 32 ) (s8+32) (s8+32) 中去;从该内存中读取字符串保存到寄存器 a 0 a0 a0 中(为了在 phase_1 中使用);
3. 最后跳转到 phase_1 中进行炸弹 1 的拆解。
...
0x00400ba4 <+676>: lw gp,16(s8)
0x00400ba8 <+680>: jal 0x401fec <read_line> //调用函数读取字符串
0x00400bac <+684>: nop
0x00400bb0 <+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,-32
0x00400d70 <+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,0x40
0x00400d88 <+28>: addiu a1,v0,10092
0x00400d8c <+32>: jal 0x401cf8 <strings_not_equal>
0x00400d90 <+36>: nop
0x00400d94 <+40>: beqz v0,0x400da4 <phase_1+56>
0x00400d98 <+44>: nop
0x00400d9c <+48>: jal 0x4021f0 <explode_bomb>
0x00400da0 <+52>: nop
0x00400da4 <+56>: move sp,s8
0x00400da8 <+60>: lw ra,28(sp)
0x00400dac <+64>: lw s8,24(sp)
0x00400db0 <+68>: addiu sp,sp,32
0x00400db4 <+72>: jr ra
0x00400db8 <+76>: nop
首先,前四行:
这是为了在栈中开辟空间,即为 phase_1 开辟足够的空间,以便函数运行;
接下来,可以看见一个 string_not_equal 函数,用于对字符串进行比较。那么比较的是哪两个字符串呢?我们可以使用 GDB 调试来查看相关寄存器的值:
在函数 string_not_equal 调用之前,涉及到了 a 0 a0 a0 和 a 1 a1 a1 寄存器的处理,我们先看 a 0 a0 a0:
再往后面输出 12 个字符:
刚好是我们输入的字符串 " H e l l o w o r l d ! " "Hello\ world!" "Hello world!",那么可以确定 a 0 a0 a0 中存的就是输入的字符串;
接着在 0x00400d8c 设置断点之后,看 a 1 a1 a1:
发现,我们输出 17 个字符之后,第 17 个字符就是 ‘\000’ 了,看前面 16 个字符,刚好组成: " L e t ′ s b e g i n n o w ! " "Let's\ begin\ now!" "Let′s begin now!",就是内部存储的要比较的字符串啦~
结合 string_not_equal 可知,phase_1 比较的就是我们输入的字符串和 " L e t ′ s b e g i n n o w ! " "Let's\ begin\ now!" "Let′s begin now!" 是否相等,并将返回值存入了专门存放返回值的寄存器 v 0 v0 v0(相等为 1,不相等为 0)。接下来就是一个比较过程,根据注释食用哈~
0x00400d6c <+0>: addiu sp,sp,-32
0x00400d70 <+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,0x40
0x00400d88 <+28>: addiu a1,v0,10092 //a1 = m[0x40 + 10092],到内存中找到我们要比较的字符串
0x00400d8c <+32>: jal 0x401cf8 <strings_not_equal> //进入函数比较字符串是否相等,相等则返回1,不相等返回0,这个返回存入了寄存器 $v0 中!
0x00400d90 <+36>: nop
0x00400d94 <+40>: beqz v0,0x400da4 <phase_1+56> //$v0 为 0 则继续(炸弹爆炸),不为 0 跳转到 <+56> 的位置,可以看到跳转后便跳过了炸弹,第一个炸弹 avoided!
0x00400d98 <+44>: nop
0x00400d9c <+48>: jal 0x4021f0 <explode_bomb>
0x00400da0 <+52>: nop
0x00400da4 <+56>: move sp,s8 //这后面就是归还栈空间,并跳回 $ra 寄存器中的地址,即return,返回主函数了!
0x00400da8 <+60>: lw ra,28(sp)
0x00400dac <+64>: lw s8,24(sp)
0x00400db0 <+68>: addiu sp,sp,32
0x00400db4 <+72>: jr ra
0x00400db8 <+76>: nop
爆炸点1: 比较输入字符串是否与 " L e t ′ s b e g i n n o w ! " "Let's\ begin\ now!" "Let′s begin now!" 是否相等,不相等就 B O O M ! BOOM! BOOM!。
0x00400dbc <+0>: addiu sp,sp,-64
0x00400dc0 <+4>: sw ra,60(sp)
0x00400dc4 <+8>: sw s8,56(sp)
0x00400dc8 <+12>: move s8,sp
0x00400dcc <+16>: lui gp,0x42
0x00400dd0 <+20>: addiu gp,gp,-20080
0x00400dd4 <+24>: sw gp,16(sp)
0x00400dd8 <+28>: sw a0,64(s8)
0x00400ddc <+32>: addiu v0,s8,28
0x00400de0 <+36>: lw a0,64(s8)
0x00400de4 <+40>: move a1,v0
0x00400de8 <+44>: jal 0x401ba8 <read_six_numbers>
0x00400dec <+48>: nop
0x00400df0 <+52>: lw gp,16(s8)
0x00400df4 <+56>: lw v1,28(s8)
0x00400df8 <+60>: li v0,1
0x00400dfc <+64>: beq v1,v0,0x400e10 <phase_2+84>
0x00400e00 <+68>: nop
0x00400e04 <+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>
将寄存器 v 0 v0 v0 与寄存器 v 1 v1 v1 进行了比较,那么我们可以使用 p $v0
和 p $v1
来查看一下两个寄存器中的值:
发现 v 0 v0 v0 中存正好是我们输入的第一个 2,这会不会是巧合呢?我们向上查找,有这么一句:
0x00400df4 <+56>: lw v1,28(s8) //m[$s8+28] = v1
v 1 v1 v1 中的值是从内存中来的,如果我们的猜想正确的话,继续往后读 7 个数:
前 6 个是我们输入的数,第 7 个数由于我们没有输入,因此是无效数据,这就说明我们的猜想是正确的,内存 m [ m[ m[s8+28]$ 中存着我们输入的第一个数,并使用了 load 访存将它放入了寄存器 v 1 v1 v1 中。
结合着这句话:
0x00400dfc <+64>: beq v1,v0,0x400e10 <phase_2+84>
意识到是要将我们输入的第一个数与 1 作比较,相等则跳转到 phase_2+84 行去(也就避开了炸弹咯~),否则炸弹就爆炸了!
做个记录:炸弹2:第一个数必须是1,否则爆炸!
接下来循环开始,大家根据注释食用哈~
0x00400e08 <+76>: nop
0x00400e0c <+80>: lw gp,16(s8)
0x00400e10 <+84>: li v0,1 //循环变量,相当于 for 循环中的 i=1
0x00400e14 <+88>: sw v0,24(s8) //m[$s8+24] = $v0,将该循环变量入栈
0x00400e18 <+92>: b 0x400ea8 <phase_2+236> //循环开始,跳转到 phase_2+236 的位置进行条件判断
0x00400e1c <+96>: nop
0x00400e20 <+100>: lw v0,24(s8) //$v0 = m[$s8+24],取出栈中之前存好的 i,即v0=1
0x00400e24 <+104>: nop
0x00400e28 <+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>: nop
0x00400e48 <+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 为存入 $v0
0x00400e5c <+160>: nop
0x00400e60 <+164>: mult a0,v0
0x00400e64 <+168>: mflo a0 //上一句将 $a0 中的值与 $v0 中的值相乘,此句意为将结果存入 $a0 中
0x00400e68 <+172>: lw v0,24(s8) //$v0 = m[$s8+24],取出之前的循环变量 i
0x00400e6c <+176>: nop
0x00400e70 <+180>: sll v0,v0,0x2 //将 i 变为int型变量的长度
0x00400e74 <+184>: addiu v1,s8,24
0x00400e78 <+188>: addu v0,v1,v0 //$s8 + 24 + $v0 * 4
0x00400e7c <+192>: lw v0,4(v0) //这里与<+116>处类似,但要注意的是,上面的 $v0 是自减之后,那时得到的是输入的第 i 个数;在这里,$v0 = i,没有自减,因此取得的是第 i + 1 个数!
//<+192>有点难以想到~
0x00400e80 <+196>: nop
0x00400e84 <+200>: beq a0,v0,0x400e98 <phase_2+220> //比较乘积项 a0 与 输入的第 i+1 个数 v0 是否相等
//是则跳转到 phase_2+220 处咯,避开了炸弹;否则就爆炸啦!
0x00400e88 <+204>: nop
0x00400e8c <+208>: jal 0x4021f0 <explode_bomb>
0x00400e90 <+212>: nop
0x00400e94 <+216>: lw gp,16(s8) //末尾循环体
0x00400e98 <+220>: lw v0,24(s8) //取得之前的循环变量 i
0x00400e9c <+224>: nop
0x00400ea0 <+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>: nop
0x00400eb0 <+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,s8
0x00400ec0 <+260>: lw ra,60(sp)
0x00400ec4 <+264>: lw s8,56(sp)
0x00400ec8 <+268>: addiu sp,sp,64
0x00400ecc <+272>: jr ra
0x00400ed0 <+276>: nop
做个记录:炸弹3:第 i 个输入数据与学号的倒数第 i 位相乘,结果若不与第 i + 1 个输入数据相同则爆炸!
爆炸点1: 输入的数字个数必须是 6 个,否则爆炸!
爆炸点2: 第一个数必须是1,否则爆炸!
爆炸点3: 第 i 个输入数据与学号的倒数第 i 位相乘,结果若不与第 i + 1 个输入数据相同则爆炸!
首先,从 bomb.s 文件中 phase_3 前面的 $LC13 中可以看到输入的格式为 " % d % s % d " "\%d\ \%s\ \%d" "%d %s %d",即整数+字符+整数的格式。
接着往下:
0x00400ed4 <+0>: addiu sp,sp,-56
0x00400ed8 <+4>: sw ra,52(sp)
0x00400edc <+8>: sw s8,48(sp)
0x00400ee0 <+12>: move s8,sp //为程序留出空间
0x00400ee4 <+16>: lui gp,0x42
0x00400ee8 <+20>: addiu gp,gp,-20080
0x00400eec <+24>: sw gp,24(sp)
0x00400ef0 <+28>: sw a0,56(s8)
0x00400ef4 <+32>: lw a0,56(s8)
0x00400ef8 <+36>: lui v0,0x40
0x00400efc <+40>: addiu a1,v0,10112
0x00400f00 <+44>: addiu v1,s8,44
0x00400f04 <+48>: addiu v0,s8,40
0x00400f08 <+52>: addiu a2,s8,36
0x00400f0c <+56>: sw a2,16(sp)
0x00400f10 <+60>: move a2,v1
0x00400f14 <+64>: move a3,v0
0x00400f18 <+68>: lw v0,-32636(gp) //从 bomb.s 中可以得到这里调用了 scanf() 函数
0x00400f1c <+72>: nop
0x00400f20 <+76>: move t9,v0
0x00400f24 <+80>: jalr t9 //这一步干了啥???
根据注释可以看出,以上的汇编主要是为了留出空间,调用 scanf() 输入,以及一个不知所踪的跳转(以至于在 GDB 中单步调试无法访存,根据后面盲猜是计算参数个数???)
马上就要遇见炸弹了,但还不知道 phase_3 是干嘛的,有点慌!!!!
不过往下看:
0x00400f28 <+84>: nop
0x00400f2c <+88>: lw gp,24(s8)
0x00400f30 <+92>: slti v0,v0,3
0x00400f34 <+96>: beqz v0,0x400f48 <phase_3+116>
0x00400f38 <+100>: nop
0x00400f3c <+104>: jal 0x4021f0 <explode_bomb>
0x00400f40 <+108>: nop
似乎这个炸弹是根据比较来进行是否爆炸的,跟谁比呢?<+92> 有语句 slti v0, v0, 3
,说明是将 v 0 v0 v0 的值与 3 进行了比较,那么 v 0 v0 v0 又是什么呢?由于前面知道了输入应该是整数 + 字符 + 整数,因此此时查看 v 0 v0 v0 的值是3:
嘶!!!这之前只进行了栈分配,输入,并没有出现 3 吖!栈分配无从下手,那就从输入下手!
没有输入3,却出现了 3,而我输入的是三个参数,会不会 v 0 v0 v0 里面存的就是参数个数呢?为此,不妨改变输入试试!
我将输入改为只输入 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>: nop
0x00400f50 <+124>: sltiu v1,v0,8
0x00400f54 <+128>: beqz v1,0x401190 <phase_3+700>
0x00400f58 <+132>: nop
<phase_3+700>
0x00401190 <+700>: li v0,120
0x00401194 <+704>: sb v0,32(s8)
0x00401198 <+708>: jal 0x4021f0 <explode_bomb>
0x0040119c <+712>: nop
这段汇编 load 了一些数据, g p gp gp 是一个全局指针,无从下手; v 0 v0 v0 是一个寄存器,后面使用了它来进行比较,那么我们就先看看 v 0 v0 v0 里面存的是什么:
可以看到,$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,0x40
0x00400f64 <+144>: addiu v0,v0,10124
0x00400f68 <+148>: addu v0,v1,v0
0x00400f6c <+152>: lw v0,0(v0) //以上三句是为了找到 switch...case... 语句中我们应该跳转到的子句的地址,并保存在 $v0 中
//可以使用 x $v0 来进行查看跳转的地址
0x00400f70 <+156>: nop
0x00400f74 <+160>: jr v0 //跳转到对应的 switch...case... 语句
0x00400f78 <+164>: nop
我现在输入的第一个参数是 5,因此跳转到 phase_3+504 的位置。
接下来是相应的一些 case 语句,下面就来说说 switch…case… 语句中的相关操作,我们就以 case 5 为例(从上面可以看到跳转到了 <+504> 的位置哈~):
0x004010cc <+504>: li v0,116 //$v0 = 116
0x004010d0 <+508>: sb v0,32(s8) //m[$s8+32]=$v0=116
0x004010d4 <+512>: lw v0,-32660(gp)
0x004010d8 <+516>: nop
首先,将立即数 116 存入了内存之中;
0x004010dc <+520>: lw v1,44(v0)
0x004010e0 <+524>: lw v0,36(s8)
0x004010e4 <+528>: nop
0x004010e8 <+532>: mult v1,v0
0x004010ec <+536>: mflo v1
0x004010f0 <+540>: li v0,513
0x004010f4 <+544>: beq v1,v0,0x4011e8 <phase_3+788>
0x004010f8 <+548>: nop
0x004010fc <+552>: jal 0x4021f0 <explode_bomb>
0x00401100 <+556>: nop
在这段汇编中,我们先查看 v 1 v1 v1 中存的是什么,根据前面的经验以及不断试错可以发现,将全局指针 $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>: nop
0x004011ec <+792>: b 0x4011f8 <phase_3+804>
0x004011f0 <+796>: nop
0x004011f4 <+800>: nop
0x004011f8 <+804>: lb v0,40(s8)
0x004011fc <+808>: lb v1,32(s8)
0x00401200 <+812>: nop
0x00401204 <+816>: beq v1,v0,0x401218 <phase_3+836>
0x00401208 <+820>: nop
0x0040120c <+824>: jal 0x4021f0 <explode_bomb>
0x00401210 <+828>: nop
0x00401214 <+832>: lw gp,24(s8)
0x00401218 <+836>: move sp,s8
0x0040121c <+840>: lw ra,52(sp)
0x00401220 <+844>: lw s8,48(sp)
0x00401224 <+848>: addiu sp,sp,56
0x00401228 <+852>: jr ra
0x0040122c <+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 之前的 $LC14 发现我们输入的形式是 % d \%d %d 即一个整数。首先我们看调用递归函数之前的部分:
先看第一个炸弹怎样才能避开呢?
0x004012bc <+0>: addiu sp,sp,-40
0x004012c0 <+4>: sw ra,36(sp)
0x004012c4 <+8>: sw s8,32(sp)
0x004012c8 <+12>: move s8,sp
0x004012cc <+16>: lui gp,0x42
0x004012d0 <+20>: addiu gp,gp,-20080
0x004012d4 <+24>: sw gp,16(sp)
0x004012d8 <+28>: sw a0,40(s8)
0x004012dc <+32>: lw v1,40(s8)
0x004012e0 <+36>: lui v0,0x40
0x004012e4 <+40>: addiu v0,v0,10156
0x004012e8 <+44>: move a0,v1
0x004012ec <+48>: move a1,v0
0x004012f0 <+52>: addiu v0,s8,24
0x004012f4 <+56>: move a2,v0 //前面是为了给函数申请空间
0x004012f8 <+60>: lw v0,-32636(gp) //调用输入函数进行输入
0x004012fc <+64>: nop
0x00401300 <+68>: move t9,v0
0x00401304 <+72>: jalr t9 //去判断是否有输入???有输入返回1,无输入返回0
0x00401308 <+76>: nop
0x0040130c <+80>: lw gp,16(s8)
--Type <RET> for more, q to quit, c to continue without paging--c
0x00401310 <+84>: move v1,v0
0x00401314 <+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>: nop
0x00401338 <+124>: lw gp,16(s8)
0x0040133c <+128>: nop
0x00401340 <+132>: lw v0,-32660(gp)
0x00401344 <+136>: nop
0x00401348 <+140>: lw v0,44(v0)
0x0040134c <+144>: nop
首先,我们在 phase_3 的分析中说过,全局指针 $gp-32660 之后,再访问其 +44 后的内存存储着我们输入的学号的最后一位,所以这一段意为取出输入的学号的最后一位到 $v0 中去。
下面这段汇编进行了一个与运算:
0x00401350 <+148>: andi v0,v0,0x1
0x00401354 <+152>: andi v0,v0,0xff
0x00401358 <+156>: beqz v0,0x40139c <phase_4+224>
0x0040135c <+160>: nop
前两句对学号的最后一位 $v0 进行了两个与运算以判断奇偶,举个例子:
( 5 ) 10 = ( 00000101 ) 2 (5)_{10}=(00000101)_2 (5)10=(00000101)2将它与 0x1 进行与运算 ∵ 0 x 1 = ( 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>: nop
0x00401360 <+164>: lw v0,24(s8)
0x00401364 <+168>: nop
0x00401368 <+172>: move a0,v0
0x0040136c <+176>: jal 0x401230 <func4>
0x00401370 <+180>: nop
0x00401374 <+184>: lw gp,16(s8)
0x00401378 <+188>: move v1,v0
0x0040137c <+192>: li v0,8
0x00401380 <+196>: beq v1,v0,0x4013d0 <phase_4+276>
0x00401384 <+200>: nop
0x00401388 <+204>: jal 0x4021f0 <explode_bomb>
0x0040138c <+208>: nop
首先,load 进了一个数,通过在 GDB 中使用 p $v0
可以看到,此时 $v0 中的数是我们输入的那个数。
接着,我们将 $v0 中的值(即56)传入了寄存器 $a0 中,然后便进入递归函数 func4 了。下面我们就去看看这个递归函数干了什么!(大家根据注释食用哈~)
0x00401230 <+0>: addiu sp,sp,-40
0x00401234 <+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],将我们传入的参数给 $v0
0x0040124c <+28>: nop
0x00401250 <+32>: slti v0,v0,2 //比较这个参数是否小于 2
0x00401254 <+36>: bnez v0,0x40129c <func4+108> //若小于 2,$v0=1,跳转到 <+108> 的地方去;反之则继续!
//先到 <+108> 看看发生了什么哈~
0x00401258 <+40>: nop
0x0040125c <+44>: lw v0,40(s8) //$v0=m[$s8+40]=56,将之前存在内存中的参数给到 $v0
0x00401260 <+48>: nop
0x00401264 <+52>: addiu v0,v0,-1 //$v0--;相当于 $v0 = 55了
0x00401268 <+56>: move a0,v0 //$a0=$v0=55
0x0040126c <+60>: jal 0x401230 <func4> //再次执行函数,就是递归调用了
//仔细看,这里传入的参数是 55,联系函数开始时是通过 $a0 传参的,这一切就说得通了
//这一步相当于是递归调用 f(x-1)!
0x00401270 <+64>: nop
0x00401274 <+68>: move s0,v0 //$v0 是函数的返回值,因此这里就是 f(x-1) 函数执行后的返回值
//若在这一步在 GDB 中进行调试,会有大量continue需要输入,因此是个递归函数hhh
0x00401278 <+72>: lw v0,40(s8) //$v0=m[$s8+40]=56,同样将之前存在内存中的参数给到 $v0
0x0040127c <+76>: nop
0x00401280 <+80>: addiu v0,v0,-2 //$v0 -= 2; $v0 自减2
--Type <RET> for more, q to quit, c to continue without paging--c
0x00401284 <+84>: move a0,v0 //又是一个传参
0x00401288 <+88>: jal 0x401230 <func4> //再次执行函数,相当于递归调用 f(x-2) 啦~
0x0040128c <+92>: nop
0x00401290 <+96>: addu v0,s0,v0 //此时 $s0 中是 f(x-1) 的返回值, $v0 是 f(x-2) 的返回值,二者相加返回给 $v0
0x00401294 <+100>: b 0x4012a0 <func4+112> //跳转到 <+112> 归还空间啦~
0x00401298 <+104>: nop
0x0040129c <+108>: li v0,1 //$v0=1,下面就是归还栈空间了。这就是递归的终止条件
//if($v0 < 2) return 1;
0x004012a0 <+112>: move sp,s8
0x004012a4 <+116>: lw ra,36(sp)
0x004012a8 <+120>: lw s8,32(sp)
0x004012ac <+124>: lw s0,28(sp)
0x004012b0 <+128>: addiu sp,sp,40
0x004012b4 <+132>: jr ra
0x004012b8 <+136>: nop
因此,这个递归函数实质上就是:
if (x < 2)
return 1;
else
return f(x-1) + f(x-2);
这个递归函数就是一个求斐波那契数列的值,得到了递归函数的返回值并保存在 $v0 中,我们继续看下面的汇编:(根据注释食用哈!)
0x00401370 <+180>: nop
0x00401374 <+184>: lw gp,16(s8)
0x00401378 <+188>: move v1,v0 //将函数调用的返回值给到 $v1
0x0040137c <+192>: li v0,8 //$v0 = 8
0x00401380 <+196>: beq v1,v0,0x4013d0 <phase_4+276> //比较 $v1 和 $v0 中的值,即函数返回值是否等于 8,相等则跳转,避开了炸弹;否则炸弹爆炸啦!
0x00401384 <+200>: nop
0x00401388 <+204>: jal 0x4021f0 <explode_bomb>
0x0040138c <+208>: nop
0x00401390 <+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,40
0x004013e0 <+292>: jr ra
0x004013e4 <+296>: nop
以上是奇数的情况,偶数的情况相同,只是对递归函数返回值的要求不同,我们直接看需要比较的值:
...
0x004013ac <+240>: nop
0x004013b0 <+244>: lw gp,16(s8)
0x004013b4 <+248>: move v1,v0
0x004013b8 <+252>: li v0,13
0x004013bc <+256>: beq v1,v0,0x4013d0 <phase_4+276>
0x004013c0 <+260>: nop
...
是 13!
因此,为了避开这个炸弹,就要使我们的斐波那契数列 f ( i n p u t ) f(input) f(input) 的返回值等于相应的值:
I n o r d e r a v o i d t h e b o m b , t h e n { f ( i n p u t ) = 8 , i f 学号最后一位是奇数 f ( i n p u t ) = 13 , i f 学号最后一位是偶数 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) 是否与目标值相等。不相等则爆炸!
爆炸点1: 判断有无输入;
爆炸点2: 输入n,斐波那契求值得到 f(n)。根据学号最后一位的奇偶性来判断 f(n) 是否与目标值相等。不相等则爆炸!
首先,说个题外话,在看这道题之前我们看怎样输入时,发现 $LC15 那儿有一个 “giants”,不是平常的 % d \%d %d 什么的。莫非…就是输入“giants”?尝试输入了一下,似乎并不是hhh
直接看汇编吧!(第一个炸弹之前)
0x004013e8 <+0>: addiu sp,sp,-72
0x004013ec <+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>: nop
0x00401408 <+32>: move v1,v0
0x0040140c <+36>: li v0,6
0x00401410 <+40>: beq v1,v0,0x401420 <phase_5+56>
0x00401414 <+44>: nop
0x00401418 <+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>: nop
0x00401408 <+32>: move v1,v0 //$v1=$v0,将长度放入 $v0 中
0x0040140c <+36>: li v0,6 //$v0=6
0x00401410 <+40>: beq v1,v0,0x401420 <phase_5+56> //比较字符串长度是否为6,是则跳转,就避开炸弹啦;否则炸弹爆炸!
0x00401414 <+44>: nop
0x00401418 <+48>: jal 0x4021f0 <explode_bomb>
0x0040141c <+52>: nop
做个记录:爆炸点1:输入的字符串长度若不为 6,炸弹爆炸!
接着往下,
0x00401420 <+56>: sw zero,24(s8) //m[$s8+24]=0,初始化循环变量 i = 0
0x00401424 <+60>: b 0x4014a8 <phase_5+192> //跳转至条件判断,下面请看 <+192> 了哈!
0x00401428 <+64>: nop
0x0040142c <+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--c
0x00401438 <+80>: nop
0x0040143c <+84>: addu v1,a0,v1 //到达字符串中的第 i+1 个字符,注意 i 是从 0 开始的哈!第一次拿到第 1 个字符,后面以此类推~
0x00401440 <+88>: lb v1,0(v1) //拿到对应的第 i+1 个字符
0x00401444 <+92>: nop
0x00401448 <+96>: andi v1,v1,0xff
0x0040144c <+100>: andi v1,v1,0xf //取得字符所表示的二进制数的后四位
//比如:第一个字符‘y’,其ASCII码的十六进制表示为:0x79(01111001),后四位:1001
0x00401450 <+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) //拿到之前存储的循环变量i
0x004014ac <+196>: nop
0x004014b0 <+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) //取得之前的循环变量 i
0x00401464 <+124>: lw v0,24(s8) //取得之前的循环变量 i
0x00401468 <+128>: nop
0x0040146c <+132>: sll v0,v0,0x2 //将 i 变为int型长度
0x00401470 <+136>: addiu v1,s8,24 //读得 i 的地址
0x00401474 <+140>: addu v0,v1,v0
0x00401478 <+144>: lw v1,12(v0) //回想之前存字符后四位的地方,这里就是取得字符的后四位,存到 $v1 中
0x0040147c <+148>: lui v0,0x41
0x00401480 <+152>: addiu v0,v0,12524
0x00401484 <+156>: addu v0,v1,v0
0x00401488 <+160>: lb v1,0(v0)
0x0040148c <+164>: addiu v0,s8,24
0x00401490 <+168>: addu v0,v0,a0
0x00401494 <+172>: sb v1,4(v0)
看到 <+148> 时,有点懵,这是干了啥?4 句过后,又是去访问循环变量的地址。因此,我们在这一句设断点看看发生了啥。
这个 +9 提示了我们,莫非前面还有字符?我们使用 x/s $v0-9
发现:
这个地址存的就是这样一个字符串。现在我们回到那 4 句汇编:
0x00401478 <+144>: lw v1,12(v0)
0x0040147c <+148>: lui v0,0x41
0x00401480 <+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,24
0x00401490 <+168>: addu v0,v0,a0
0x00401494 <+172>: sb v1,4(v0) //将它们存入 m[$s8 + 28 + i]的位置
Going on:
0x004014b8 <+208>: nop
0x004014bc <+212>: sb zero,34(s8) //字符串的尾部置空 '\0'
0x004014c0 <+216>: addiu v0,s8,28
0x004014c4 <+220>: move a0,v0 //这两句到达之前存经过映射后的字符串('bgargt')的位置
0x004014c8 <+224>: lui v0,0x40
0x004014cc <+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,而不是 1
0x004014dc <+244>: nop
0x004014e0 <+248>: jal 0x4021f0 <explode_bomb>
0x004014e4 <+252>: nop
0x004014e8 <+256>: move sp,s8 //归还空间,拆弹结束!
0x004014ec <+260>: lw ra,68(sp)
0x004014f0 <+264>: lw s8,64(sp)
0x004014f4 <+268>: addiu sp,sp,72
0x004014f8 <+272>: jr ra
0x004014fc <+276>: nop
爆炸点1: 输入的字符串长度若不为 6,炸弹爆炸!
爆炸点2: 通过内置字符串映射,若结果为“giants”,则通过;否则炸弹爆炸!
0x00401500 <+0>: addiu sp,sp,-96
0x00401504 <+4>: sw ra,92(sp)
0x00401508 <+8>: sw s8,88(sp)
0x0040150c <+12>: move s8,sp
0x00401510 <+16>: lui gp,0x42
0x00401514 <+20>: addiu gp,gp,-20080
0x00401518 <+24>: sw gp,16(sp)
0x0040151c <+28>: sw a0,96(s8)
0x00401520 <+32>: lui v0,0x41
0x00401524 <+36>: addiu v0,v0,12592
0x00401528 <+40>: sw v0,32(s8)
0x0040152c <+44>: addiu v0,s8,36
0x00401530 <+48>: lw a0,96(s8)
0x00401534 <+52>: move a1,v0
0x00401538 <+56>: jal 0x401ba8 <read_six_numbers>
前面就是申请程序空间,然后 read_six_numbers() 读取 6 个数。
接下来就是循环开始了:
0x0040153c <+60>: nop
0x00401540 <+64>: lw gp,16(s8)
0x00401544 <+68>: sw zero,28(s8) //m[$s8+28]=0,设置循环变量 i = 0
0x00401548 <+72>: b 0x40163c <phase_6+316> //跳转到第一重循环的条件判断,看<+316>的位置哈
0x0040154c <+76>: nop
--Type <RET> for more, q to quit, c to continue without paging--c
0x00401550 <+80>: lw v0,28(s8) //取得循环变量 i
0x00401554 <+84>: nop
0x00401558 <+88>: sll v0,v0,0x2 //将 i 变成int型长度
0x0040155c <+92>: addiu v1,s8,24
0x00401560 <+96>: addu v0,v1,v0
0x00401564 <+100>: lw v0,12(v0) //取得输入的第 i 个数,可以在 GDB 中 ‘p $v0’ 查看
0x00401568 <+104>: nop
0x0040156c <+108>: slti v0,v0,7 //条件判断,第 i 个数是否小于 7,input[i] < 7?
0x00401570 <+112>: beqz v0,0x40159c <phase_6+156> //若大于 7,则跳转到<+156>,炸弹爆炸;否则继续
0x00401574 <+116>: nop
0x00401578 <+120>: lw v0,28(s8) //取得之前的循环变量 i
0x0040157c <+124>: nop
0x00401580 <+128>: sll v0,v0,0x2 //将 i 变成int型长度
0x00401584 <+132>: addiu v1,s8,24
0x00401588 <+136>: addu v0,v1,v0
0x0040158c <+140>: lw v0,12(v0) //取得输入的第 i 个数,可以在 GDB 中 ‘p $v0’ 查看
0x00401590 <+144>: nop
0x00401594 <+148>: bgtz v0,0x4015a8 <phase_6+168> //判断第 i 个数是否大于 0,若是,则发生跳转,避开了炸弹;否则炸弹爆炸!
0x00401598 <+152>: nop
0x0040159c <+156>: jal 0x4021f0 <explode_bomb>
0x004015a0 <+160>: nop
0x004015a4 <+164>: lw gp,16(s8)
0x004015a8 <+168>: lw v0,28(s8) //取得之前的循环变量 i
0x004015ac <+172>: nop
0x004015b0 <+176>: addiu v0,v0,1 //第二重循环变量的设定,j = i + 1
0x004015b4 <+180>: sw v0,24(s8) //m[$s8+24]=v0,将第二重循环的循环变量进行存储
0x004015b8 <+184>: b 0x401618 <phase_6+280> //第二重循环的条件判断,下面看<+280>哈
0x004015bc <+188>: nop
0x004015c0 <+192>: lw v0,28(s8) //取得第一重循环变量 i
0x004015c4 <+196>: nop
0x004015c8 <+200>: sll v0,v0,0x2 //将 i 变成int型长度
0x004015cc <+204>: addiu v1,s8,24
0x004015d0 <+208>: addu v0,v1,v0
0x004015d4 <+212>: lw v1,12(v0) //取得输入的第 i 个数字
0x004015d8 <+216>: lw v0,24(s8) //取得第二重循环变量 j
0x004015dc <+220>: nop
0x004015e0 <+224>: sll v0,v0,0x2
0x004015e4 <+228>: addiu a0,s8,24
0x004015e8 <+232>: addu v0,a0,v0
0x004015ec <+236>: lw v0,12(v0) //取得第 j 个数字
0x004015f0 <+240>: nop
0x004015f4 <+244>: bne v1,v0,0x401608 <phase_6+264> //比较 $v0 和 $v1 的值,若不相等则跳转,避开了炸弹;不相等则炸弹爆炸!
0x004015f8 <+248>: nop
0x004015fc <+252>: jal 0x4021f0 <explode_bomb>
0x00401600 <+256>: nop
0x00401604 <+260>: lw gp,16(s8)
0x00401608 <+264>: lw v0,24(s8) //取得第二重循环变量 j
0x0040160c <+268>: nop
0x00401610 <+272>: addiu v0,v0,1 //j++
0x00401614 <+276>: sw v0,24(s8)
0x00401618 <+280>: lw v0,24(s8) //第二重循环的条件判断
0x0040161c <+284>: nop
0x00401620 <+288>: slti v0,v0,6 //条件判断,i < 6 ?
0x00401624 <+292>: bnez v0,0x4015c0 <phase_6+192> //若 < 6,则 $v0 为1,再次执行循环题;反之,则继续往下执行啦~
0x00401628 <+296>: nop
0x0040162c <+300>: lw v0,28(s8)
0x00401630 <+304>: nop
0x00401634 <+308>: addiu v0,v0,1
0x00401638 <+312>: sw v0,28(s8)
0x0040163c <+316>: lw v0,28(s8) //取得之前的循环变量
0x00401640 <+320>: nop
0x00401644 <+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>: nop
0x00401650 <+336>: sw zero,28(s8) //设置第一重循环变量 i = 0
0x00401654 <+340>: b 0x4016f8 <phase_6+504> //第一重循环条件判断,下面看<+504>部分哈!
0x00401658 <+344>: nop
0x0040165c <+348>: lui v0,0x41
0x00401660 <+352>: addiu v0,v0,12592
0x00401664 <+356>: sw v0,32(s8)
看到<+352>的时候会有些疑惑,究竟存储了个什么。我们在 GDB 中进行调试看看:
是一个 node1 标识,盲猜是一个节点,不妨先放在这儿,等会儿回来推一下
0x0040164c <+332>: nop
0x00401650 <+336>: sw zero,28(s8) //设置第一重循环变量 i = 0
0x00401654 <+340>: b 0x4016f8 <phase_6+504> //第一重循环条件判断,下面看<+504>部分哈!
0x00401658 <+344>: nop
0x0040165c <+348>: lui v0,0x41
0x00401660 <+352>: addiu v0,v0,12592
0x00401664 <+356>: sw v0,32(s8)
0x00401668 <+360>: li v0,1 //设置第二重循环变量 j = 1
0x0040166c <+364>: sw v0,24(s8) //m[$s8+24]=$v0
0x00401670 <+368>: b 0x40169c <phase_6+412> //第二重循环条件判断,下面看 <+412> 吧
0x00401674 <+372>: nop
0x00401678 <+376>: lw v0,32(s8) //找到一个位置
0x0040167c <+380>: nop
0x00401680 <+384>: lw v0,8(v0) //将它向后移
0x00401684 <+388>: nop
0x00401688 <+392>: sw v0,32(s8) //保存移动之后的位置
0x0040168c <+396>: lw v0,24(s8) //取得第二重循环变量 j
0x00401690 <+400>: nop
0x00401694 <+404>: addiu v0,v0,1 //j++
0x00401698 <+408>: sw v0,24(s8)
0x0040169c <+412>: lw v0,28(s8) //获得第一重循环变量 i
0x004016a0 <+416>: nop
0x004016a4 <+420>: sll v0,v0,0x2 //将 i 变成int型长度
0x004016a8 <+424>: addiu v1,s8,24
0x004016ac <+428>: addu v0,v1,v0
0x004016b0 <+432>: lw v1,12(v0) //取得输入的第 i 个数
0x004016b4 <+436>: lw v0,24(s8) //取得第二重循环变量 j
0x004016b8 <+440>: nop
0x004016bc <+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>: nop
0x004016c8 <+456>: lw v0,28(s8) //第一重循环变量
0x004016cc <+460>: nop
0x004016d0 <+464>: sll v0,v0,0x2
0x004016d4 <+468>: addiu v1,s8,24
0x004016d8 <+472>: addu v0,v1,v0
0x004016dc <+476>: lw v1,32(s8) //$v1=m[$s8+32],取了一个数是什么呢?可以在 GDB 中看一看(下面有截图哈!),可以看到 $v1 存的就是对应节点的值
0x004016e0 <+480>: nop
0x004016e4 <+484>: sw v1,36(v0) //m[$v0+36]=$v1,将这个值存到内存中。
//由于这是一个循环,因此存的地址是连续的。因此可以看作将节点的值存到一个数组里面去了,假设这个数组就是newList哈~
0x004016e8 <+488>: lw v0,28(s8) //取得第一重循环变量 i
0x004016ec <+492>: nop
0x004016f0 <+496>: addiu v0,v0,1 //++i
0x004016f4 <+500>: sw v0,28(s8) //保存循环变量
0x004016f8 <+504>: lw v0,28(s8) //取得第一重循环变量
0x004016fc <+508>: nop
0x00401700 <+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>: nop
0x00401714 <+532>: sw v0,32(s8) //到达 newList[0]
0x00401718 <+536>: li v0,1 //设置循环变量 i = 1
0x0040171c <+540>: sw v0,28(s8) //m[$s8+28]=$v0,存储循环变量
0x00401720 <+544>: b 0x40177c <phase_6+636> //循环条件判断,下面看<+636>哈
0x00401724 <+548>: nop
0x00401728 <+552>: lw v0,28(s8) //获得循环变量 i
0x0040172c <+556>: nop
0x00401730 <+560>: sll v0,v0,0x2 //将 i 变成int型长度
0x00401734 <+564>: addiu v1,s8,24
0x00401738 <+568>: addu v0,v1,v0
0x0040173c <+572>: lw v1,36(v0) //得到 newList[i],第一次中这个是第二个节点哦!
0x00401740 <+576>: lw v0,32(s8) //第一次中这是第一个节点node
0x00401744 <+580>: nop
0x00401748 <+584>: sw v1,8(v0) //node->next=newList[i]
0x0040174c <+588>: lw v0,28(s8) //取得循环变量 i
0x00401750 <+592>: nop
0x00401754 <+596>: sll v0,v0,0x2 //将 i 变成int型长度
0x00401758 <+600>: addiu v1,s8,24
0x0040175c <+604>: addu v0,v1,v0
0x00401760 <+608>: lw v0,36(v0) //node = node->next
0x00401764 <+612>: nop
0x00401768 <+616>: sw v0,32(s8) //m[$s8+32]=$v0,存储当前节点
0x0040176c <+620>: lw v0,28(s8) //获得循环变量 i
0x00401770 <+624>: nop
0x00401774 <+628>: addiu v0,v0,1 //++i
0x00401778 <+632>: sw v0,28(s8) //保存循环变量
0x0040177c <+636>: lw v0,28(s8) //获得循环变量
0x00401780 <+640>: nop
0x00401784 <+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>: nop
0x00401798 <+664>: sw zero,8(v0) //node->next = null
0x0040179c <+668>: lw v0,60(s8)
0x004017a0 <+672>: nop
0x004017a4 <+676>: sw v0,32(s8) //重置node至链表的firstNode
0x004017a8 <+680>: sw zero,28(s8) //设置循环变量 i = 0
0x004017ac <+684>: b 0x401878 <phase_6+888> //条件判断
0x004017b0 <+688>: nop
0x004017b4 <+692>: lw v0,-32660(gp)
0x004017b8 <+696>: nop
0x004017bc <+700>: lw v0,44(v0) //获得学号的最后一位
0x004017c0 <+704>: nop
0x004017c4 <+708>: andi v0,v0,0x1
0x004017c8 <+712>: andi v0,v0,0xff //老规矩,判断奇偶
0x004017cc <+716>: beqz v0,0x401818 <phase_6+792> //奇数 1,不跳转;偶数 0,跳转
0x004017d0 <+720>: nop
0x004017d4 <+724>: lw v0,32(s8) //找到前一个节点 node
0x004017d8 <+728>: nop
0x004017dc <+732>: lw v1,0(v0) //获得前一个节点的值
0x004017e0 <+736>: lw v0,32(s8) //重置node
0x004017e4 <+740>: nop
0x004017e8 <+744>: lw v0,8(v0) //node = node->next
0x004017ec <+748>: nop
0x004017f0 <+752>: lw v0,0(v0) //获得后一个节点的值
0x004017f4 <+756>: nop
0x004017f8 <+760>: slt v0,v1,v0 //比较,升序则 $v0 = 1,反之 $v0 = 0
0x004017fc <+764>: beqz v0,0x401854 <phase_6+852> //升序则不跳转,炸弹爆炸;反之避开炸弹
0x00401800 <+768>: nop
0x00401804 <+772>: jal 0x4021f0 <explode_bomb>
0x00401808 <+776>: nop
0x0040180c <+780>: lw gp,16(s8)
0x00401810 <+784>: b 0x401854 <phase_6+852>
0x00401814 <+788>: nop
0x00401818 <+792>: lw v0,32(s8)
0x0040181c <+796>: nop
0x00401820 <+800>: lw v1,0(v0) //$v1 是前一个节点
0x00401824 <+804>: lw v0,32(s8)
0x00401828 <+808>: nop
0x0040182c <+812>: lw v0,8(v0)
0x00401830 <+816>: nop
0x00401834 <+820>: lw v0,0(v0) //$v0 是后一个节点
0x00401838 <+824>: nop
0x0040183c <+828>: slt v0,v0,v1 //降序则 1,反之则 0
0x00401840 <+832>: beqz v0,0x401854 <phase_6+852> //降序则爆炸;反之避开炸弹;
0x00401844 <+836>: nop
0x00401848 <+840>: jal 0x4021f0 <explode_bomb>
0x0040184c <+844>: nop
0x00401850 <+848>: lw gp,16(s8)
0x00401854 <+852>: lw v0,32(s8) //更新 node
0x00401858 <+856>: nop
0x0040185c <+860>: lw v0,8(v0) //node = node->next
0x00401860 <+864>: nop
0x00401864 <+868>: sw v0,32(s8) //保存当前节点地址
0x00401868 <+872>: lw v0,28(s8) //获得循环变量 i
0x0040186c <+876>: nop
0x00401870 <+880>: addiu v0,v0,1 //++i
0x00401874 <+884>: sw v0,28(s8) //保存循环变量
0x00401878 <+888>: lw v0,28(s8) //获得循环变量
0x0040187c <+892>: nop
0x00401880 <+896>: slti v0,v0,5 //条件判断,i < 5 ?
0x00401884 <+900>: bnez v0,0x4017b4 <phase_6+692> //是则跳转,反之结束循环
0x00401888 <+904>: nop
0x0040188c <+908>: move sp,s8 //归还空间
0x00401890 <+912>: lw ra,92(sp)
0x00401894 <+916>: lw s8,88(sp)
0x00401898 <+920>: addiu sp,sp,96
0x0040189c <+924>: jr ra
0x004018a0 <+928>: nop
做个记录:爆炸点3:学号最后一位若是奇数,节点值排序后是升序,则爆炸!若是偶数,节点值排序后是降序,则爆炸!
爆炸点1: 若输入的数 < 0 或者 > 7,则炸弹爆炸!
爆炸点2: 若第 i 个数和它后面的某个数相等,则炸弹爆炸!
爆炸点3: 学号最后一位若是奇数,节点值排序后是升序,则爆炸!若是偶数,节点值排序后是降序,则爆炸!(现在是2021-11-21 1:29)
拆完这 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>: nop
0x004022e4 <+128>: addiu v0,s8,24
0x004022e8 <+132>: move a0,v0
0x004022ec <+136>: lui v0,0x40
0x004022f0 <+140>: addiu a1,v0,10416
0x004022f4 <+144>: jal 0x401cf8 <strings_not_equal> //出现了一个字符串的比较。一个炸弹已经拆解的函数,为什么要比较字符串呢?我们在 GDB 中进行查看
0x004022f8 <+148>: nop
0x004022fc <+152>: lw gp,16(s8)
0x00402300 <+156>: bnez v0,0x402354 <phase_defused+240>
0x00402304 <+160>: nop
0x00402308 <+164>: lui v0,0x40
0x0040230c <+168>: addiu a0,v0,10432
0x00402310 <+172>: lw v0,-32712(gp)
0x00402314 <+176>: nop
0x00402318 <+180>: move t9,v0
0x0040231c <+184>: jalr t9
0x00402320 <+188>: nop
0x00402324 <+192>: lw gp,16(s8)
0x00402328 <+196>: lui v0,0x40
0x0040232c <+200>: addiu a0,v0,10472
0x00402330 <+204>: lw v0,-32712(gp)
0x00402334 <+208>: nop
0x00402338 <+212>: move t9,v0
0x0040233c <+216>: jalr t9
0x00402340 <+220>: nop
0x00402344 <+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,-40
0x00401994 <+4>: sw ra,36(sp)
0x00401998 <+8>: sw s8,32(sp)
0x0040199c <+12>: move s8,sp
0x004019a0 <+16>: lui gp,0x42
0x004019a4 <+20>: addiu gp,gp,-20080
0x004019a8 <+24>: sw gp,16(sp)
0x004019ac <+28>: jal 0x401fec <read_line>
0x004019b0 <+32>: nop
0x004019b4 <+36>: lw gp,16(s8)
0x004019b8 <+40>: sw v0,28(s8) //存储输入的数据(这里我输入的是 10)
0x004019bc <+44>: lw v0,28(s8) //load 保存输入的数据
0x004019c0 <+48>: nop
0x004019c4 <+52>: move a0,v0
0x004019c8 <+56>: move a1,zero
0x004019cc <+60>: li a2,10
0x004019d0 <+64>: lw v0,-32656(gp)
0x004019d4 <+68>: nop
0x004019d8 <+72>: move t9,v0
0x004019dc <+76>: jalr t9
0x004019e0 <+80>: nop
--Type <RET> for more, q to quit, c to continue without paging--c
0x004019e4 <+84>: lw gp,16(s8)
0x004019e8 <+88>: sw v0,24(s8) //保存输入的数据(10)
0x004019ec <+92>: lw v0,24(s8) //load 输入的数据
0x004019f0 <+96>: nop
0x004019f4 <+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>: nop
0x00401a04 <+116>: jal 0x4021f0 <explode_bomb>
0x00401a08 <+120>: nop
0x00401a0c <+124>: lw gp,16(s8)
0x00401a10 <+128>: lui v0,0x41
0x00401a14 <+132>: addiu a0,v0,12676 //$a0 = 0x24
0x00401a18 <+136>: lw a1,24(s8) //输入的数据(10)
0x00401a1c <+140>: jal 0x4018a4 <fun7> //跳转至 fun7 啦
做个记录:爆炸点1:输入数据若 > 1001,则爆炸!
这里发生了一个跳转,那么接下来我们看看 fun7 干了啥:
0x004018a4 <+0>: addiu sp,sp,-32
0x004018a8 <+4>: sw ra,28(sp)
0x004018ac <+8>: sw s8,24(sp)
0x004018b0 <+12>: move s8,sp
0x004018b4 <+16>: sw a0,32(s8) //$a0=0x24,第二次:$a0=0x08
0x004018b8 <+20>: sw a1,36(s8) //$a1=10,第二次:$a1=10
0x004018bc <+24>: lw v0,32(s8) //$v0=0x24,第二次:$v0=0x08
0x004018c0 <+28>: nop
0x004018c4 <+32>: bnez v0,0x4018d8 <fun7+52> //$a0=0x24,不为 0,所以跳转至 <+52>
0x004018c8 <+36>: nop
0x004018cc <+40>: li v0,-1
0x004018d0 <+44>: b 0x401978 <fun7+212>
0x004018d4 <+48>: nop
0x004018d8 <+52>: lw v0,32(s8) //$v0=0x24(n1),第二次$v0=0x08
0x004018dc <+56>: nop
0x004018e0 <+60>: lw v1,0(v0) //$v1=36,第二次 $v1=8
0x004018e4 <+64>: lw v0,36(s8) //$v0=10,第二次 $v0=10
0x004018e8 <+68>: nop
0x004018ec <+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--c
0x004018f8 <+84>: lw v0,32(s8) //$v0 = 0x24(n1)
0x004018fc <+88>: nop
0x00401900 <+92>: lw v0,4(v0) //$v0 = 0x08(n21)
0x00401904 <+96>: nop
0x00401908 <+100>: move a0,v0 //$a0 = 0x08(n21)
0x0040190c <+104>: lw a1,36(s8) //$a1 = 10
0x00401910 <+108>: jal 0x4018a4 <fun7>
0x00401914 <+112>: nop
0x00401918 <+116>: sll v0,v0,0x1 //左移 1 位,末尾补 1
0x0040191c <+120>: b 0x401978 <fun7+212>
0x00401920 <+124>: nop
0x00401924 <+128>: lw v0,32(s8) //第二次:$v0=0x08
0x00401928 <+132>: nop
0x0040192c <+136>: lw v1,0(v0) //第二次:$v1=8
0x00401930 <+140>: lw v0,36(s8) //第二次:$v0=10
0x00401934 <+144>: nop
0x00401938 <+148>: slt v0,v1,v0 //$v1 < $v0 ? 若是,则不跳转,反之跳转
0x0040193c <+152>: beqz v0,0x401974 <fun7+208>
0x00401940 <+156>: nop
0x00401944 <+160>: lw v0,32(s8) //第二次:$v0=0x08
0x00401948 <+164>: nop
0x0040194c <+168>: lw v0,8(v0) //第二次:$v0=0x16
0x00401950 <+172>: nop
0x00401954 <+176>: move a0,v0 //$a0=0x16
0x00401958 <+180>: lw a1,36(s8) //$a1=10
0x0040195c <+184>: jal 0x4018a4 <fun7>
0x00401960 <+188>: nop
0x00401964 <+192>: sll v0,v0,0x1
0x00401968 <+196>: addiu v0,v0,1 //左移 1 位,末尾补 1
0x0040196c <+200>: b 0x401978 <fun7+212>
0x00401970 <+204>: nop
0x00401974 <+208>: move v0,zero
0x00401978 <+212>: move sp,s8
0x0040197c <+216>: lw ra,28(sp)
0x00401980 <+220>: lw s8,24(sp)
0x00401984 <+224>: addiu sp,sp,32
0x00401988 <+228>: jr ra
0x0040198c <+232>: nop
那么,这个函数就相当于是一个二叉搜索树,根据 $v0 和 $v1 的大小关系来确定下一节点是左孩子还有右孩子。同时,没执行结束一个函数都会将 $v0 向左移动一位,末位补 1.
下面来看这个返回值在 secret_phase 中的作用:
0x00401a20 <+144>: nop
0x00401a24 <+148>: lw gp,16(s8)
0x00401a28 <+152>: move v1,v0 //$v1=$v0,即将返回值给 $v1
0x00401a2c <+156>: li v0,7 //$v0 = 7
0x00401a30 <+160>: beq v1,v0,0x401a44 <secret_phase+180> //$v1 = 7 ? 相等则跳转,避开炸弹;反之炸弹爆炸!
0x00401a34 <+164>: nop
0x00401a38 <+168>: jal 0x4021f0 <explode_bomb>
0x00401a3c <+172>: nop
0x00401a40 <+176>: lw gp,16(s8)
0x00401a44 <+180>: lui v0,0x40
按照之前所说,每一次函数的执行,都会将 $v0 左移 1 位,末尾补 1,那么
000 → 001 → 011 → 111 000 \rightarrow 001 \rightarrow 011 \rightarrow 111 000→001→011→111因此,我们需要做的就是控制函数的执行次数是 3 次。即二叉搜索树的搜索次数是 3 次。
做个记录:爆炸点2:二叉搜索数的搜索次数为 3 次,否则爆炸
在 GDB 中进行查看,很容易得到这棵二叉搜索树,我们给出其节点值(作图如下):
做个记录:爆炸点1: 输入数据若 > 1001,则爆炸!
做个记录:爆炸点2: 二叉搜索数的搜索次数为 3 次,否则爆炸!
写完文章,CSDN 已经开始卡顿了hhh,不妨静下心来一个一个看这些炸弹,跟着思路走一走。相信真正理解了这些汇编,这个实验才算真正达到了目的吧!
有什么问题烦请告知哈!