【CMU CS15213】Bomb Lab CSAPP 实验报告

文章详细介绍了如何通过阅读和理解x86-64汇编代码来完成CMUCS15-213课程的拆弹实验,涉及字符串比较、循环、数组处理、函数调用等多个技术点。每个阶段的炸弹都对应一个特定的解密逻辑,读者需要理解并利用这些逻辑来找出正确答案。实验中还涉及到了GDB的使用,以及对《计算机系统概论》(CSAPP)中知识的实际应用。

前言

这学期做完了计组实验后,我终于有时间挑战一下一直心心念念的CMU CS15-213的拆炸弹实验了。
看了几眼题目,发现需要读懂汇编代码,于是又屁颠屁颠跑回去学CSAPP第三章……

学完之后,我大致描述一下这个Lab:这个Lab重点考察x86-64汇编代码阅读能力,但考察的难度不深,主要是读懂条件分支、循环和数组链表等代码的实现。
而入门x86-64汇编代码的最好教程就是CSAPP第三章!
而入门x86-64汇编代码的最好教程就是CSAPP第三章!
而入门x86-64汇编代码的最好教程就是CSAPP第三章!
(此发言综合了找资源等时间精力上的消耗的考量,含有较强的个人主观成分;当然若您有更好的资料,您对,您也可分享)

在学CSAPP第三章的时候,首先要简单掌握各个指令分别是什么意思,而重点在于后面的“控制”“过程”两节,讲解了分支、循环、函数调用过程中的返回值被调用者保存局部变量参数等概念的汇编实现,学会了对看懂汇编程序大有帮助!

Bomb Lab 提供两个文件,可执行文件bomb和程序主函数bomb.c,我们目前只有程序的主函数,需要使用objdump反编译bomb得到炸弹程序完整的汇编代码,再用所学知识反编译为C代码。

因此实际上呀,是有6个阶段,要拆6个炸弹!

但是这个Lab并没有想象中的难,最好是尽量自己做,不会再查相关资料,而不是直接看别人的总结。

准备工作

  1. 在VM安装Ubuntu(bomb程序在Linux下才能运行)
  2. 下载lab文件夹CASPP实验官网官方学生资源界面
  3. 使用objdump反汇译得到汇编代码程序(objdump安装方式上网搜索即可,windows也可以反汇编)
  4. 学习使用GDB调试bomb程序,用Linux的gdb才能运行bomb(GDB指令手册
  5. 学习CSAPP第三章

正式开始吧

  1. 下载并解压程序文件,得到bomb可执行文件与bomb.c
  2. 在该文件夹下终端输入指令objdump -S -d main > main.txt,从bomb中反汇编出汇编文件bomb.txt
  3. 再用指令gbd bomb进入到gdb功能

那现在,我们就有汇编代码与gdb啦
在这里插入图片描述

main函数

(main的源码我就不贴了)
我们从bomb.c文件中得知,我们的目标就是解决6个phase函数,每次一个输入,当6次输入都不会引爆各自phase函数中的<explode_bomb> 时,就能够正常通过phase函数啦!

phase_1

那就开始看<phase_1>函数吧!

0000000000400ee0 <phase_1>:
phase_1():
  400ee0:	48 83 ec 08          	sub    $0x8,%rsp  			# 开8字节的栈
  400ee4:	be 00 24 40 00       	mov    $0x402400,%esi   	# 第二个参数(第一个是你刚输进入的数)
  400ee9:	e8 4a 04 00 00       	call   401338 <strings_not_equal> #查一下
  400eee:	85 c0                	test   %eax,%eax
  400ef0:	74 05                	je     400ef7 <phase_1+0x17> # BOMB if %eax == 0
  400ef2:	e8 43 05 00 00       	call   40143a <explode_bomb>
  400ef7:	48 83 c4 08          	add    $0x8,%rsp # 关栈
  400efb:	c3                   	ret

(p.s. 里面的两个函数<strings_not_equal><explode_bomb>,先姑且按字面意思理解,一个是比较两字符串是否相等并返回布尔值,另一个是炸弹爆炸。lab做完后再去看源码相信对你来说已是小菜一碟)

书上说,x86-64给函数传参使用的寄存器是(依次列出):%rdi %rsi %dx rcx r8 r9。因此我们在调用函数<strings_not_equal>前设置的%rsi就是它的第二个参数,那我们的第一个参数则是我们的%rdi。为了验证想法,我们可以用GDB查看在0x400ee9位置时这两个参数的值(此时0x400ee9位置的指令尚未执行):
在这里插入图片描述
忽略中间执行信息,我们直接看头尾,我先是在0x400ee9设置了一个断点,然后输入r执行调试,输入hhhhhhhWhat?。随后达到断点,查看两寄存器指向位置的字符串,证实了我们的想法。

继续看<phase_1>的代码:

400ee9:	e8 4a 04 00 00       	call   401338 <strings_not_equal> #查一下
400eee:	85 c0                	test   %eax,%eax
400ef0:	74 05                	je     400ef7 <phase_1+0x17> # BOMB if %eax == 0
400ef2:	e8 43 05 00 00       	call   40143a <explode_bomb>

这一段是非常经典的分支代码了,我们知道调用函数<strings_not_equal>会返回一个布尔值,而这个返回值是放在%rax中传出的。test %eax,%eax使%eax = %eax & %eax,与je指令一起使用则是比较%rax == 0,满足则跳转,不满足则不跳转然后炸弹爆炸。

那就简单了嘛,只要我的输入和%rsi指向的字符串相同,那我就成了嘻嘻~

成功通过<phase_1>
在这里插入图片描述

phase_2

这是<phase_2>开头部分,信息量比较小:

0000000000400efc <phase_2>:
  400efc:	55                   	push   %rbp
  400efd:	53                   	push   %rbx 		# 保存被调用者保存寄存器,可以不理会
  400efe:	48 83 ec 28          	sub    $0x28,%rsp   # 开栈(开栈的用途有多种,看后面才能知道这里的目的)
  400f02:	48 89 e6             	mov    %rsp,%rsi	# 把栈交给%rsi

这时候也还看不出%rsi的作用:
在这里插入图片描述
接下来是一个函数调用与一个分支判断,“read_six_numbers”是吧,那我就随便输入个1 1 1 1 1 1吧。暂时不管函数<read_six_numbers>内部进行了什么工作,直接看调用后的结果。

400f05:	e8 52 05 00 00       	call   40145c <read_six_numbers> # 获取6个int数
400f0a:	83 3c 24 01          	cmpl   $0x1,(%rsp)
400f0e:	74 20                	je     400f30 <phase_2+0x34> # 若nums[0]==1
400f10:	e8 25 05 00 00       	call   40143a <explode_bomb>

在这里插入图片描述
这不就来了嘛,看来这个栈,相当于一个int数组呀!(姑且命名为nums数组)
然后的分支判断比较1 == (%rsp)相当于1 == nums[0],不满足就爆炸,还好我满足了哇。

下面又直接跳到了位置0x400f30,不好,书上说,这是循环的征兆!

  							# 跳到循环初始化
400f15:	eb 19                	jmp    400f30 <phase_2+0x34>
  # 循环体begin
400f17:	8b 43 fc             	mov    -0x4(%rbx),%eax  # %eax = nums[i-1]
400f1a:	01 c0                	add    %eax,%eax
							# if(2*nums[i-1] != nums[i]) BOMB();
400f1c:	39 03                	cmp    %eax,(%rbx)  	# if(2*nums[i-1] != nums[i]) BOMB()
400f1e:	74 05                	je     400f25 <phase_2+0x29>
400f20:	e8 15 05 00 00       	call   40143a <explode_bomb>
  							# 即总要有nums[i] == 2 * nums[i-1]!!
400f25:	48 83 c3 04          	add    $0x4,%rbx 		# i++
  							# 循环终止判断
400f29:	48 39 eb             	cmp    %rbp,%rbx
400f2c:	75 e9                	jne    400f17 <phase_2+0x1b>
400f2e:	eb 0c                	jmp    400f3c <phase_2+0x40>
							# 循环初始化
400f30:	48 8d 5c 24 04       	lea    0x4(%rsp),%rbx 	# %rbx加4,rbx即for循环中的i 
400f35:	48 8d 6c 24 18       	lea    0x18(%rsp),%rbp	# 终止条件
400f3a:	eb db                	jmp    400f17 <phase_2+0x1b> 
  # 循环体end

循环的流程已经给出来了,直接看注释。目前得到的最重要的信息有:

  1. nums[0] == 1
  2. 总要有nums[i] == 2 * nums[i-1]

因此答案就是1 2 4 8 12 32啦! 剩下只是收尾代码:

# 退出函数
400f3c:	48 83 c4 28          	add    $0x28,%rsp
400f40:	5b                   	pop    %rbx
400f41:	5d                   	pop    %rbp
400f42:	c3                   	ret    

(p.s.有兴趣的同学可以看看<read_six_numbers>函数,我只补充其中函数<__isoc99_sscanf@plt>的返回值是读取到的数字的数目,在read_six_numbers中若其返回值不大于5,则爆炸。至于它的参数%rsi,我用GDB看了一下:指向"%d %d %d %d %d %d"

  000000000040145c <read_six_numbers>:
  40145c:	48 83 ec 18          	sub    $0x18,%rsp  # 开18字节的栈
  401460:	48 89 f2             	mov    %rsi,%rdx  
  401463:	48 8d 4e 04          	lea    0x4(%rsi),%rcx # 要打草稿
  401467:	48 8d 46 14          	lea    0x14(%rsi),%rax
  40146b:	48 89 44 24 08       	mov    %rax,0x8(%rsp)
  401470:	48 8d 46 10          	lea    0x10(%rsi),%rax
  401474:	48 89 04 24          	mov    %rax,(%rsp)
  401478:	4c 8d 4e 0c          	lea    0xc(%rsi),%r9
  40147c:	4c 8d 46 08          	lea    0x8(%rsi),%r8
  401480:	be c3 25 40 00       	mov    $0x4025c3,%esi
  401485:	b8 00 00 00 00       	mov    $0x0,%eax
  40148a:	e8 61 f7 ff ff       	call   400bf0 <__isoc99_sscanf@plt>
  40148f:	83 f8 05             	cmp    $0x5,%eax # if(5 >= %eax) BOMB();
  401492:	7f 05                	jg     401499 <read_six_numbers+0x3d>
  401494:	e8 a1 ff ff ff       	call   40143a <explode_bomb>
  401499:	48 83 c4 18          	add    $0x18,%rsp
  40149d:	c3                   	ret    

phase_3

phase_3真长呀。
这次,上面讲过的知识点我只用注释注明。再提一嘴,我是用GDB研究地址指向内容的。

下面的代码获取两个数,姑且设为x,y。

0000000000400f43 <phase_3>:
 400f43:	48 83 ec 18          	sub    $0x18,%rsp		# 开栈
 400f47:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx	# 指针%rcx
 400f4c:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx	# 指针%rdx
 400f51:	be cf 25 40 00       	mov    $0x4025cf,%esi 	# 指向"%d %d"
 400f56:	b8 00 00 00 00       	mov    $0x0,%eax
		 			# 获取两个数,设为x,y,存储在(%rdx)和(%rcx),返回读取数目,保存在%rax返回
 400f5b:	e8 90 fc ff ff       	call   400bf0 <__isoc99_sscanf@plt>
   					# 和`<read_six_numbers>`一样的把戏,检查输入的数量
 400f60:	83 f8 01             	cmp    $0x1,%eax 
 400f63:	7f 05                	jg     400f6a <phase_3+0x27>	
 400f65:	e8 d0 04 00 00       	call   40143a <explode_bomb>
   					# x 无符号超过7则爆炸
 400f6a:	83 7c 24 08 07       	cmpl   $0x7,0x8(%rsp) 
 400f6f:	77 3c                	ja     400fad <phase_3+0x6a>

从上面代码得到的最重要的信息是0 <= x <= 7

下面有好长一段代码呀,而且很有规律,都是jmp-mov-jmp-mov的,我做的时候不知道这是啥呀就反推分析了,直接去看要避开爆炸需要什么条件了,但现在我决心要弄懂整串代码的逻辑。

 	# 间接跳转到(8*x + 0x402470)保存的值
400f71:	8b 44 24 08          	mov    0x8(%rsp),%eax
400f75:	ff 24 c5 70 24 40 00 	jmp    *0x402470(,%rax,8) 
# 分别为0x400f7c  0x400fb9 0x400f83 0x400f8a 0x400f91 0x400f98 0x400f9f 0x400fa6
400f7c:	b8 cf 00 00 00       	mov    $0xcf,%eax
400f81:	eb 3b                	jmp    400fbe <phase_3+0x7b>
400f83:	b8 c3 02 00 00       	mov    $0x2c3,%eax
400f88:	eb 34                	jmp    400fbe <phase_3+0x7b>
400f8a:	b8 00 01 00 00       	mov    $0x100,%eax
400f8f:	eb 2d                	jmp    400fbe <phase_3+0x7b>
400f91:	b8 85 01 00 00       	mov    $0x185,%eax
400f96:	eb 26                	jmp    400fbe <phase_3+0x7b>
400f98:	b8 ce 00 00 00       	mov    $0xce,%eax
400f9d:	eb 1f                	jmp    400fbe <phase_3+0x7b>
400f9f:	b8 aa 02 00 00       	mov    $0x2aa,%eax
400fa4:	eb 18                	jmp    400fbe <phase_3+0x7b>
400fa6:	b8 47 01 00 00       	mov    $0x147,%eax
400fab:	eb 11                	jmp    400fbe <phase_3+0x7b>
     # 这个爆炸函数只和上一段代码的条件分支有关,不知为何出现于此
400fad:	e8 88 04 00 00       	call   40143a <explode_bomb>
400fb2:	b8 00 00 00 00       	mov    $0x0,%eax
400fb7:	eb 05                	jmp    400fbe <phase_3+0x7b>
400fb9:	b8 37 01 00 00       	mov    $0x137,%eax		# x == 1
400fbe:	3b 44 24 0c          	cmp    0xc(%rsp),%eax 	# y == 0x137(即311) 则结束
400fc2:	74 05                	je     400fc9 <phase_3+0x86> 
400fc4:	e8 71 04 00 00       	call   40143a <explode_bomb>
400fc9:	48 83 c4 18          	add    $0x18,%rsp
400fcd:	c3                   	ret    

这段代码开头的间接跳转(书上有介绍间接跳转),它的可能跳转目标地址有8个(根据x的取值)为:
在这里插入图片描述
而中间一连串代码的共同特征是:

  1. 都能从开头的间接跳转访问,
  2. 给%eax赋值,
  3. 再跳转到目标地址400fbe,即最后的爆炸判断代码。

而最后不爆炸的条件是"y == %eax"。

综上,我们要先输入一个0~7范围的整数x,然后输入一个y等于对应目标地址程序的%eax值(注意程序与地址不是按顺序对应的),即可能的答案有8种。
我用的是1 311

phase_4

000000000040100c <phase_4>:
  40100c:	48 83 ec 18          	sub    $0x18,%rsp
  401010:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx 		# y
  401015:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx 		# x
  40101a:	be cf 25 40 00       	mov    $0x4025cf,%esi
  40101f:	b8 00 00 00 00       	mov    $0x0,%eax
  401024:	e8 c7 fb ff ff       	call   400bf0 <__isoc99_sscanf@plt>
  401029:	83 f8 02             	cmp    $0x2,%eax			# 两个输入
  40102c:	75 07                	jne    401035 <phase_4+0x29>
  40102e:	83 7c 24 08 0e       	cmpl   $0xe,0x8(%rsp)  		# x <= 14
  401033:	76 05                	jbe    40103a <phase_4+0x2e>
  401035:	e8 00 04 00 00       	call   40143a <explode_bomb>
  					 # 调用函数: int func4(x,0,14);
  40103a:	ba 0e 00 00 00       	mov    $0xe,%edx
  40103f:	be 00 00 00 00       	mov    $0x0,%esi
  401044:	8b 7c 24 08          	mov    0x8(%rsp),%edi
  401048:	e8 81 ff ff ff       	call   400fce <func4>
  40104d:	85 c0                	test   %eax,%eax        # eax == 0(即x==7)
  40104f:	75 07                	jne    401058 <phase_4+0x4c>
  401051:	83 7c 24 0c 00       	cmpl   $0x0,0xc(%rsp)   # 0 == y
  401056:	74 05                	je     40105d <phase_4+0x51>
  401058:	e8 dd 03 00 00       	call   40143a <explode_bomb>
  40105d:	48 83 c4 18          	add    $0x18,%rsp
  401061:	c3                   	ret    

上面<phase_4>函数的逻辑还是很清晰的,我们得到了两个信息:

  1. y == 0
  2. <func4>函数的返回值%eax == 0
    那接下来的重点就在于看它调用的<func4>函数究竟返回了个什么%eax 了。我发现<func4>使用大量的数值运算和分支跳转,更要命的是还是递归函数,我就先将它写成C语言函数再来分析了:
# 写成C函数(源码我就不贴了):
# 初始时si = 0,dx = 14
int func4(int x, int si, int dx){
  int ax = dx - si;
  int cx = ax / (2^31);
  ax = (ax+cx) / 2;
  cx = ax + si;
  if(cx <= x){
      ax = 0;
      if(cx >= x){		# cx == x 则返回0
          return ax;
      }
      si = cx + 1;
      func(x,si,dx);
      return 2 * ax + 1;
  }
  else {
      dx = cx - 1;
      func(x,si,dx);
      return ax * 2;
  }
}

看,多么简单,甚至不用在意那两处自调用,只需要开始时"x == 7"就能获得为0的返回值!
综上答案是“7 0”。

phase_5

这题可有意思嘿嘿。

0000000000401062 <phase_5>:
  401062:	53                   	push   %rbx
  401063:	48 83 ec 20          	sub    $0x20,%rsp
  401067:	48 89 fb             	mov    %rdi,%rbx		# rbx指向我的输入
   								# fs是一个段寄存器,但此时%fs == 0(具体不清楚不管先)
  40106a:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax	
  401071:	00 00 
  401073:	48 89 44 24 18       	mov    %rax,0x18(%rsp)	# 
  								# 输入长度为6
  401078:	31 c0                	xor    %eax,%eax		# 令eax = 0
  40107a:	e8 9c 02 00 00       	call   40131b <string_length>
  40107f:	83 f8 06             	cmp    $0x6,%eax  
  401082:	74 4e                	je     4010d2 <phase_5+0x70>
  401084:	e8 b1 03 00 00       	call   40143a <explode_bomb>

接着是一段循环:

  	# 跳到循环的初始化位置 (设eax=i,rbx=str)
401089:	eb 47                	jmp    4010d2 <phase_5+0x70>
  					# 循环体begin	# 'b'证明了数组str里存放的是char型
40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx		# ecx = str[i] 
40108f:	88 0c 24             	mov    %cl,(%rsp)
401092:	48 8b 14 24          	mov    (%rsp),%rdx
401096:	83 e2 0f             	and    $0xf,%edx				# edx = ecx & 0xf
  									# 又是一个字符串,查查看! 
401099:	0f b6 92 b0 24 40 00 	movzbl 0x4024b0(%rdx),%edx		# 以%edx为偏移取一个char型
4010a0:	88 54 04 10          	mov    %dl,0x10(%rsp,%rax,1)	# 放进另一个字符串内
4010a4:	48 83 c0 01          	add    $0x1,%rax				# i++
					# 循环终止判断
4010a8:	48 83 f8 06          	cmp    $0x6,%rax    			# rax == 6 则终止循环
4010ac:	75 dd                	jne    40108b <phase_5+0x29>	# 否则接着遍历
  					# 离开循环,竟马上判断爆炸。(很有趣,虽然被编译进循环体里,却不会被循环访问到)
4010ae:	c6 44 24 16 00       	movb   $0x0,0x16(%rsp)		# 设置字符串最后的'\0'
4010b3:	be 5e 24 40 00       	mov    $0x40245e,%esi		# 又一字符串,查到为"flyers"
4010b8:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi		# 刚才我们的一番操作得到的字符串 
4010bd:	e8 76 02 00 00       	call   401338 <strings_not_equal>
4010c2:	85 c0                	test   %eax,%eax    			# 两字符串相等
4010c4:	74 13                	je     4010d9 <phase_5+0x77> 	# 否则爆炸
4010c6:	e8 6f 03 00 00       	call   40143a <explode_bomb>
4010cb:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
4010d0:	eb 07                	jmp    4010d9 <phase_5+0x77>
					# 循环初始化
4010d2:	b8 00 00 00 00       	mov    $0x0,%eax            	# i=0
4010d7:	eb b2                	jmp    40108b <phase_5+0x29>	# 跳到循环体首行
					# 循环end(因此循环的作用是遍历我的输入字符串,算出另一个字符串)

我查看0x4024b0位置的内容(作者真皮):
“maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?”

综上我们的任务就是,输入一串6位字符串,将每个字符的二进制码的后8位表示的数字作为偏移量,在上面的字符串中选出"flyers"即可。因此答案也有多种,只要满足最后8位为9、15、14、5、6、7即可,我使用的是“IONEFG”。(有空格也不行哦)

剩下一些无伤大雅的函数收尾工作,我就不赘述了。不过我还是不知道%fs是什么呜呜。

phase_6

终于来到的最终环节,这次的函数也是至今为止最长的家伙(虽然结构挺明显的):

00000000004010f4 <phase_6>:
  4010f4:	41 56                	push   %r14
  4010f6:	41 55                	push   %r13
  4010f8:	41 54                	push   %r12
  4010fa:	55                   	push   %rbp
  4010fb:	53                   	push   %rbx
  4010fc:	48 83 ec 50          	sub    $0x50,%rsp   # %rsp 保存了我的输入
  401100:	49 89 e5             	mov    %rsp,%r13	# %rsi = &nums[0]
  401103:	48 89 e6             	mov    %rsp,%rsi	# %rsi = &nums[0]
							# 读六位int型,姑且称为数组nums
  401106:	e8 51 03 00 00       	call   40145c <read_six_numbers>
  40110b:	49 89 e6             	mov    %rsp,%r14	# %r14 = &nums[0]
  
  # 开头便是2层嵌套数组真猛# 外循环初始化
  40110e:	41 bc 00 00 00 00    	mov    $0x0,%r12d	
						# 外循环
  401114:	4c 89 ed             	mov    %r13,%rbp
  401117:	41 8b 45 00          	mov    0x0(%r13),%eax         
  40111b:	83 e8 01             	sub    $0x1,%eax
  								# 确保 1 <= nums[i] <= 6
  40111e:	83 f8 05             	cmp    $0x5,%eax
  401121:	76 05                	jbe    401128 <phase_6+0x34>  # 0 <= eax <= 5
  401123:	e8 12 03 00 00       	call   40143a <explode_bomb>
  401128:	41 83 c4 01          	add    $0x1,%r12d           # i++
  40112c:	41 83 fc 06          	cmp    $0x6,%r12d   		# r12 == 5 则离开外循环
  401130:	74 21                	je     401153 <phase_6+0x5f>
  401132:	44 89 e3             	mov    %r12d,%ebx   # ebx = r12
       						# 内循环
  401135:	48 63 c3             	movslq %ebx,%rax    	# j = i
  401138:	8b 04 84             	mov    (%rsp,%rax,4),%eax  # nums[j]
  								# 6个输入互相不能相等
  40113b:	39 45 00             	cmp    %eax,0x0(%rbp)   # num[j~5] != nums[i-1]
  40113e:	75 05                	jne    401145 <phase_6+0x51>
  401140:	e8 f5 02 00 00       	call   40143a <explode_bomb>
  401145:	83 c3 01             	add    $0x1,%ebx    	# ebx++
  401148:	83 fb 05             	cmp    $0x5,%ebx    	# ebx > 5 则离开内循环
  40114b:	7e e8                	jle    401135 <phase_6+0x41>
      						# 内循环end 
  40114d:	49 83 c5 04          	add    $0x4,%r13    # i++
  401151:	eb c1                	jmp    401114 <phase_6+0x20>
    					# 外循环end (综上,6个输入的值只能在1~6之间)
    					
	# 又是循环			# 循环初始化				
  401153:	48 8d 74 24 18       	lea    0x18(%rsp),%rsi  # %rsi指向空栈
  401158:	4c 89 f0             	mov    %r14,%rax        # %rax指向nums[0],相当于i
  40115b:	b9 07 00 00 00       	mov    $0x7,%ecx
    					# 循环体begin
  401160:	89 ca                	mov    %ecx,%edx
  401162:	2b 10                	sub    (%rax),%edx      # edx = 7 - nums[0]
  401164:	89 10                	mov    %edx,(%rax)      # 令nums元素等于7-自己
  401166:	48 83 c0 04          	add    $0x4,%rax        # i++
  40116a:	48 39 f0             	cmp    %rsi,%rax        # 循环遍历终止条件
  40116d:	75 f1                	jne    401160 <phase_6+0x6c>
    					# 循环end (将nums中的元素变为"7-自己的值")
  
  # 下面是循环套循环套分支跳转,因此显得复杂(先大致看看代码,然后我会讲解)
						# 外循环初始化
  40116f:	be 00 00 00 00       	mov    $0x0,%esi        		# 设%rsi为i
  401174:	eb 21                	jmp    401197 <phase_6+0xa3>    # 直接跳到1
  							# 内循环体begin
  401176:	48 8b 52 08          	mov    0x8(%rdx),%rdx  # %rdx=next 	# 着陆点2
  40117a:	83 c0 01             	add    $0x1,%eax       # %rax++
  							# 内循环终止判断
  40117d:	39 c8                	cmp    %ecx,%eax       # %eax == nums[i]则退出
  40117f:	75 f5                	jne    401176 <phase_6+0x82>  # 即遍历链表找到与nums[i]相等的结点
							# 内循环体end
  401181:	eb 05                	jmp    401188 <phase_6+0x94>	# 直接跳到3
  401183:	ba d0 32 60 00       	mov    $0x6032d0,%edx			# 着陆点4
  401188:	48 89 54 74 20       	mov    %rdx,0x20(%rsp,%rsi,2)   # 着陆点3,执行操作
  						# 外循环终止判断
  40118d:	48 83 c6 04          	add    $0x4,%rsi                # i++
  401191:	48 83 fe 18          	cmp    $0x18,%rsi               
  401195:	74 14                	je     4011ab <phase_6+0xb7>
   						# 外循环体begin	
  401197:	8b 0c 34             	mov    (%rsp,%rsi,1),%ecx       # 着陆点1
						# 跳过内循环判断(内循环的功能是遍历链表找对应节点)
  40119a:	83 f9 01             	cmp    $0x1,%ecx                # nums[i]==1则跳到4
  40119d:	7e e4                	jle    401183 <phase_6+0x8f>
  							# 内循环初始化
  40119f:	b8 01 00 00 00       	mov    $0x1,%eax                # 通过内循环找到对应结点
  4011a4:	ba d0 32 60 00       	mov    $0x6032d0,%edx           # 链表首地址
							# 进入内循环
  4011a9:	eb cb                	jmp    401176 <phase_6+0x82>    
  						# 外循环体end
  # (即按照输入的序号,将链表的地址重新排列到新的数组(0x20+%rsp)上(姑且称为locations))

讲一下这个“循环套循环套分支跳转”哈,现已经梳理了大致的程序流程了,我们重点来看看这个程序它干了什么。这个外循环,除开和内循环有关的代码,它就只做了一件事:把%rdx指向的内容以8字节长度赋值到%rsp指向的数组location中。来看看当"nums[i]==1"时%rdx(即0x6032d0)指向的内容:
在这里插入图片描述
有没有看出来呀,这是一个链表!结点内部结构为:|val1|val2|*next|0|
看来外循环就是把指向结点的指针放进了location里。

再来看内循环,私以为内循环的工作好懂一些,即遍历链表找到val2nums[i]相等的结点,并将结点指针交给%rdx

破案拉破案啦,这一串操作猛如虎下来,其实就只是遍历结点,并按照我们的输入重新“排列”结点。

# 继续看代码吧(又是循环)	 # 循环的初始化
4011ab:	48 8b 5c 24 20       	mov    0x20(%rsp),%rbx  # rbx = loca[i]
4011b0:	48 8d 44 24 28       	lea    0x28(%rsp),%rax  # rax = &loca[i+1]
4011b5:	48 8d 74 24 50       	lea    0x50(%rsp),%rsi  # rsi终止条件
4011ba:	48 89 d9             	mov    %rbx,%rcx        # rcx = loca[i]
                         # 循环体begin
4011bd:	48 8b 10             	mov    (%rax),%rdx      # rdx = loca[i+1]
4011c0:	48 89 51 08          	mov    %rdx,0x8(%rcx)   # loca[i+1] = loca[i+1](有意义吗)
4011c4:	48 83 c0 08          	add    $0x8,%rax        # i++
 						 # 终止判断
4011c8:	48 39 f0             	cmp    %rsi,%rax        
4011cb:	74 05                	je     4011d2 <phase_6+0xde># 离开循环
4011cd:	48 89 d1             	mov    %rdx,%rcx            # rcx = loca[i]
4011d0:	eb eb                	jmp    4011bd <phase_6+0xc9>
                         # 循环体end(似乎啥事也没做?)
                         
 # 最后这段程序要求location中,结点的val1递减
 						 # 循环初始化					# 此时rbx指向链表首地址
4011d2:	48 c7 42 08 00 00 00 	movq   $0x0,0x8(%rdx) 	# 尾指针指向null
4011d9:	00                                              
4011da:	bd 05 00 00 00       	mov    $0x5,%ebp        # 终止条件 i = 5
 						# 循环体begin
4011df:	48 8b 43 08          	mov    0x8(%rbx),%rax   # rax = rbx->next
4011e3:	8b 00                	mov    (%rax),%eax      # eax = rax->val1
4011e5:	39 03                	cmp    %eax,(%rbx)      # %eax <= (%rbx)
4011e7:	7d 05                	jge    4011ee <phase_6+0xfa>
4011e9:	e8 4c 02 00 00       	call   40143a <explode_bomb>
4011ee:	48 8b 5b 08          	mov    0x8(%rbx),%rbx	# rbx = rbx->next
4011f2:	83 ed 01             	sub    $0x1,%ebp		# i--
4011f5:	75 e8                	jne    4011df <phase_6+0xeb>
 						# 循环体end

综上,<phase_6>做了这些事:

  1. 输入一个六位整型数组
  2. 元素值范围必须在1~6
  3. 元素值依次被替换为7-自己(结果还是1~6)
  4. 按元素值与链表val2的顺序重新排序链表
  5. 新排列中链表的val1必须递减

因此答案是4 3 2 1 6 5!!

答案汇总

Border relations with Canada have never been better.
1 2 4 8 16 32
1 311 
7 0 
IONEFG
4 3 2 1 6 5

隐藏关卡

如果你已经能够独立做出上面的6个phase,说明你已具有较强的能力,可以用这个隐藏关卡来试试手,这里因为我不会画图等原因,只简要地展示一下注释与答案。

对了,其中涉及的几个函数<__isoc99_sscanf@plt><strtol@plt>你最好自己查一下,加深理解。

00000000004015c4 <phase_defused>:
	# 不重要
  4015c4:	48 83 ec 78          	sub    $0x78,%rsp
  4015c8:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
  4015cf:	00 00 
  4015d6:	31 c0                	xor    %eax,%eax
    # 输入6串字符串(在第六个炸弹时触发) # 0x603760指向一个数字,记载着你输入的字符串数目(也就是你到哪关了)
  4015d8:	83 3d 81 21 20 00 06 	cmpl   $0x6,0x202181(%rip)          # 603760 <num_input_strings>
  4015df:	75 5e                	jne    40163f <phase_defused+0x7b>  # 跳过秘密
    # 炸弹4要输入3个数才能解锁秘密关卡(第三个数是字符串)
  4015e1:	4c 8d 44 24 10       	lea    0x10(%rsp),%r8   # r8 = 1
  4015e6:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx   # rcx = 6
  4015eb:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx   # rdx = 5
  4015f0:	be 19 26 40 00       	mov    $0x402619,%esi   # esi指向"%d %d %s"
  4015f5:	bf 70 38 60 00       	mov    $0x603870,%edi   # edi指向"7 0 某某"
  4015fa:	e8 f1 f5 ff ff       	call   400bf0 <__isoc99_sscanf@plt>
  4015ff:	83 f8 03             	cmp    $0x3,%eax
  401602:	75 31                	jne    401635 <phase_defused+0x71>  #跳过秘密
    # 函数<__isoc99_sscanf@plt> 的输入是:输入字符串,格式化字符串,一些赋值的变量;返回值是成功赋值的个数
    # 炸弹4的第三个输入是"DrEvil",phase_1用过的把戏了
  401604:	be 22 26 40 00       	mov    $0x402622,%esi
  401609:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi
  40160e:	e8 25 fd ff ff       	call   401338 <strings_not_equal>
  401613:	85 c0                	test   %eax,%eax
  401615:	75 1e                	jne    401635 <phase_defused+0x71>  #跳过秘密
    # 不重要
  401617:	bf f8 24 40 00       	mov    $0x4024f8,%edi
  40161c:	e8 ef f4 ff ff       	call   400b10 <puts@plt>
  401621:	bf 20 25 40 00       	mov    $0x402520,%edi
  401626:	e8 e5 f4 ff ff       	call   400b10 <puts@plt>
    # 进入秘密关卡
  40162b:	b8 00 00 00 00       	mov    $0x0,%eax
  401630:	e8 0d fc ff ff       	call   401242 <secret_phase>
    # 不重要
  401635:	bf 58 25 40 00       	mov    $0x402558,%edi
  40163a:	e8 d1 f4 ff ff       	call   400b10 <puts@plt>
  40163f:	48 8b 44 24 68       	mov    0x68(%rsp),%rax
  401644:	64 48 33 04 25 28 00 	xor    %fs:0x28,%rax
  40164b:	00 00 
  40164d:	74 05                	je     401654 <phase_defused+0x90>
  40164f:	e8 dc f4 ff ff       	call   400b30 <__stack_chk_fail@plt>
  401654:	48 83 c4 78          	add    $0x78,%rsp
  401658:	c3                   	ret    


  0000000000401242 <secret_phase>:
  401242:	53                   	push   %rbx
  401243:	e8 56 02 00 00       	call   40149e <read_line>
    # longint strtol(str,&ptr,base);
    # // 将字符串以base进制转化为长整数,并产生一个字符串(转化不成功返回0然后爆炸)
    # 翻译成人话:输入一个0~1000的数,它会帮你把字符串变成long int型数
  401248:	ba 0a 00 00 00       	mov    $0xa,%edx    # 获取返回的数字
  40124d:	be 00 00 00 00       	mov    $0x0,%esi    # 获取返回的字符串
  401252:	48 89 c7             	mov    %rax,%rdi    # eax是指向输入的指针
  401255:	e8 76 f9 ff ff       	call   400bd0 <strtol@plt>
  40125a:	48 89 c3             	mov    %rax,%rbx    # eax = 1
  40125d:	8d 40 ff             	lea    -0x1(%rax),%eax
  401260:	3d e8 03 00 00       	cmp    $0x3e8,%eax      # 0 <= eax <= 0x3e8
  401265:	76 05                	jbe    40126c <secret_phase+0x2a>
  401267:	e8 ce 01 00 00       	call   40143a <explode_bomb>
    # 0x6030f0是什么?  二叉树!?
    # 调用<func7>返回2则通过
  40126c:	89 de                	mov    %ebx,%esi
  40126e:	bf f0 30 60 00       	mov    $0x6030f0,%edi
  401273:	e8 8c ff ff ff       	call   401204 <fun7>
  401278:	83 f8 02             	cmp    $0x2,%eax
  40127b:	74 05                	je     401282 <secret_phase+0x40>
  40127d:	e8 b8 01 00 00       	call   40143a <explode_bomb>
	# 不重要,祝贺语罢了
  401282:	bf 38 24 40 00       	mov    $0x402438,%edi
  401287:	e8 84 f8 ff ff       	call   400b10 <puts@plt>
  40128c:	e8 33 03 00 00       	call   4015c4 <phase_defused>
  401291:	5b                   	pop    %rbx
  401292:	c3                   	ret    

贴上<func7>的C函数:
ax = input-1;
si = input;
BinaryTree *di;
long int func7(BinaryTree * di, int si){
    if(di == null) return ax;
    int dx = di->val;
    if(dx == si) return 0;
    else if(dx < si){
        ax = func(di->right,si);
        return 2 * ax + 1;
    }
    else{
        ax = func(di->left,si);
        return 2 * ax;
    }
}
而树长这样:
				0x24
      0x8                0x32
  0x6     0x16      0x2d      0x66
0x1 0x7 0x14 0x23 0x28 0x2f 0x63 0x2f

答案是22(即0x16)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值