CSAPP

深入理解计算机系统

0x ff 杂项

Instruction set Architecture:ISA,指令集体系架构

软件和硬件之间的一层抽象层

冯诺依曼计算机,即程序存储型计算机

重要思想:程序就是一系列被编码了的字节序列(看上去和数据一模一样)


https://www.cnblogs.com/SovietPower/p/14877143.html

0x 00 参考资料 && lab

official:

官网

实验


note:

视频详解

笔记参考视频的源码


lab:

比较详细的Attack,Data,Boom Lab参考

Boom,Attack,Shell Lab

全部实验的详细参考–知乎

全部实验的详细参考–CSDN

全部实验的详细参考–Github


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

IMAGE

0x 02 二进制运算

位运算的循环圈:

IMG

​ (int类型有符号数)

img

​ (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?

其实这也没有一个官方的说法,不过为了让自己接受这个设定,我们可以从两个角度考虑:

  1. 首先,bias采用127时绝对值的范围比较对称
  2. 其次,bias采用127时最大的指数是127比bias=128时的126大,虽然只大1,但是我们直到指数的增长是“爆炸”的,因此其表示的范围也大得多。

浮点的根据exp和frac分为三种情况:

  1. exp=111..1,指数全1。此时又分为两种情况:(1)当frac全0时表示无穷大,根据符号位又分为正无穷和负无穷。(2)frac不全为0,表示NaN,一种未定义行为。(可以这样区分无穷和NaN,由于未定义的行为有很多,因此需要根据frac进一步区分,所以frac不是固定的全0,(胡乱猜的),可以这样记忆)。
  2. exp=000..0,指数全0。表示不规格化的浮点数。这里的主要目的是为了拓展精度和范围(往值小的方向)。
  3. else,规格化浮点数。

将一个无符号数转换为一个浮点数的表示形式并保存在一个无符号数字中

IEEE 754浮点数十六进制相互转换

关于浮点数舍入的讨论

#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就是一个复杂的时序电路。

组合逻辑电路和时序逻辑电路的最根本区别在于:组合逻辑电路的输出在任一时刻只取决于当时的输入信号;而时序逻辑电路的输出,不仅和当前的输入有关,还和上时刻的输出有关,它具有记忆元件(触发器),可以记录前一时刻的输出状态,它可以没有输入,仅在时钟的驱动下,给出输出。

时序电路的基本结构:

img

结构特征:电路由组合电路和存储电路组成,电路存在反馈

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)

(图-数组地址)

img

(图-视频测试运行gets前的栈)

img

(图-视频测试运行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 文件中相关规则中的命令。

  1. 目标是通过依赖生成的,因此正常情况下:目标时间戳 > 所有依赖的时间戳 , 如果执行 make 命令的时候检测到规则中的目标和依赖满足这个条件,那么规则中的命令就不会被执行。
  2. 当依赖文件被更新了,文件时间戳也会随之被更新,这时候 目标时间戳 < 某些依赖的时间戳 , 在这种情况下目标文件会通过规则中的命令被重新生成。
  3. 如果规则中的目标对应的文件根本就不存在, 那么规则中的命令肯定会被执行。

(4)对于不生成目标文件的目标称为伪目标,为了避免微伪目标的名字和真实的文件名重复,我们可以在伪目标的前面加上关键字:.PHONY(假) 例如:

.PHONY: clean
clean: 
	rm *.o

声明位伪目标主要是避免这种情况:

如果目标不存在规则的命令肯定被执行, 如果目标文件存在了就需要比较规则中目标文件和依赖文件的时间戳,满足条件才执行规则的命令,否则不执行。

加入目标是clean,而恰好有一个真实的clean文件,只要clean文件不更新,那么clean目标就无法执行。

(提醒)目录连接到博客中的实例6可以好好看看👀

.2 变量

make中的变量分为三种:

1.自定义变量:即用户自己定义的变量,makefile中的变量是没有类型的,直接创建变量然后给其赋值就可以了。通过$(obj) 可以取出自定义的obj变量。

obj = main.c
target = main
depend = main.o

$(target): $(depend)
	gcc $(obj) -o $(target)

# --------------
# 上面的命令等价于下面:

main: main.o
	gcc main.c -o main

2.预定义变量:在makefile中有一些已经定义好的变量,用户可以直接使用这些变量,不用进行定义,预定义变量的名字一般是大写的。

![96D31374-3040-4B27-8A65-B9DE685E3351](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/96D31374-3040-4B27-8A65-B9DE685E3351.png)

3.自动变量:makefile智能鼓的规则语句经常会出现目标文件和依赖文件,自动变量用来代表这些规则中的目标文件和依赖文件,并且衙门只能在规则的命令总使用。

![DC05ED8E-B70B-44FB-A799-E6D0C938CF7F](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/DC05ED8E-B70B-44FB-A799-E6D0C938CF7F.png)

.3 模式匹配

模式匹配常常与自动变量结合使用,用来简化makefile,减少冗余和重复书写。

.4 函数

1.wildcard:通配符,用来匹配制定目录下的文件

# 使用举例: 分别搜索三个不同目录下的 .c 格式的源文件
src = $(wildcard /home/robin/a/*.c /home/robin/b/*.c *.c)  # *.c == ./*.c
# 返回值: 得到一个大的字符串, 里边有若干个满足条件的文件名, 文件名之间使用空格间隔
/home/robin/a/a.c /home/robin/a/b.c /home/robin/b/c.c /home/robin/b/d.c e.c f.c

2.patsubst:pattern subsitude,匹配代替,用来替换文件名的后缀

src = a.cpp b.cpp c.cpp e.cpp
# 把变量 src 中的所有文件名的后缀从 .cpp 替换为 .o
obj = $(patsubst %.cpp, %.o, $(src)) 
# obj 的值为: a.o b.o c.o e.o

0x 08 gdb

.0 参考

![9523F5A0-416A-4635-99DB-47685282748F](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/9523F5A0-416A-4635-99DB-47685282748F.png)

本文档参考来源,功能基础而简单

设计多线程,多进城等高级功能,较为复杂

知乎

.1 添加命令行参数

set args … 启动gdb后,在程序启动之前设置参数

show args 查看设置的命令行参数

.2 启动程序

在整个gdb调试过程中,启动饮用程序的命令只能使用一次。

run 可以缩写为 r,如果程序中设置了断点会停在第一个断点的位置,如果没有设置断点,程序就执行完了。

start 启动程序,最终会阻塞在main函数的第一行,等待输入后续其他 gdb 命令。

start 是要开始运行, run 是真的运行。

.3 退出 gdb

quit 缩写为 q

.4 查看代码

list 可以缩写为 l ,通过这个命令可以查看项目中任意一个文件中的内容,并且还可以通过文件行号,函数名等方式查看。

(gdb) list
(gdb) list 行号
(gdb) list 函数名

一个项目通常由多个源文件构成,默认情况下通过 list 查看的是程序入口 main 函数对应的文件。

(gdb) list 文件名:行号
(gdb) list 文件名:函数名

默认情况下 list 之显示 10 行的内容。如果想显示更多,可以通过 set listsize 设置,同时如果想查看当前显示的行数可以通过 show listsize 查看。这里的 listsize 可以缩写为 list

(gdb) set listsize 行号
(gdb) show listsize

.5 断点操作

如果想通过 gdb 掉时某一行或者得到某个变量在运行状态下的实际值,就需要在这一行设置断点,程序指定到断点的位置就会阻塞。我们就可以通过 gdb 的调试命令得到我们想要的信息了。

设置断点:break 缩写为 b

断点的设置方式由两种:

  1. 常规断点:程序只要运行到这个位置就会阻塞
  2. 条件断点:只有指定的条件被满足了程序才会在断点处阻塞
# 设置普通断点到当前文件
(gdb) b 行号
(gdb) b 函数名 # 停在函数的第一行
# 设置普通断点到某个非当前文件
(gdb) b 文件名:行号
(gdb) b 问价名:函数名 # 停在函数的第一行
# 设置条件断点
# 通常情况下,在循环中条件断点用的比较多
(gdb)  b 行号 if 变量名 == 某个值

查看断点:info break ,其中 info 可以缩写为 i , break 可以缩写为 b

info break 查看断点信息时的一些常用的属性:Num: 断点的编号,删除断点或者设置断点状态的时候都需要使用
Enb: 当前断点的状态,y 表示断点可用,n 表示断点不可用
What: 描述断点被设置在了哪个文件的哪一行或者哪个函数上


如果确定设置的某个断点不再被使用了,可用将其删除,删除命令是 delete 断点编号 , 这个 delete 可以简写为 del 也可以再简写为 d

删除断点的方式有两种: 删除(一个或者多个)指定断点或者删除一个连续的断点区间,具体操作如下:

# delete == del == d
# 需要 info b 查看断点的信息, 第一列就是编号
(gdb) d 断点的编号1 [断点编号2 ...]
# 举例: 
(gdb) d 1          # 删除第1个断点
(gdb) d 2 4 6      # 删除第2,4,6个断点

# 删除一个范围, 断点编号 num1 - numN 是一个连续区间
(gdb) d num1-numN
# 举例, 删除第1到第5个断点
(gdb) d 1-5

如果某个断点只是临时不需要了,我们可以将其设置为不可用状态,设置命令为 disable 断点编号,当需要的时候再将其设置回可用状态,设置命令为 enable 断点编号。

# 让断点失效之后, gdb调试过程中程序是不会停在这个位置的
# disable == dis
# 设置某一个或者某几个断点无效
(gdb) dis 断点1的编号 [断点2的编号 ...]

# 设置某个区间断点无效
(gdb) dis 断点1编号-断点n编号
# 查看断点信息
(gdb) i b
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x0000000000400cce in main() at test.cpp:14
4       breakpoint     keep y   0x0000000000400cdd in main() at test.cpp:16
5       breakpoint     keep y   0x0000000000400d46 in main() at test.cpp:23
6       breakpoint     keep y   0x0000000000400d4e in main() at test.cpp:25
7       breakpoint     keep y   0x0000000000400d6e in main() at test.cpp:28
8       breakpoint     keep y   0x0000000000400d7d in main() at test.cpp:30

# 设置第2, 第4 个断点无效
(gdb) dis 2 4

# 查看断点信息
(gdb) i b
Num     Type           Disp Enb Address            What
2       breakpoint     keep n   0x0000000000400cce in main() at test.cpp:14
4       breakpoint     keep n   0x0000000000400cdd in main() at test.cpp:16
5       breakpoint     keep y   0x0000000000400d46 in main() at test.cpp:23
6       breakpoint     keep y   0x0000000000400d4e in main() at test.cpp:25
7       breakpoint     keep y   0x0000000000400d6e in main() at test.cpp:28
8       breakpoint     keep y   0x0000000000400d7d in main() at test.cpp:30

# 设置 第5,6,7,8个 断点无效
(gdb) dis 5-8

# 查看断点信息
(gdb) i b
Num     Type           Disp Enb Address            What
2       breakpoint     keep n   0x0000000000400cce in main() at test.cpp:14
4       breakpoint     keep n   0x0000000000400cdd in main() at test.cpp:16
5       breakpoint     keep n   0x0000000000400d46 in main() at test.cpp:23
6       breakpoint     keep n   0x0000000000400d4e in main() at test.cpp:25
7       breakpoint     keep n   0x0000000000400d6e in main() at test.cpp:28
8       breakpoint     keep n   0x0000000000400d7d in main() at test.cpp:30

让无效的断点生效:

# enable == ena
# 设置某一个或者某几个断点有效
(gdb) ena 断点1的编号 [断点2的编号 ...]

# 设置某个区间断点有效
(gdb) ena 断点1编号-断点n编号

.6 调试命令

如果调试的程序被断点阻塞了又想让程序继续执行,这时候就可以使用 continue 命令。程序会继续运行,直到遇到下一个有效的断点。``continue可以缩写为c`。

在 gdb 调试的时候如果需要打印变量的值, 使用的命令是 print, 可缩写为 p。如果打印的变量是整数还可以指定输出的整数的格式,格式化输出的整数对应的字符表如下:

![9BDD57D6-6D87-4080-B269-951C45DEC259](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/9BDD57D6-6D87-4080-B269-951C45DEC259.png)

printf 的语法格式如下:

# print == p
(gdb) p 变量名

# 如果变量是一个整形, 默认对应的值是以10进制格式输出, 其他格式请参考上表
(gdb) p/fmt 变量名

例如:

# 举例
(gdb) p i       # 10进制
$5 = 3
(gdb) p/x i     # 16进制
$6 = 0x3
(gdb) p/o i     # 8进制
$7 = 03

如果在调试过程中需要查看某个变量的类型,可以使用命令 ptype, 语法格式如下:

# 语法格式
(gdb) ptype 变量名

举例:

# 打印变量类型
(gdb) ptype i
type = int
(gdb) ptype array[i]
type = int
(gdb) ptype array
type = int [12]

单步调试

step 命令可以缩写为 s, 命令被执行一次代码被向下执行一行,如果这一行是一个函数调用,那么程序会进入到函数体内部。

如果通过 s 单步调试进入到函数内部,想要跳出这个函数体, 可以执行 finish 命令。如果想要跳出函数体必须要保证函数体内不能有有效断点,否则无法跳出。

next 命令和 step 命令功能是相似的,只是在使用 next 调试程序的时候不会进入到函数体内部,next 可以缩写为 n

通过 until 命令可以直接跳出某个循环体,这样就能提高调试效率了。如果想直接从循环体中跳出,必须要满足以下的条件,否则命令不会生效:

0x e5 结构体字节对齐规则

结构体的大小绝大部分情况下不会直接等于各个成员大小的总和,编译器为了优化对结构体成员的访问总会在结构体中插入一些空白字节,有如下结构体:

struct align_basic
{
	char c;
	int i;
	double d;
};

那么此时sizeof(align_basic)的值会是sizeof(char)+sizeof(int)+sizeof(double)的值么?

img

如上图经过测试我们发现其大小为16个字节并不等于1+4+8=13个字节,可知编译器给align_basic结构体插入了另外3个字节,接下来我们将分析编译器对齐字节的规则以及结构体在内存中的结构,首先感谢结构体在内存中的对齐规则 - 咕唧咕唧shubo.lk的专栏 - 博客频道 - CSDN.NET这篇文章的作者,在此之前我对内存对齐也是一知半解,很多时候也解释不明白。

规则一:结构体中元素按照定义顺序依次置于内存中,但并不是紧密排列。从结构体首地址开始依次将元素放入内存时,元素会被放置在其自身对齐大小的整数倍地址上。这里说的地址是元素在结构体中的偏移量,结构体首地址偏移量为0。

在align_basic中元素c是第一个元素,那么它的地址为0,第二个元素i不会被放在地址1处,int的对齐大小为4个字节,此时虽然元素c只占据一个字节,但是由于i的地址必须在4字节的整数倍上,所以地址必须再向后在移动三个字节,故而需要放在地址4上,此时前两个元素已经占据了8个字节的空间,第三个元素d会被直接放在地址8上,因为double的对齐大小为8个字节,而前面两个元素已经占据了8个字节,正好是double对齐大小的整数倍,所以元素d不需要再往后移动。说了这么多也不如让机器给我们验证下有说服力:

printf("%d %d %d %d\n", sizeof(align_basic), &align_basic::c, &align_basic::i, &align_basic::d);

img

img

那么这样就够了吗,会不会太简单?我们把元素i和d的位置交换下,此时结构体的大小会是20吗,我们仍然先让机器说话,(⊙o⊙)…毕竟后面打脸有证据:

struct align_basic
{
	char c;
	double d;
	int i;
};
printf("%d\n", sizeof(align_basic));

img

我们发现此时结构体的大小并不是20而是24,那么多出来的这4个字节如何解释?我们引出第二条规则。

规则二:如果结构体大小不是所有元素中最大对齐大小的整数倍,则结构体对齐到最大元素对齐大小的整数倍,填充空间放置到结构体末尾。

运用规则一,此时c仍然是第一个元素,其地址为0,第二个元素地址为8, 第三个元素地址为16,然后运用规则二,c,d,i中d的对齐大小为8最大所以整个结构必须对齐到8的整数倍,前面是三个元素已经占据了20个字节的空间,只需要在结构体的尾部填充4个字节的空间就是8的倍数了,所以此时整个结构体的大小为24个字节。

printf("%d %d %d %d\n", sizeof(align_basic), &align_basic::c, &align_basic::d, &align_basic::i);

img

img

规则三:基本数据类型的对齐大小为其自身的大小,结构体数据类型的对齐大小为其元素中最大对齐大小元素的对齐大小。 规则三可以由规则二推导出来。

char类型的对齐大小为1字节,short类型的对齐大小为2字节,int类型的大小为4字节,double的对齐大小为8字节,align_basic结构体中最大对齐大小元素为d是double类型,所以align_basic的对齐大小是8。有人会问如果结构体中有数组呢?很简单将数组看做是连续数个相同类型的元素即可。

0x e6 第一章小结

深入理解计算机系统的“系统”,并不是操作系统,这个系统包括了硬件,操作系统,网络,编译等等

学习计算机系统应该具备的三个抽象能力:问题抽象,系统抽象(csapp),数据抽象

计算机系统是由硬件和系统软件组成的。

数字的机器表示方法是对真值的有限近似值

指令的执行:

  1. 从磁盘读取指令和数据到内存
  2. 从内存送到cpu中去执行
  3. 将返回的数据送到屏幕

0x e7 bomb lab

.1 phase1

disas main,可以发现我们输入的字符串赋值给了 $rdi
并且之后调用了函数<phase_1>
disas phase_1
发现没有修改寄存器 $rdi 的值
然后把一个立即数 0x402400 传给了寄存器 $esi
之后调用函数 <strings_not_euqal>
在之后test $eax $eax
如果 je,即 $eax = 0
调用函数 <eoplode_bomb>,炸弹爆炸
否则正常返回

进入函数 <strings_not_equal>
该函数又会调用 <string_length> 函数
这个函数会计算 $rdi 内字符串的长度

p/x $rdx :以x(16进制)方式打印寄存器$rdx的值
x $rdx 检查(examine) $rdx内存中的值

watch = sepcial break

.2 phase2

.3 phase3

![78E9B95E-D7EC-4E49-8A30-94EF4B0A4D48](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/78E9B95E-D7EC-4E49-8A30-94EF4B0A4D48.png)

# phase_3
if(eax > 1)
{
    if(7 < rsp + 8)
    {
        eax = rsp + 0x8; // first input
        switch(eax)
        {
            case 0: 
                eax = 0xcf;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 1:
                eax = 0x137;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 2:
                eax = 0x2c3;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 3:
                eax = 0x100;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 4:
                eax = 0x185;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 5:
                eax = 0xce;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 6:
                eax = 0x2aa;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
        }
        	case 7:
        		eax = 0x147;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;       		
    }
    else
    {
        return BOOM!!!
    }
}
else
{
    return BOOM!!!;
}

有多组答案:注意第二个参数不能输入十六进制数,只能输入10进制数,因为这里的数据的读如是采用sscanf,把我们的输入作为str,如果我们的第二个参数是个十六进制数,那么一定以0x开头,结果0会被读取到第二个参数,读到x不合法就结束了。

第一个参数 第二个参数
0 207
1 311
2 707
3 256
4 389
5 206
6 682
7 327

.4 phase4

第一个参数

.func4:
eax = edx
eax -= edx
ecx = eax
ecx >>= 0x1f // unsigned
eax += ecx
eax >>= 1
ecx = &(rax+rso+1)
if(ecx <= edi)  
{
    eax = 0;
    if(exc >= edi)  return 0; // 只有当 ecx<=edi<=ecx,即edi=ecx=7时可以正常退出并返回0
    else
    {
        。。。
    }
}
else
{
    edx = &(rcx - 1)
    call func4
}
// goal: make eax = 0

第二个参数看phrase4的汇编很容易得出为0

.5 phase5

reference

.6 phase6

不想做了

.7 phase7

no

.8 answer(2016)

Border relations with Canada have never been better.
1 2 4 8 16 32
7 327
7 0
)/.%&'

0x 09 Assembly实验

BE9A5FC6EBB55797FF78C5D5105D31DF

如上图,我们用(gdb) x mingling打印 0x7fffffffe3b0附近的值,这个地址是个虚拟地址,它在内存中的值为0x0

栈指针是会浮动的!但是rsp和rbp的差值应该是不变的。

gdb(ni) :会跳出函数执行

gdb(si):会进入函数执行

![A44D5C36-2816-49B3-9E01-23E15BC5DA72](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/A44D5C36-2816-49B3-9E01-23E15BC5DA72.png)

小端存储的又一个例子啊,我们把寄存器 %rbp(0x7fffffffe3d0) 放入 %rsp,观察可以发现,0x00007ffff倍放在了后面的地址,而0xffffe3d0被放在了前面的地址。x命令打印的地址从左到右,从上到下是以4为单位递增的,

0x 0a ld_preload环境变量劫持函数

首先在目录下创建两个文件 main.c 和 txt

#include <stdio.h>
int main() // main.c
{
	FILE *fd = fopen("txt", "r");
	if(fd == NULL)
	{
		printf("*** open file error!\n");
		return 1;
	}
	printf("open file success!\n");
	return 0;
}

正常来说最后程序会正确执行

但如果我们更改动态链接库

先创建一个trik动态链接库

#include <stdio.h> // trik.c
FILE *fopen(const char *path, const char *mode)
{
	printf("*** Always open error!");
	return NULL;
}
gcc -shared -fPIC trik.c -o trik.so
LD_PRELOAD=$PWD/trik.so ./a.out

最后文件会打开失败

![53DE4182-359A-4F3E-80BB-4B97508E7F9B](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/53DE4182-359A-4F3E-80BB-4B97508E7F9B.png)

原理就是通过自己写的库函数劫持系统的库函数,使得程序执行我们的库函数。

0x 0b attack lab

0x0c 链接 points

1.引入哑节点dummy

2.引入数据结构–elf

3.静态链接的过程:elf定位到符号->符号解析->重定位

4.*.o, elf 都是二进制文件

5.unix下大部分工具都在/usr/bin或者/bin目录下的。使用hexdump可以查看二进制文件

6.第一个section的name为空(其实叫做 undefine section),且数据全为0,里面存放的内容是undefine的数据。

7.将函数定义为一个弱符号:attribute__((weak)) int add*() {} ,这里的 add 函数被定义为一个弱符号,它可以被强符号函数 add 覆盖。

8.对于 C Language 来说,出现 Warning 说明你的语句有歧义 ,但是 C 语言为你选择了一种结果,注意这种结果可能与你的本意不同!

9.对于初始化为 0 的全局变量和静态变量,也被划分到 .bss,这是因为全局变量和静态变量默认初始化就是 0

10.为什么在可重定位目标文件中有 COMMON,在可执行目标文件中就没有 COMMON 了呢。

回想一下COMMON的定义,对于未初始化的全局变量, 属于COMMON

对于未初始化的全局变量, 在链接之后它有三种可能的情况(假设这里有两个文件 s1.c, s2.c,在 s1.c 中定义有未初始化的全局变量 g

  1. 如果在 s2.c 中也定义了一个全局变量 g 并且初始化为 0,则 g 属于 .bss

  2. 如果初始化不是 0,就属于 .data

  3. 如果 s2.c 没有定义 g ,那么 s2 就属于 .bss

    因为有如上三种(合法)情况,所以把它划分到 COMMON,而之所以在可执行目标文件中没有了 COMMON ,是因为此时已经链接完了,g 属于那个节已经很明确了,因此也就不需要了。

0x0d 修改 ROF 信息的实验

首先编译源文件 add.c 生成可重定位目标文件 add.o

int addcnt = 0;

int add(int a, int b)
{
	addcnt ++ ;
	return a + b;
}

使用 hexdump -S add.o 查看 Section Headers

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000003c  0000000000000000  AX       0     0     4
  [ 2] .rela.text        RELA             0000000000000000  00000228
       0000000000000060  0000000000000018   I       9     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000007c
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000007c
       0000000000000004  0000000000000000  WA       0     0     4
  [ 5] .comment          PROGBITS         0000000000000000  0000007c
       0000000000000027  0000000000000001  MS       0     0     1
  [ 6] .note.GNU-stack   PROGBITS         0000000000000000  000000a3
       0000000000000000  0000000000000000           0     0     1
  [ 7] .eh_frame         PROGBITS         0000000000000000  000000a8
       0000000000000030  0000000000000000   A       0     0     8
  [ 8] .rela.eh_frame    RELA             0000000000000000  00000288
       0000000000000018  0000000000000018   I       9     7     8
  [ 9] .symtab           SYMTAB           0000000000000000  000000d8
       0000000000000138  0000000000000018          10    11     8
  [10] .strtab           STRTAB           0000000000000000  00000210
       0000000000000018  0000000000000000           0     0     1
  [11] .shstrtab         STRTAB           0000000000000000  000002a0
       0000000000000059  0000000000000000           0     0     1

可以发现下标为 1 的节是 .text

我们现在要修改 add.o 使其显示为 .ext

首先需要下载 hexedit

然后拷贝一份 add.o 的副本 badadd.o

(不在源文件上直接修改是个好习惯)

然后执行命令hexdump -c badadd.o 找到 .text 的位置。

通过 elf header 中的信息可以得到 Section header tableoffset0x300,其中每个条目(entry) 的 size0x40 ,由此可以得到第二个条目(下标为1)的 .text 节的位置为 0x340,并通过 struct elf64_shdr 得到前 4 个字节为 name

00000340 20 00 00 00 01 00 00 00 06 00 00 00 00 00 00 00 | ...............|

name = 0x00000020 ,我们只需要修改其为 0x00000022,就可以实现 name 往后偏移两个字节

这样 name 就从 ``.text变成了ext`

执行命令:hexedit badadd.o 找到位置并修改即可。

F10 退出

最后结果如下:

readelf -S badadd.o

[Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] ext               PROGBITS         0000000000000000  00000040
       000000000000003c  0000000000000000  AX       0     0     4
  [ 2] .rela.text        RELA             0000000000000000  00000228
       0000000000000060  0000000000000018   I       9     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000007c
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000007c
       0000000000000004  0000000000000000  WA       0     0     4
  [ 5] .comment          PROGBITS         0000000000000000  0000007c
       0000000000000027  0000000000000001  MS       0     0     1
  [ 6] .note.GNU-stack   PROGBITS         0000000000000000  000000a3
       0000000000000000  0000000000000000           0     0     1
  [ 7] .eh_frame         PROGBITS         0000000000000000  000000a8
       0000000000000030  0000000000000000   A       0     0     8
  [ 8] .rela.eh_frame    RELA             0000000000000000  00000288
       0000000000000018  0000000000000018   I       9     7     8
  [ 9] .symtab           SYMTAB           0000000000000000  000000d8
       0000000000000138  0000000000000018          10    11     8
  [10] .strtab           STRTAB           0000000000000000  00000210
       0000000000000018  0000000000000000           0     0     1
  [11] .shstrtab         STRTAB           0000000000000000  000002a0
       0000000000000059  0000000000000000           0     0     1

0x0e vim tabe

vim中的分页命令,多窗口vim

通过help tab-page-intro命令,可以获得关于标签页使用的更多信息。

:tabnew 新建标签页
:tabs 显示已打开标签页的列表
:tabc 关闭当前标签页
:tabe <filename> 打开新文件(tabedit)
:tabp 移动到上一个标签页
:tabn 移动到下一个标签页(tabnext)
:gt 移动到下一个标签页
:tabr 移动到第一个标签页(tabrewind,tabfirst)
:tabl 移动到最后一个标签页(tablast)
$vim -p <f1> <f2> <f3> vim开启多个标签页

0x0f bilbili 链接

链接步骤:

  1. parse text
  2. symbol parse
  3. Relocation

2 和 3 都依赖于 1 的 text

Csapp Link

::English

separate compliation:分离编译

mangling:重整

:: Tool

GNU READELF:查看目标文件内容的很方便的工具。

0x00 introduce

1. 链接的执行阶段

  1. compile time
  2. load time
  3. run time
  1. 理解链接器将帮助你构造大型程序
  2. 理解链接器将帮助你避免一些危险的编程错误。
  3. 理解链接器将帮助你理解语言的作用域规则是如何实现的。
  4. 理解链接将帮助你理解其他重要的系统概念。(加载和运行程序,虚拟内存,分页,内存映射)
  5. 理解链接将使你能够利用共享库。

0x01 compiler driver

compiler dirver:编译器驱动程序

它代表用户在需要的时候调用:

  1. cpp
  2. cc1
  3. as
  4. ld

可以使用 -v 选项查看这个过程

当我们在 Linux 命令行输入:./proc

shell 调用操作系统中一个叫做加载器的函数,它将可执行文件 proc 中的代码和数据复制到内存,然后将控制转移到这个程序的开头。

Relocaable object file: 由各种不同的代码和数据节(section)组成,每一节都是一个连续的字节序列。

为了构造 executable file,linker 必须完成两个主要任务:

  1. Symbol resolution(符号解析):符号解析的目的是将每个符号引用正好和一个符号定义关联起来。
  2. relocation(重定位)。

Symbol(符号):目标文件定义和引用符号,每个符号对应于一个函数,一个局部变量或一个静态变量(即C语言任何非 static 属性声明的变量)。

Compiler and Assembly generate code and data section start at address 0, linker connect every symbol define with one memory address, so can relocate those sections, and then modify all the symbol define, make them point the address. Linker use the detailed instructions of relocation entry(重定位条目) which generated by assembly to execute those relocation with no check.

0x03 object file

object file(目标文件) types:

  1. relocatable object file:在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
  2. executable object file
  3. Share object file(共享目标文件): 一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。

Compiler and Assembly generate relocatable object file. Linker generate executable object file.

Technically talking, a object module(目标模块) is a byte sequence, and a object file is a object module which storage in disk as a type of file.

目标文件是按照特定的目标文件格式来组织的,各个系统的目标文件格式都不相同。

  1. Unix: a.out
  2. Windows: PE(Portable Executable)(可移植可执行)
  3. MacOS-X: Mach-O
  4. Modern x86-64 and Unix: ELF(Executable and Linkable Format)(可执行可链接格式)

0x04 relocatable object file

典型的ELF可重定位目标文件

IMG

ELF contains: ELF header,Sections,Section header table(节头部表)。

(1) ELF header:

  1. 以一个 16 字节的序列开始,这个序列描述了生成该文件的系统的字的大小字节顺序。
  2. 剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包含:
    • ELF 头的大小
    • 目标文件的类型(可重定位、可执行或者共享)
    • 机器类型(x86-64)
    • 节头部表的文件偏移
    • 节头部表中条目的大小和数量

(2) Section headere table: 不同 Section 的位置和大小是由节头部表描述的,其中目标文件中的每个节都有一个固定大小的条目(entry)。

(3) Section:

  1. .text:已编译程序的机器代码。
  2. .rodata:只读数据。
  3. .data:已初始化的全局和静态 C 变量。(局部变量在栈中,既不出现在 .data中,也不出现在 .bss汇总)
  4. .bss:未初始化的全局和静态 C 变量,以及所有被初始化为 0 的全局或静态变量(默认初始化)。在目标文件中这个节不占用实际的空间,它仅仅是一个占位符。 目标文件中区分 .bss 和 .data 是为了空间效率:在目标文件中,未初始化变量不需要占用任何实际的磁盘空间。运行时,在内存中分配这些变量,初始化为0。
  5. .symtab;符号表。存放在程序中引用定义的函数和全局变量的信息。(不包含局部变量的条目)。
  6. .rel.text:relocation。一个 .text 节总位置的列表。当 Linker 把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。
  7. .rel.data:被模块定义或引用的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的值是一个全局变量地址或者外部定义的函数的地址,都需要被修改。
  8. .debug:调试符号表。只有使用 -g 选项时才会得到这张表。
  9. .line:原始 C 源程序中的行好和 .text 节 中机器指令之间的映射。只有使用 -g 选项时才会得到这张表。
  10. .strtab:字符串表。其内容包含 .symbol 和 .debug节中的符号表,已经节头部中的节名字。字符串表就是以 null 结尾的字符串的序列。

为什么未初始化的数据成为 .bss

起始于 IMB 704 汇编语言(大约在1957年) Block Storage Start(块存储开始)指令的首字母缩写。并沿用至今。

你可以这样理解并区分于 .data:Better Save Space(更好的节省空间)的缩写。

0x05 symbol and symbol table

每个 relocatable object module m 都有一个符号表,它包含 m 定义和引用的符号的信息。在 Linker 的上下文中,有三种不同的符号:

  1. m 定义的并且能被其他 module 引用的全局符号。
  2. 其他 module 定义并被模块 m 引用的全局符号,
  3. 只被 m 定义和引用的局部符号。

符号表是由 Assembly 构造的,使用 Compiler 输出到汇编语言 .s 文件中的符号。

.symtab 节的内容是一个数组,数组的元素是一个符号条目:

typedef struct {
    int name;
    char type: 4,
    	binding: 4;
    char reserved;
    short section;
    long value;
    long size;
} Elf_64_Symbol;

name:是字符串表中的字节串,指向符号的以 null 结尾的字符串名字。

section(base_address):到节头部表的索引,指明被分配到那个节。

value(offset_address):是符号的地址。对于可重定位的 module 来说,value 是距定义目标的节的其实地址的 offset。

size:是目标的大小(byte)。

type:data or function。

binding:static or global

有三个特殊的伪节,它们在节头部表中是没有条目的(只有可重定位目标模块才有):

  1. ABS:不应该被重定位的符号。
  2. UNDEF:未定义的符号,也就是在本目标模块中引用,但是在其它地方定义的符号。
  3. COMMON:还未被分配位置的未初始化的数据目标。对于 common u符号,value 字段给出对其要求

common 和 .bss 的区别很细微,现代的 GCC 根据以下规则分配符号:

  1. Common: 未初始化的全局变量
  2. .bss:未初始化的静态变量,及其初始化为0的全局变量和静态变量

0x06 symbol parse

1.链接器解析符号引用的方法

链接器解析符号引用的方法是将每个引用于它输入的可重定位目标文件的符号表的一个确定的符号定义关联起来。

对那些和引用定义在相同模块中的局部符号的引用,符号解析是非常简单明了的。编译器只允许每个模块中每个局部符号有一个定义。静态局部变量也会有本地链接器符号,编译器还要确保他们拥有唯一的名字。

不过,对全局符号的引用解析就棘手的多。当编译器遇到一个不是在当前模块中定义的符号(变量或者函数名)时,会假设该符号是在其它某个模块中定义的,升成一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就输出一条(通常很难阅读的)错误信息并终止。

2.c++ 和 java 中的重整恢复

C++ 和 Java 都允许重载方法,这些方法在源代码中有相同名字,却有着不同的参数列表。那么链接器是如何区别这些不同的重载函数之间的差异呢?

因此编译器将每个唯一的方法和参数列表组合编码成一个对链接器来说唯一的名字。这种编码过程叫做重整(mangling),而相反的过程叫做恢复(demangling)。

幸运的事,C++ 和 Java使用兼容的重整策略。一个被重整的类名字是由名字中字符的整数数量,后面跟上原始名字组成的。例如:类 Foo 被编码成 3Foo。方法被编码为原始方法名,后面加上‘__’(下划线),加上被重整的雷鸣,再加上每个参数的单字母编码。比如:Foo::bar(int, long) 被编码为 bar_3fooil。

重整全局变量和模版名字的策略是相似的。

例如 C++程序 :

#include <iostream>

using namespace std;

int get(int a, int b)
{
	return a + b;
}

int get(int a, int b, int c)
{
	return a + b + c;
}

int main()
{
	int a = 1, b = 2, c = 3;
	int sum1, sum2;
	sum1 = get(a, b, c);
	sum2 = get(a, b);
	cout << "sum1: " << sum1 << endl;
	cout << "sum2: " << sum2 << endl;
	return 0;
}

执行命令:

readelf mangling.o --syms

得到如下符号表:

Symbol table '.symtab' contains 30 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS mangling.cpp
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 .data
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 .bss
     5: 0000000000000000     1 OBJECT  LOCAL  DEFAULT    4 _ZStL8__ioinit
     6: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    4 $d
     7: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    1 $x
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     9: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    5 $d
    10: 00000000000000fc    96 FUNC    LOCAL  DEFAULT    1 _Z41__static_ini[...]
    11: 000000000000015c    28 FUNC    LOCAL  DEFAULT    1 _GLOBAL__sub_I__[...]
    12: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 .init_array
    13: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    6 $d
    14: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 .note.GNU-stack
    15: 0000000000000014     0 NOTYPE  LOCAL  DEFAULT   10 $d
    16: 0000000000000000     0 SECTION LOCAL  DEFAULT   10 .eh_frame
    17: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 .comment
    18: 0000000000000000    32 FUNC    GLOBAL DEFAULT    1 _Z3getii
    19: 0000000000000020    44 FUNC    GLOBAL DEFAULT    1 _Z3getiii
    20: 000000000000004c   176 FUNC    GLOBAL DEFAULT    1 main
    21: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZSt4cout
    22: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZStlsISt11char_[...]
    23: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZNSolsEi
    24: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZSt4endlIcSt11c[...]
    25: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZNSolsEPFRSoS_E
    26: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZNSt8ios_base4I[...]
    27: 0000000000000000     0 NOTYPE  GLOBAL HIDDEN   UND __dso_handle
    28: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZNSt8ios_base4I[...]
    29: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __cxa_atexit

可以观察到,两个 get 函数分别被标识为:_Z3getii_Z3getiii

3. Linux 处理多重定义的符号

强符号:函数和已初始化的全局变量

弱符号:未初始化的全局变量

Linux 处理多重定义的符号名的三个规则:

  1. 不允许多个同名的强符号。
  2. 如果有一个强符号和多个弱符号同名,选择强符号。
  3. 如果有多个弱符号同名,任意选择一个。

4. 静态库

4.1 为什么要引入静态库?

如果不引入静态库的话,试想一下编译器开发人员会使用什么方法来向用户提供这些函数。

编译器代劳!

一种方法是让编译器辨认出对标准函数的调用,并直接生成相应的代码。对于那些提供了一小部分标准函数的语言(例如 Pascal)是可以的,但是对于 C 这种标准定义了大量的标准函数是不可以的。因为每次添加、修改或删除一个标准库函数时,就需要一个新的编译器版本。然而,对于应用程序猿而言,这种方法是非常方便的,因为标准函数将总是可用(只需要你编译器开发人员搞定就行了,管我什么事 - -

所有函数对应一个可重定位目标模块!

另一种方法是将所有的 C 函数都放在一个单独的可重定位目标模块中(比如说 libc.a),应用程序猿可以把这个模块连接到他们的可执行文件中:

gcc main.c /usr/lib/libc.o

IOS C99 定义的 C库:libc.a; 数学函数库:libm.a

通过把函数放在目标模块中,可以把编译器的实现与标准函数的实现分离开来。但是,现在每个可执行文件都包含着一份标准函数集合的副本(除非你不链接它,但这怎么可能呢?),这是对磁盘的极度浪费!在一个典型的系统中,libc.a 大约是 5MB,llib.a 大约是 2MB)。另外,每个运行的程序都将它的这些函数的副本放在内存中,这是对内存的极大浪费。此外,只要标准库修改了一个小小的地方,无论多么小,你都要重新编译整个源文件,非常耗时

每个函数对应一个可重定位目标模块!

我们可以通过为每个库函数创建一个独立的可重定位模块,把他们放在一个为大家都知道的目录中来解决其中的一些问题。然而,问题也是相当明显的:

  1. 那你要手写多少模块啊?
  2. 太多了不小心写错名字了怎么办?从头再检查一遍吧!
  3. 太多了,你得写到什么时候?
  4. 。。。
  5. 真是一个麻烦又耗时又糟心的过程!

gcc main.c /usr/lib/printf.o /usr/lib/scanf.o ........

静态库!

于是,为了解决这些问题,静态库诞生了!!!

我们可以结合上面的方法,既不把所有函数划分到一个模块,也不每个函数对应一个模块,而是把一些相关的函数划分到一个模块(例如 C 标准库和数学库等),然后封装成一个单独的静态库文件。而不是每个函数对应一个模块。

gcc main.c /usr/lib/libc.a /usr/lib/libm.a ..

你可能会问:这个静态库和前面把所有函数放在一个可重定位目标模块有什么区别吗?不就是一个叫(模块 .o),一个叫静态库(.a)罢了!

那我可就得给你好好讲讲了:当所有函数封装在一个模块中,那我们链接的时候,就不得不链接所有库函数了。

但是!接下来好好听了!

如果说模块是函数的集合,那么静态库就是模块的集合!所以,你可能想到了,虽然我们链接到了静态库,但并不链接静态库中的所有模块,而是只链接需要用到的模块,这样既避免了类似于一个函数一个模块那样链接模块太多的问题,又避免了链接所有模块的问题。

你可能会问:这怎么实现呢?

答案是:暴力出奇迹,循环判断是否用到就好了。用不到的模块就舍弃掉。

妙不妙!再看一看静态库的定义吧。

在 Linux 中,静态库是以一种称为 存档(archive) 的特殊文件形式存放在磁盘中的。存档是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件由后缀(.a)标识。

4.2 创建静态库

静态库和动态库创建参考

(1) 首先,我们需要源文件(.c)

这里为 mul.c 和 add.c

int mulcnt = 0;

int mul(int a, int b)
{
	mulcnt ++ ;
	return a * b;
}// mul.c
int addcnt = 0;

int add(int a, int b)
{
	addcnt ++ ;
	return a + b;
}// add.c

(2) 然后,我们需要将源文件处理成可重定位目标文件

gcc -c add.c mul.c

(3) 最后,将需要的可重定位目标文件封装到静态库中。

例如: ar rcs mylib.a a.o b.o...

r: replace and insert

c : create

s: add index

ar rcs mylib.a add.o mul.o

(4) 别以为就这样结束了,编写个 main 程序测试你下你的库吧!

#include <stdio.h>

int main()
{
	int x = 1, y = 2;
	int s1 = add(x, y);
	int s2 = mul(x, y);
	printf("x = %d, y = %d\nsum = %d, mul = %d\n", x, y, s1, s2);

	return 0;
}
gcc -c testar.c # 先编译生成可执行文件
gcc --static -o main testar.o -L. mylib.a # 与静态库链接

–static 参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到内存并运行,在加载时无需更进一步的链接。所以说不加也是可以的。

-Ldir 指明了链接器在那个目录下查找 mylib.a,dot就表示当前目录。

0x07 relocation

1. 重定位的任务:

重定位合并输入模块,并为每个符号分配运行时地址。

由两步组成:

  1. 重定位节和符号定义:

    1. 将所有相同类型的节合并为一个节
    2. 将运行时内存地址赋给新的聚合节
    3. 赋给输入模块定义的每个符号

    完成后,程序中的每条指令和全局变量都有唯一的运行时内存地址了。

  2. 重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。

2. 重定位条目

为什么需要重定位条目?

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。

它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。

所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个 “重定位条目”,告诉链接器在将目标文件合并成可执行文件时许和修改这个引用。

代码的重定位条目放在 .rel.text 中,已初始化数据的重定位条目放在 .rel.data 中。

ELF 重定位条目的格式:

typedef struct {
    long offset;	// 我在那
    long type: 32;	// 怎么引用
    	smybol: 32; // 我引用了谁
    long addend;	// 我的偏移量
} Elf64_Rela;

offset 是需要被修改的引用的在节内的偏移。(一般是一个地址)

symbol 标识被修改引用应该指向的符号。

type 告知链接器如何修改新的引用。

addend 是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。(addend的值一般是当前引用的地址距离下一条指令的偏移)(讲的标准一点就是对 rip 的修正,因为重定位所在的地址并不是下一条指令的 rip 地址)

两种最基本的重定位类型(type):

  1. R_X86_64_PC32:重定位一个使用 32 位 PC 相对地址的引用。(一个 PC 相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当 CPU 执行一条使用 PC 相对寻址的指令时,它就将在指令中编码的 32 位值加上 PC 的当前运行时值,得到有效地址, PC 值通常是下一条指令在内存中的地址)。

    简而言之,相对的意思就是,相对于下一条指令的偏移量。

  2. R_X86_64_32:重定位一个 32 位绝对地址的引用。通过绝对寻址,CPU 直接使用在指令中编码的 32 位值作为有效地址。

3. 重定位符号引用

相对引用

call addr 
	sym.offset: R_X86_64_PC32 sym

首先,要清楚我们的目标:通过 addr 的相对偏移得到该符号的运行时地址,这个地址我们是已知的。(我们用ADDR(x)表示符号 x 的运行时地址)

当前引用的地址 + 距离下一条指令的偏移量 + addr = 目标符号的运行时地址

addr = ADDR(sym) - (当前引用的地址 + 距离下一条指令的偏移量)

不过,距离下一条指令的偏移量通常以 sym.addend 的形式存在,于是,上式变成了:

addr = ADDR(sym) - 当前引用的地址 + sym.addend

我们发现,公式在经过转换后,由 “距离” 下一条指令的偏移量变成了 “加上” sym.addend。

而偏移量肯定是一个正数(不然怎么偏移到下一条指令),所以说 sym.addend 肯定是个负数。

自己推导的,不一定对??

而当前引用的地址 = 引用所在节的运行时地址 + 引用的偏移(sym.offset)

所以,上式最终等于如下:

addr = ADDR(sym) - (ADDR(Section) + sym.offset)+ sym.addend


绝对引用

call addr
	sym.offset: R-X86_64_32 sym

addr = ADDR(sym) + sym.addend

在绝对引用中,我们依然需要加上偏移量addend,只不过 sym.addend=0。

可以发现,相较于绝对引用,相对引用只需要减去当前引用的地址即可,距离下一条指令的偏移保存在了 addend 中。

0x08 executable object file

典型的 ELF 可执行目标文件(EOF,段和节):

img

ELF头还包括了程序的入口点?也就是程序的第一条指令的地址。

通过图可以发现,EOF 文件中还多了 .init 节。.init节定义了一个小函数,叫做 _init_,程序的初始化代码会调用它。

.text,.data,.rodata 与可重定位目标文件的节是相似的,除了这些节已经被重定位到它们最终的运行时内存地址以外。

因为 EOF 文件是完全链接的(已被重定位),所以它不再需要 .rel 节。

EOF 文件还有对其要求。这主要与虚拟内存有关

0x09 load EOF

我们通常在 Linux Shell 命令行输入可执行目标文件的名字 (例如prog) 来执行它:

Linux> ./prog

因为 prog 不是一个内置的 shell 命令,所以 shell 会认为 prog 是一个可执行目标文件。

通过调用某个驻留在内存中称为加载器(loader)的操作系统代码来运行它。

任何 Linux 程序都可以通过调用 execve() 调用加载器。

加载器将 EOF 文件的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序从磁盘复制到内存并运行的过程叫做 “加载”。

img

1. 为什么引入动态库

当然是因为静态库有一些缺点了。

第一个问题,静态库不方便后续的更新和维护。

静态库和所有软件一样,需要定期维护和更新。

如果应用程序员想要使用一个库的最新版本,他们必须以某种方式了解到该库的更新情况,然后显式的将他们的程序与更新了的库重新链接。

第二个问题,静态户仍然会造成对内存资源的极大浪费。

虽然在上面 “引入静态库” 一节中我们已经说明了,静态库已经是一种比较节约内存资源的方式。

但那仅仅是在只针对一个文件的情况下,我们尽可能只引用必须用到的模块而避免引用了许多不会用到的模块造成内存浪费。

但试想一下,如果我们存在许多文件呢,几乎每个文件都会用到 printf() 函数等标准 IO 函数。在运行时,这些函数的代码会被复制到每个运行进程的文本段中(试想一下如果我们 printf() 了几百次,难道每一次调用都要复制一份 printf() 的代码吗?那也太浪费内存了!)。

特别是在一个运行上百个金层的典型系统上,这将是对稀缺的内存资源的极大浪费。

(内存的一个有趣属性就是无论系统的内存多大,他总是一种奇缺资源。磁盘空间和厨房的垃圾桶具有同样的属性)。

于是,为了致力解决静态库的缺憾,共享库诞生了。

共享库是一个目标模块,在运行或加载时,可以加载到任意的内存空间,并和一个在内存中的程序链接起来。这个链接的过程就叫做 “动态链接”,是由一个叫做动态链接器的程序来执行的。

共享库也称为 “共享目标”(shared object)。在 Linux 系统中用 .so 后缀来标识。微软的操作系统大量的使用了共享库,它们称为 DLL(动态链接库)

2. 共享库的工作方式

共享库是以两种不同的方式来实现 “共享”的。

首先,在任何给定的文件系统中,对于一个酷只有一个 .so 文件,所有引用该库的可执行目标文件分享这个 .so 文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行目标文件中。(解决了静态库内存浪费的问题)

其次,再内存中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享(与虚拟内存有关)。

img

如何构造一个共享库:

gcc -shared -fpic -o libname.so module1.o module2.o ....

-fpic 选项指示编译器生成与位置无关的代码。

-shared 选项指示编译器创建一个共享的目标文件。

下面将将这个共享库链接到程序当中:

gcc -o prog main.c ./libname.so

根据上图(7-16)我们可以发现,可执行目标文件 prog21 在加载之后,也就是运行时可以和动态库 livvector.so 链接。基本的思路就是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程

by xjy:

注意上面的话并不矛盾,前一句话说程序运行时和动态库链接,下一句又说在程序加载时动态完成链接。一个是在运行时,一个是在加载时。

这可能是因为程序并不是直接全部加载到内存的(操作系统),它用到一点就加载一点,所以说,加载和运行是交叉的。

注意,再整个链接的过程当没有任何动态库的代码和数据真的被复制到可执行文件 prog21 当中。反之,链接器复制了一些重定位和符号表信息,它们使得运行时可以解析对动态库中代码和数据的引用。

3. 小实验

下面是一个小实验,1.c,2.c 用来构建动态库和静态库,main.c 是测试函数。

app 是链接静态库生成的可执行文件。

prog 是链接动态库生成的可执行文件。

可以发现,prog 的大小比 app 小的多(小了50多倍)。

IMG

csapp memory

一、cache

0x01 一种初始化方式

#include <stdio.h>

typedef struct Node
{
    int l, r;
    char s[100];
} node_t;

int main()
{
    node_t p = {
        .l = 100,
        .r = 200,
        .s = "hello,world!",
    };
    
    printf("node: %d %d %s\n", p.l, p.r, p.s);
    
    return 0;
}

0x02 note

我们知道内存是分页的,cache的 line 只会存在于某一个页,它不会跨页存在。

0x03 true/fake sharing

罪魁祸首:MESI 协议

false sharing有一个问题,就是对于sum求和这个例子,虽然我们设置sum1和sum2分别求和,但是sum1和sum2都是分配在栈上的,并且地址十分接近,所以它们可能在同一个cache当中,这样不管是sum1修改还是sum2修改,都会触法 MESI 的同步协议,这样 false sharing的速度和true sharing相差几乎无几。

0x04 MESI protocol

exclusive:独有的

exclusive 和 shared 不能共存

四种状态:(由于读数据不会产生数据一致性问题,因此这里只考虑写数据操作)

M: (exclusive) modify, like dirty. 物理地址被缓存到某一个 cache,并且数据已经被修改

E: exclusive (clean).物理地址被缓存到某一个 cache,并且数据没有被修改

S: (exclusive) shared clean.物理地址被缓存到 cache,并且多个 cache 共享。

如果修改一个状态为 s 的 cache,它会发送一个广播,将所有其他状态为 s 的 cache 的状态修改为 invalid(具体方法是将其拥有数据写入到 dram,然后修改状态为 invalid),然后将自己的状态修改为 M,这样就可以保证全局状态下只有一个 M,也就是 exclusive的。

I: invalid.物理地址并没有缓存到 cache。

此时如果发生 cache write

  1. 如果其他 cache 的状态都是 invalid,从内存 load 数据,修改器状态为 M。
  2. 如果存在 (shared)S状态 的 cache,将它们的数据写入到 dram,然后修改状态为 invalid。

每个处理器的cache line都是 dram 的 cache line 的拷贝

二、page table

0x01 tips

地址翻译由硬件实现,操作系统为应用提供这个功能。

TLB 也是一个 cache。

现在 64 位的处理器(cpu)的虚拟地址一般其实只有 48 位,剩下的 16 位属于内核。

虚拟地址空间呈现局部密集,整体稀疏的特征。

多级页表在最坏的情况下(满映射,每页都必须有有效数据)是一棵完全二叉树,此时页表条目会比朴素页表多出来一倍。但这种情况几乎不可能出现(虚拟地址空间的稀疏性和程序的局部性)。

页表分配在操作系统的内核态。

在windows下,资源管理器的内存中可以看到:分页缓冲池和非分页缓冲池。分页缓冲池指的是可以和磁盘进行换入(page in)和换出(page out)的页,而非分页并不是指不分页,而是不能喝磁盘进行 swap。

0x02 how to reflect va2pa

在我们编写的地址转换函数中,我们简单的通过去模数将物理地址转换为虚拟地址,然而,这是极为不合理的,例如:

  1. 产生不合法的地址(地址越界)。例如:0x200(1024)%0x200=0x000,它产生了一个地址为 0 的地址,这显然是错误的。
  2. 不同进程间地址冲突的问题。因为每个进程的地址都是从 0x00400000 开始的,而相同地址取模之后的值是相同的,这就会导致地址冲突。

一种可行的方法是使用 hashmap 完成物理地址到虚拟地址地址映射。它解决了使用取模方法产生的冲突和越界问题,但是,它又会产生以下两个问题:

  1. 内存浪费严重。在 hashmap 中,我们需要额外的两份空间来分别存储物理地址和虚拟地址以记录他们的映射关系,并且,由于 hashmap 并不是全部使用的,它的内部会有空闲,因此我们还需要乘上一个空闲率 k(k>=1),因此 hashmap 就需要额外的 2k 倍的额外内存空间要保存映射信息。
  2. 破坏程序的局部性。由于 hashmap 的映射是离散的,这就会导致程序会被离散化,破坏程序的局部性。

但是,hashmap 产生的这两个问题属于 性能 问题,它只是导致程序运行效率不好,并不会导致程序运行错误。而取模方法则会导致程序运行出错。

现在我们再来想,hashmap 中记录如此之多的映射信息是否有必要?

肯定是有必要的,不然我们就无法找到物理地址了。但是!如果我们通过虚拟地址映射到物理地址不是离散的,例如:

虚拟地址 0x1,0x2,通过 hashmap 地址映射为物理地址:0xa, 0xabcd。如果我们想找到这两个物理地址,我们必须保存映射信息,因为 0xa, 0xabcd 之间毫无关联。但是这种离散性是毫无必要的,如果我们将地址映射为 0xa, 0xb 这种连续的地址的话,它不仅可以避免破坏程序的局部性,还能减少地址映射需要保存的信息。

比如虚拟地址 [0x0, 0xffff] 这一块区域,如果我们采用 hashmap,它需要 0xffff 份映射信息,这也太多了。但是,如果 hashmap 映射的地址是连续的,我们就可以通过三元组(va0, pa0, offset) (offset表示偏移量)来找到这个区域内任意一个地址的映射,并且仅仅只需要一份映射信息,对于任意 va,pa=pa0+va-va0(va >= va0 && va <= va0 + offset)。

现在, 完成地址映射需要的额外信息由 2k 变成了 3M,M 就是上述三元组的数量,这个 M 远小于地址的数量。

这就是分段思想。

当然,分段也是有问题的,例如:

  1. 碎片。内部碎片和外部碎片。
  2. 每次计算都需要比较 va 是否越界。(va >= va0 && va <= va0 + offset)
  3. 不方便拓展。当我们的段太大或者或许频繁拓展的时候,寻找一个合适的空间比较麻烦。

所以说,我们需要把 offset 变成一个较小且固定的数值,这就是分页思想。

0x03 address transfer

ARM64架构下地址翻译相关的宏定义

else

else

0x04 page falult

MM: main memory,主存

page table is the cache from disk to main memory

交换空间:当我们页表缓存的页满了之后,我们想再往内存映射一页,此时需要将该页 page out,但是如果该页的数据被修改了 dirty,我们该怎么办?

  1. 不管它,这肯定不行
  2. 将该页写回文件 program file,这也肯定不行,我们不应该修改源文件。
  3. 放到别的地方 – swap space。

将一页从 mm 放到 swap space 的过程就叫做 swap out

相反的,将页从 swap space 再放到 mm 的过程叫做 swap in

所以说,一个文件占用的空间包括了 mm 和 swap

swap space 也在磁盘

demand paging: waiting until the miss to copy the page to DRAM is konwn as deman paging

程序的代码文件,例如 .data 段它是存储在磁盘当中的,所以它与内存之间可以存在映射关系,但是 .data 段,stack, heap 不是存储在磁盘当中的,当我们需要把这些短存放在磁盘当中时,我们需要放入 swap space 中。它们又称为“匿名页”(在磁盘中没有文件与它对应)。

三、virtual memory overview

​ virtual memory 主要是为了解决物理内存和进程所看到的虚拟内存不匹配的问题,所以说每个 virtual memory 肯定是提供给每一个进程的。

每个进程就是一段 active 的内存,例如:

  1. .text 是死的
  2. .data 是活的,因为它需要写入操作等

如果区分 user 的虚拟地址空间和 kernel 的虚拟地址空间:kernel 的64位虚拟地址的最高位是1,user 的64位虚拟地址的最高位是 0。

我们通常看到的程序的虚拟地址空间图中, user 的虚拟地址空间地址的高部分都被 stack 占用了,但是这通常是作者的简化,实际上地址的最高部分被 kernel 部分占用了,只不过一半不标识出来。

只有第一级页表可以区分user mode or kernel mode,因为只有第一级页表可以得到地址的最高位。

用户的虚拟地址空间中的 user 部分映射到程序的虚拟地址空间的user 部分,映射方法为:0x0 + addr,kernel mode 部分的映射方法为:0xffff + addr,user的虚拟地址空间的地址最高为2^48。0xffff正好是16位。

pgb 在 kernel 中只有唯一一份。

kernel 的虚拟地址从 2^47?

内核的地址翻译全局一致。

四、TLB

hardware acceleration:硬件加速

TLB is the cache of va2pa

我们可以把 cache 看作一个 key-value 库

posted @ 2024-07-31 21:23  光風霽月  阅读(4)  评论(0编辑  收藏  举报