【收藏】CSAPP深入理解计算机系统三万字长文解析

CSAPP深入理解计算机系统

文章目录

信息

信息存储

字长

每个计算机都有一个字长的属性,是指针数据的标称大小(虚拟地址宽度)

目前64位字长是主流,寻址能力达到了18EiB

注:1EiB = 1024PiB = 1024 * 1024TiB

C语言数据类型位宽

数据类型32位系统64位系统x86-64
char111
short222
int444
long488
float444
double888
long double--10/16
pointer488
字节序

字节序只是字节的顺序,与每个字节中位的顺序无关

小端序

Little endian (Intel)

低地址存放低位数据,高地址存放高位数据

大端序

Big endian (IBM, Sun Microsystem)

低地址存放高位数据,高地址存放低位数据

例如:0x1234567

Little endian 小端序

0x1000x1010x1020x103
67452301

Big endian 大端序

0x1000x1010x1020x103
01234567

注:字符串的表示与字节序无关,大小端兼容

位运算

位运算符
|Or位或
&And位与
~Not位非
^exclusive-Or位异或
逻辑运算
||logical or逻辑或
&&logical and逻辑与
!logical not逻辑非

短路效应

  • x && 5/x 避免除0运算
  • p && *p++ 避免空指针运算
  • 5 || x=y 赋值语句不会执行
移位运算

右移运算有逻辑移位和算数移位

逻辑移位:左侧补0

算数移位:左侧补原最高位值

对于无符号数,右移一定是逻辑的
对于有符号数,默认算数右移

优先级:位运算优先级最低,注意加括号

整数

C语言中,有符号数与无符号数混用,默认将有符号数作为无符号数处理

补码规则

非负:补码等于二进制数,位长不够补0

负:绝对值的二进制数逐位取反加1,符号位置1

表示范围

无符号数 0 ∼ 2 w − 1 0\sim 2^w-1 02w1

有符号数 − 2 w − 1 ∼ 2 w − 1 − 1 -2^{w-1}\sim 2^{w-1}-1 2w12w11

  • 等价性:无符号数和有符号数非负值编码相同
  • 唯一性:每个编码对应唯一整数,每个整数有唯一编码
  • 可以反向映射: U 2 B ( x ) = B 2 U − 1 ( x ) U2B(x)=B2U^{-1}(x) U2B(x)=B2U1(x), T 2 B ( x ) = B 2 T − 1 ( x ) T2B(x)=B2T^{-1}(x) T2B(x)=B2T1(x)

∣ T M i n ∣ = T M a x + 1 \left | T_{Min} \right | = T_{Max}+1 TMin=TMax+1

U M a x = 2 × T M a x + 1 U_{Max} = 2 \times T_{Max}+1 UMax=2×TMax+1

Value8163264
U M a x w UMax_w UMaxw0xFF0xFFFF0xFFFFFFFF0xFFFFFFFFFFFFFFFF
25565535 4 × 1 0 9 4 \times 10^{9} 4×109 1.8 × 1 0 19 1.8 \times 10^{19} 1.8×1019
T M i n w TMin_w TMinw0x800x80000x800000000x80000000000000
-128-32768 − 2 × 1 0 9 -2 \times 10^{9} 2×109 − 9 × 1 0 18 -9 \times 10^{18} 9×1018
T M a x w TMax_w TMaxw0x7F0x7FFF0x7FFFFFFF0x7FFFFFFFFFFFFFFF
12732767 2 × 1 0 9 2 \times 10^{9} 2×109 − 9 × 1 0 18 -9 \times 10^{18} 9×1018
-10xFF0xFFFF0xFFFFFFFF0xFFFFFFFFFFFFFFFF
00x000x00000x000000000x0000000000000000

扩展:无符号数零扩展,有符号数符号位扩展

截断:多余的位被直接丢弃

整数运算

加法

编码的二进制加法,然后按位长截断

判断 u + v = s u+v=s u+v=s 是否溢出

OF = (u<0 == v<0) && (u<0 != s<0)

相反数

减法运算就是加上它的相反数

计算相反数的结果是其二进制逐位取反加一 − b =   ∼ b + 1 -b =\ \sim b + 1 b= b+1

− 2 w − 1 -2^{w-1} 2w1 相反数是其本身,其二进制表示除第一位为 1,其余全是 0

乘法

两个 w w w 位数的乘法,结果是 2 w 2w 2w 位,否则无法精确表示结果

否则会发生截断,丢掉高位

short a, b;
int c;
c = a * b;

移位

用来实现除 2 n 2^{n} 2n

浮点数

IEEE754 规范

数字的表示形式:

( − 1 ) s × M × 2 E (-1)^s \times M \times 2^E (1)s×M×2E

  • 符号位s:决定数字正负
  • 尾数M:一个小数,通常在[1.0, 2.0)或[0.0, 1.0)
  • 阶码E:浮点数的权重,2的E次幂
编码
符号sexpfrac
单精度18-bits23-bits
双精度111-bits52-bits
规格化数

exp不等于全 0 或全 1

e x p ≠ 000 … 0 a n d e x p ≠ 111 … 1 exp \ne 000\dots 0 \quad and \quad exp\ne 111\dots 1 exp=0000andexp=1111

阶码

阶码是一个有偏置的指数

Exp: exp域无符号数编码值

E = E x p − B i a s E = Exp - Bias E=ExpBias

b i a s = 2 k − 1 − 1 bias = 2^{k-1} - 1 bias=2k11 k是exp的位宽

符号biasExpE = Exp - Bias
单精度1271 ~ 254-126 ~ 127
双精度10231 ~ 2046-1022 ~ 1023
尾数

尾数编码包含一个隐式前置的1,这个1始终存在,因此在frac中不需要包含

M = 1. x x x … x 2 M=1.xxx \dots x_2 M=1.xxxx2

x x x … x xxx\dots x xxxx 为frac域的各位的编码bits of frac

  • 最小值frac = 000…0 (M = 1.0)
  • 最大值frac = 111…1 (M = 2.0 - )
非规格化数

e x p = 000 … 0 exp = 000\dots 0 exp=0000

阶码

E = − B i a s + 1 E = - Bias + 1 E=Bias+1

floatdouble
E=-126E=-1022
尾数

尾数编码包含一个隐式前置的 0

M = 0. x x x … x 2 M=0.xxx \dots x_2 M=0.xxxx2

x x x … x xxx\dots x xxxx 为frac域的各位的编码bits of frac

表示0

e x p = 000 … 0 a n d f r a c = 000 … 0 exp = 000\dots 0 \quad and \quad frac = 000\dots 0 exp=0000andfrac=0000

非常接近0.0

e x p = 000 … 0 a n d f r a c ≠ 000 … 0 exp = 000\dots 0 \quad and \quad frac\ne 000\dots 0 exp=0000andfrac=0000

表示非常接近0.0的数字,是等间距的

特殊值

e x p = 111 … 1 exp = 111\dots 1 exp=1111

表示无穷∞

e x p = 111 … 1 a n d f r a c = 000 … 0 exp = 111\dots 1 \quad and \quad frac = 000\dots 0 exp=1111andfrac=0000

意味着运算出现了溢出,正向溢出或负向溢出,如: 1.0 0.0 = − 1.0 − 0.0 = + ∞ , − 1.0 0.0 = 1.0 − 0.0 = − ∞ \frac{1.0}{0.0}=\frac{-1.0}{-0.0}=+\infty,\quad\frac{-1.0}{0.0}=\frac{1.0}{-0.0}=-\infty 0.01.0=0.01.0=+,0.01.0=0.01.0=

表示NaN

e x p = 111 … 1 a n d f r a c ≠ 000 … 0 exp = 111\dots 1 \quad and \quad frac \ne 000\dots 0 exp=1111andfrac=0000

不是一个数字,表示数值无法确定,如 − 1 , ∞ − ∞ , ∞ × 0 \sqrt{-1}, \infty -\infty,\infty\times0 1 ,,×0

向偶数舍入

几种舍入模式

  • 向下舍入,舍入结果不大于实际结果
  • 向上取整,舍入结果不小于实际结果
  • 向0舍入,舍入结果向0的方向靠近
  • 向偶数舍入

浮点数运算默认向偶数舍入,其他的舍入模式都会统计偏差,如一组正数的总和将总被高估或低估

对于十进制数:当需要舍入的数字处于恰好5时,其后方无更小的有效位,向保证舍入后保留的最后一位是偶数的方向进行舍入,其他情况按照四舍五入计算

向偶数舍入说明
1.34999991.3四舍
1.35000011.4五入
1.35000001.4中间,向上舍入,偶数方向
1.45000001.4中间,向下舍入,偶数方向

对于二进制数:如果舍入部分为 1000 … 2 1000\dots_2 10002,则舍入方向为舍入后最低位为0

舍入结果舍入说明
10.0001110.00Down
10.001110.01Up
10.111011.00Up
10.101010.10Down
计算

基本思想

  • 先计算出精确的值
  • 将结果调整至目标精度(注意舍入、溢出)

( − 1 ) s 1 M 1 ⋅ 2 E 1 × ( − 1 ) s 2 M 2 ⋅ 2 E 2 (-1)^{s1}M_1\cdot 2^{E1} \times (-1)^{s2}M_2\cdot 2^{E2} (1)s1M12E1×(1)s2M22E2

( − 1 ) s 1 M 1 ⋅ 2 E 1 + ( − 1 ) s 2 M 2 ⋅ 2 E 2 (-1)^{s1}M_1\cdot 2^{E1} + (-1)^{s2}M_2\cdot 2^{E2} (1)s1M12E1+(1)s2M22E2

汇编

顺序执行

程序计数器PC,将要执行的下一条指令地址,即%rip

C代码中,内联汇编

指令
    操作码  源操作数, 目标操作数

操作数可以是:

  • 寄存器
  • 立即数
  • 内存寻址

立即数只可以作为源操作数
源操作数和目标操作数不可以都是内存寻址

寄存器
寄存器类型寄存器
参数%rdi; %rsi; %rdx; %rsx; %r8; %r9
被调用者保存%rbx; %rbp; %r12; %r13; %r14; %r15
调用者保存%r10; %r11
返回值%rax
栈指针%rsp

32位操作符,将目标64位寄存器高4字节置0

movq,只以32位补码立即数作源操作数,符号扩展为64位

movabsq,任意64位立即数作源操作数,以寄存器为目的

movz_ _零扩展,movs_ _符号扩展

不存在movzlq,因为movl就是这个作用

cltp寄存器符号扩展,指令无操作数,只作用于%eax%rax

栈向下增长,栈顶地址更低

pushq S%rsp减8后将S存入%rsp存储的地址

popq D%rsp存储地址的值赋给D,%rsp再加8

leaq,源操作数只能是内存地址或寄存器,目的数只能是寄存器

lea_只计算地址,不进行内存访问,有时候甚至计算的不是地址,只是将源操作数进行简单的加法和乘法运算存入目的数

计算指令

一些计算指令,都有b/w/l/q的不同版本:

	inc_ , dec_ , neg_ , not_
	add_ , sub_ , imul_ , xor_ , or_ , and_
	sal_ , shl_ , sar_ , shr_ 

对应的功能:

	++, --, -D, ~D
	+, -, *, ^, |, &
	左移,左移,算数右移,逻辑右移

关于减法:
目标操作数 - 源操作数
注意顺序

关于64位乘法:
第一种是 imul_,双操作数
第二种是单操作数,分为有符号 imulq和无符号 mulq
另一个参数存在 %rax(低64位),并用 %rdx(高64位)扩展为全128位

关于除法:
idivl单操作数,有符号除法
%rax(低64位)和 %rdx(高64位)中的128位作为被除数,操作数作为除数,商存储在 %rax,余数存储在 %rdx

cqto,符号扩展,将R[%rax]扩展为R[%rdx]:R[%rax]

控制

条件码

​ CF(carry flag),最高位进位,无符号操作溢出
​ ZF(zero flag),零标志,结果0则标志置1
​ SF(sign flag),符号标志,0正1负
​ OF(overflow flag),补码溢出,正溢出或负溢出
​ PF(parity flag),奇偶标志,0偶1奇

cmp_ S1, S2 考察S2-S1
test_ S1, S2 考察S1&S2
这两个指令不改变寄存器,只改变条件码
test_常被用来判断是否为0

testq %rax, %rax
je zero

访问条件码,通过set_ D指令

相等与否(equal,zero),负数与否(sign)

sete, setz ZF
setne, setnz ~ZF
sets SF
setns ~SF

有符号大于等于小于(greater,equal,less)
setg, setnle ~(SF^OF)&~ZF
setge,setnl ~(SF^OF)
setl, setnge SF^OF
setle, setng (SF^OF) | ZF

无符号超过相等低于(above,equal,below)
seta, setnbe ~CF & ~ZF
setae, setnb ~CF
setb, setnae CF
setbe, setna CF | ZF

跳转

直接跳转

jmp .L1

间接跳转

jmp *%rax,jmp *(%rax)

条件跳转
相等与否(equal,zero),负数与否(sign)
je, jz ZF
jne, jnz ~ZF
js SF
jns ~SF

有符号大于等于小于(greater,equal,less)
jg, jnle ~(SF^OF)&~ZF
jge, jnl ~(SF^OF)
jl, jnge SF^OF
jle, jng (SF^OF) | ZF

无符号超过相等低于(above,equal,below)
ja, jnbe ~CF & ~ZF
jae, jnb ~CF
jb, jnae CF
jbe, jna CF | ZF

注:条件跳转只能是直接跳转

跳转指令寻址:
PC相对寻址,PC为跳转指令下一条的地址,加上偏移量(编码为1/2/4字节),得到目标地址

条件控制实现条件分支

	t = test-expr;
	if(!t)
		goto false;
	//then-statement
	goto done;
false:
	//else-statement
done:

分支预测错误处罚
T a v g ( p ) = T O K + p T M P T_{avg}(p) = T_{OK} + pT_{MP} Tavg(p)=TOK+pTMP

条件传送
cmov__ S, R

源寄存器或内存地址S,目的寄存器R,S和R可以是16/32/64位长,不可以是8位

相等与否(equal,zero),负数与否(sign)
cmove, cmovz ZF
cmovne, cmovnz ~ZF
cmovs SF
cmovns ~SF

有符号大于等于小于(greater,equal,less)
cmovg, cmovnle ~(SF^OF)&~ZF
cmovge, cmovnl ~(SF^OF)
cmovl, cmovnge SF^OF
cmovle, cmovng (SF^OF) | ZF

无符号超过相等低于(above,equal,below)
cmova, cmovnbe ~CF & ~ZF
cmovae, cmovnb ~CF
cmovb, cmovnae CF
cmovbe, cmovna CF | ZF

条件传送实现条件分支
使用数据的条件转移,即算一个条件操作的两种结果,再根据条件是否满足从中选一

原始语句形式

v = test-expr ? then-expr : else-expr;

基于条件传送

v = then-expr;
ve = else-expr;
t = test-expr;
if(!t) v = ve;
循环

do-while循环
基本形式

do
	// body-statement
	while (test-expr);

条件和goto形式

loop:
	// body-statement
	t = test-expr;
if (t)
	goto loop;

while循环

基本形式

while (test-expr);
	// body-statement

方法一:jump to middle 跳转到中间

先跳转到结尾,执行初始的测试,再根据条件进入循环

	goto test;
loop:
	// body-statement
test:
	t = test-expr;
	if (t)
		goto loop;

方法二:guarded-do

先用条件分支,初始条件不成立就跳过循环,再把while循环变换成do-while循环

t = test-expr;
if (!t)
	goto done;
loop:
	//body-statement
	t = test-expr;
	if (t)
		goto loop;
done:

for循环

标准C语言形式

for (init-expr; test-expr; update-expr)
	// body-statement

转化成标准的while形式

init-expr;
while (test-expr) {
	// body-statement
	update-expr;
}

跳转到中间策略的goto代码

	init-expr;
	goto test;
loop:
	// body-statement
	update-expr;
test:
	t = test-expr;
	if (t)
		goto loop;

guarded-do策略的goto代码

	init-expr;
	t = test-expr;
	if (!t)
		goto done;
loop:
	// body-statement
	update-expr;
	t = test-expr;
	if (t)
		goto loop;
done:
switch语句

跳转表jump table,程序通过开关索引值来执行跳转表内的数组引用,确定跳转目标

应用情况:开关数量较多,值的范围跨度较小

C语言下switch

switch (n)
{
case 100:
case 102:
	// code
	break;
case 103:
	// code
	break;
case 104:
	//  code
	break;

扩展的C语言

	static void *jt [5] = {
		&&loc_A, &&loc_def, &&loc_A, &&loc_B, &&loc_C
	};
	unsigned long index = n - 100;

	if (index > 6)
		goto loc_def;
	goto *jt [index];

loc_A:
	// case 100, 102
	goto done;
loc_B:
	// case 103
	goto done;
loc_C:
	//case 104
	goto done;
loc_def:
	//def
done:
	// after all

过程

P调用Q,Q返回P

  1. 传递控制:进入Q前PC设为Q地址,返回P前PC设为P的下一条地址
  2. 传递数据:P向Q多个,Q返回P最多一个
  3. 分配和释放内存
栈帧

过程的栈帧,在栈上为过程分配的空间

转移控制

过程调用
call Label 直接调用
call *Operand 间接调用

从过程调用中返回
ret

注:callq和retq的q是反汇编产生的,用来表示是x86-64版本的调用和返回

执行call,将返回地址压入栈,并将PC设为跳转地址

数据传送

寄存器最多传递6个整形参数(整数或指针)按照顺序使用

bit123456
64%rdi%rsi%rdx%rcx%r8%r9
32%edi%esi%edx%ecx%r8d%r9d
16%di%si%dx%cx%r8w%r9w
8%dil%sil%dl%cl%r8b%r9b

参数大于6个:1~6存入对应寄存器,7~n

P调用Q

callee被调用者保存:%rbx,%rbp,%r12~%r15由Q来保存

caller调用者保存:除了栈指针%rsp以外其他所有寄存器,P来保存

变长栈帧

%rbp基指针(base pointer),帧指针(frame pointer)

数组
T A[N]

起始位置 x A x_{A} xA,首先在内存中分配一个 L ⋅ N L \cdot N LN字节的连续区域(L是数据类型T的大小),其次引入标识符A,可用来作为指向数组开头的指针 x A x_{A} xA

假设E是int型的数组,想计算E[i]

E的地址存在%rdx中,i存储在%rcx

movl (%rdx, %rcx, 4), %eax

指针运算

x p + L ⋅ i x_p+L \cdot i xp+Li

其中L是数据类型T的大小

单操作数操作符

&产生指针:给出该对象地址的指针

*产生间接引用指针:给出该地址处的值

嵌套数组

T D[R][C];

数组元素D[i][j]的内存地址是:

& D [ i ] [ j ] = x D + L ( C ⋅ i + j ) \& D[i][j]=x_D+L(C \cdot i+j) &D[i][j]=xD+L(Ci+j)

定长数组

#define N 16
typedef int fix_matrix[N][N]

变长数组

允许数组的维度是表达式,在数组被分配时才计算出来

int A[expr1][expr2]
int val_ele(long n, int A[n][n], long i, long j) {
    return A[i][j];
}
结构体

structure

(*rp).width
rp->width

两种表达方式等价,间接引用了这个指针

struct rec {
    int i;
    int j;
    int a[2];
    int *p;
}
0481216 20 24
i i i j j j a [ 0 ] a[0] a[0] a [ 1 ] a[1] a[1] p p p

r → i r→i ri 复制到 r → j r→j rj

movl (%rdi), %eax
movl %eax, 4(%rdi)

要产生一个指向结构内部对象的指针,只需要将结构的地址加上该字段的偏移量

联合体

union

用不同的字段引用相同的内存块

struct S {
    char c;
    int i[2];
	double v;
};
union U {
    char c;
    int i[2];
    double v;
};

下表展示了 SU 各字段的偏移量和完整大小

类型civ大小
S041624
U000

用途:

事先知道一个数据结构中两个不同字段的使用是互斥的,使用联合可以减小分配空间的总量;

用来访问不同数据类型的位模式,如以double存储,以unsigned long long读取

数据对齐

对齐原则:任何 K K K字节的基本对象的地址必须是 K K K的倍数

K K K类型
1char
2short
4int, float
8long, double, char*

指明全局数据所需的对齐的汇编代码命令

.align 8

对于包含structure的代码,编译器可能会在字段的分配中插入间隙,以保证每个结构元素都满它的对齐要求

struct S {
    int i;
    char c;
    int j;
};

编译器在 c c c j j j 字段中间插入一个3字节的间隙

i (0-3)c (4-7)j (8-12)
■■■■■□□□■■■■

结构的末尾也需要填充,来满足对齐要求

struct S {
    int i;
    int j;
    char c;
};

编译器在 c c c 字段结尾插入一个3字节的间隙

i (0-3)j (4-7)c (8-12)
■■■■■■■■■□□□

每个结构体有一个对齐要求K

K为结构体中所有元素中的最大对齐需求,结构体的初始地址和大小必须为K的整数倍

指针

每个指针都对应一种类型

将指针从一种类型转化为另一种类型,只改变指针的类型,不改变指针的值

(注:强制类型转换的优先级高于加法)

指针也可以指向函数

int fun(int x, int *p);

int (*fp)(int, int *);
fp = fun;

int y = 1;
int result = fp(3, &y);

函数指针的值是该函数机器代码表示中第一条指令的地址

缓冲区溢出

在栈中分配的字符数组,保存的字符串长度超过为数组分配的空间

echo:
	subq  $24, %rsp
	movq  %rsp, %rdi
	call  gets
	movq  %rsp, %rdi
	call  puts
	addq  $24, %rsp
	ret
输入字符串数量附加的被破坏的状态
0~7
9~23未被使用的栈空间
24~31返回地址
32+caller中保存的状态

对抗缓冲区溢出攻击

  1. 栈随机化

    程序开始时,在栈上分配一段0~n字节的随机大小的空间,程序不使用这段空间,但会导致程序每次执行时后续栈位置发生变化

    地址空间布局随机化

    Address-Space Layout Randomization

  2. 栈破坏检测

    在代码中加入栈保护者stack protector

    其思想是在栈帧中任何局部缓冲区与栈状态之间存储金丝雀值canary,也被称为哨兵值guard value

    该值是在程序每次运行时随机产生的

  3. 限制可执行代码区域

    rwx读、写、执行

处理器

指令集体系结构

指令集体系结构 ISA

Instruction-Set Architecture指一个处理器支持的指令和指令的字节级编码

CISC

复杂指令集计算机 Complex instruction set computer

x86家族:IA32 (x86-32), x86-64
System/360, PDP-11, VAX, Data General Nova
嵌入式处理器:Motorola 6800, Zilog Z80, 8051-family

以IA32为例

  1. 面向栈的指令集:
  • 使用栈传递参数,保存程序计数器
  • 显式的入栈和出栈指令
  1. 算术运算指令可以直接访问内存
addq %rax, 12(%rbx, %rcx, 8)
  • 包含了存储器的读和写
  • 包含了复杂的地址计算
  1. 条件码:可以通过算数逻辑运算的指令的副作用设置

  2. 设计理念:使用指令实现典型的任务

RISC

精简指令集计算机 Reduced instruction set computer

IBM/Freescale Power, ARM, MIPS, LoongISA, SPARC, RISC-V

以MIPS为例

  1. 更少的,更简单的指令
  • 需要花费更多的指令
  • 可在更小更快的硬件上执行
  • 对于嵌入式处理器,RISC更有意义
  1. 面向寄存器的指令集
  • 更多的寄存器(典型值32)
  • 用于传递参数,返回地址,临时数据
  1. 只有加载和存储指令可以访问内存
lw $t1, 0($s0)
sw $s0, 0($sp)
  1. 没有条件码
  • 测试指令将返回结果0/1写入寄存器

x86-64借鉴了很多RISC的特征,如更多的寄存器,用来传递参数

x86是CISC,但仅有一个CISC的壳,内部核心是RISC的

顺序执行CPU

阶段英文执行操作
取指fetch从指令存储器读取指令
译码decode读取寄存器
执行executeALU计算值和地址
访存memory从内存读数据 或 向内存写数据
写回write back写程序相关的寄存器
PC更新PC update更新程序计数器到下一条指令
SEQ 硬件结构

SEQ Sequential Logic:时序逻辑

ALU 算数/逻辑单元
CC 条件码寄存器
PC 程序计数器
Register 寄存器文件
内存:指令内存/数据内存

SEQ 时序

考虑两类存储设备:

  • 时钟寄存器(寄存器):PC,CC
  • 随机访问存储器(内存):虚拟内存,寄存器文件

一个时钟变化会引发一个经过组合逻辑的流,来执行整个指令

组合逻辑

组合逻辑(如ALU)不需要任何时序或控制,只要输入变化,值就通过逻辑门网络传播

读随机访问存储器,可看作组合逻辑。指令内存只有读操作

寄存器文件和内存的读操作可看作是组合逻辑,而写操作是由时钟控制的

状态单元

还有四个硬件需要对时序进行明确控制,这些单元通过一个时钟信号来控制

  • 程序计数器 PC
  • 条件码寄存器 CC
  • 数据内存
  • 寄存器文件

要控制处理器中活动的时序,只需要对寄存器和内存的时钟控制

在时钟上升开始下一周期时,处理器同时执行寄存器写和内存写

PC、CC、Register、数据内存,所有状态更新同时发生

时序过程

从不回读原则

处理器从来不需要为了完成一条指令的执行而去读由该指令更新了的状态

一个时钟周期

在进入一个时钟周期时,状态单元保持的是上一条指令更新过的状态,这些状态单元由上一条指令在这个时钟周期波形的上升段进行更新。此时组合逻辑尚未对变化了的状态作出反应

时钟周期开始时,地址载入状态单元中的程序计数器,这个更新由上一个指令执行。接着就会取出和处理当前这条指令。值沿着组合逻辑流动,包括读随机访问存储器。

在这个时钟周期的末尾,组合逻辑产生了新的条件码、程序寄存器的更新值、程序计数器的新值,此时组合逻辑已经根据当前这条指令被更新了,但是状态单元还是保持着上一条指令更新后的状态

当时钟上升开始下一个周期时,会更新程序计数器、条件码寄存器、寄存器文件、数据内存

SEQ 阶段实现

1. 取指阶段

P C ↓ p c 增加 ⟶ [ 指令内存 ] { B y t e 0 ⟶ [ S p l i t ] ⟶ i c o d e ⟶ i f u n   B y t e 1 − 9 ⟶ [ A l i g n ] ⟶ r A ⟶ r B ⟶ v a l C \begin{matrix}PC \\ \downarrow \\ pc增加 \end{matrix}\longrightarrow \begin{bmatrix} \\指令内存 \\ \\ \end{bmatrix}\left\{ \begin{matrix}Byte 0\longrightarrow\begin{bmatrix}\\Split\\\\\end{bmatrix}\begin{matrix}\longrightarrow icode \\ \\ \longrightarrow ifun\end{matrix}\\ \\ \ Byte 1-9 \longrightarrow \begin{bmatrix}\\Align\\\\\end{bmatrix} \begin{matrix} \longrightarrow rA \\ \longrightarrow rB \\ \longrightarrow valC \end{matrix} \end{matrix}\right. PCpc增加 指令内存 Byte0 Split icodeifun Byte19 Align rArBvalC

以PC作为第一个字节的地址(字节0),一次从内存取出10个字节

第一个字节由Splic单元分割,然后标号为icodeifun的控制逻辑模块计算出指令和功能码

当地址不合法时,产生信号imem_error,上述值被设置为nop指令

根据icode的值,可以计算三个一位的信号:

instr_valid 这个字节是否是合法的指令

need_regids 这个指令包括寄存器指示符字节吗

need_calC 这个指令包括常数吗

当指令地址越界时会产生的信号instr_validimem_error在访存阶段被用来产生状态码

2. 译码和写回阶段

设寄存器文件有四个端口,两个读两个写

指令字段译码,产生寄存器文件使用的四个地址的寄存器标识符(两个读,两个写)

3. 执行阶段

执行阶段包括算术/逻辑单元,执行阶段的第一步就是每条指令的ALU计算

4. 访存阶段

访存阶段的任务就是读或者写程序数据,两个控制块产生内存地址和内存输入的值,另外两个块产生表明应该执行读操作还是写操作的控制信号

5. 更新PC阶段

SEQ中的最后一个阶段会产生程序计数器的新值,依据指令的类型和是否要选择分支

流水线

流水线化的一个重要特征,就是提高了系统的吞吐量,也会轻微的增加延迟

吞吐量和延迟

假设组合逻辑需要 300ps,而加载寄存器需要 20ps,从头到尾执行一条指令所需要的时间称为延迟,在此系统中延迟为320 PS,也就是吞吐量的倒数

如下图的这样一个非流水线化的计算机硬件,每320ps的周期内,系统用300ps计算组合逻辑函数,20ps将结果存到输出寄存器中

→ [ 组合逻辑 A 300 p s ] → ∣ 寄 存 器 20 p s ∣ \to \begin{bmatrix}\\组合逻辑A\\300ps\\\\\end{bmatrix}\to \begin{vmatrix}寄\\存\\器\\20ps\end{vmatrix} 组合逻辑A300ps 20ps

时钟 ⊓ ⊔ \sqcap \sqcup ⊓ ⊔ \sqcap \sqcup ⊓ ⊔ \sqcap \sqcup
I1A
I2B
I3C

延迟

指令延迟 = 时钟周期 * 阶段数

描述单位ps,微微秒或皮秒,picosecond

上图非流水线的延迟:320ps

吞吐量

吞吐量描述单位 GIPS,每秒千兆条指令,即每秒十亿条指令

上图非流水线的吞吐量:

吞吐量 = 1 条指令 ( 20 + 300 ) p s ⋅ 1000 p s 1 n s ≈ 3.12   G I P S 吞吐量=\frac{1条指令}{(20+300)ps} \cdot \frac{1000ps}{1ns} \approx 3.12\ GIPS 吞吐量=(20+300)ps1条指令1ns1000ps3.12 GIPS

流水线示例

假设将系统执行的计算分成三个阶段,A, B, C每个阶段需要100ps,然后各个阶段之间放上流水线寄存器 pipeline register,这样每条指令都会按照三步经过这个系统,从头到尾需要三个完整的时钟周期

这样的时钟周期设置为 100+20=120ps,得到的吞吐量大约为8.33GIPS

→ [ 组合逻辑 A 100 p s ] → ∣ 寄 存 器 20 p s ∣ → [ 组合逻辑 B 100 p s ] → ∣ 寄 存 器 20 p s ∣ → [ 组合逻辑 C 100 p s ] → ∣ 寄 存 器 20 p s ∣ \to \begin{bmatrix}组合逻辑A\\100ps\end{bmatrix}\to \begin{vmatrix}寄\\存\\器\\20ps\end{vmatrix}\to \begin{bmatrix}组合逻辑B\\100ps\end{bmatrix}\to \begin{vmatrix}寄\\存\\器\\20ps\end{vmatrix}\to \begin{bmatrix}组合逻辑C\\100ps\end{bmatrix}\to \begin{vmatrix}寄\\存\\器\\20ps\end{vmatrix} [组合逻辑A100ps] 20ps [组合逻辑B100ps] 20ps [组合逻辑C100ps] 20ps

时钟 ⊓ ⊔ \sqcap \sqcup ⊓ ⊔ \sqcap \sqcup ⊓ ⊔ \sqcap \sqcup ⊓ ⊔ \sqcap \sqcup ⊓ ⊔ \sqcap \sqcup
I1ABC
I2ABC
I3ABC
时间线 0 ⟼ 0\longmapsto\quad 0 120 ⟼ 240 120\longmapsto\quad240 120240 240 ⟼ 360 240\longmapsto\quad360 240360 360 ⟼ 480 360 \longmapsto\quad480 360480$480 \longmapsto\quad 600 $
时钟 ⊓ ⊔ \sqcap \sqcup ⊓ ⊔ \sqcap \sqcup
I1BC
I2AB
I3A
时间线 120 ⟼ 120\longmapsto\quad 120 ⊢ 240 ⟼ 360 ⊣ \vdash240\longmapsto\quad\quad\quad360\dashv 240360
关注点 ↗ ①   \begin{matrix}\nearrow\\① &\ \end{matrix}   ↖ ↑ ↗ ② ③ ④ \begin{matrix}\nwarrow&&\uparrow&&\nearrow\\&②&③&④&\end{matrix}

① 阶段 A 中计算的指令 I2 的值已经达到第一个流水线寄存器的输入,但是该寄存器的状态和输出还保持为指令 I1 在阶段 A 中计算的值,指令 I1 在阶段 B 中计算的值已经达到第二个流水线寄存器的输入

② 当时钟上升时,这些输入被加载到流水线寄存器中,成为寄存器的输出

③ 阶段 A 的输入被设置成发起指令 I3 的计算,然后信号传播通过各个阶段的组合逻辑

④ 在时刻360之前,结果值到达流水线寄存器的输入,当时刻360时钟上升时,各条指令会前进,经过一个流水线阶段

减缓时钟不会影响流水线的行为信号传播到流水线寄存器的输入,但是直到时钟上升时才会改变寄存器的状态

局限性

不一致的划分

运行时钟的速率是由最慢的阶段的,延迟限制的而不同阶段的延迟从50ps到150ps不等

处理器中的某些硬件单元如ALU和内存是不能被划分成多个延迟较小的单元的,这就导致创建一组平衡的阶段非常困难

流水线过深,收益反而下降

由于通过流水线的寄存器的延迟,设置过多的阶段,吞吐量并不会有相应的增加

五阶段流水线

为实现流水线化,更新pc阶段将在一个时钟周期开始时执行

在命名系统中大写的前缀 F, D, E, M, W 指的是流水线寄存器,小写的前缀 f, d, e, m, w 是指流水线的阶段

  • F 保存程序计数器PC预测值
  • D 位于取值和译码阶段之间,保存关于最新取出的指令的信息,即将由译码阶段进行处理
  • E 位于译码和执行阶段之间,保存关于最新译码的指令和从寄存器文件读出的值的信息,即将由执行阶段进行处理
  • M 位于执行和访存阶段之间,保存最新的指令执行结果,即将由访存阶段进行处理,还保存关于用于处理条件转移的分支条件和分支目标的信息
  • W 位于访存阶段和反馈路径之间,反馈路径将计算出来的值提供给计算器文件写,而当完成 ret 指令时,他还要向PC选择逻辑提供返回地址
代码指令123456789T
movq $1, %raxI1FDEMW
movq $2, %rbxI2FDEMW
movq $3, %rcxI3FDEMW
movq $4, %rdxI4FDEMW
haltI5FDEMW
全流程图

∣ F ∣ → [ p r e d P C ] → S e l e c t P C ↗ ↘ [ 指令 内存 ] { s t a t i c o d e i f u n r A r B v a l C [ P C 增加 ] → v a l P v a l C , v a l P → < P r e d i c t P C > → [ p r e d P C ] ⏟ 取指 → ∣ D ∣ → [ 寄存器 文件 ] → ↙ v a l P [ S e l + F w d A ] → v a l A ⟶ [ F w d B ] → v a l B ⟶ s t a t , i c o d e , i f u n , v a l C ⟶ [ d s t E ] , [ d s t M ] ⟶ ⟨ d _ s r c A ⟩ , ⟨ d _ s r c B ⟩ → s r c A , s r c B → ⏟ 译码 → ∣ E ∣ → v a l C → v a l A → [ A L U A ] → v a l B → [ A L U B ] → [ A L U ] → [ C C ] → C n d ⟶ v a l E ⟶ s t a t , i c o d e , v a l A , d s t E , d s t M ⟶ [ S e l + F w d   A ] [ F w d B ] ← v a l E ⏟ 执行 → ∣ M ∣ → v a l E → v a l A → [ A d d r ] v a l A ⟶ } ⟶ [ 数据 内存 ] ↑ ↓ M e m . c o n t r o l → v a l M ⟶ s t a t , i c o d e , v a l E , d s t E , d s t M ⟶ [ S e l + F w d   A ] [ F w d B ] ← M _ v a l E , m _ v a l M ⟨ S e l e c t P C ⟩ ← M _ v a l A ⏟ 访存 → ∣ W ∣ → v a l E → [ W _ v a l E ] → v a l M → [ W _ v a l M ] → { [ 寄存器 文件 ] [ S e l + F w d A ] [ F w d B ] s t a t → [ s t a t ] ⏟ 写回 \begin{vmatrix}\\\\\\\\F\\\\\\\\\\\end{vmatrix}\to \underbrace{\begin{matrix} [ predPC ]\to Select PC\begin{matrix} \nearrow \\\searrow \end{matrix}\begin{matrix}\begin{bmatrix}指令\\内存\end{bmatrix}\left\{\begin{matrix}stat\\icode\\ifun\\rA\\rB\\valC\end{matrix}\right.\\\\\begin{bmatrix}PC\\增加\end{bmatrix}\to valP\\\\\end{matrix}\\valC,valP\to \left < PredictPC \right >\to \left [ predPC \right ]\\\\\end{matrix}}_{取指}\to \begin{vmatrix}\\\\\\\\D\\\\\\\\\\\end{vmatrix}\to \underbrace{\begin{matrix}\begin{bmatrix}\\\\寄存器\\文件 \\\\\\\end{bmatrix}\begin{matrix}\\\to \begin{matrix}\swarrow valP \\\begin{bmatrix}Sel+Fwd\\A\end{bmatrix}\end{matrix}\to valA \\\\\longrightarrow \begin{bmatrix}Fwd\\B\end{bmatrix}\to valB \\\\\end{matrix}\\\longrightarrow stat,icode,ifun,valC\longrightarrow \\\\\left [ dstE \right ] ,\left [ dstM \right ] \longrightarrow \\\left \langle d\_srcA \right \rangle ,\left \langle d\_srcB \right \rangle \to srcA,srcB\to \\\end{matrix}}_{译码}\to \begin{vmatrix}\\\\\\\\E\\\\\\\\\\\end{vmatrix}\to \underbrace{\begin{matrix}\\\begin{matrix}\\\\\begin{matrix}\begin{matrix}valC\to \\valA\to \end{matrix}\begin{bmatrix}ALU\\A\end{bmatrix}\to \\valB\to\begin{bmatrix}ALU\\B\end{bmatrix}\to \end{matrix}\begin{bmatrix}\\ALU\\\\\end{bmatrix}\begin{matrix}\to \begin{bmatrix}CC\end{bmatrix}\to Cnd\\\\\longrightarrow valE\end{matrix}\end{matrix}\\\\\longrightarrow stat,icode,valA,dstE,dstM\longrightarrow \\\\\left [ Sel+Fwd\ A \right ] \left [ Fwd B \right ] \gets valE\\ \\\end{matrix}}_{执行}\to \begin{vmatrix}\\\\\\\\M\\\\\\\\\\\end{vmatrix}\to \underbrace{\begin{matrix}\left.\begin{matrix}\\\begin{matrix}\begin{matrix}valE\to \\valA\to \end{matrix}\left [ Addr\right ] \\\\valA\longrightarrow \end{matrix}\end{matrix}\right\}\longrightarrow \begin{matrix}\begin{bmatrix}数据\\内存\end{bmatrix}\\\uparrow \downarrow \\Mem. control\end{matrix}\to valM\\\\\\\longrightarrow stat,icode,valE,dstE,dstM\longrightarrow \\\\\left [ Sel+Fwd\ A \right ] \left [ Fwd B \right ] \gets M\_valE,m\_valM\\ \left \langle SelectPC \right \rangle \gets M\_valA\\\\\end{matrix}}_{访存}\to \begin{vmatrix}\\\\\\\\W\\\\\\\\\\\end{vmatrix}\to \underbrace{\begin{matrix}\begin{matrix}\\\\\\valE\to \left [ W\_valE \right ] \to \\\\valM\to \left [ W\_valM \right ] \to \\\\\\\\\end{matrix}\left\{\begin{matrix}\begin{bmatrix}&寄存器&\\&文件&\\ \end{bmatrix}\\\\\begin{bmatrix}Sel+Fwd\\A \end{bmatrix}\begin{bmatrix}Fwd\\B \end{bmatrix}\\\end{matrix}\right.\\stat\to \left [ \quad stat\quad \right ] \\\\\\\end{matrix}}_{写回} F 取指 [predPC]SelectPC[指令内存] staticodeifunrArBvalC[PC增加]valPvalC,valPPredictPC[predPC] D 译码 寄存器文件 valP[Sel+FwdA]valA[FwdB]valBstat,icode,ifun,valC[dstE],[dstM]d_srcA,d_srcBsrcA,srcB E 执行 valCvalA[ALUA]valB[ALUB] ALU [CC]CndvalEstat,icode,valA,dstE,dstM[Sel+Fwd A][FwdB]valE M 访存 valEvalA[Addr]valA [数据内存]↑↓Mem.controlvalMstat,icode,valE,dstE,dstM[Sel+Fwd A][FwdB]M_valE,m_valMSelectPCM_valA W 写回 valE[W_valE]valM[W_valM] [寄存器文件][Sel+FwdA][FwdB]stat[stat]

PC预测

条件分支指令,指令通过执行阶段之后才能知道是否选择分支

ret指令,通过访存阶段才能确定返回地址

call和jmp(无条件转移),下一条指令地址就是指令中的常数字valC,对于其他指令就是valP

对于条件转移,预测选择了分支,则新PC是valC;若没选择分支,则新PC是valP

分支预测

  • 总是选择策略always taken,成功率60%
  • 从不选择策略never taken,NT,成功率40%
  • 反响选择、正向不选择backward taken, forward not-taken, BTFNT,成功率65%,即循环是由后向分支结束的,前向分支用于条件操作,循环通常会执行多次

栈的返回地址预测

高性能处理器在取址单元放入一个硬件栈,保存过程调用 指令产生的返回地址

流水线冒险

将流水线技术引入一个带反馈的系统,当相邻指令间存在相关时,会导致出现问题,这些相关有两种形式

  • 数据相关,下一条指令会用到这一条指令计算出的结果
  • 控制相关,一条指令要确定下一条指令的位置,例如执行跳转,调用或返回指令

冒险也可以分为两类,数据冒险控制冒险

解决方案

  • 暂停Stalling
  • 旁路Bypassing
  • 乱序执行out-of-order execution
暂停

暂停技术就是让一组指令阻塞在他们所处的阶段,而允许其他指令继续通过流水线

每次要把一条指令阻塞在译码阶段,就在执行阶段插入一个气泡,气泡就像一个自动产生的 nop 指令,不会改变寄存器、内存、条件码或程序状态

地址指令1234567891011
0x000movq $10,%rdxFDEMW
0x00amovq $3,%raxFDEMW
bubbleEMW
bubble|EMW
bubble||EMW
0x014addq %rdx,raxFDDDDEMW
0x016haltFFFFDEMW
转发

将结果值直接从一个流水线阶段传到较早阶段的技术称为数据转发data forwarding,简称转发,有时称为旁路bypassing,它使得指令能通过流水线而不需要任何暂停

一共有五个不同的转发源以及两个不同的转发目的

转发源:

  • e_valE
  • m_valM
  • M_valE
  • W_valM
  • W_valE

转发目的:

  • valA
  • valB

1. W→D

在译码阶段,从寄存器文件读入源操作数,但是对这些源寄存器的写有可能在写回阶段才能进行,与其暂停直到写完成,不如简单地将写值传到流水线寄存器 E 作为原操作数

如下表指令实现了:

[ D _ v a l A ] [ D _ v a l B ] ⟵ W _ v a l E \begin{matrix}\left [ D\_valA \right ] \\\left [ D\_valB \right ]\end{matrix} \longleftarrow W\_valE [D_valA][D_valB]W_valE

地址指令12345678910
0x000movq $10,%rdxFDEMW
0x00amovq $3,%raxFDEMW
0x014nop|
0x015nop
0x016addq %rdx,raxFDEMW
0x018haltFDEMW

2. W,M→D

当访存阶段中,有对寄存器未进行的写时,也可以使用数据转发

如下表指令实现了:

[ D _ v a l A ] [ D _ v a l B ] ⟵ W _ v a l E M _ v a l E \begin{matrix}\left [ D\_valA \right ] \\\left [ D\_valB \right ]\end{matrix} \longleftarrow \begin{matrix}W\_valE\\M\_valE \end{matrix} [D_valA][D_valB]W_valEM_valE

地址指令123456789
0x000movq $10,%rdxFDEMW
0x00amovq $3,%raxFDEMW
0x014nop
0x015addq %rdx,raxFDEMW
0x017haltFDEMW

3. M,E→D

为充分利用数据转发技术,还可以将新计算出来的值从执行阶段转到译码阶段

如下表指令实现了:

[ D _ v a l A ] [ D _ v a l B ] ⟵ M _ v a l E e _ v a l E \begin{matrix}\left [ D\_valA \right ] \\\left [ D\_valB \right ]\end{matrix} \longleftarrow \begin{matrix}M\_valE\\e\_valE \end{matrix} [D_valA][D_valB]M_valEe_valE

地址指令12345678
0x000movq $10,%rdxFDE|M|W
0x00amovq $3,%raxFD|E|MW
0x014addq %rdx,raxF|D|EMW
0x016haltFDEMW
加载互锁

有一类数据冒险不能单纯用转发来解决,因为内存读在流水线发生的比较晚。加载使用冒险其中一条指令从内存中读出寄存器的值,而下一条指令需要该值作为源操作数

可以将暂停和转发结合起来避免加载使用数据冒险

地址指令123456789101112
0x000movq $128,%rdxFDEMW
0x00amovq $3,%rcxFDEMW
0x014movq %rcx,0(%rdx)FDEMW
0x01emovq $10,%rbxFDEMW
0x028movq 0(%rdx),%raxFDEMW
bubbleEMW
0x032addq %rbx,%raxFDEMW
0x034haltFDEMW

这种使用暂停来处理,加载使用冒险的方法称为加载互锁 load interlock,加载互锁和转发技术结合起来,足以处理所有可能类型的数据冒险

只有加载互锁会降低流水线的吞吐量

避免控制冒险

当处理器无法根据处于取指阶段的当前指令来确定下一条指令的地址时,就会出现控制冒险

控制冒险只会发生在ret指令和跳转指令,而跳转指令只有在条件跳转方向预测错误时,才会造成麻烦

1. ret 指令处理

当 ret 指令经过译码执行和访存阶段时,流水线应该暂停,在处理过程中插入三个气泡,一旦 ret 指令到达写回阶段,PC选择逻辑就会选择返回地址作为指令的取指地址,然后取指阶段就会取出位于返回点处的 movq 指令

地址指令1234567891011
0x000movq Stack,%rspFDEMW
0x00acall procFDEMW
0x020retFDEMW
bubbleFDEMW
bubbleFDEMW
bubbleFDEMW
0x013movq $10,%rdxFDEMW

2. 预测错误的分支指令处理

暂停和向流水线中插入气泡的技术,可以动态调整流水线的流程

如下表,指令按照他们进入流水线的顺序列出,而不是按照他们出现在程序中的顺序,因为跳转指令会选择分支

  • 周期3 中会取出位于跳转目标处的指令,而周期4 会取出该指令后的那条指令
  • 周期4 分支逻辑发现不应该选择分支之前已经取出了两条指令,这两条指令不该继续执行
  • 此时,两条错误指令并未到执行阶段,因此没有发生程序员可见的状态改变(如执行阶段指令会改变条件码)
  • 因此,需要在下个周期向译码和执行阶段插入气泡,并同时取出跳转后面的那条指令,这样可以取消两条预测错误的指令,有时也称为指令排除 instruction squashing
地址指令12345678910
0x000xorq %rax,%raxFDEMW
0x002jne target # Not takenFDEMW
0x016movl $2,%rdx # TargetFD
bubbleEMW
0x020movl $3,%rbx # Target+1F
bubbleDEMW
0x00bmovq $1,%rax # Fall throughFDEMW
0x015haltFDEMW
性能分析

CPI,即每指令周期数Cycles Per Instruction

这种衡量值是流水线平均吞吐量的倒数,不过时间单位是时钟周期

符号英文中文解释
C P I CPI CPICycles Per Instruction每指令周期数流水线平均吞吐量的倒数,单位时钟周期
C i C_i Ci执行指令数
C b C_b Cb插入气泡数
l p lp lpload penalty加载处罚加载使用冒险造成暂停时插入气泡的平均数
m p mp mpmispredicted branch penalty预测错误分支处罚预测错误,取消指令时插入气泡的平均数
r p rp rpreturn penalty返回处罚RET指令造成暂停时插入气泡的平均数
CPI 计算

C P I = C i + C b C i = 1.0 + C b C i CPI=\frac{C_i+C_b}{C_i}=1.0+\frac{C_b}{C_i} CPI=CiCi+Cb=1.0+CiCb

C P I = 1.0 + l p + m p + r p CPI=1.0+lp+mp+rp CPI=1.0+lp+mp+rp

例:用以下这组频率计算CPI

原因名称指令频率条件频率气泡乘积
加载/使用 l p lp lp0.250.2010.05
预测错误 m p mp mp0.200.4020.16
返回 r p rp rp0.021.0030.06
总处罚CPI
0.271.27

链接

程序的诞生

编译器驱动程序

语言预处理器,编译器,汇编器,链接器

过程
  1. 预处理

    C预处理器(cpp)将C的源程序main.c翻译成一个ASCII码的中间文件main.i

cpp [other arguments] main.c main.i
  1. 编译

    C编译器(ccl)将main.i翻译成一个ASCII汇编语言文件main.s

	ccl main.i -Og [other arguments] -o main.s
  1. 汇编

    汇编器(as)将main.s翻译成一个可重定位目标文件 (relocatable object file)main.o

    as [other arguments] -o main.o main.s
  1. 链接

    运行链接器程序ld,将main.o和其他可重定位目标文件(例如other.o)以及一些必要的系统目标文件组合起来,创建一个可执行目标文件 (executable object file)

    ld -o prog [system object files and args] main.o other.o
  1. 运行
    linux> ./prog
静态链接

符号解析

目标文件定义和引用符号,每个符号对应一个函数,全局变量,静态变量

符号解析的目的是将每个符号引用和一个符号定义关联起来

重定位

编译器和汇编器生成的代码和数据节地址从0开始,链接器重定位这些节,就是把每个符号定义和一个内存位置关联起来,然后修改所有对这些符号的引用,使得他们指向这个内存位置

  1. 静态变量就是C语言中用 static 属性声明的变量,只在本文件中起作用

  2. 函数的局部变量不被考虑,它们只存在于栈中

目标文件
  1. 可重定位目标文件

    可在编译时与其他可重定位目标文件合并,创建一个可执行目标文件

  2. 可执行目标文件

    可直接被复制到内存并执行

  3. 共享目标文件

    可以在加载或者运行时被动态的加载进内存并链接

可重定位目标文件
介绍
ELF头用于描述系统字的大小和字节顺序的16字节,以及帮助链接器语法分析和解释目标文件的信息
.text已编译程序的机器代码
.rodataread only只读数据,如printf格式串和跳转表
.data已初始化的全局变量和静态C变量
.bssBlock Storage Start未初始化的和被初始化为0的全局变量和静态C变量,可记忆为Better Save Space
.symtab符号表,存放程序中定义和引用的函数和全局变量信息
.rel.text.text节中位置的列表,存放需要在可执行目标文件中被修改的指令地址信息
.rel.data被模块引用的全局变量的重定位信息,在合并后的可执行文件中需要修改的数据所在的地址
.debug调试符号表,-g生成,包含:局部变量和类型定义,程序中定义和引用的全局变量,原始C文件
.lineC源程序行号和.text机器指令的映射,-g生成
.strtab字符串表,就是以null结尾的字符串序列,包括.symtab.debug节中的符号表,以及节头部中的节名字
节头部表描述不同节的位置和大小
可执行目标文件
介绍读写
ELF头描述文件的总体格式,以及程序的入口点entry point只读
段头部表将连续的文件节映射到运行时内存段只读
.init一个叫_init的小函数,程序的初始化代码会调用只读
.text同上,机器代码只读
.rodata同上,只读数据只读
.data同上,初始化数据读/写
.bss同上,未初始化及赋0读/写
.symtab同上不加载
.debug同上不加载
.line同上不加载
.strtab同上不加载
节头部表描述目标文件的节不加载

注:可执行目标文件不再需要.rel节,因为可执行文件是完全链接的,即已被重定位的

Linux x86-64 运行时内存映像

内存地址内存映像备注
2 48 − 1 2^{48}-1 2481内核内存 ↑ \uparrow 对用户代码不可见的内存
↑ \uparrow 用户栈(运行时创建) ↙ % r s p \swarrow \quad \%rsp %rsp
↓ ↑ \begin{matrix} \downarrow \\ \uparrow \end{matrix}
共享库的内存映射区域
  ↑ \begin{matrix} \ \\ \uparrow \end{matrix}  
运行时堆(malloc) ↖ b r k \nwarrow \quad brk brk
↑ \uparrow 读\写段(.data, .bss)从可执行文件中加载
0x400000只读代码段(.init, .text, .rodata)从可执行文件中加载
0

符号解析

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

全局符号

  • 由模块m定义的并能被其他模块所引用

  • 例如:非static的C函数和非static的全局变量

外部符号

  • 由其他模块定义并被模块m中引用的全局符号

局部符号

  • 只能被模块m定义和引用的符号

  • 局部符号不是局部变量

  • 例如:使用static关键字修饰的C函数和变量

局部非static的C变量:存储在栈中

局部static的C变量:存储在.bss或.data中

int f() {
    static int x = 0;
    return x;
}
int g() {
    static int x = 1;
    return x;
}
  • 编译器为每个x在:data节中分配空问

  • 使用唯一的名称在符号表中创建局部符号,例如:x.1和x.2

解析多重定义

强符号

函数和已初始化的全局变量

弱符号

未初始化的全局变量

规则

  1. 不允许有多个重名的强符号
  2. 如果有一个强符号和多个弱符号同名,对弱符号的引用解析为强符号
  3. 如果多个弱符号同名,任意选择之一

例如

  • 链接错误:两个同名强符号 (p1)
//file1.c
    int x;
    p1 () {}

//file2.c
    p1 () {}   
  • 引用x将指向相同的未初始化int型变量
//file1.c
    int x;
    p1 () {}
    
//file2.c
    int x;
    p2 () {}
  • 在p2中向x中写入值,可能会覆盖y
//file1.c
    int x; int y;
    p1 () {}

//file2.c
    double x;
    p2 () {}
  • 在p2中向x中写入值将会覆盖y
//file1.c
    int x = 7;
    int y = 5;
    p1 () {}

//file2.c
    double x;
    p2 () {}
  • 对x的引用都指向相同的已初始化变量
//file1.c
    int x = 7; 
    p1 () {}
    
//file2.c
    int x;
    p2() {}

噩梦场景:两个相同的弱符号结构体,由不同的编译器使用不同的对齐规则进行编译

重定位

链接器一旦完成了符号解析,就将代码中的每个符号引用和正好一个符号定义关联起来,此时就可以开始重定位步骤了。重定位主要有两步:

  1. 重定位节和符号定义

链接器将所有同类型的节合并为同一类型的新的聚合节,然后链接器将运行时内存地址赋值给新的聚合节、输入模块定义的每个节、输入模块定义的每个符号

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

  1. 重定位节中的符号引用

链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址

执行这一步,链接器依赖可重定位目标模块中称为重定位条目relocation entry的数据结构

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

typedef struct {
    long offset;
    long type:32,
        symbol:32;
    long addend;
} Elf64_Rela;

offset 需要被修改的引用的节偏移
symbol 被修改的引用应该指向的符号
type 告知连接器如何修改新的引用,ELF定义了32种,其中最基本两种如下:

  1. R_X86_64_PC32: 重定位一个使用32位PC相对地址的引用
  2. R_X86_64_32: 重定位一个使用32位绝对地址的引用

addend 有符号常数,对被修改引用的值做偏移调整

动态链接共享库

KaTeX parse error: Undefined control sequence: \hfill at position 49: …}\begin{matrix}\̲h̲f̲i̲l̲l̲ ̲main.c\longrigh…

库打桩技术

库打桩

library interpositioning 允许截获对共享库函数的调用,取而代之执行自己的代码

使用打桩机制,可以追踪某个特殊库函数的调用次数,验证和追踪它的输入输出值,甚至替换成一个完全不同的实现

打桩可以发生在编译时链接时、或当程序被加载和执行的运行时

基本思想

  1. 给定一个需要打桩的目标函数
  2. 创建一个包装函数,它的原型与目标函数完全一样
  3. 使用打桩机制,欺骗系统调用包装函数而非目标函数

异常控制流

控制转移

假设程序计数器按照 a 0 , a 1 , … , a n − 1 a_0, a_1, \dots,a_{n-1} a0,a1,,an1,每次从 a k a_k ak a k + 1 a_{k+1} ak+1的过渡称为控制转移control transfer

控制流

这样的控制转移序列成为处理器的控制流flow of control control flow

异常控制流 ECF

现代系统通过使控制流发生突变来对系统状态的变化做出反应,一般把这种突变称为异常控制流Exceptional Control Flow

  1. 硬件层,硬件检测到的事件会触发控制,突然转移到异常处理程序
  2. 操作系统层,内核通过上下文切换,将控制从一个用户进程转移到另一个用户进程
  3. 应用层,一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到它的一个信号处理程序,一个程序可以通过回避通常的栈规则,并执行到其他函数中任意位置的非本地跳转来对错误做出反应

异常

exception异常是控制流中的突变,用来响应处理器状态中的某些变化,一部分由硬件实现,一部分由操作系统实现

事件

event处理器状态变化

假设事件发生时的指令是 I c u r r I_{curr} Icurr,下一条指令是 I n e x t I_{next} Inext

异常号

exception number系统中可能的每种类型的异常都被分配了一个唯一的非负整数的异常号

1. 处理器的设计者分配的

  • 零除
  • 缺页
  • 内存访问违例
  • 断点
  • 算术运算溢出

2. 操作系统内核设计者分配的

  • 系统调用
  • 来自外部的 I/O 设备的信号
异常表
序号内容
0处理异常程序 0 的代码
1处理异常程序 1 的代码
2处理异常程序 2 的代码
n − 1 n-1 n1处理异常程序 n-1 的代码

操作系统启动时,操作系统分配和初始化一张称为异常表的跳转表

表目 k k k 包含异常 k k k 的处理程序的地址,运行时处理器检测到了发生一个事件,并确定了相应的异常号 k k k ,随后处理器触发异常方法是执行间接的过程调用,通过异常表的表目 k k k 转到相应的处理程序,异常表的起始地址放在异常表基址寄存器 exception table base register的特殊CPU寄存器里

当处理器检测到有事件发生时,会通过异常表进行一个间接过程,调用到专门设计用来处理这类事件的操作系统子程序,异常处理程序完成处理后,根据引起异常的事件类型选择:

  • 处理程序将控制返回给当前指令 I c u r r I_{curr} Icurr,即当前事件发生时正在执行的指令
  • 处理程序将控制返回给 I n e x t I_{next} Inext,如果没有发生异常,将继续执行下一条指令
  • 处理程序终止被中断的程序
异常类型
类别原因异步/同步返回行为
中断来自的 I/O 设备的信号异步总是返回到下一条指令
陷阱有意的异常同步总是返回到下一条指令
故障潜在可恢复的错误同步可能返回到当前指令
终止不可恢复的错误同步不会返回

异步异常

异步异常是由处理器外部的I/O设备中的事件产生的

同步异常

同步异常是执行的一条指令的直接产物

同步中断是执行当前指令的结果,把这类指令称作故障指令

中断

中断是异步发生的,是来自处理器外部的 I/O 设备的信号的结果,硬件中断不是任何一条专门的指令造成的,硬件中断的异常处理程序常常称作中断处理程序 interrupt handler

I/O设备通过向处理器芯片上的一个引脚发信号,并将异常号放在系统总线上来触发中断,这个异常号标识了引起中断的设备

陷阱和系统调用

陷阱是有意的异常,是执行一条指令的结果,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用

用户程序向内核请求服务,例如:

  • 读文件 read
  • 创建新进程 fork
  • 加载新程序 execve
  • 终止当前进程 exit

处理器提供一个特殊的 syscall n 指令,用户程序想要请求服务 n 时执行这条指令,会导致一个到异常处理程序的陷阱,该程序解析参数并调用适当的内核程序

普通的函数运行在用户模式中,限制了函数可以执行的指令类型,只能访问与调用函数相同的站系统,调用运行在内核模式中,允许系统调用执行特权指令并访问定义在内核中的栈

故障

故障由错误情况引起,可能能够被故障处理程序修正

如果故障处理程序能够修正,错误情况就将控制返回到引起故障的指令,从而重新执行它

否则,处理程序返回到内核中的 abort 例程,终止引起故障的应用程序

例如缺页异常,当指令引用一个虚拟地址,而其物理页面不在内存中,因此必须从磁盘中取出时就会发生缺页故障

终止

终止是不可恢复的致命错误造成的结果,通常是一些硬件错误

例如 DRAM 或者 SRAM 位被损坏时发生的奇偶错误,处理程序将控制返回给一个 abort 例程,该例程会终止这个应用程序

x86-64系统中异常示例

异常号描述异常类别
0除法错误故障
13一般保护故障故障
14缺页故障
18机器检查终止
32~255操作系统定义的异常中断或陷阱

进程

进程的经典定义是一个执行中的程序实例

异常是允许操作系统内核提供进程概念的基本构造块

  • 一个独立的逻辑控制流,提供一种程序独占使用处理器的假象,

  • 一个私有的地址空间,提供程序独占内存系统的假象

上下文 context

系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的,包括:

  • 存放在内存中的程序和代码数据
  • 通用目的寄存器的内容
  • 程序计数器
  • 环境变量
  • 打开文件描述符的集合
逻辑控制流

进程为每个程序提供了一种假象,好像程序在独占的使用处理器

每个程序的程序计数器PC值的序列叫做逻辑控制流,简称逻辑流

进程轮流使用处理器,每个进程执行它的流的一部分,然后被抢占 preempted ,暂时挂起,轮到其他进程

并发流

一个逻辑流的执行在时间上,与另一个流重叠,称为并发流 concurrent flow ,这两个流被称为并发的运行,更准确的说是流X与Y互相并发

多个流并发的执行的一般现象被称为并发 concurrency

一个进程和其他进程轮流运行的概念,称为多任务 multitasking

一个进程执行它的控制流的一部分的每一段时间叫做时间片 time slice ,因此多任务也叫做时间分片 time slicing

并行流

并发流可以运行在同一个处理器上,如果两个流并发的运行在不同的处理器或者计算机上,则称之为并行流 parallel flow ,他们并行的运行,且并行的执行

用户模式和内核模式

处理器通常使用某个控制寄存器中的一个模式位,来限制一个应用可以执行的指令及它可访问的地址空间范围

该寄存器描述了进程当前享有的特权,当设置了模式位时,进程就运行在内核模式中,有时叫超级用户模式

没有设置模式位时,进程就运行在用户模式中

上下文切换

操作系统内核使用一种称为上下文切换 context switch 的较高层形式的异常控制流来实现多任务

内核为每一个进程维持一个上下文 context ,上下文就是内核重新启动一个被抢占的进程所需的状态,由一些对象的值组成:

  • 通用目的寄存器
  • 浮点寄存器
  • 程序计数器
  • 用户栈
  • 状态寄存器
  • 内核栈
  • 各种内核数据结构
    • 页表
    • 进程表
    • 包含进程已打开文件的信息的文件表

内核可以决定抢占当前进程并重新开始一个先前被抢占了的进程,这种决策就叫做调度 scheduling ,是由内核中称为调度器 scheduler 的代码处理的

调度器使用上下文切换机制来将控制转移到新的进程:

  • 保存当前进程的上下文
  • 恢复某个先前被抢占的进程被保存的上下文
  • 将控制传递给这个新恢复的进程

引发上下文切换的原因:

  • 系统调用,因为等待某个事件发生而阻塞,如read系统调用需要访问磁盘
  • sleep
  • 系统调用
  • 中断
进程A进程B运行模式
↓ \downarrow 用户模式
↘ \searrow 内核模式上下文切换
↓ \downarrow 用户模式
↙ \swarrow 内核模式上下文切换
↓ \downarrow 用户模式

存储器

存储技术

随机访问存储器

RAM Random-Access Memory

​ SRAM 静态RAM,每个位存储在双稳态存储器单元里,六个晶体管电路

​ 应用在高速缓存存储器

​ DRAM 动态RAM,每个位存储为对一个电容的充电,对干扰敏感,周期性刷新

​ 应用在主存,帧缓冲区

传统DRAM

DRAM中的位被分成 d d d 个超单元,每个超单元由 w w w 个 DRAM 单元组成, d × w d \times w d×w 的 DRAM 存储了 d w dw dw 位信息。

超单元被组织成了 r 行 c 列的阵列, r c = d rc = d rc=d ,每个超单元有坐标 ( i , j ) (i, j) (i,j)

内存控制电路一次可以传送 w w w 位,为读取坐标 ( i , j ) (i, j) (i,j),内存请求坐标 ( i , j ) (i, j) (i,j) i i i j j j,行地址 i i i 称为RAS请求Row Access Strobe,列地址 j j j 称为CAS请求Column Access Strobe,两者共享 DRAM 地址引脚

DRAM 内有一个内部行缓冲区,内存控制器先发出行坐标,DRAM 将整行超单元复制到内部行缓冲区,内存控制器再发出列坐标,DRAM 再从行缓冲区复制出超单元中的 w w w

多个 DRAM并联读取数据,

非易失性存储器

ROM Read-Only Memory

PROM Programmable ROM

EPROM Erasable Programmable ROM

EEPROM Electrically Erasable PROM

SSD Silid State Disk

磁盘存储

盘片platter,面surface,磁道track,扇区sector,间隙gap,柱面cylinder

寻道时间 seek time

读/写头定位到包含目标位置的扇区磁道上,移动传动臂时间

T s e e k T_{seek} Tseek

旋转时间 rotational latency

目标扇区第一个位旋转到读/写头下

T m a x   r o t a t i o n = 1 R P M × 60 s 1 m i n T_{max\ rotation} = \frac{1}{RPM} \times \frac{60s}{1min} Tmax rotation=RPM1×1min60s

T a v g   r o t a t i o n = 1 2 × T m a x   r o t a t i o n T_{avg\ rotation} = \frac{1}{2} \times T_{max\ rotation} Tavg rotation=21×Tmax rotation

传送时间 transfer time

依赖于旋转速度和每条磁道的扇区数目

T a v g   t r a n s f e r = 1 R P M × 1 a v g   s e c t o r   p e r   t r a c k × 60 s 1 m i n T_{avg\ transfer} = \frac{1}{RPM} \times \frac{1}{avg\ sector\ per\ track} \times \frac{60s}{1min} Tavg transfer=RPM1×avg sector per track1×1min60s

访问时间 access time

T a c c e s s = T a v g   s e e k + T a v g   r o t a t i o n + T a v g   t r a n s f e r T_{access} = T_{avg\ seek} + T_{avg\ rotation} + T_{avg\ transfer} Taccess=Tavg seek+Tavg rotation+Tavg transfer

直接内存访问

DMA Direct Memory Access

磁盘控制器收到来自CPU的读命令后,将逻辑块号翻译成扇区地址,读扇区内容后直接传给主存,不需要CPU干涉,称为DMA传送DMA transfer

DMA传送完成后,磁盘控制器给CPU发送中断信号

局部性

局部性原理

程序倾向于引用邻近于其他最近引用过的数据项,或最近引用过的数据项本身

时间局部性:同一内存位置较短时间内多次引用

空间局部性:被引用内存位置的附近短时间内被引用

顺序引用模式:步长为1的引用模式(相对于元素大小)

另外,步长为k的引用模式每隔k个元素进行访问

取指令的局部性

循环的时间和空间局部性更好,循环体越小,迭代次数越多,局部性越好

存储器层次结构

层次存储器数据来源
L0寄存器L1→CPU
L1高速缓存 L1(SRAM)L2→L1
L2高速缓存 L2(SRAM)L3→L2
L3高速缓存 L3(SRAM)L4→L3
L4主存 (DRAM)L5→L4
L5本地二级存储L6→L5
L6远程二级存储L6
缓存

高速缓存 cache 读作cash,使用高速缓存的过程称为缓存 caching 读作cashing

k + 1 k+1 k+1 层存储器被划分成连续的数据对象组块chunk ,称为块block,地址或名字唯一

k k k 层被划分成较少的块的集合,每个块大小与第 k + 1 k+1 k+1 层相同,包含第 k + 1 k+1 k+1 层块的一个子集的副本

缓存命中

程序需要第 k + 1 k+1 k+1 层的数据 d d d ,要先在第 k k k 层寻找,找到了就是缓存命中(cache hit)

缓存不命中

k k k 层缓存从第 k + 1 k+1 k+1 层取出 d d d ,若第 k k k 层满了就覆盖一个现存块,即 “替换” 或 “驱逐” ,被替换的叫 “牺牲块”,决定的叫 “替换策略”

缓存不命中种类

  1. 强制性不命中(冷不命中):第 k k k 层缓存为空

  2. 冲突不命中:第 k + 1 k+1 k+1 层的某块限制放置在第 k k k 层的部分块中,这种限制性的放置策略导致的不命中

  3. 容量不命中:工作集大小超过缓存大小(缓存太小了)

数据与指令

只保存指令的高速缓存: i − c a c h e i-cache icache

只保存数据的高速缓存: d − c a c h e d-cache dcache

指令和数据都保存的:统一的高速缓存(unified cache)

i-cache通常是只读的,因此较简单

例如某多核处理器,每个核有私有的L1 i-cache,L1 d-cache,L2统一的高速缓存;所有核共享L3统一的高速缓存;所有的SRAM高速缓存存储器都在CPU芯片上

性能

不命中率

  m i s s   r a t e = 不命中数量 引用数量 \ miss\ rate =\frac{不命中数量}{引用数量}  miss rate=引用数量不命中数量

命中率

  h i t   r a t e = 1 − 不命中率 \ hit\ rate=1-不命中率  hit rate=1不命中率

命中时间   h i t   t i m e \ hit\ time  hit time

对L1来说数量级是几个时钟周期

不命中处罚   m i s s   p e n a l t y \ miss\ penalty  miss penalty

通常L1不命中后,从L2中得到服务处罚10个时钟周期,从L3是50个周期,从主存是200个周期

平均内存访问时间

( 1 − 未命中率 ) × 命中时间 + 未命中率 × 未命中惩罚 (1 - 未命中率) \times 命中时间 + 未命中率 \times 未命中惩罚 (1未命中率)×命中时间+未命中率×未命中惩罚

缓存友好的代码

可以让程序在常见的场景下更快的运行

  • 关注核心函数的内部循环

减少内部循环中的缓存末命中

  • 对变量更好的重复利用(时间局部性)

  • 尽量使用步长为1的模式访问存储器(空间局部性)

核心思想:通过理解高速缓存的工作机制,量化我们对局部性 原理的定性认知

高速缓存存储器

设每个存储器地址有 m m m 位,形成 M = 2 m M=2^m M=2m 个不同的地址

高速缓存,被组织成有 S = 2 s S=2^s S=2s 个高速缓存组的数组

每个组包含 E E E 个高速缓存行

每行由一个 B = 2 b B=2^b B=2b 字节的数据块组成

现代处理器(如Core i7)高数缓存块包含64个字节,L1和L2是8路组相联,L3是16路组相联的

高速缓存大小

C 数据字节,(S, E, B, m) 的通用组织

C = B × E × S C=B \times E \times S C=B×E×S

S = 2 s S=2^s S=2s组号1个有效位t个标记位高速缓存块 B = 2 b B=2^b B=2b 字节每组E行
组0有效标记0, 1, 2, … , B-1 0 0 0
有效标记0, 1, 2, … , B-1~
组0有效标记0, 1, 2, … , B-1 E − 1 E-1 E1
组1有效标记0, 1, 2, … , B-1 0 0 0
有效标记0, 1, 2, … , B-1~
组1有效标记0, 1, 2, … , B-1 E − 1 E-1 E1
组S-1有效标记0, 1, 2, … , B-1 0 0 0
有效标记0, 1, 2, … , B-1~
组S-1有效标记0, 1, 2, … , B-1 E − 1 E-1 E1

地址 ( m m m位)

标记组索引块偏移
( m − 1 ) (m-1) (m1) ~ ( s + b ) (s+b) (s+b) ( s + b − 1 ) (s+b-1) (s+b1) ~ b b b ( b − 1 ) (b-1) (b1) ~ 0 0 0
t t t s s s b b b

符号及参数

参数描述
S = 2 s S=2^s S=2s组数
E E E每个组的行数
B = 2 b B=2^b B=2b块大小(字节)
m = l o g 2 ( M ) m=log_2(M) m=log2(M)主存物理地址位数
M = 2 m M=2^m M=2m内存地址最大数量
s = l o g 2 ( S ) s=log_2(S) s=log2(S)组索引位数量
b = l o g 2 ( B ) b=log_2(B) b=log2(B)块偏移位数量
t = m − ( s + b ) t=m-(s+b) t=m(s+b)标记位数量
C = B × E × S C=B \times E \times S C=B×E×S高速缓存有效大小
直接映射高速缓存

直接映射高速缓存,每个组只有一行,即 E = 1 E=1 E=1 的高速缓存

步骤

  1. 组选择

    从字 w w w 的地址中抽取 s s s 个组索引位,解释成对应组号的无符号整数,映射为高速缓存中对应的组

  2. 行匹配

    假设已经选择了组 i i i,当该行设置了有效位,且高速缓存行中标记与地址中标记匹配,则 w w w 就在此行中,则缓存命中;否则缓存不命中

  3. 字抽取

    块偏移位:所需要字节的第一个字节的偏移

01234567
w 0 w_0 w0 w 1 w_1 w1 w 2 w_2 w2 w 3 w_3 w3

则上图示例中(假设字长为4字节),块偏移为 10 0 2 100_2 1002

运行过程

地址被划分成三个部分

标记位索引位偏移位

索引位用来确定缓存中的组号,地址中间的一段而非前几位作为索引位,这样连续的一段内存会映射到不同的组,从而调用连续的一段内存时缓存会频繁不命中

标记位用来在同一个组中唯一的确定一个块,映射到这个组的内存块有相同的索引位,但标记位不同,这样就能唯一的确定出一个块

偏移位用来确定已经存入缓存的一个块中,需要的数据具体的位置,该字在块中的偏移量

运行步骤:

  1. 读取地址,分成标记位,索引位,偏移位
  2. 在高速缓存中查找组,索引位就是组号偏移量
  3. 查看有效位和标记位,有效位1,标记位相同则命中
  4. 缓存不命中则先加载,缓存命中则直接读取
  5. 返回偏移位对应偏移量的数据

抖动

高速缓存反复地加载和驱逐相同的高速缓存块的组

程序访问大小为2的幂的数组时,两个数组映射在相同的组中,直接映射高速缓存中通常会发生冲突不命中

组相联高速缓存

组相联高速缓存,每组都保存有多于一个的高速缓存行

一个 1 < E < C B 1 < E < \frac{C}{B} 1<E<BC 的高速缓存通常称为 E E E路组相联高速缓存

例如下图2路组相联高速缓存

组号有效位标记位数据
组0有效标记高速缓存块
组0有效标记高速缓存块
组1有效标记高速缓存块
组1有效标记高速缓存块
组S-1有效标记高速缓存块
组S-1有效标记高速缓存块

步骤

  1. 组选择

    与直接映射高速缓存的组选择一样,组索引位标识组

  2. 行匹配和字选择

    需检查多个行的标记位和有效位

    可以理解为一个 (key, value) 对的数组,key是标记和有效位,value是块的内容

    组中的任何一行都可以包含任何映射到这个组的内存块,高速缓存必须搜索组中的每一行,寻找有效行,其标记与地址中的标记相同,找到则命中

    字选择与直接映射高速缓存相同

行替换

最不常试用策略LFU Least-Frequently-Used

最近最少使用策略LRU Least-Recently-Used

全相联高速缓存

全相联高速缓存是由一个包含了所有高速缓行的组所组成 E = C B E=\frac{C}{B} E=BC

组号有效位标记位数据
组0有效标记高速缓存块
组0有效标记高速缓存块
组0有效标记高速缓存块

E = E = E= 唯一的一组中有 E = C B E=\frac{C}{B} E=BC

标记块偏移
r r r b b b

地址中没有组索引位,只被划分成了一个标记位和一个块偏移,默认总是选择组0

行匹配和字选择与组相联高速缓存一样

全相联高速缓存成本高,只适合做小的高速缓存,如:

虚拟内存系统中的翻译备用缓冲器 TLB,用于缓存页表项

写数据

假设需要写一个已经缓存了的字 w w w (写命中write hit)

直写

立即将 w w w 的高速缓存块写回到紧接着的低一层中

写回

尽可能的推迟更新,只有当替换算法要驱逐这个更新过的块时才把它写到紧接着的低一层中

由于局部性,写回能显著减少总线流量,缺点是增加了复杂性,需要为每个高速缓存行维护一个额外的修改位dirty bit

另一个问题是处理写不命中

写分配

write-allocate,先加载低一层后更新,每次不命中都会导致块的传送

非写分配

not-write-allocate,避开高速缓存,直接把这个字写到低一层中

直写高速缓存通常是非写分配的,写回高速缓存通常是写分配的(局部性)

虚拟内存

地址空间和寻址

虚拟寻址 virtual addressing

CPU通过内存管理单元 MMU 将生成的虚拟地址 VA 转换为物理地址 PA

优势:
更加有效的利用主存
简化内存管理
独立的地址空间

地址空间:非负整数地址的有序集合

虚拟地址空间

CPU从一个有 $ N=2^{n}$ 个地址的地址空间中生成虚拟空间
{ 0 , 1 , 2 , … , N − 1 } \{0, 1, 2, \dots, N-1\} {0,1,2,,N1}

物理地址空间

对应系统中物理内存的M个字节,M不要求是2的幂,但假设 M = 2 m M=2^{m} M=2m
{ 0 , 1 , 2 , … , M − 1 } \{0, 1, 2, \dots, M-1\} {0,1,2,,M1}

通常,SRAM高速缓存使用物理地址进行访问

虚拟页Virtual Page

VP 虚拟页,VM系统将虚拟内存分割,每个虚拟页大小 P = 2 p P=2^{p} P=2p 字节

物理页Physical Page

PP 物理页,大小也是P字节 P = 2 p P=2^{p} P=2p ,物理页也被称为页帧page frame

使用SRAM表示L1, L2, L3高速缓存
使用DRAM表示虚拟内存系统的缓存,在主存中缓存虚拟页

DRAM的未命中要由磁盘服务,因此未命中惩罚大:

  1. 虚拟页很大(4KB~2MB)
  2. DRAM缓存是全相联的,即任何虚拟页都可以放置在任何的物理页中
  3. 复杂精密的替换算法
  4. DRAM使用回写,而非直写

页表

页表是一个PTE页表项(页表条目)的数组,用于建立虚拟页到物理页的映射,常驻内存

每个PTE由一个有效位和一个n位地址字段组成(物理页号或磁盘地址)

由于DRAM缓存是全相联的,所以任意物理页都可以包含任意虚拟页

操作系统为每个进程提供了一个独立的页表,也就是一个独立的虚拟地址空间,多个虚拟页面可以映射到同一个共享物理页面上

有效位

valid表明该虚拟页当前是否缓存在DRAM中

虚拟页面valid状态
未分配的0VM系统未分配或创建的页,不占磁盘空间,非法地址
未缓存的0地址已分配,数据未缓存
缓存的1地址已分配,数据已缓存在DRAM中
条目号有效位物理页号或磁盘地址
PTE00null
PTE11VP1 在内存中起始地址
PTE21VP2 在内存中起始地址
PTE30VP3在磁盘中起始地址
PTE41VP4在内存中起始地址
PTE50null
PTE60VP6在磁盘中起始地址
PTE71VP7 在内存中起始地址
页命中

通过虚拟地址构造物理地址

MMU将虚拟地址作为索引来定位PTE,因为设置了有效位,MMU就知道该虚拟页缓存在内存中,于是使用PTE中的物理内存地址构造出这个字的物理地址

缺页

DRAM缓存不命中

地址翻译硬件从内存中读取PTE,从有效位推断出该VP未被缓存,触发缺页异常,调用内核缺页异常处理程序

程序选择一个牺牲页,即存在物理内存某PP中的VP,若该VP已修改,则将其内容复制回磁盘。从磁盘中复制新的页到内存中,并更新页表,随后返回

重新启动导致缺页的指令,该指令重新将虚拟地址发送给地址翻译硬件,此时可正常读取

所有现代系统都使用按需页面调度,即直到有不命中发生时,才将页面从磁盘换入DRAM

统计缺页次数:Linux的getrusage函数

分配页面

例如调用malloc,分配VP

VP的分配过程,是在磁盘上创建空间并更新PTE,使它指向磁盘上这个新创建的页面

内存管理与保护

内存管理

  1. 简化链接

  2. 简化加载

  3. 简化共享

  4. 简化内存分配

    例如调用malloc,分配VP

    VP的分配过程,是在磁盘上创建空间并更新PTE,使它指向磁盘上这个新创建的页面

    在虚拟内存分配 k k k个连续的页面,其映射到物理内存的 k k k个物理页面可以随机分散不必连续

内存保护

通过在PTE上添加一些额外的许可位,来控制对一个虚拟页面内容的访问

例如下图带许可位的页表

SUPREADWRITE地址
VP0:PP1
VP1:PP3
VP2:PP2

SUP:是否必须运行在内核(超级用户)模式下才能访问该页

READ/WRITE:控制对页面的读和写访问

指令违反许可则CPU触发一个一般保护故障,段错误segmentation fault

地址翻译

地址翻译是一个 N N N元素的虚拟地址空间(VAS)中的元素和一个 M M M元素的物理地址空间(PAS)中元素之间的映射

M A P : V A S → P A S   ∪   ∅ MAP:VAS \rightarrow PAS\ \cup\ \varnothing MAP:VASPAS  

名词解释
缩写英文中文译名
VPVirtual Page虚拟页
PPPhysical Page物理页
PTEPage Table Entry页表条目
TLBTranslation Lookaside Buffer快表
PTBRPage Table Base Register页表基地址寄存器
符号英文描述
N = 2 n N=2^{n} N=2nnone虚拟地址空间中的地址数量
M = 2 m M=2^{m} M=2mnone物理地址空间中的地址数量
P = 2 p P=2^{p} P=2pnone页的大小,单位字节
VPOVP Offset虚拟页面偏移量,单位字节
VPNVP Number虚拟页号
TLBITLB IndexTLB索引
TLBTTLB TagTLB标记
PPOPP Offset物理页面偏移量,单位字节
PPNPP Number物理页号
COCache Offdet缓冲块内的字节偏移量
CICache Index高速缓存索引
CTCache Tag高速缓存标记
映射方法

虚拟地址

n − 1 n-1 n1 ~ p p p p − 1 p-1 p1 ~ 0 0 0
虚拟页号(VPN虚拟页偏移量(VPO

页表

有效位物理页号(PPN)
————————————————
————————————————
————————————————

物理地址

m − 1 m-1 m1 ~ p p p p − 1 p-1 p1 ~ 0 0 0
物理页号(PPN物理页偏移量(PPO

页表基地址寄存器

Page Table Base Register CPU中的一个控制寄存器,记录了页表在内存中的基地址,指向当前页表

n n n 位的虚拟地址包括两个部分, p p p 位的虚拟页面偏移量VPO ( n − p ) (n-p) (np) 位的虚拟页号VPN

m m m 位的物理地址也同样包括了两个部分, p p p 位的物理页面偏移量PPO ( m − p ) (m-p) (mp) 位的物理页号PPN

其中虚拟页面偏移量和物理页面偏移量相同

虚拟页号作为基于页表基地址的偏移量,用于查找对应的PTE页表条目,若该条目有效,则条目记录了对应的物理页号

将查找到的物理页号和原虚拟地址中的偏移量拼接就组成了对应的物理地址

过程
页面命中
  1. 处理器生成虚拟地址,将虚拟地址传送给MMU
  2. MMU生成PTE地址,从高速缓存或主存中请求得到PTE
  3. 高速缓存或主存向MMU返回PTE
  4. MMU构造出物理地址,并将构造后的物理地址传送给高速缓存或主存
  5. 高速缓存或主存返回请求的数据字给处理器
页面不命中
  1. 处理器生成虚拟地址,将虚拟地址传送给MMU
  2. MMU生成PTE地址,从高速缓存或主存中请求得到PTE
  3. 高速缓存或主存向MMU返回PTE
  4. PTE中有效位为0,MMU触发异常,CPU跳转到操作系统内核中的缺页异常处理程序
  5. 缺页异常处理程序确定物理内存中的牺牲页,若已修改则将其换出到磁盘
  6. 缺页处理程序调入新的页,并更新PTE
  7. 缺页处理程序返回到原来的进程,再次执行导致缺页的指令,此次正确执行
快表 TLB

快表 TLB

Translation Lookaside Buffer 是在 MMU 中包括的一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个 PTE 组成的块,TLB通常有着高度的相联度

n − 1 n-1 n1 ~ p + t p+t p+t p + t − 1 p+t-1 p+t1 ~ p p p p − 1 p-1 p1 ~ 0 0 0
VPNVPNVPO
TLB标记(TLBTTLB索引 t t t位(TLBI偏移量

用于组选择和行匹配的索引和标记字段虚拟地址的虚拟页号中提取出来的

以下步骤中的地址翻译部分在芯片上的MMU中执行:

  1. CPU产生一个虚拟地址
  2. MMU从TLB取出相应的PTE
  3. MMU将虚拟地址翻译成物理地址,并发送到高速缓存或主存
  4. 高速缓存或主存将所请求的数据字返回给CPU

TLB不命中时,MMU需要从L1缓存中取出相应的PTE,新取出PTE放入TLB,依据情况执行相应的覆盖

多级页表

虚拟地址的虚拟页号 V P N VPN VPN 被拆分成 k k k 个部分,从高到低每一部分作为一级页表的索引

每级页表由虚拟地址中的 V P N   i VPN\ i VPN i 作为索引,查找到对应的条目PTE,每个PTE记录的是下一级页表的基地址

最后一级页表记录了物理页表的物理页号 P P N PPN PPN,它和虚拟地址中的偏移 ( V P O = P P O VPO=PPO VPO=PPO) 共同组成了物理地址

虚拟地址

n − 1 ↦ n-1\mapsto n1 p − 1 p-1 p1 ~ 0 0 0
VPN 1VPN 2VPN kVPO

多级页表

注:为方便表示,这里把不同级的页表画在了一起,实际上他们在内存里是随机分布的

1 级页表2 级页表k 级页表
VPN 1作为索引在本页表查找条目VPN 2作为索引VPN k作为索引
每个PTE对应一个 2 级页表基址每个PTE对应一个 3 级页表基址每个PTE记录一个PPN

结合SRAM和DRAM

A L U { — V A ⟶ M M U { — P T E A → ⟵ P T E — — P A — ⟶ —— ⟵ — D a t a —— ⟵ —— ⏟ C P U [ P T E A   m i s s P T E A   h i t P A   h i t P A   m i s s ] ⏟ C a c h e − P T E A ⟶ ⟵ P T E — — P A — ⟶ ⟵ D a t a — } [ D R A M ] \underbrace{ALU \begin{cases} — VA\longrightarrow MMU \begin{cases} — PTEA \to \\ \longleftarrow PTE — \\ —PA —\longrightarrow \end{cases} \\ ——\longleftarrow—Data ——\longleftarrow—— \end{cases} }_{CPU} \underbrace{ \begin{bmatrix} \quad PTEA\ miss \\ PTEA\ hit \\ PA\ hit\\ \quad PA\ miss \end{bmatrix} }_{Cache} \left.\begin{matrix} -PTEA\longrightarrow \\ \longleftarrow PTE — \\ —PA—\longrightarrow \\ \longleftarrow Data —\end{matrix}\right\} \begin{bmatrix} \\ DRAM\\ \\ \end{bmatrix} CPU ALU VAMMU PTEAPTEPA——Data————Cache PTEA missPTEA hitPA hitPA miss PTEAPTEPAData DRAM

大多数系统的高速缓存选择物理寻址

上图展示了物理寻址的高速缓存如何与虚拟内存结合,主要思路是地址翻译发生在高速缓存查找之前

页表条目也可以像其他数据一样缓存

  • 34
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Proxy(代理)是一种在计算机网络中广泛应用的中间服务器,用于连接客户端和目标服务器之间的通信。Proxy csapp是一个与计算机系统应用(Computer Systems: A Programmer's Perspective)相关的代理服务器。 Proxy csapp的设计目的是为了提供更高效的网络通信,增强系统的安全性,并提供更好的用户体验。在Proxy csapp中,客户端的请求首先会被发送到代理服务器,然后由代理服务器转发给目标服务器,并将目标服务器的响应返回给客户端。这种中间层的机制可以提供很多功能,如缓存、负载均衡、安全认证等。 在csapp中,Proxy csapp可以被用于优化网络数据传输的效率。代理服务器可以对客户端请求进行调度和协商,以减少网络延迟和数据传输量。通过缓存常用的数据和资源,代理服务器可以减少重复的数据传输和目标服务器的负载,提高网络性能和响应速度。 此外,Proxy csapp还可以提供安全的网络通信环境。代理服务器可以拦截和过滤网络流量,用于检测和阻止恶意攻击、垃圾邮件等网络安全威胁。代理服务器还可以对用户进行身份验证和授权,保护敏感数据的安全性。 最后,通过Proxy csapp可以实现更好的用户体验。代理服务器可以根据用户的需求进行个性化的服务,如按地理位置提供更快的网络连接、提供访问限制和控制等。代理服务器还可以对网络流量进行压缩和优化,提高网络传输效率,减少用户的等待时间。 总之,Proxy csapp在计算机系统应用中是一个重要的代理服务器,它可以提供高效的网络通信、增强系统的安全性,并带来更好的用户体验。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值