CSAPP实验之Bomb Lab详解
文章目录前言phase 1phase 2phase 3phase 4phase 5phase 6总结和感想前言Bomb Lab来自《深入理解计算机系统》(CSAPP)一书的第三章“程序的机器级表示”的配套实验,该实验的目的是通过反汇编可执行程序,来反推出程序执行内容,进而能够正确破解”密码“,解除“炸弹”。Bomb Lab文件目录如下:├── bomb├── bomb.c└── READMEbom
前言
Bomb Lab来自《深入理解计算机系统》(CSAPP)一书的第三章“程序的机器级表示”的配套实验,该实验的目的是通过反汇编可执行程序,来反推出程序执行内容,进而能够正确破解”密码“,解除“炸弹”。
Bomb Lab文件目录如下:
├── bomb
├── bomb.c
└── README
- bomb: 可执行程序,我们需要对其进行反汇编和gdb调试。
- bomb.c: bomb的主函数main的源文件。
- README: 无关紧要。
通过阅读 bomb.c 可以知道该程序的用法:
/***************************************************************************
* Dr. Evil's Insidious Bomb, Version 1.1
* Copyright 2011, Dr. Evil Incorporated. All rights reserved.
*
* LICENSE:
*
* Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the
* VICTIM) explicit permission to use this bomb (the BOMB). This is a
* time limited license, which expires on the death of the VICTIM.
* The PERPETRATOR takes no responsibility for damage, frustration,
* insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other
* harm to the VICTIM. Unless the PERPETRATOR wants to take credit,
* that is. The VICTIM may not distribute this bomb source code to
* any enemies of the PERPETRATOR. No VICTIM may debug,
* reverse-engineer, run "strings" on, decompile, decrypt, or use any
* other technique to gain knowledge of and defuse the BOMB. BOMB
* proof clothing may not be worn when handling this program. The
* PERPETRATOR will not apologize for the PERPETRATOR's poor sense of
* humor. This license is null and void where the BOMB is prohibited
* by law.
***************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include "support.h"
#include "phases.h"
/*
* Note to self: Remember to erase this file so my victims will have no
* idea what is going on, and so they will all blow up in a
* spectaculary fiendish explosion. -- Dr. Evil
*/
FILE *infile;
int main(int argc, char *argv[])
{
char *input;
/* Note to self: remember to port this bomb to Windows and put a
* fantastic GUI on it. */
/* When run with no arguments, the bomb reads its input lines
* from standard input. */
if (argc == 1) {
infile = stdin;
}
/* When run with one argument <file>, the bomb reads from <file>
* until EOF, and then switches to standard input. Thus, as you
* defuse each phase, you can add its defusing string to <file> and
* avoid having to retype it. */
else if (argc == 2) {
if (!(infile = fopen(argv[1], "r"))) {
printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
exit(8);
}
}
/* You can't call the bomb with more than 1 command line argument. */
else {
printf("Usage: %s [<input_file>]\n", argv[0]);
exit(8);
}
/* Do all sorts of secret stuff that makes the bomb harder to defuse. */
initialize_bomb();
printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");
/* Hmm... Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
* Let me know how they did it. */
printf("Phase 1 defused. How about the next one?\n");
/* The second phase is harder. No one will ever figure out
* how to defuse this... */
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");
/* I guess this is too easy so far. Some more complex code will
* confuse people. */
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");
/* Oh yeah? Well, how good is your math? Try on this saucy problem! */
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");
/* Round and 'round in memory we go, where we stop, the bomb blows! */
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");
/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard. */
input = read_line();
phase_6(input);
phase_defused();
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */
return 0;
}
可以看出该可执行程序要求从命令行或者文件以 行 为单位读入字符串,每行字符串对应一个phase的输入。如果phase执行完毕,会调用phase_defused 函数表明该 phase 成功搞定。
该实验共有6个 phase,难度是逐级提升,考点也不尽相同。
首先需要对bomb进行反汇编:
objdump -d bomb > bomb.s
可以在 bomb.s 中找到这6个phase的汇编:
0000000000400ee0 <phase_1>:.
...
0000000000400efc <phase_2>:
...
0000000000400f43 <phase_3>:
...
000000000040100c <phase_4>:
...
0000000000401062 <phase_5>:
...
00000000004010f4 <phase_6>:
...
对于汇编代码的解读,我自己的方法是三步走,第一步可读性调整,不改变汇编的顺序只是改变符号为c风格;第二步是c-like风格调整,将控制结构改为c风格;第三步是说人话风格调整,c代码。
phase 1
开胃菜,考点:字符串。
本题就无须三步了,直接对汇编稍作c-like风格调整后,可以表示为以下代码:
phase_1(rdi)
{
esi = 0x402400;
string_no_equal(rdi, esi);
if (eax == 0)
callq explode_bomb;
retq
}
其中0x402400
是常量字符串的地址,我们可以通过 gdb 调试 bomb,来打印出这段字符串。
$ gdb bomb
...
...
(gdb) x/s 0x402400
0x402400: "Border relations with Canada have never been better."
可以知道这串字符串是 Border relations with Canada have never been better.
。
那么这段汇编用c来表示就是:
void phase_1(char* output)
{
if( string_not_equal(output, "Border relations with Canada have never been better.") == 0)
explode_bomb();
return;
}
其中 string_not_equal是接受两个参数,对两个字符串进行比较,如果不同则输出0。
因此phase_1的功能:
- 字符串比较,如果输入的字符串与预设的字符串不相等,则”炸弹爆炸“。
答案:“Border relations with Canada have never been better.”
phase 2
考点:
- 数组
- 循环
这里分三步走,首先对汇编进行可读性调整:
phase_2(rdi)
{
rsi = rsp;
callq read_six_number;
if (*rsp == 1)
goto 400f30;
else
callq explode_bomb;
goto 400f30;
400f17:
eax = *(rbx-4);
eax += eax;
if (eax == *rbx)
goto 400f25;
else
callq explode_bomb;
400f25:
rbx += 4; # 下一个元素
if (rbx != rbp)
goto 400f17; # 循环
else
goto 400f3c;
400f30:
rbx = rsp + 4; # 初始化 init
rbp = rsp + 24; # 数组结束
goto 400f17;
400f3c:
retq
}
然后我们接着分析汇编表达的意思,从以上代码可以看出,read_six_number接受两个参数 rdi和rsi,rdi是输入的字符串,rsi 是栈上分配的整数数组。
通过阅读 read_six_number 汇编代码,可以直到 read_six_number 是把字符串转为6个数字,存储在整数数组中。
从 400f17 到 400f3c,可以看出这是一个循环,从rbx到rbp,指向的是栈上的整数数组。
因此6个数字的地址为
rsp | rsp+0x4 | rsp+0x8 | rsp+0xc | rsp+0x14 | rsp+0x18 |
---|
第二步是c-like风格的可读性调整:
phase_2(edi)
{
rsi = rsp;
read_six_number(rdi, rsi);
if (*rsp != 1)
explode_bomb();
rbx = rsp + 4;
rbp = rsp + 24;
do {
eax = *(rbx-4);
eax += eax;
if (eax != *rbx)
explode_bomb();
rbx += 4;
} while(rbx != rbp);
retq;
}
形势逐渐明朗,可以比较明确看出phase_2程序所表达的意思:
读入字符串并解析,然后通过循环对数组的每个元素进行处理。
第三步,说人话阶段,使用c代码来表示 phase_2:
void phase_2(char* output)
{
int array[6];
read_six_numbers(output, array);
if (array[0] != 1)
explode_bomb();
for (int i = 1; i < 6; i++) {
array[i-1] += array[i-1];
if (array[i] != array[i-1])
explode_bomb();
}
return;
}
通过c代码,可以看出phase_2的功能:
- 将在字符串解析为6个数字的整数数组array
- array第一个整数必须为1
- array中每个元素必须是前一个元素的两倍
答案:1 2 4 8 16 32
phase 3
考点:
- switch-case
首先对汇编代码可读性调整:
phase_3(rdi)
{
# 省略分配栈上空间
rcx = rsp + 0xc;
rdx = rsp + 0x8;
esi = 0x4025cf;
eax = 0;
sccanf(rdi, esi, rdx, rcx);
if (eax <= 1)
explode_bomb();
if(*(rsp + 8) > 7) {
explode_bomb();
}
goto *(0x402470+rax*8);
400f7c:
eax = 0xcf;
goto 400fbe;
400f83:
eax = 0x2c3;
goto 400fbe;
400f8a:
eax = 0x100;
goto 400fbe;
400f91:
eax = 0x185;
goto 400fbe;
400f98:
eax = 0xce;
goto 400fbe;
400f9f:
eax = 0x2aa;
goto 400fbe;
400fa6:
eax = 0x147;
goto 400fbe;
eax = 0;
goto 400fbe;
400fb9:
eax = 0x137;
400fbe:
if (eax != *(rsp+0xc))
explode_bomb();
retq;
}
首先是通过 sscanf 解析字符串,通过gdb调试可看出sscanf的format是%d %d
。 因此推断输出两个数字。
(gdb) x/s 0x4025cf
0x4025cf: "%d %d"
其次通过一个跳转表来进行地址跳转,该跳转表内容同样可以用gdb查看:
(gdb) x/16a 0x402470
0x402470: 0x400f7c <phase_3+57> 0x400fb9 <phase_3+118>
0x402480: 0x400f83 <phase_3+64> 0x400f8a <phase_3+71>
0x402490: 0x400f91 <phase_3+78> 0x400f98 <phase_3+85>
0x4024a0: 0x400f9f <phase_3+92> 0x400fa6 <phase_3+99>
可以看出是正好对应着8个对eax赋值的位置:
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
address | 0x400f7c | 0x400fb9 | 0x400f83 | 0x400f8a | 0x400f91 | 0x400f98 | 0x400f9f | 0x400fa6 |
%eax | 207 | 311 | 707 | 256 | 389 | 206 | 682 | 327 |
c代码表示如下:
void phase_3(char* output)
{
int x, y;
if(sscanf(output, "%d %d", &x, &y) <= 1)
explode_bomb();
if(x > 7)
explode_bomb();
int num;
switch(x) {
case 0:
num = 207;
break;
case 1:
num = 311;
break;
case 2:
num = 707;
break;
case 3:
num = 256;
break;
case 4:
num = 389;
break;
case 5:
num = 206;
break;
case 6:
num = 682;
break;
case 7:
num = 327;
}
if (num != y)
explode_bomb();
return;
}
通过分析,phase_3的功能:
- 将字符串解析为两个数字x, y
- x不能大于7
- 通过switch-case可以将x的值映射为一个num值
- y必须等于num
答案(多选):
- 0 207
- 1 311
- 2 707
- 3 256
- 4 389
- 5 206
- 6 682
- 7 327
phase 4
考点:
- 递归
phase 4会有两个函数,先来看主函数,同样第一步先对原汇编进行可读性调整:
phase_4(rdi)
{
# 省略分配栈上空间
rcx = rsp + 12; # sscanf 参数 4
rdx = rsp + 8; # sscanf 参数 3
rsi = 0x4025cf; # sscanf 参数 2
rax = 0;
callq sscanf;
if (rax != 2) { # sscanf 返回值判断
goto 401035;
}
if (0x8(rsp) <= 14) { # 第一个数字
goto 40103a;
}
401035:
goto explode_bomb;
40103a:
edx = 14; # func4 参数 3
rsi = 0; # func4 参数 2
rdi = 0x8(rsp); # func4 参数 1
goto func4;
if (rax != 0) { # func4 返回值判断
goto 401058;
}
if (0xc(rsp) == 0) { # # 第二个数字
goto 40105d;
}
401058:
goto exlode_bomb;
40105d:
return;
}
然后进行c-like风格调整:
phase_4(rdi)
{
rcx = rsp + 12;
rdx = rsp + 8;
rsi = 0x4025cf;
rax = 0;
rax = sscanf(rdi, rsi, rdx, rcx);
if (rax != 2) {
explode_bomb();
}
if (*(rsp + 8) <= 14) {
rdx = 14;
rsi = 0;
rdi = *(rsp + 8);
rax = func4(rdi, rsi, rdx);
if (rax != 0) {
explode_bomb();
}
if (*(rsp + 12) != 0) {
explode_bomb();
}
}
retq;
}
从代码中可以看出大概分为两个阶段:
- 解析字符串
- 调用 func4
解析字符串部分,sscanf 的format部分可以通过gdb查看:
(gdb) x/s 0x4025cf
0x4025cf: "%d %d"
可以知道是将字符串解析为两个数字。
第二个阶段,是将第一个数字、0、14作为func4的参数来调用func4,第二个数字必须为0。
然后用c代码表示:
void phase_4(char* input)
{
int x, y;
int ret = sscanf(input, "%d %d", &x, &y);
if (ret != 2 || x > 14) {
explode_bomb();
}
else {
int ret = func4(x, 0, 14);
if (ret != 0 || y != 0) {
explode_bomb();
}
}
return;
}
通过c代码可以看出phase_4的功能:
-
解析字符串为两个数字x、y
-
x必须小于等于14
-
调用func4,x、0、14分别为其参数
-
func4返回值必须为0,y必须为0
因此一顿分析发现主要得看func4的实现,同样对func4进行汇编代码可读性调整:
func4(rdi, rsi, rdx)
{
eax = edx;
eax -= esi;
ecx = eax;
ecx >>= 31;
eax += ecx;
eax >>= 1; # 对eax一顿操作
ecx = rax + rsi; # 对ecx一顿操作
if (ecx <= edi) {
goto 400ff2;
}
edx = rcx - 1;
goto func4; # 递归
eax += eax;
goto 401007;
400ff2:
eax = 0;
if (ecx >= edi) { # 递归基
goto 401007;
}
esi = rcx + 1;
goto func4; # 递归
eax = rax + rax + 1;
401007:
return;
}
还是一脸懵比,只知道是递归,继续c-like风格调整:
func4(edi, esi, edx)
{
eax = edx - esi;
eax = (eax + eax >> 31) >> 1;
ecx = rax + rsi;
if (ecx <= edi) {
eax = 0;
if (ecx >= edi) {
return;
}
esi = rcx + 1;
rax = func4(edi, esi, edx);
eax = 2 * rax + 1;
}
else {
edx = rcx - 1;
rax = func4(edi, esi, edx);
eax = 2 * rax;
}
retq;
}
局势逐渐明朗,可以看出是该递归有两个分支,ecx <= edi 和 ecx > edi。
第一个分支,esi = rcx +1,然后递归;第二个分支,edx = rcx -1,然后递归。
c代码如下:
int func4(int x, int a, int b)
{
int num = b - a;
num = (num + num >> 31) / 2;
int c = num + a;
if (c <= x) {
if (c >= x) return 0;
return 2 * func4(x, num+1, b) + 1;
}
return 2 * func4(x, a, num-1);
}
其中c其实是通过 (b-a)/2 + a = b/2 + a/2 = c 得到,其中a和b的初始值是0,14,因此c的初始值是 0/2+14/2 = 7。
所以事实上 c 是求 (b-a)/2,然后通过 c 与 x 进行比较决定不同的递归分支,因此这是递归版的二分法。
因此func4的功能:
- 二分法找出元素x,目标值在数组的前半部分则返回奇数,后半部分则返回偶数;
- 找到元素x返回0;
- 如果目标值在二分查找过程中一直在左半边,则会返回0。
经验证,0 1 3 7在二分过程中一直在左半边。
答案:
- 0 0
- 1 0
- 3 0
- 7 0
phase 5
考点:
- 字符串
- 数组
- for循环
这一题的难度不小了,代码也需要细细打磨,因此继续三步走,先可读性调整:
phase_5(rdx)
{
rbx = rdi;
rax = fs:0x28; # 金丝雀
*(rsp+0x18) = rax;
eax = eax ^ eax; # 清0
callq string_length; # 计算字符串长度
if (eax == 6) {
goto 4010d2;
}
else callq explode_bomb;
goto 4010d2;
40108b:
ecx = rbx + rax;
*rsp = cl;
rdx = *rsp;
edx = edx & 0xf;
edx = rdx + 0x4024b0;
*(rsp+rax+0x10) = dl; # 数组操作
rax += 1; # 自增
if (rax != 6) # 循环退出条件
goto 40108b;
*(rsp + 0x16) = 0;
esi = 0x40245e;
rdi = rsp + 10;
callq strings_not_equal;
if (eax == 0)
callq explode_bomb;
nopl
goto 4010d9;
4010d2:
eax = 0; # init
goto 40108b; # 循环
4010d9:
rax = *(rsp + 0x18);
rax = rax ^ fs:0x28;
if (rax == 0) # 栈是否被破坏
goto 4010ee;
else
callq __stack_chk_fail@plt;
4010ee:
retq;
}
通过这三个部分:
1:
rax += 1; # 自增
2:
if (rax != 6) # 循环退出条件
3:
eax = 0; # init
可以判断这是一个for loop,rax范围是从0到5。
干掉多余的部分和临时变量,继续c-like风格调整如下:
phase_5(rdi)
{
rbx = rdi;
eax = eax ^ eax;
eax = string_length(rdi);
if (eax == 6) {
goto 4010d2;
}
else explode_bomb();
for (eax = 0; eax != 6; eax++) {
ecx = rbx + rax;
*rsp = cl;
rdx = *rsp;
edx = edx & 0xf;
edx = *(rdx + 0x4024b0);
*(rsp+rax+0x10) = dl;
}
*(rsp + 0x16) = 0;
esi = 0x40245e;
rdi = rsp + 10;
strings_not_equal(rdi, rsi);
if (eax != 0) {
explode_bomb();
}
}
不容易理解的是这一段:
# 取output字符串rax位置的字符
ecx = rbx + rax;
*rsp = cl;
rdx = *rsp;
# 该值与0xf与操作
edx = edx & 0xf;
# 取0x4024b0数组edx位置的值
edx = *(rdx + 0x4024b0);
这一段事实上是在取output字符串的字符,然后逐次将每个字符与0xf与操作,得到的值做为0x4024b0
处字符串的下标。
与0xf
与操作意味着能取到0x4024b0
处字符串的范围是0-15,通过gdb查看0x4024b0
处字符串:
(gdb) x/s 0x4024b0
0x4024b0 <array.3449>: "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
可以得到前16为字符串:maduiersnfotvbyl
。
通过for循环能够生成一个字符串,该字符串由output的 每个字符&0xf
来作为maduiersnfotvbyl
的下标,来选择字符。
然后*(rsp + 0x16) = 0
是字符串结尾符,最后将该字符串与0x40245e
处的字符串进行比较。
0x40245e
处的字符串:
(gdb) x/s 0x40245e
0x40245e: "flyers"
说人话就是:
const char g_str[16] = "maduiersnfotvbyl";
void phase_5(char* output)
{
char str[7];
if (string_length(output) != 6) {
explode_bomb();
}
// 9 15 14 5 6 7
// I O N E F G
for (int i = 0; i != 6; i++) {
str[i] = g_str[output[i] & 0xf];
}
str[7] = '\0';
if(string_not_equal(str, "flyers") != 0) {
explode_bomb();
}
}
最后可以分析出该代码的功能:
- output长度必须等于6
- 生成一个新的字符串str,由output每个字符&0xf作为
maduiersnfotvbyl
下标得到 - str必须为
flyers
可以发现flyers
六个字母对应maduiersnfotvbyl
的下标分别为 9 15 14 5 6 7。
查ascii表,可以找到六个&0xf分别为 9 15 14 5 6 7的字符:
- IONEFG
- 其他省略
phase 6
考点:
- 循环
- 结构体(异构数组)
- 链表
phase 6代码看似又臭由长,实际上也是比较难…
这一题需要细细品位,耐心打磨,先不遗余力地可读性调整:
phase_6(rdi)
{
r13 = rsp;
rsi = rsp;
callq read_six_numbers;
r14 = rsp;
r12 = 0; # i = 0
401114:
rbp = r13;
eax = *r13;
eax -= 1;
if (eax <= 5)
goto 401128;
else callq explode_bomb;
401128:
r12 += 1; # i++
if (r12 == 6)
goto 401153; # 外层循环退出条件
ebx = r12;
401135:
rax = ebx; # i = j
eax = *(rsp+rax*4);
if (*rbp != eax)
goto 401145;
else callq explode_bomb;
401145:
ebx += 1; # j++
if (ebx <= 5) # 内层循环退出条件
goto 401135; # 内层循环
r13 += 4;
goto 401114; # 外层循环
--------------------------
401153:
rsi = rsp + 0x18;
rax = r14; # i = r14
ecx = 7;
401160:
edx = ecx;
eax -= *rax;
*rax - edx;
rax += 4; # i += 4
if (rax != rsi) # 循环退出条件
goto 401160;
---------------------------
esi = 0; # i = 0
goto 401197;
401176:
rdx = *(rdx+8);
eax += 1; # j++
if (eax != ecx) # 内层循环退出条件
goto 401176;
else goto 401188;
401183:
edx = 0x6032d0;
401188:
*(rsp+2*rsi+20) = rdx;
rsi += 4; # i += 4
if (rsi == 0x18) # 外层循环退出条件
goto 4011ab;
401197:
ecx = *(rsp+rsi)
if (ecx <= 1)
goto 401183;
eax = 1; # j = 1
edx = 0x6032d0;
goto 401176;
-----------------------------
4011ab:
rbx = *(rsp+0x20);
rax = *(rsp+0x28); # i = *(rsp+0x28);
rsi = *(rsp+0x50);
rcx = rbx;
4011bd:
rdx = *rax;
*(rcx+8) = rdx;
rax += 8; # i += 8
if (rax == rsi) # 循环退出条件
goto 4011d2;
rcx = rdx;
goto 4011bd;
-----------------------------
4011d2:
*(rdx+8) = 0;
ebp = 5; # i = 5
4011df:
rax = *(rbx+0x8);
eax = *rax;
if(eax >= *(rbx))
goto 4011ee;
else callq explode_bomb;
4011ee:
rbx = *(rbx+8);
ebp -= 1; # i--
if(ebp != 0) # 循环退出条件
goto 4011df;
}
我们可以大致分出五部分,分别是五个循环,其中第一个和第三个是双重循环(果真是又长又臭)。
分析清楚整体逻辑,可以进一步改为c-like风格:
phase_6(rdi)
{
r13 = rsp;
rsi = rsp;
read_six_numbers(rdi, rsi);
r14 = rsp;
for (r12 = 0; r12 != 6; r12++) {
rbp = r13;
eax = *r13;
eax -= 1;
if (eax > 5)
explode_bomb();
for (ebx = r12+1; ebx <= 5; ebx++) {
rax = ebx;
eax = *(rsp+rax*4);
if (*rbp == eax)
explode_bomb();
}
r13 += 4;
}
rsi = rsp + 0x18;
rax = r14;
ecx = 7;
for (rax = r14; rax != rsi; rax += 4) {
edx = ecx;
eax -= *rax;
*rax = edx;
}
for (rsi = 0; rsi != 0x18; rsi += 4) {
ecx = *(rsp+rsi);
if (ecx <= 1)
edx = 0x6032d0;
else {
edx = 0x6032d0;
for (eax = 1; eax != ecx; eax++) {
rdx = *(rdx+8);
}
}
*(rsp+2*rsi+20) = rdx;
}
rbx = *(rsp+0x20);
rcx = rbx;
for (rax = rsp+0x28; rax != rsp+0x50; rax += 8) {
rdx = *rax;
*(rcx+8) = rdx;
rcx = rdx;
}
*(rdx+8) = 0;
for (ebp = 5; ebp != 0; ebp -= 1) {
rax = *(rbx+0x8);
eax = *rax;
if(eax < *(rbx))
explode_bomb();
rbx = *(rbx+8);
}
还是一头雾水,需要细细分析这些逻辑的意思,我们首先大概知道需要输入6个数字。
- 第一个循环对这个整数数组做了一些判断,应该是要符合某种规则;
- 第二个循环修改了整数数组的值;
- 第三个循环根据某种可以 嵌套解指针的结构 构造了一个指针数组;
- 第四个循环对指针数组做了修改;
- 第五个循环对 嵌套解指针的结构 进行了一些判断,需要符合某种规则。
这里的难点是
- 判断数组符合某种规则的逻辑;
- 可以 嵌套解指针的结构 是什么;
经过查阅才恍然大悟,嵌套解指针的结构就是链表。
该链表的地址和值可以通过gdb查出:
332 168 924 691 477 443
0x6032d0->0x6032e0->0x6032f0->0x603300->0x603310->0x603320
这里有一个细节,第一个循环中将 eax -= 1,然后和5比较通过 jbe跳转,目的防止eax取负数和0,eax -= 1是防止取0,jbe防止取负数。
因为jbe跳转是用无符号数比较,负数在无符号数编码下是很大的数所以必然大于5,因此输入的数字范围是1-6。
经过整理可以得到c代码:
typedef struct {
int val;
ListNode* next;
} ListNode;
void phase_6(char* output)
{
int array[6];
ListNode* node_array[6];
read_six_numbers(output, array);
// 数字范围必须为1-6且互不重复
for (int i = 0; i != 6; i++) {
int num = array[i];
num--;
if ((unsigned int)num > 5) // 最大为6
explode_bomb();
for (int j = i+1; j <= 5; j++) {
if (array[i] == array[j]) // 每个元素都不重复
explode_bomb();
}
}
// 修改 array
for (int i = 0; i < 6; i ++) {
array[i] = (7 - array[i]);
}
// 生成 node_array
for (int i = 0; i < 6; i ++) {
int cur = array[i];
ListNode* node = 0x6032d0; // 链表head
if (cur > 1) {
for (int j = 1; j < cur; j++) {
node = node->next;
}
}
node_array[i] = node;
}
for (int i = 0; i < 5; i++) {
node_array[i]->next = node_array[i+1];
}
//0x6032d0 0x6032e0 0x6032f0 0x603300 0x603310 0x603320
//332 168 924 691 477 443
// 6 5 4 3 2 1
ListNode* ptr = node_array[0];
for (int i = 5; i > 0; i--) {
if (ptr->val < ptr->next->val)
explode_bomb();
ptr = ptr->next;
}
}
相信c代码写出,逻辑就比较清晰了,可以知道功能为下:
- 输入的数组array必须为 1-6,且不可重复;
- 对array每个数字 7 - array[i];
- 根据array的值,取到对应位置的listnode,并由ptr_array存储;
- 将ptr_array每个元素按序连接(链表重排);
- 链表必须为降序。
因此phase_6就是链表排序,输入的数字为原链表位置,输入的次序为新链表的位置。
又因为第二个循环对array每个数字 7 - array[i],因此输入的数字需要将链表重排成为升序。
原链表的从小到大顺序是 5 6 1 2 3 4, 重排后 4 3 2 1 6 5。
总结和感想
《CSAPP》其实看了挺久了,第三章汇编一直搁着是一个遗憾,总觉得是一块硬骨头(当时觉得太枯燥)。这次重新又杀回来通过视频+阅读+做题的方式对整个第三章来了一次横扫,结果是收获巨大,每次理解完一个知识点都有种恍然大悟之感。
看了相关教学视频后才知道第三章的重要性,整整花了五节课,占了将近课程量的1/3,可以看出这一章的重要性。
CSAPP的实验系列一直是经典,我认为在某种程度上书甚至可以不看,但lab不可不做…,配套的bomb lab更是如此。我从phase_1到phase_6的过程中对c的理解也是更上一层楼:
- 字符串的传递方式
- 数组的表示方法
- 函数入参和返回值
- 栈破坏检测
- 链表的操作方式
- 栈空间的使用
Bomb Lab带来的成就感是巨大的,所以尽量自己硬着头皮啃一啃,忍住不要看答案…
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)