未完成的博客

《高性能MySQL》读书笔记

把目录全写出来只是单纯为了激励自己,不知道啥时候能全部看完这本书,有点厚。。。。

很多原理性的东西已经在 《数据密集型应用设计》 这本书里看过了,可以先看看这本书

第一章 MySQL架构与历史


MySQL逻辑架构
摘抄自该博客:MySQL逻辑架构
MySQL最重要、最与众不同的特性就是它的存储引擎架构,这种架构将:查询处理、其他系统任务、数据的存储与提取 三部分分离。所以,带来的好处就是可以在使用时根据性能、特性,以及其他需求来选择数据存储方式。

存储引擎架构分为三层,自上而下,分为第一层:连接层;第二层:服务层;第三层:引擎层。

第一层:连接层:

MySQL的最上层是连接服务,引入了线程池的概念,允许多台客户端连接。主要工作是:连接处理、授权认证、安全防护等。

连接层为通过安全认证的接入用户提供线程,同样,在该层上可以实现基于SSL 的安全连接。

第二层:服务层:

服务层用于处理核心服务,如标准的SQL接口、查询解析、SQL优化和统计、全局的和引擎依赖的缓存与缓冲器等等。所有的与存储引擎无关的工作,如过程、函数等,都会在这一层来处理。在该层上,服务器会解析查询并创建相应的内部解析树,并对其完成优化,如确定查询表的顺序,是否利用索引等,最后生成相关的执行操作。如果是SELECT 语句,服务器还会查询内部的缓存。如果缓存空间足够大,这样在解决大量读操作的环境中能够很好的提升系统的性能。

第三层:引擎层:

存储引擎层,存储引擎负责实际的MySQL数据的存储与提取,服务器通过API 与 存储引擎进行通信。不同的存储引擎功能和特性有所不同,这样可以根据实际需要有针对性的使用不同的存储引擎。

锁粒度
所谓锁策略,就是在锁的开销和数据安全性之间寻求平衡,这种平衡当然也会影响到性能。将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时也会失去另外一些应用场景的良好支持。锁策略一般分为表示(table lock)和行级锁(row lock)。

可重复读与MVCC
可重复读是MySQL的默认事务隔离级别。MVCC(多版本并发控制)是通过保存某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存了行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。
在可重复读隔离级别下,MVCC各操作实现:
查询:
InnoDB会根据以下两个条件检查每行记录:
 a. InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
 b. 行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行在事务开始之前未被删除。
只有符合上述两个条件的记录才能返回作为查询结果。

插入:
InnoDB为新插入的每一行保存当前系统版本号作为行版本号

删除:
InnoDB为删除的每一行保存当前系统版本号作为行删除标识

更新:
InnoDB为插入一行新记录,保存当前系统版本号为行版本号,同时保存当前系统版本号到原来的行作为行删除标记(拆分为删除+插入操作)

InnoDB采用MVCC来支持高并发,并且实现了四个标准的隔离级别。其默认级别为可重复读,并且通过间隙锁(next-key locking)策略来防止幻读的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。

推荐阅读:通过各种简单案例,让你彻底搞懂 MySQL 中的锁机制与 MVCC

LFS比较

Linux From Scratch Version 记录

版本:Linux From Scratch Version 10.1 Published March 1st, 2021

宿主机版本:Ubuntu 21.04

前期准备

1 虚拟机安装

使用vmware创建虚拟机,内存10G 硬盘20G 。 在software&update中修改软件源为阿里源,创建root账户,设置成不锁屏,安装常用的软件(chrome,vscode) 安装中文输入法

经验:

时不时创建虚拟机快照,避免重新来过

安装过程中尽量不跳过(除了下载环节–没有换源),同一个镜像连续安装两次的分辨率都不一样。。。

设置root账号密码时即使报”BAD PASSWORD: The password is shorter than 8 characters“信息仍然可以设成很短的密码

安装fish,更好地敲命令

update是更新软件列表,upgrade是更新软件

安装中文输入法 blog 搜狗输入法Linux版 blog

安装deb包显示依赖未安装可以使用命令安装相关依赖

1
sudo apt-get install -f

执行shell文件的方法 blog

1
2
1 chmod u+x hello.sh 后 ./hello.sh执行
2 sh ./hello.sh

2 安装特定版本的内核(放弃)

本来看书中要求宿主机内核版本为3.2,打算将系统内核降到该版本blog,加入源:deb http://security.ubuntu.com/ubuntu trusty-security main后出现”the following signatures couldn’t be verified because the public key is not avai“问题,使用命令将公钥添加到服务器blog

1
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 8D5A09DC9B929006

apt-cache search linux中并没有找到3.2版本的内核,故在http://kernel.ubuntu.com/~kernel-ppa/mainline/网站上下载3.2版本对应的安装包,安装之后显示缺少module-init-tools,安装完module-init-tools之后修改内核启动顺序。**重启后系统崩溃,只能重装系统** ———————————————————-重装系统x1

重新阅读LFS教程这段话我又觉得好像内核版本大于3.2就行,真是白费功夫。。。

There are two ways you can go about this. First, see if your Linux vendor provides a 3.2 or later kernel package. If so,

you may wish to install it.

3 宿主机上安装特定版本的软件

tips:可以先执行version-check.sh脚本,提示缺啥软件就安啥软件,除了gcc g++需要配置一下,/bin/sh需要改一下指向,其他的软件都是直接安装就完事了,没报错的就不安。bison和gawk都自动链接上了,都不用我改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bash, version 5.1.4(1)-release
/bin/sh -> /usr/bin/bash
Binutils: (GNU Binutils for Ubuntu) 2.36.1
bison (GNU Bison) 3.7.5
/usr/bin/yacc -> /usr/bin/bison.yacc
bzip2, Version 1.0.8, 13-Jul-2019.
Coreutils: 8.32
diff (GNU diffutils) 3.7
find (GNU findutils) 4.8.0
GNU Awk 5.1.0, API: 3.0 (GNU MPFR 4.1.0, GNU MP 6.2.1)
/usr/bin/awk -> /usr/bin/gawk
gcc (Ubuntu 9.3.0-23ubuntu2) 9.3.0
g++ (Ubuntu 9.3.0-23ubuntu2) 9.3.0
(Ubuntu GLIBC 2.33-0ubuntu5) 2.33
grep (GNU grep) 3.6
gzip 1.10
Linux version 5.11.0-31-generic (buildd@lcy01-amd64-009) (gcc (Ubuntu 10.3.0-1ubuntu1) 10.3.0, GNU ld (GNU Binutils for Ubuntu) 2.36.1) #33-Ubuntu SMP Wed Aug 11 13:19:04 UTC 2021
m4 (GNU M4) 1.4.18
GNU Make 4.3
GNU patch 2.7.6
Perl version='5.32.1';
Python 3.9.5
sed (GNU sed) 4.7
tar (GNU tar) 1.34
texi2any (GNU texinfo) 6.7
xz (XZ Utils) 5.2.5
g++ compilation OK
1
2
3
4
5
6
7
8
9
10
#查看已安装软件版本
dpkg -l
apt list --installed
#ubuntu安装特定版本软件
#列出所有软件版本
apt-cache madison 软件名
#安装特定版本软件
apt-get install 软件名=版本号
#删除软件
sudo apt-get --purge remove 软件名

软链接

  • 1.软链接,以路径的形式存在。类似于Windows操作系统中的快捷方式
  • 2.软链接可以 跨文件系统 ,硬链接不可以
  • 3.软链接可以对一个不存在的文件名进行链接
  • 4.软链接可以对目录进行链接

硬链接

  • 1.硬链接,以文件副本的形式存在。但不占用实际空间。
  • 2.不允许给目录创建硬链接
  • 3.硬链接只有在同一个文件系统中才能创建
1
2
3
4
#/bin/sh 指向 /bin/dash,将其指向/bin/bash
#先备份文件
sudo cp -d /bin/sh /bin/sh_bak
sudo ln -snf /bin/bash /bin/sh

安装gcc blog

原来 “gcc | 4:10.3.0-1ubuntu1 | http://mirrors.aliyun.com/ubuntu hirsute/main amd64 Packages”是指4-10版本都有,直接运行命令就成了,我还以为只有10.3版本呢。。。

1
2
sudo apt install gcc-9 g++-9 
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 90 --slave /usr/bin/g++ g++ /usr/bin/g++-9 --slave /usr/bin/gcov gcov /usr/bin/gcov-9

glibc(放弃)

开始想安装2.15版的 参考博客,但实际上是找错了,是redhat系列教程,执行以下命令时

1
../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bi

报错“These critical programs are missing or too old: as ld”,blog里面说可以修改configure文件内容,想了想还是算了。故换2.32版本的glibc安装,执行make -j4命令时报错*/usr/include/linux/errno.h:1:10: fatal error: asm/errno.h: No such file or directory 和 asm/prctl.h No such file or directory*,找了一下发现prctl.h在/usr/include/x86_64-linux-gnu/asm文件夹中,故执行以下命令生成执行该文件夹的软链。

1
ln -s /usr/include/x86_64-linux-gnu/asm  /usr/include/asm

然后执行make -j4命令时一大堆输出,最后报一大堆错误

1
2
3
4
5
6
7
8
9
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so: undefined reference to `fstat64@GLIBC_2.33'
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so: undefined reference to `lstat@GLIBC_2.33'
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so: undefined reference to `stat@GLIBC_2.33'
collect2: error: ld returned 1 exit status
make[2]: *** [../Rules:215: /opt/glibc-2.32/build/support/links-dso-program] Error 1
make[2]: Leaving directory '/opt/glibc-2.32/support'
make[1]: *** [Makefile:470: support/others] Error 2
make[1]: Leaving directory '/opt/glibc-2.32'
make: *** [Makefile:9: all] Error 2

懒得找错误原因,直接make install

1
2
3
4
5
make[1]: [Makefile:119: install] Segmentation fault (core dumped) (ignored)
LD_SO=ld-linux-x86-64.so.2 CC="gcc -B/usr/bin/" /usr/bin/perl scripts/test-installation.pl /opt/glibc-2.32/build/
make[1]: *** [Makefile:120: install] Segmentation fault (core dumped)
make[1]: Leaving directory '/opt/glibc-2.32'
make: *** [Makefile:12: install] Error 2

而后执行啥命令都显示aborted(core dump),本着重启解决一切问题的原则,我重启了系统,而后系统崩溃—————————————————–重装系统x2

后面发现原来教程就不是ubuntu的。我真傻,真的。祥林嫂.jpg

我用以下命令查看系统glibc版本的时候,发现没有,还以为系统没有glibc呢。看到rpm的时候就应该反应过来的

1
2
3
4
5
6
7
#1、查看系统glibc支持的版本
strings /lib64/libc.so.6 |grep GLIBC
rpm -qa | grep glibc


#实际上ubuntu系统查看glibc版本的命令
ldd --version

奇怪的vmware

在第二次系统崩溃后,为了拿回虚拟机中的文件,将该虚拟机(虚拟机1)的硬盘挂载到另一个虚拟机(虚拟机2)上,将虚拟机1桌面的文件的文件都移了出来。反正虚拟机1系统都崩溃了,我就删除了这个虚拟机。而后奇怪的点来了,当我将虚拟机1的硬盘移除之后,虚拟机2启动的时候检测不到操作系统,一直从尝试网络引导操作系统,改BIOS选项也没用。没办法,我就试着在虚拟机2中添加了新安装的虚拟机(虚拟机3)的一个硬盘,没想到系统就启动成功了。为了将虚拟机2独立起来,不能一直依赖虚拟机3的硬盘。我保存了虚拟机2的快照。而后虚拟机3显示“父虚拟磁盘在子虚拟磁盘创建之后被修改过。父虚拟磁盘的内容 ID 与子虚拟磁盘中对应的父内容 ID 不匹配”,在一通操作之后,虚拟机2,虚拟机3全炸裂———————————————————————————————–重装系统x3

措施:

为了将虚拟机2分离出来,直接复制虚拟机2的硬盘文件到另一个文件夹,而后重装系统时指定现有磁盘(由于硬盘上有操作系统了所以并没有真的重装系统)。而后将CD/DVD驱动器都移除了。虚拟机2搞定!

为了修复虚拟机3 “父虚拟磁盘在子虚拟磁盘创建之后被修改过。父虚拟磁盘的内容 ID 与子虚拟磁盘中对应的父内容 ID 不匹配”还有“VMware:无法打开磁盘xxx.vmdk 或者某一个快照所依赖的磁盘”的问题。打开xxx.vmsd(我这里是LFS.vmsd)。记住每个快照对应的vmk文件。纠正这些快照的parentCID和CID,使得当前快照的parentCID等于是一个快照的CID。改完之后转回快照就成功了。

我的这些操作真的憨。。。。

CMU15213

lab地址

datalab

关于位操作,补码,浮点数的,觉得有点无聊,先跳了,我觉得对着抄一抄,能理解就行了,没必要认真研究。
Introduction to CSAPP(八):Datalab

bomb

这个实验是通过看汇编代码,找到每个阶段的密码,我觉得设计的挺有趣的。

背景知识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
objdump -S bomb > bomb.asm  # 反汇编结果输出到文件bomb.asm
用asm后缀在vscode可以高亮,更好看一点。

gdb
break [function_name] or *[address]
在某个函数或某条指令处下断点
可以使用examine命令(简写是x)来查看内存地址中的值。x命令的语法如下所示:

x/<n/f/u> <addr>

n:是正整数,表示需要显示的内存单元的个数,即从当前地址向后显示n个内存单元的内容,
一个内存单元的大小由第三个参数u定义。

f:表示addr指向的内存内容的输出格式,s对应输出字符串,此处需特别注意输出整型数据的格式:
x 按十六进制格式显示变量.
d 按十进制格式显示变量。
u 按十进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。

u:就是指以多少个字节作为一个内存单元-unit,默认为4。u还可以用被一些字符表示:
如b=1 byte, h=2 bytes,w=4 bytes,g=8 bytes.

<addr>:表示内存地址。

LEA是微机8086/8088系列的一条指令,取自英语Load effect address——取有效地址,也就是取偏移地址。在微机8086/8088中有20位物理地址,由16位段基址向左偏移4位再与偏移地址之和得到。地址传送指令之一。
指令格式:LEA 目的,源
指令功能:取源操作数地址的偏移量,并把它传送到目的操作数所在的单元
lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数,
例如:lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。
而mov指令则恰恰相反,例如:mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。

注意目的与源的位置可能不一样
windows下汇编和linux下汇编这个参数顺序是反的

JE ;等于则跳转
JNE ;不等于则跳转

JZ ;为 0 则跳转
JNZ ;不为 0 则跳转


JA ;无符号大于则跳转
JNA ;无符号不大于则跳转
JAE ;无符号大于等于则跳转
JNAE ;无符号不大于等于则跳转

JG ;有符号大于则跳转
JNG ;有符号不大于则跳转
JGE ;有符号大于等于则跳转
JNGE ;有符号不大于等于则跳转

JB ;无符号小于则跳转
JNB ;无符号不小于则跳转
JBE ;无符号小于等于则跳转
JNBE ;无符号不小于等于则跳转

cmpl指令将两个操作数相减,但计算结果并不保存,只是根据计算结果改变eflags寄存器中的标志位。

SHR(右移)指令使目的操作数逻辑右移一位,最高位用 0 填充。最低位复制到进位标志位,而进位标志位中原来的数值被丢弃
汇编语言中SAR和SHR指令都是右移指令,SAR是算数右移指令(shift arithmetic right),而SHR是逻辑右移指令(shift logical right)。

两者的区别在于SAR右移时保留操作数的符号,即用符号位来补足,而SHR右移时总是用0来补足。

64位寄存器:rax
32位寄存器:eax
16位寄存器:ax
8位寄存器:ah,al

movzbl指令负责拷贝一个字节,并用0填充其目的操作数中的其余各位,这种扩展方式叫“零扩展”。
movsbl指令负责拷贝一个字节,并用源操作数的最高位填充其目的操作数中的其余各位,这种扩展方式叫“符号扩展”

FS寄存器指向当前活动线程的TEB结构(线程结构)
偏移 说明
000 指向SEH链指针
004 线程堆栈顶部
008 线程堆栈底部
00C SubSystemTib
010 FiberData
014 ArbitraryUserPointer
018 FS段寄存器在内存中的镜像地址
020 进程PID
024 线程ID
02C 指向线程局部存储指针
030 PEB结构地址(进程结构)
034 上个错误号

fs:0x28:金丝雀,防止栈溢出

(gdb) i registers 查看寄存器的值

学 Win32 汇编[28] - 跳转指令: JMP、JECXZ、JA、JB、JG、JL、JE、JZ、JS、JC、JO、JP 等
函数参数使用的寄存器

汇编代码指令有一个字符的后缀,表明操作数的大小

AT&T汇编

绝大多数 Linux 程序员以前只接触过DOS/Windows 下的汇编语言,这些汇编代码都是 Intel 风格的。但在 Unix 和 Linux 系统中,更多采用的还是 AT&T 格式,两者在语法格式上有着很大的不同

在 AT&T 汇编格式中,寄存器名要加上 ‘%’ 作为前缀 pushl %eax

在 AT&T 汇编格式中,用 ‘$’ 前缀表示一个立即操作数 pushl $1

目标操作数在源操作数的右边 movl $1,%eax

操作数的字长由操作符的最后一个字母决定 b,w,l

绝对转移和调用指令(jmp/call)的操作数前要加上’*’作为前缀

远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为 “ljmp” 和 “lcall”

内存寻址:section:disp(base, index, scale) ——由于 Linux 工作在保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地址计算方法:
disp + base + index * scale

AT&T汇编

金丝雀

GCC在产生的代码中加人了一种栈保护者机制,来检测缓冲区越界。其思想是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀( canary)值
这个金丝雀值,也称为哨兵值,是在程序每次运行时随机产生的,因此,攻击者很难猜出这个哨兵值。在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者该函数调用的某个函数的某个操作改变了。如果是的,那么程序异常中止。
面试官不讲武德,居然让我讲讲蠕虫和金丝雀!
phase 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Hmm...  Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!

step1:反汇编
objdump -S bomb > bomb.asm # 反汇编结果输出到文件bomb.asm
400e32: e8 67 06 00 00 callq 40149e <read_line>
400e37: 48 89 c7 mov %rax,%rdi # 将输入字符串的起始地址移入rdi
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1>

0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi # 对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

观察strings_not_equal函数,此时函数有两个参数,分别为输入字符串的起始地址与给点字符串的起始地址,可以看给定字符串的起始地址就是0x402400,在gdb中输出该内存地址的字符串。

1
2
(gdb) x/s 0x402400
0x402400: "Border relations with Canada have never been better."

故phase 1的答案为Border relations with Canada have never been better.

phase 2:
首先看read_six_numbers函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp
401460: 48 89 f2 mov %rsi,%rdx # num1
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx # num2
401467: 48 8d 46 14 lea 0x14(%rsi),%rax
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp) # num6
401470: 48 8d 46 10 lea 0x10(%rsi),%rax
401474: 48 89 04 24 mov %rax,(%rsp) # num5 在栈顶
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9 # num4
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8 # num3
401480: be c3 25 40 00 mov $0x4025c3,%esi #格式字符串:"%d %d %d %d %d %d"
401485: b8 00 00 00 00 mov $0x0,%eax
40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt>
40148f: 83 f8 05 cmp $0x5,%eax
401492: 7f 05 jg 401499 <read_six_numbers+0x3d>
401494: e8 a1 ff ff ff callq 40143a <explode_bomb>
401499: 48 83 c4 18 add $0x18,%rsp
40149d: c3 retq

如果以rsi为基准,则六个输入数字的地址分别为rsi + 0 4 8 12 16 20
不过我不知道第一个寄存器rdi传的是什么?scanf还需要其他的参数吗?
看错了,原来是sscanf函数,rdi就是输入字符串的起始地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*C 库函数 int sscanf(const char *str, const char *format, ...) 从字符串读取格式化输入。*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
int day, year;
char weekday[20], month[20], dtm[100];

strcpy( dtm, "Saturday March 25 1989" );
sscanf( dtm, "%s %s %d %d", weekday, month, &day, &year );

printf("%s %d, %d = %s\n", month, day, year, weekday );

return(0);
}

然后回到phase_2函数
全部以rsp为基准,看起来更清晰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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) # 此时[0] = 1
400f0e: 74 20 je 400f30 <phase_2+0x34> # 跳转到400f30
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) # 这三行语句表示[addr + 4] = 2 * [addr]
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 # 一直加4 ,而后与24对比
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 # 将4 与 24 对比
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp
400f3a: eb db jmp 400f17 <phase_2+0x1b> # 跳到400f17
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq

这代码逻辑用c语言表示就是这样

1
2
3
4
5
6
[0] = 1
addr = 4;
while(addr < 24){
[addr] = [addr - 4] + [addr - 4];
addr += 4
}

也就是说6个数字分别是1 2 4 8 16 32

phase 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
400e65:	e8 a6 fc ff ff       	callq  400b10 <puts@plt>
400e6a: e8 2f 06 00 00 callq 40149e <read_line>
400e6f: 48 89 c7 mov %rax,%rdi
400e72: e8 cc 00 00 00 callq 400f43 <phase_3>

0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx # num2
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx # num1
400f51: be cf 25 40 00 mov $0x4025cf,%esi # x/s:"%d %d"
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 # 输入的个数大于1才可以
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) # num1 <= 7
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 # 207
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 # num2需等于
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

phase 3最关键的部分便是在jmpq语句,q表示后面操作数为8个字节。按我的理解rax寄存器中应该存储的为字符串的起始地址,也就是num1的地址,但0x402470(,%rax,8)能猜到是0x402470 + num1 * 8,为啥变成了num1的值了呢?

更新: 没看到前面一行mov 0x8(%rsp),%eax,对64位寄存器的低32位赋值会使高32位清零吗?

由于num1 <= 7,故地址也在0x402470 ~ 0x402470 + 7 * 8之间,我也不知道是否大于0,看下面语句使用的为ja,是无符号跳转,觉得应该大于0(实际上就算可以小于0也无所谓,选一个值通过就行)

1
jmpq   *0x402470(,%rax,8) # 跳转表

故输出0x402470 ~ 0x402470 + 7 * 8地址存储的地址

1
2
3
4
5
(gdb) x/8xg 0x402470  # 8表示显示内存单元的个数,x表示内容输出格式为16进制,g指8个字节作为一个内存单元
0x402470: 0x0000000000400f7c 0x0000000000400fb9
0x402480: 0x0000000000400f83 0x0000000000400f8a
0x402490: 0x0000000000400f91 0x0000000000400f98
0x4024a0: 0x0000000000400f9f 0x0000000000400fa6

选择最简单的num1 = 0,则跳到0x400f7c处,eax = 207,num2 = 207。通过!

phase 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx # num2
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx # num1
40101a: be cf 25 40 00 mov $0x4025cf,%esi # 0x4025cf: "%d %d"
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) # num1 <= 14
401033: 76 05 jbe 40103a <phase_4+0x2e>
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
# 3个32位参数,arg1:num1 arg2:0 arg3:14
40103a: ba 0e 00 00 00 mov $0xe,%edx
40103f: be 00 00 00 00 mov $0x0,%esi
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi # num1
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) # num2 =0
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
0000000000400fce <func4>:
400fce: 48 83 ec 08 sub $0x8,%rsp
400fd2: 89 d0 mov %edx,%eax # eax = 14
400fd4: 29 f0 sub %esi,%eax # eax = 14 - 0
400fd6: 89 c1 mov %eax,%ecx # ecx = 14
400fd8: c1 e9 1f shr $0x1f,%ecx # ecx = 14 / 2^31 = 0
400fdb: 01 c8 add %ecx,%eax # eax = 14 + 0
400fdd: d1 f8 sar %eax # eax = 14 / 2 = 7
#ecx = rax(eax) + 1 * rsi(esi) = 7 + 1 * 0 = 7
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx
400fe2: 39 f9 cmp %edi,%ecx # 比较num1 和 7
400fe4: 7e 0c jle 400ff2 <func4+0x24>
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 # 比较num1 和 7
400ff9: 7d 0c jge 401007 <func4+0x39>
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

由于phase 3的经验,我默认对32位寄存器赋值时会使高32位清零,所以rax 与eax等价,rsi与esi等价。从格式字符串可以看出这一关又是两个数,func4是一个有3个32位参数的函数

1
func4(num1,0,14)

400fe2和400ff7两次分别拿num1与7对比,要求其大于大于7,小于等于7,否则就再次进入函数func4,也就是说这是一个递归函数,那最简单的值便是7了,直接跳过递归调用过程。
故num1 = 7,而cmpl $0x0,0xc(%rsp)表示num2=0。故最终答案为7 0

递归函数有点复杂,抄了一个代码过来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void func4(int x,int y,int z)
{//x in %rdi,y in %rsi,z in %rdx,t in %rax,k in %ecx
//y的初始值为0,z的初始值为14
int t=z-y;
int k=t>>31;
t=(t+k)>>1;
k=t+y;
if(k>x)
{
z=k-1;
func4(x,y,z);
t=2t;
return;
}
else
{
t=0;
if(k<x)
{
y=k+1;
func4(x,y,z);
t=2*t+1;
return;
}
else
{
return;
}
}
}

CSAPP: Bomb Lab 实验解析
phase 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx # 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 # 代表字符串长度为6
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 # 得到0x4024b0 + rdx位置的值
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 # 0x40245e: "flyers"
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 # 将eax置为0后跳转
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

这一段理解起来倒并不难,输入一个长度为6的字符串,对字符串的每一个字符(同时也是一个8位整数),取其低四位,设为off,取0x4024b0 + off地址的值。将该值存入堆栈中,最终也得到一个字符串。
故首先输出0x4024b0后16字节的内容:

1
2
3
(gdb) x /16cb 0x4024b0
0x4024b0: 109 'm' 97 'a' 100 'd' 117 'u' 105 'i' 101 'e' 114 'r' 115 's'
0x4024b8: 110 'n' 102 'f' 111 'o' 116 't' 118 'v' 98 'b' 121 'y' 108 'l'

目标字符串为”flyers”,则输入字符串低四位的值依次为9 f e 5 6 7,查看ascii码,选取相应的字符即可。
我选择的字符为yonefg

我刚开始没注意到与操作,看到cl寄存器以为直接按8位计算,故我输出了后面126位的字符,在其中找对应字符

最终对应起来结果是”P;CLJK”。。。。
phase 6
前面几个阶段的密码难度让我对这个实验的难度产生了误判,这第六阶段的汇编代码给我看麻了都没看出来。
我只分析出前面几个约束条件,实在看麻了就去网上找博客才知道的答案。
我推出来的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 # x-1 <5
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>

把jmp语句跳转的语句分割开来比较好看,这段代码中有两层循环,外层循环用以表示6个数字皆小于7,内层循环表示6个数字都不同

1
2
3
4
5
6
7
8
9
10
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) # 赋值为7-x
401166: 48 83 c0 04 add $0x4,%rax
40116a: 48 39 f0 cmp %rsi,%rax
40116d: 75 f1 jne 401160 <phase_6+0x6c>

这段代码表示堆栈上的6个数字都替换成了7-x。
之后的代码就是跳来跳去,完全看不懂了。
即使我随便输出了0x6032d0地址的值,也没发现node这一点,实际上我也不知道node的含义。

在看了博客1博客2后,才知道后面是链表排序过程(但我是完全没看出来),按照博客的思路,输出0x6032d0地址的值,按它们的排定顺序得到序列{3 4 5 6 1 2},最后7-x即得到答案{4 3 2 1 6 5}


虽然知道还有秘密阶段,我还是不尝试了,下次一定。。。