csapp学习笔记

信息的表示和处理

2进制与16进制

信息在计算机中以2进制的形式表达,不过16进制更直观,很多例子都是通过16进制展示(二进制太长了)
关于2进制与16进制,每4位二进制可以表达一位16进制,如1111 == 0xf

不同整数类型所占字节数

1byte表示1个字节,word == 2byte,dword = 4byte,qword = 8byte
unsigned表示无符号,当使用unsigned后,符号位变成数值位,对应的范围* 2 + 1
char 占 1byte,范围位 -128,127
short 2byte
int 4
long 4 (64位中为8)
int32_t 4
int64_t 8

大端与小端表示法
例子0x123456
在小端中56 34 12
大端中 12 34 56

补码、反码和原码

如图

浮点数

32位浮点数的计算方法float=(-1)^s * M * 2^E float的表示方法,1个二进制位表示符号s,
8个二进制位表示阶码(e为无符号数),23个二进制位表示小数部分(f) (规格化数,即e!=0,且e!=255)
E = e - Bias Bias =2^(k-1) - 1, 举个栗子,e 为 1111 1110 ,
此时 Bias = 127, e = 254 - 127 = 127 假设 M 为 1 00 0000 0000 0000 0000 0000 M = 1 + f ,
f部分为f = 1 * 2^-1 …… + 0 = 0.5 用小数点表示 M
即 1.100000……000 (非规格化数)E = 1 - Bias, M = f = 1

程序的机器级表示

Intel和ATT格式区别

1.Intel省略大小后缀q,如push和mov,而不是pushq和movq
2.Intel省略寄存器前的%,如eax,而不是%eax
3.Intel使用不同的方式描述内存中的位置,如’QWORD PTR [rbx]’而不是’%rbx’
4.当带有多个操作数时,列出操作数的顺序相反,如mov eax,ebx(eax=ebx)

内联汇编

当使用c无法实现、而使用汇编能实现时,可以在将程序与汇编结合
1.编写完整的函数,放进一个独立的汇编代码文件中,让汇编器和链接器把它和用c语言书写的代码合并
2.使用GCC的内联汇编特性,用asm伪指令可以在c中包含简短的汇编代码

各寄存器的作用

寻址方式


数据传送指令

movb 传byte
movw 传word
movl 传dw
movq 传qw
mvoabsq I,R R<-I 传送绝对的四字(常规的movq只能表示为32为补码数字的立即数作为源操作数,然后把这个值符号扩展得到64位的值)
关于零扩展的数据传送指令

整数算数操作

关于lea还有dec、inc、neg等操作(csapp yyds,早知道当初就认认真真的学一遍csapp)

条件码寄存器

用以描述最近的算术或逻辑操作的属性。
CF:进位标志。可以用来检查无符号的溢出
ZF:零标志。最近的操作得出的结果为0
SF:符号标志。最近的操作得到的结果为负数
OF:溢出标志。最近的操作导致一个补码溢出———正溢出或者负溢出
除了lea,6中的所有指令都会设置条件码

set指令

如图

跳转指令

看到这里感觉自己知识还是太浅薄了

条件传送指令

媒体寄存器

浮点传送指令

浮点数运算指令

处理器体系架构

处理指令操作

处理指令操作大致流程如下:
1.取指:
地址为PC的值,将从指令中去除指令指示符的四位部分,成为icode(指令代码)和ifun(指令功能),
可能继续取出一个寄存器指示符字节,指明一个或者两个寄存器操作数指令符号rA和rB,还有可能再继续取出一个四字节
常数字valC,它按顺序方式计算当前指令的下一条指令的地址valP,即valP=PC + len(opcode)
2.译码:
译码阶段从寄存器文件读入最多两个操作数,得到值valA或valB,通过,它读入指令rA和rB字段指明的寄存器。
3.执行:
在执行阶段,,ALU要么执行指令指明的操作(根据ifun的值),计算内存引用的有效地址,要么增加或者减少栈指针
。得到的值我们称为valE。在此也可能设置条件码。对于一条条件传送指令来说,这个阶段会检查条件码和传送条件(
由ifun给出),如果条件成立,则更新目标寄存器。同样对于一条跳转指令来说,这个阶段也会决定是否选择分支。
4.访存:
访存阶段可以将数据写入内存,或者从内存中读出数据。读出的值为valM。
5.写回:
写回阶段最多可以写两个结果到寄存器文件。
6.更新PC:
将PC设置为下一条指令的地址
1.seq取指阶段,如图
avatar
icode计算指令,ifun计算功能码,instr_valid用来发现不合法的指令。信号instr_valid 和 immem_error在访
存阶段用来产生状态码。
2.译码和写回阶段
如图(寄存器示意图)
avatar
寄存器文件有四个端口。支持同时进行两个读(A和B上)和两个写(在端口E和M上)。每个端口都有一个地址连接和
一个数据连接,地址连接是一个寄存器ID,而数据连接是一组64根线路,既可以作为寄存器文件的输出字(对读端口)
,也可以作为它的输入字(对写端口来说)。两个读端口的地址输入为srcA和srcB,而两个写端口的地址输入为destE和
destM。如果某个地址端口上的值为特殊标识符0xF,则表明不需要访问寄存器。
irmovq 指令 和 rrmovq指令实现如图
avatar
seq硬件实现抽象图
avatar
更加详细一点的
avatar

缓存

存储器层次结构

如图
avatar

存储器层次结构中的缓存

如图
avatar
假设程序需要读取第k+1层的对象d,首先会在存储在第k层的一个块中寻找,若k中缓存了d,则缓存命中,否则缓存不命中,当发生缓存不命中
时,k层的缓存将从k+1层缓存取出包含d块,如果k层满了,则替换现存的块,至于替换哪个块,由缓存的替换策略决定,如随机替换策略和LRU(具有最近最少被使用策略)。

通用的高速缓存存储器组织结构

如图
avatar
高速缓存的结构可以使用元组(S,E,B,m)来描述,组索引可以索引到对应的组,标记位用于告知组的行数(索引时),块偏移即,在B个字节的字偏移(这里用索引一个字节举例)

直接映射高速缓存

如图
avatar
组选择,如图
avatar
行匹配
avatar

组相联高速缓存

如图
avatar
在进行行匹配时,需要搜索组中的每一行

全相联高速缓存

在全相联高速缓存中,一个组包含所有的行,因此没有组索引位,地址只被划分为了一个标记和一个块偏移
行选择和字选择,如图
avatar

真实的结构

i7中缓冲的设计如图
avatar

编写高速缓存友好的代码

高速缓存的数据为连续的空间,访问数据时,发生不命中的次数越少,运行结果越快,应按照数据在内存中的顺序,读数据以步长为1的方式,此外局部变量存储在寄存器中,可
以反复引用
例子,如图
avatar
最后的结果
avatar

异常

异常是指控制流中的突变情况,比如虚拟内存缺页、算术溢出、除0等,系统启动时,会生成一张异常处理表,可以跳转到对应的异常处理程序,根据异常类型会发生这三种之一的情况:
1)处理程序返回到当前指令,即当事件发生时正在执行的指令
2)处理程序返回将会执行的下一条指令
3)处理程序终止杯中断的程序
原理如图
avatar
异常处理程序处于内核态,对所有资源具有完全的访问权限
关于异常的种类: 中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort)
关于异常的属性如图
avatar
中断为异步发生,是来自处理器外部的I/O设备的信号的结果,不是由任何一条专门的指令造成的,例子,比如网络适配器、磁盘控制器和定时器行骗,通过向处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断,这个异常号标识了引起中断的设备,原理图
avatar

陷阱和系统调用

陷阱是有意的异常,会返回下一条指令,陷阱最重要的用途是提供系统调用,一个用户程序到内核的接口,用户程序通过系统调用,来向内核请求服务,用法为syscall + n,n为对应的服务
(一般语言中封装好了,比如c语言的read,其实最后就是封装好的的系统调用
原理图
avatar

故障

故障由错误情况引起,可能被故障处理程序修正,如果能成功修正,则返回到发生故障的指令,否则,返回到内核的abort例程,将引起故障的错误程序终止,比如缺页异常
原理图
avatar

终止

终止一般由不可恢复的错误造成的结果,通常是硬件错误,处理程序将控制返回abort例程终止应用程序
原理图
avatar

进程

进程的经典定义为一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中(context),上下文由程序正常运行所需要的状态组成,包括存放在内存中的程序的代码和数据,相关栈、环境变量以及打开文件描述符等。

逻辑控制流与并发

使用调试器可以看到一系列程序计数器(PC)值,这些值唯一包含在对应可执行目标文件或者包含在运行时动态链接到程序的共享对象的指令。这个PC值的序列叫做逻辑控制流,简称逻辑流。
avatar
关于程序独占处理器确实是种假象,进程是轮流使用寄存器,每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后到其他进程,对于一个运行在这些进程之一的上下文的程序,看上去确实像在独占
寄存器。唯一的反面例证是,如果精确的测量每条指令使用的时间,在程序一些指令的执行之间,CPU会周期的停顿。当停顿后,会继续执行程序,不改变程序内存位置或寄存器的内容。
一个逻辑流的执行时间在时间上与另一个流重叠,称为并发流,这两个流被称为并发的运行。
多个流并发的指令的一般现象被称为并发。一个进程和其他进程轮流运行的概念叫做多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。

私有地址空间

进程会为程序提供自己的私有地址空间,不能被其他进程读写。
一般形式如图
avatar

main函数参数解析

main函数栈帧
avatar
关于获取与打印参数c语言代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char **argv, char **envp)
{
puts("Command-ine arguments:");
for(int i = 0; argv[i] != NULL; i++)
{
printf(" argv[%d]: %s\n",i,argv[i]);
}
puts("Environment variables:");
for(int j = 0; envp[j] != NULL; j++)
{
printf(" envp[%d]: %s\n",j,envp[j]);
}
}

信号

信号号码及意义如图
avatar
信号处理如图
avatar
发送信号原因:
1.内核检测系统事件,如除0错误
2.一个进程调用kill函数
发出没有被接受的信号叫做待处理信号,一个待处理信号最多被接受一次,如果一个进程已经有待处理信号,则接下来发送到该进程的信号会被丢弃。
内核为每个进程在pending位向量中维护者待处理信号的集合,而在blocked位向量中维护被阻塞的信号集合,只要传送了一个类型为k的信号,内核就会设置pending中的第k为,而只要接受了一个类型为k的信号,内核就会清除pending中的第k位。
信号处理程序可以被其他信号处理程序中断,如图
avatar
应用态进程由于系统调用、中断或异常,而陷入内核态后,在返回应用态之前,内核会进行信号的检查和处理。
目的进程响应信号的方式:从发送的过程看到,信号只是简单加到当前线程task_struct的结构中(struct sigpending属于task_struct)。所以,目的进程的信号响应,一定是由内核检查并完成的。
执行一个信号处理程序中,在用户态和内核态之间切换时,需要谨慎地处理栈中的内容。
handle_signal() 运行在内核态,而信号处理程序运行在用户态,当前进程恢复”正常“执行前,必须首先执行用户态的信号处理程序。
此外,当内核打算恢复进程的正常执行时,内核态堆栈不再包含被中断程序的硬件上下文,因为每当从内核态向用户态转换时,内核态堆栈都被清空。
信号处理程序可以执行系统调用。这种情况下,执行了系统调用的服务例程后,控制权必须返回到信号处理程序,而不是被中断程序的正常代码流。
Linux 所采用的解决方法是,把保存在内核态堆栈中的硬件上下文拷贝到当前进程的用户态堆栈中。
用户态堆栈也以同样方式修改:即当信号处理程序终止时,自动调用 sigreturn() 把这个硬件上下文拷贝回内核态堆栈中,并恢复用户态堆栈中原来的内容。
信号的捕捉如图
avatar
三种基本的地址映射方式:
直接映射、全相联映射、组相联映射