深入理解计算机系统
0x ff 杂项
Instruction set Architecture:ISA,指令集体系架构
软件和硬件之间的一层抽象层
冯诺依曼计算机,即程序存储型计算机
重要思想:程序就是一系列被编码了的字节序列(看上去和数据一模一样)
https://www.cnblogs.com/SovietPower/p/14877143.html
0x 00 参考资料 && lab
official:
note:
lab:
video:
book:
lab操作流程
# 1.datalab:
在源文件 bits.c 中完善函数即可
./dlc bits.c // 用于检查程序是否合法,是否使用了程序规定的符号
make btest // btest是评分(检查对错工具),每次执行btets前都要重新make一下
./btest bits.c // 评分
# 2.bomblab
./bomb
输入答案
导读P3-52分钟有第一关的实操
# 3.attacklab
./hex2raw < att1.txt > attraw1.txt // 将字节序列at t1转换为字符串attraw1
./ctarget -q -i attraw1.txt //测试答案
// (https://github.com/wuxueqian14/csapp-lab/tree/master/Attack%20Lab)
0x 01 二进制
内存中存储的是电压,然后通过(不知道)某种方式抽象为数字01,然而计算机的内存太大了,以致于01的个数实在太多了,于是,我们把原有的0和1分块,并再次抽象为0,1…。
![img](file:///C:\Users\24072\AppData\Roaming\Tencent\Users\2407217576\QQ\WinTemp\RichOle\7E[W6J9]YPX$8MS~3CCM[DG.png)
加入内存中有n bit,每m bit分为一块,则最多可以分为2^m块,因为m bit的排列组合数为2 ^ n个序列(sequence)
例如十进制数字123,它应该表示为1*10^2 + 2*10^1 + 3*10^0
,所以这里的123准确来说应该是一个sequence,而不是一个数。
数是一个比较唯心的抽象的概念,你说一个数3,它可以是十进制序列3,也可以是二进制序列11…,3和11都是这个真正的(唯心的)3,这些序列之间是一一对应的,不仅如此,他们的运算也是一一对应的。十进制的序列1+2,对应的二进制下序列为1+01
取反对称:对称轴的两侧是相反数
对于1,2,3,4,他们分别取反对称于-1,-2,-3,-4
对于二进制000,001,010,011,他们分别取反对称于111,110,101,100
0x 02 二进制运算
位运算的循环圈:
(int类型有符号数)
(int类型无符号数)
通过这张图,你可能会更好地理解补码和无符号数运算是在mod 2^n 下计算的意义。
看一下树状数组lowbit函数
int lowbit(int x) {
return x & -x; // <==> x & (~x + 1);
}
这个函数为什么能求得最后一个1所在位置的代表的权值呢?
首先 -x,其实就是x的补码。关于补码,我们有一个求补码的方法:从右到左直到第一个1保持不变,后面的位取反,我们将x和x的补码做与运算,最后得到的结果一定是这样的形式:00..010..0,最后一个1左侧全为0,右侧也全为0。
#include <iostream>
using namespace std;
unsigned func1(unsigned x) {
// 输出一个无符号数x,判断x在十六进制下的的每一位是不是字母
// 如果该位是字母就返回1,否则返回0
// 并以一个16进制数的形式返回
unsigned x1 = (x & 0x22222222) >> 1;
unsigned x2 = (x & 0x44444444) >> 2;
unsigned x3 = (x & 0x88888888) >> 3;
// printf("[1]:%04x\n[2]:%04x\n[3]:%04x\n", x1, x2, x3);
return x3 & (x2 | x1);
}
unsigned func2(unsigned x) {
// 输出一个无符号数x,判断x在十六进制下的每一位是不是字母
// 如果所有位都是字母返回1,否则返回0
x = func1(x); //得到了每一位的结果
x = x & (x >> 16); // 每次判断一半
x = x & (x >> 8);
x = x & (x >> 4);
return x;
}
unsigned func3(unsigned x) {
// bigCount
unsigned c;
c = (x & 0x55555555) + ((x >> 1) & 0x55555555);
c = (c & 0x33333333) + ((c >> 2) & 0x33333333);
c = (c & 0x0f0f0f0f) + ((c >> 4) & 0x0f0f0f0f);
c = (c & 0x00ff00ff) + ((c >> 8) & 0x00ff00ff);
c = (c & 0x0000ffff) + ((c >> 16) & 0x0000ffff);
return c;
}
int main()
{
unsigned x = 0x1;
// printf("0x%X = %X\n", x, func1(x));
// printf("0x%X = %X\n", x, func2(x));
printf("0x%X = %d\n", x, func3(x));
return 0;
}
0x 03 浮点数
为什么 IEEE 754浮点数Float类型的bias=127而不是128?
其实这也没有一个官方的说法,不过为了让自己接受这个设定,我们可以从两个角度考虑:
- 首先,bias采用127时绝对值的范围比较对称
- 其次,bias采用127时最大的指数是127比bias=128时的126大,虽然只大1,但是我们直到指数的增长是“爆炸”的,因此其表示的范围也大得多。
浮点的根据exp和frac分为三种情况:
- exp=111..1,指数全1。此时又分为两种情况:(1)当frac全0时表示无穷大,根据符号位又分为正无穷和负无穷。(2)frac不全为0,表示NaN,一种未定义行为。(可以这样区分无穷和NaN,由于未定义的行为有很多,因此需要根据frac进一步区分,所以frac不是固定的全0,(胡乱猜的),可以这样记忆)。
- exp=000..0,指数全0。表示不规格化的浮点数。这里的主要目的是为了拓展精度和范围(往值小的方向)。
- else,规格化浮点数。
将一个无符号数转换为一个浮点数的表示形式并保存在一个无符号数字中
#include <iostream>
#include <cstring>
#include <stdint.h>
#include <algorithm>
using namespace std;
uint32_t uint2float(uint32_t u){ // 将一个服务号数u转换成浮点数存储的形式
// 特判
if (u == 0x00000000)
{
return 0x00000000;
}
// 找到最后一个1的后面的一个位置,求得该1后面还有多少个数
int n = 31;
while (n >= 0 && (((u >> n) & 0x1) == 0x0))
{
n = n - 1;
}
cout << "n: " << n << endl;
uint32_t e, f; // exp, frac
// <= 0000 0000 1.111 1111 1111 1111 1111 1111 : 32位
// u的位数<=24,此时再隐藏一个1,就<=23位,于是frac就可以保存所有位,不需要舍入
if (u <= 0x00ffffff)
{
// no need rounding
uint32_t mask = 0xffffffff >> (32 - n); // mask就是frac的掩码
f = (u & mask) << (23 - n); // f = u & mask得到frac,但还需要左移移动到最右侧[frac00..0],而不是[00..0frac]
e = n + 127;
printf("e: 0x%x, f: 0x%x\n", e, f);
return (e << 23) | f;
}
// >= 0000 0001 0000 0000 0000 0000 0000 0000
// 总位数>=25,一位可以隐藏,还剩下至少24位,frac无法全部保存,需要舍入(rounding)
else
{
// expand to 64 bit for situations like 0xffffffff
uint64_t a = 0;
a += u;
// compute g, r, s
uint32_t g = (a >> (n - 23)) & 0x1;
uint32_t r = (a >> (n - 23 - 1)) & 0x1;
uint32_t s = 0x0;
for (int j = 0; j < n - 23 - 1; ++ j)
{
s = s | ((u >> j) & 0x1);
}
// compute carry
a = a >> (n - 23);
// 0 1 ? ... ?
// [24] [23] [22] ... [0]
if (r & (g | s) == 0x1)
{
a = a + 1;
}
// check carry
if ((a >> 23) == 0x1) /
{
// 0 1 ? ... ?
// [24] [23] [22] ... [0]
f = a & 0x007fffff; // 0x0000 0000 0111 1111 1111 1111 1111 1111只保留frac
e = n + 127;
return (e << 23) | f;
}
else if ((a >> 23) == 0x2)
{
// 1 0 0 ... 0
// [24] [23] [22] ... [0]
e = n + 1 + 127;
return (e << 23);
}
}
// INF as default error
return 0x7f800000; // 0 1111 1111 000 0000 0000 0000 0000 0000
}
int main()
{
int x; cin >> x;
printf("%x", uint2float(0x10000000));
return 0;
}
0x 04 时序电路和组合电路
数字电路根据逻辑功能的不同特点,可以分成两大类,一类叫组合逻辑电路(简称组合电路),另一类叫做时序逻辑电路(简称时序电路)。组合逻辑电路在逻辑功能上的特点是任意时刻的输出仅仅取决于该时刻的输入,与电路原来的状态无关。而时序逻辑电路在逻辑功能上的特点是任意时刻的输出不仅取决于当时的输入信号,而且还取决于电路原来的状态,或者说,还与以前的输入有关。
时序电路,是由最基本的逻辑门电路加上反馈逻辑回路(输出到输入)或器件组合而成的电路,与组合电路最本质的区别在于时序电路具有记忆功能。
时序电路的特点是:输出不仅取决于当时的输入值,而且还与电路过去的状态有关。它类似于含储能元件的电感或电容的电路,如触发器、锁存器、计数器、移位寄存器、存储器等电路都是时序电路的典型器件,时序逻辑电路的状态是由存储电路来记忆和表示的。
时序电路和组合电路的区别:
时序电路具有记忆功能。时序电路的特点是:输出不仅取决于当时的输入值,而且还与电路过去的状态有关。组合逻辑电路在逻辑功能上的特点是任意时刻的输出仅仅取决于该时刻的输入,与电路原来的状态无关
时序电路是 时序 逻辑 电路。时序,时间 顺序,是在时钟的推动下工作的,cpu就是一个复杂的时序电路。
组合逻辑电路和时序逻辑电路的最根本区别在于:组合逻辑电路的输出在任一时刻只取决于当时的输入信号;而时序逻辑电路的输出,不仅和当前的输入有关,还和上时刻的输出有关,它具有记忆元件(触发器),可以记录前一时刻的输出状态,它可以没有输入,仅在时钟的驱动下,给出输出。
时序电路的基本结构:
结构特征:电路由组合电路和存储电路组成,电路存在反馈
0x 05 缓冲区漏洞实验
#include <stdio.h> //bomb.c
void echo()
{
char buffer[4];
gets(buffer); //缓冲区溢出的关键
puts(buffer);
}
int main()
{
puts("pls input: ");
echo();
return 0;
}
操作步骤:
1. gcc bomb.c -o main -fno-stack-protector -g
# -fno-stack-protector取消栈保护?
# -g调试模式,因为后面还需要调试
2. gdb main
2.1 在echo函数的gets函数加上一个断点:b 6
# echo函数位于main.c的第六行
2.2 r
# run运行程序,此时会在断点gets函数停下
2.3 info f
# 显示栈信息,如下方图-栈信息所示
# 在这些信息中,我们需要注意三个地址:
# (1)frame at 0x7ff.f3d0
# (2)rbp at 0x7ff.f3c0
# (3)bip at. 0x7ff.f3c8
# 其中frame at的地址是函数echo占用栈的地址
# 此时,返回地址rip和旧的栈顶指针rbp已经入栈
# 由此可见,程序还没运行,返回地址和旧的栈顶指针就会入栈
2.4 p/a &buffer[0]
# 打印数组buffer的首地址
# 通过结构图,我们可以发现,数组与返回地址rip之间差了12(c8-bc)字节,如果我们gets的数组大于等于12字节,那么返回地址的数据就会被破坏,
![image-20220907100422585](/Users/epoch/Library/Application Support/typora-user-images/image-20220907100422585.png)
(图-栈信息)
![13C288AA-6A07-463D-A689-CC7FEF2DCB91](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/13C288AA-6A07-463D-A689-CC7FEF2DCB91.png)
(图-数组地址)
(图-视频测试运行gets前的栈)
(图-视频测试运行gets后的栈)
0x 06 Computer English
common:注释
override:覆盖
entry:入口,条目,输入
Place holder:站位
ascending:升序
descending:降序
comma:逗号
brackets:括号
determine: 确定,决定,判定,下决心
deterministic: 确定行
finite: 有限的
infinite: 无限的
automaton: 自动机
positive: 正数
negative: 负数
decimal: 十进制
hexadecimal:十六进制
octal: 八进制
optimazation:优化
pruning:剪枝
decode:译码
instance: 例子,实例
cpu和memory 就组成了一个状态机
operand 操作数
opreator:操作符
memory:内存/存储器
recursion:递归
reduce:归约
iterate: 迭代
transistor:晶体管
complement:补充,补运算(~),辅
parse: 解析
simulator: 模拟器
simulate: 模拟,仿真,假装
converter:转换器
verbose: 冗长的,啰嗦
handler: 管理者,处理程序
illustrate: 说明
universal: 通用的
pecuilar: 特有,奇特,一场
0x 07 makefile
.1 规则
(1)make命令具有自动推导的功能,例如依赖中的.o文件,即使不存在,make会使用内部默认的构造规则生成这些.o文件。
(2)make后面不带参数默认执行第一条命令
(3)mak的时间戳规则:
make 命令执行的时候会根据文件的时间戳判定是否执行 makefile 文件中相关规则中的命令。
- 目标是通过依赖生成的,因此正常情况下:目标时间戳 > 所有依赖的时间戳 , 如果执行 make 命令的时候检测到规则中的目标和依赖满足这个条件,那么规则中的命令就不会被执行。
- 当依赖文件被更新了,文件时间戳也会随之被更新,这时候 目标时间戳 < 某些依赖的时间戳 , 在这种情况下目标文件会通过规则中的命令被重新生成。
- 如果规则中的目标对应的文件根本就不存在, 那么规则中的命令肯定会被执行。
(4)对于不生成目标文件的目标称为伪目标,为了避免微伪目标的名字和真实的文件名重复,我们可以在伪目标的前面加上关键字:.PHONY(假) 例如:
.PHONY: clean
clean:
rm *.o
声明位伪目标主要是避免这种情况:
如果目标不存在规则的命令肯定被执行, 如果目标文件存在了就需要比较规则中目标文件和依赖文件的时间戳,满足条件才执行规则的命令,否则不执行。
加入目标是clean,而恰好有一个真实的clean文件,只要clean文件不更新,那么clean目标就无法执行。
(提醒)目录连接到博客中的实例6可以好好看看
标签:CSAPP,int,地址,0000000000000000,gdb,main,断点 From: https://www.cnblogs.com/ALaterStart/p/18335531