CSAPP: ICS 期中复习
Chapter2
字节:最小的可寻址的内存单位。
char是[signed] char,short<->unsigned short,int <-> unsigned,long<->unsigned long,
int32_t<->uint32_t,int64_t <-> uint64_t(ISO C99)
unsigned long/ unsigned long int/ long unsigned/ long unsigned int 都是一个意思。
大多数Intel兼容机都只用小端模式,IBM和Oracle(收购Sun Microsystems开始)大多数极其按大端模式。
双端法:ARM选定了操作系统,字节顺序固定。(Android、IOS小端)
同一段代码,指令编码在不同机器上是不一样的。不同的机器类型使用不同的且不兼容的指令和编码方式。二进制代码是不兼容的,很少能在不同的机器和操作系统组合之间移植。
移位运算从左至右可以结合。x<<j<<k <==> (x<<j)<<k.
逻辑右移:补零,算术右移:带符号。
移动k位,k大于本身数据的位数,则移动k mod w位。(C语言标准,许多机器上)
移位运算的操作符优先级:比加减法低,于是1<<2+3<<4 <==> 1<<(2+3)<<4
k | 2的k次幂 |
---|---|
6 | 64 |
7 | 128 |
8 | 256 |
9 | 512 |
10 | 1024 |
11 | 2048 |
12 | 4096 |
15 | 32768 |
16 | 65536 |
31 | 2,147,483,648 |
32 | 4,294,967,295 |
B2U_w :0~2w-1
B2T_w:-2w-1~2w-1-1
-1和UMax位表示相同。
<limits.h>定义了一组常量INT_MAX/INT_MIN/UINT_MAX
<stdint.h>定义了int32_t/uint32_t,这些数据类型打印需要使用宏,PRIu64/PRIU32,在不同字长的程序里面会展开为d/l/u等等。
反码(Ones’ Complement):B2O——最高有效位的权是-(2-w-1-1)。
原码(Sign-Magnitude):B2S——最高有效位是符号位。
对于反码,-x表示为x的反码取反,也就是说-x = [11111…1]-x,ones有很多1.
对于补码,-x表示为~x+1。也就是取反后+1,x=[1000…0000]-x=2w-x,two’s,只有一个2.
补码转换为无符号数,在负数的部分加上2w。无符号数转化为补码:超过TMax的部分减掉2w。
声明常量时,大多数认为是有符号的,要创建无符号常量需要加上后缀uorU。
因式类型转换:
//case1:assignment
int tx,ty;
unsigned ux,uy;
tx = ux; //cast to signed
uy = ty; //cast to unsigned
//attention: cast to the type being assigned
//case2:printf
int x = -1;
unsigned u = 2147483648; /* 2 to the 31st */
printf("x = %u = %d\n",x, x); // x = 4294967295 = -1 (UMax,-1)
printf("u = %u = %d\n",u, u); // u = 2147483648 = -2147483648 (TMax+1,TMin)
//case3:operation (对标准的算术运算没有差别,对>,<有差别,整型升级)
0 == 0u; //cast to unsigned
C语言中TMin的写法:
#define INT_MAX 2147483647
#define INT_MIN (-INT_MAX - 1)
符号扩展:保持原有数值大小不变。
小的有符号数转换为大的无符号数,先改变大小(符号扩展)再从有符号到无符号转换。例如:
short sx = -12345;
unsigned uy = sx; // (unsigned)(int)sx = ff ff cf c7 = 4294954951
截断补码:把位表示超出范围的部分扔掉,再把低位表示转化为低位补码。
检测无符号加法的溢出:溢出等价于x+y<x。
无符号数求反:-x = 2w-x。
补码加法:正溢出、负溢出、正常
TMin的补码非等于自己:-TMin = TMin。
无符号乘法和补码乘法具有位级等价性。
整数除法总是舍入到0。
除以2的幂的补码除法,用移位会向下舍入。
除以2的幂的补码,偏置可以使之向上舍入。
因此计算除以2的幂的表达式:x/2^k = x<0? (x+(1<<k)-1:x)>>k
。
IEEE标准754——1985年。
小数的二进制表示法只能表示那些能够写成x2y线性组合的数。其他只能近似表示,比如1/5。
精度:小数位数;范围:表示大小
浮点标准:V = (-1)s x M x 2E
范围:尾数(M),二进制小数,范围为1到2-epi或者是0到1-epi(全0表示和全1表示)。
float 1,8,23,double 1,11,52
规格化:exp不全为0且不全为1,E=e-Bias,Bias = 2k-1-1,指数的取值范围为 1-(2k-1-1) = 2-2k-1 到 2k-2-(2k-1-1) = 2k-1-1。单精度为-126到127,双精度是-1022到+1023。
非规格化:exp全0,阶码E=1-Bias,尾数M不包含隐含的1.可以表示+0和-0。还可以表示非常接近0的数(逐渐溢出)。
特殊值:exp全1.M全0为无穷,M不为0为NaN。
注意:如果将位表达式解释为无符号整数,只看正数,最小的非规格化数到最大的非规格化数,到最小的规格化数到最大的规格化数,到无穷大,位表示按照无符号解释是依次递增的。——IEEE设计为了浮点数可以使用整数的排序函数。
整数转浮点数:将二进制小数点左移到最高位后面(创建规格化表示),丢掉最高位的1,在末尾补够0(构造小数字段),再构造阶码字段(指数+bias),再加上符号位。
一个具有n为小数的浮点格式(假设阶码无限大),则不能准确描述的最小正整数=2n+1+1,因为准确表示它需要n+1位小数。
舍入:向偶数舍入——当在两个可能值的正中间时,倾向于使最低有效为为0.
1/-0 = -infty,1/+0 = +infty
浮点数运算:Round(x + y)因此不能结合——3.14+(1e10-1e10)
浮点乘法同样:Round(x*y),(1e20 * 1e20) * 1e-20,没有分配性。
注意NaN的一切逻辑运算都是非,除了NaN != xx(包括NaN)。
单调性:对于a,b,c都不等于NaN,浮点乘法满足单调性。
From | to | Overflow | Rounding |
---|---|---|---|
int | float | no | Yes |
int | double | no | No |
float | int | yes | yes(向0舍入) |
double | int | yes(整数不确定) | Yes(向0舍入) |
float | double | no | no |
double | float | yes | yes |
Chapter 3
用gcc编译.c文件:gcc -Og(优化等级) -o p(最终生成的文件名) p1.c
用gcc生成.s文件:gcc -Og -S mstore.c
用gcc编译并汇编成.o文件:gcc -Og -c mstore.c(二进制字节序列)
反汇编器:OBJDUMP—— objdump -d mstore.o
程序内存包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,用户分配的内存块(malloc)。
x86-64的64位虚拟地址的高16位必须设置为0.(地址实际上用的是248或64TB范围内的一个字节)。
计算机系统的两种抽象:1.指令集体系结构ISA;2.虚拟内存地址。
X86-64的指令长度从1到15个字节不等。常用的指令字节数少,不常用的多。
设计指令格式的方式是,从某个给定的位置开始,可以将字节唯一地解码成机器指令。例如只有pushq %rbx是由字节值53开头的。
我们表述的是ATT格式的汇编代码,与Intel格式的汇编代码有一些区别,比如寄存器名称前没有%,push后面没有q等等。
汇编代码使用同样的后缀‘l’来表示double和int,但是不产生歧义是因为浮点数使用一组完全不同的指令和寄存器。
功能 | 四字 | 双字 | 字 | 字节 |
---|---|---|---|---|
返回值 | %rax | %eax | %ax | %al |
栈指针 | %rsp | %esp | %sp | %spl |
被调用者保存 | %rbp | %ebp | %bp | %bpl |
被调用者保存 | %rbx | %ebx | %bx | %bl |
被调用者保存 | %r12 | %r12d | %r13w | %r12b |
被调用者保存 | %r13 | %r13d | %r13w | %r13b |
被调用者保存 | %r14 | %r14d | %r14w | %r14b |
被调用者保存 | %r15 | %r15d | %r15w | %r15b |
参数1 | %rdi | %edi | %di | %dil |
参数2 | %rsi | %esi | %si | %sil |
参数3 | %rdx | %edx | %dx | %dl |
参数4 | %rcx | %ecx | %cx | %cl |
参数5 | %r8 | %r8d | %r8w | %r8b |
参数6 | %r9 | %r9d | %r9w | %r9b |
调用者保存 | %r10 | %r10d | %r10w | %r10b |
调用者保存 | %r11 | %r11d | %r11w | %r11b |
指令以寄存器为目标时有两条规则:生成1字节和2字节数字的指令会保持剩下字节不变;生成四字节的数字会把高位4个字节置为0.
movabsq I,R,表示传送绝对的四字(64位立即数)I到寄存器R(只能以寄存器作为目的)。常规的movq指令只能以表示为32位补码数字的立即数作为源操作数。然后把这个值符号扩展得到64位值。
x86-64的限制:传送指令的两个操作数不能都指向内存。
将较小的源值放到较大的目的时使用movz、movs两种指令(3种源,目的只能是R),一个是零扩展,一个是符号扩展。
cltq指令专门用于将%eax符号扩展为%rax。没有操作数。效果与movslq %eax,%rax相同
注意没有指令movzlq。(用movl实现即可)
寻址模式:比例因此必须是1,2,4或者8!(立即数M可以任意没事)
寻址模式:不能使用%eax这些不满64字节的寄存器!因为地址为64位!
pushq:先将栈指针减8,再把数据写入栈指针所在位置。所有数据类型都行。
popq操作数不能是立即数,其他都可以。
INC、DEC、NEG、NOT分别是加1,减1,取负,取补。
INC的操作数可以是内存或者寄存器。
INC和DEC会设置溢出和零标志,但不会改变进位标志!
移位操作的进位标志会设置为最后一个被移出的位,而溢出标志设为0.
LEAQ操作的目的不能是内存,只能是寄存器。
注意:移位操作SAL等等的操作数可以是立即数,也可以放在单字节寄存器%cl中,移位量是由%cl的低m位决定的,高位会被忽略。salb最多会移动7位,salw最多会移动15位,sall最多移动31位,salq最多移动63位。
SAL和SHL的效果是一样的。
intel引入16字节的数称为八字。
指令 | 效果 | 描述 |
---|---|---|
imulq S mulq S | R[%rdx]: R[%rax]<-S X R[%rax] R[%rdx]: R[%rax]<-S X R[%rax] | 有符号全乘法 无符号全乘法 |
clto | R[%rdx]: R[%rax]<- 符号扩展(R[%rax]) | 转换为八字 |
Idivq S | R[%rdx]: R[%rdx]<-R[%rax] mod S R[%rdx]: R[%rax]<-R[%rax] / S | 有符号除法 |
Divq S | R[%rdx]<- R[%rdx]: R[%rax] mod S R[%rdx]<= R[%rdx]: R[%rax] / S | 无符号除法 |
乘法必须把第一个参数放在%rax中,乘积高64位存放在%rdx,低64位存放在%rax。
Divq前需要cqto。
算术操作:leaq、inc、dec、neg、not、add、sub、imul、xor、or、and、sal、shl、sar、shr。其中只有leaq不设置条件码。
除了算术操作设置条件码,CMP和TEST也会设置条件码。
条件码通常不会被直接读取,但是可以有三种方法间接获取:(1)SET指令;(2)条件跳转;(3)条件传送。
SET指令可以把条件(逻辑值)赋给目的操作数。
注意指令具有同义名。(比如setg和setnle)
jmp可以直接和间接跳转——跳转目标可以是label,*(操作数)。比如jmp .L1, jmp *%rax, jmp *(%rax)。
条件跳转只能是直接跳转!!
跳转指令有几种不同的编码,但是最常用的是PC相对的(偏移量编码为1,2,4个字节)。也可以给出绝对地址,用4个字节直接指定目标。
条件传送指令源操作数为寄存器或内存,目的一定是寄存器。**不支持单字节的条件传送!!**条件传送不指定操作数长度,因为汇编器可以从目标寄存器推断(无条件传送不行,因为目的可能是内存)
基于条件传送的代码会对两个expr都求值,但是如果计算量都很大,可能性能也会下降。
jump to middle -Og; guarded-do -O1
跳转表的优点:执行开关语句的时间与开关情况的数量无关。GCC根据开关情况的数量和开关情况值的稀疏程度来翻译开关语句。
call指令和jmp指令的目的没什么区别。
对抗缓冲区溢出攻击的方法:栈随机化(空操作雪橇)、栈破坏检测(金丝雀)、限制可执行代码区域。
变长栈帧(alloca)使用%rbp作为帧指针(基指针,base pointer)。使用帧指针时,先将%rbp保存(callee saved)。
Chapter4
指令集体系结构:一个处理器支持的指令和指令的字节级编码。
程序员可见状态:15个程序寄存器(没有%r15)+3个条件码(没有CF)+ PC + 内存 + 状态码Stat
虚拟地址:字节数组 v.s. 物理地址:硬件和操作系统软件联合起来将虚拟地址翻译,指明数据实际存在内存中哪个地方
Y86地址计算不支持第二变址寄存器和任何寄存器值的伸缩。形如3(%rax)
注意指令中有常数字会变成8个字节然后再按小端法反序。
CISC | 早期RISC |
---|---|
指令数量很多。 | 指令数量少得多,通常少于100个 |
有些指令延迟很长。 | 没有较长延迟的指令(早期RISC甚至没有整数乘法) |
编码可变长度1到15字节 | 编码固定长度(4个字节) |
指定操作数方式很多样,比如内存操作数指示符 | 简单寻址方式。通常只有基址和偏移量寻址 |
可以对内存和寄存器操作数进行算术和逻辑运算 | 只能对寄存器操作数进行算术和逻辑运算,允许使用内存已用的只有load/store |
机器及程序实现细节不可见。ISA提供抽象 | 对机器级程序来说实现细节是可见的。 |
有条件码。并设置了一些特殊的标志位 | 没有条件吗。条件检测要用明确的测试指令 |
栈密集的过程链接。栈被用来存取过程参数和返回地址 | 寄存器密集的过程连接。寄存器被用来存取过程参数和返回地址。通常由更多寄存器(最多的有32个) |
RISC处理器产品(Sun Microsystem),IBM,Motorola(PowerPC)。ARM提出了自己的体系结构(Acorn RISC Machine),广泛应用在嵌入式系统中。
伪指令:.pos 0告诉汇编器应该从地址0处开始产生代码。(所有Y86-64程序的起点)。
Y86的汇编器:YAS
指令集模拟器:YIS(模拟机器代码程序的执行)
pushq %rsp压入%rsp的原始值再讲%rsp-8,popq将栈指针的值修改为内存读出来的值(先写入valE,后写入valM)。
逻辑门总是活动的,一旦一个门的输入变化了,在很短时间内输出就会变化。
HCL中所有字级信号都声明为int,不指定字的大小。
不要求选择表达式互斥,前面的没选中自动执行后面的。
向寄存器文件写入字是由时钟信号控制的。每次时钟上升时,valW上的值会被写入输入dstW上的寄存器ID指示的程序寄存器。当dstW设为0xF时,不会写任何程序寄存器。写端口可以每个时钟周期更新两个寄存器。
SEQ的实现包括组合逻辑和两种存储器设备:时钟寄存器(PC和Cnd)、随机访问存储器(RF、DataM、InsM)
由于只从指令内存读取数据,可以看做组合逻辑。(不需要时序)
PC/CND、DataM、RF通过一个时钟信号控制,除法将新值装载到寄存器以及将值写回随机访问存储器。
Icode,ifun要么等于从内存读出的值,或者当指令地址不合法时使其对应于nop。
寄存器文件支持同时进行两个读和两个写,每个端口有一个地址连接和一个数据连接。
根据icode以及rA、rB、Cnd,控制逻辑块srcA会选择相应的寄存器。
SEQ的控制逻辑单元:
更新PC:new_pc
访存:Stat/mem_read/mem_write/mem_addr/mem_data
执行:set_cc/aluA/aluB/alu_fun
译码写回:dstE/dstM/srcA/srcB
取指:instr_valid/need_valC/need_regids/icode/ifun
Chapter 6
SRAM可以onchip也可以offchip,作为高速缓存存储器(不超过几兆,6晶体管电路每单元,密集度低,贵,功耗更大,干扰不敏感)
DRAM用来作为主存以及图形系统的帧缓冲区(几百或几千兆,每个位存储为对电容的充电)
传统的DRAM:d个超单元,每个超单元由w个DRAM单元组成,每个超单元被组织成r行c列的长方形阵列。信息通过成为引脚的外部连接器流入和流出芯片,每个引脚携带一个1位的信号。每个DRAM芯片被连接到某个称为内存控制器的电路,这个电路可以一次传送w位到每个DRAM芯片或一次从每个DRAM芯片传出w位。为了读出超单元(i,j)的内容,内存控制器将行地址i发送到DRAM,然后是列地址j。行地址i称为CAS(Column Access Strobe)请求。RAS和CAS请求共享相同的DRAM地址引脚。
内存模块:DRAM芯片封装在内存模块中,它插到主板的扩展槽上。
增强的DRAM:
-
快页模式(FPM DRAM,fast page mode DRAM),允许行缓冲区(有点像cache hit)
-
扩展数据输出(EDO DRAM, extended Data Out DRAM),FPM DRAM的增强型是,允许各个CAS信号在时间上靠的更紧密。
-
同步DRAM(Synchronous DRAM, SDRAM),同步,更快!
-
双倍数据速率同步DRAM(DDR SDRAM),比SDRAM速度翻倍!
-
视频RAM(Video RAM, VRAM)用在图形系统的帧缓冲区中。思想和FPM DRAM类似。
非易失性存储器:ROM以它们能够被重编程(写)的次数和对它们进行重编程所用的机制来区分
- 可编程ROM(PROM)只能被编程一次。
- 可擦写可编程ROM(EPROM)紫外光
- 电子可擦除PROM(EEPROM)105
- 闪存,基于EEPROM(基于闪存:固态硬盘SSD)
**固件:**存储在ROM设备中的程序,通电后运行
访问主存:数据流通过总线(CPU<->系统总线<->IO桥<->内存总线<->主存)在处理器和DRAM主存之间来来回回。
磁盘存储:数量级可以达到几百到几千千兆字节,基于RAM的存储器只能有几百或几千兆字节。不过读信息的时间为毫秒级。
多区记录技术:柱面的集合被分割成不相交的子集,称为记录区,每个区包含一组连续的柱面。一个区中的每个柱面的每条磁道都有相同数量的扇区。
制造商以千兆字节(GB)或兆兆字节(TB)为单位表达磁盘容量,1GB=109字节,1TB=1012字节。
K,M,G,T这样的前缀含义依赖于上下文:DRAM和SRAM容量相关的计量单位,K=210,M=220,G=230,而T=240,而对于像磁盘和网络这样的IO设备相关的计量单位就不是这样。速率和吞吐量也是后者。
平均旋转时间是最大旋转延迟的一半。
连接IO设备:通过IO总线(比如Intel的PCI总线)连接到CPU和主存。系统总线和内存总线是CPU相关的,但IO总线不是。IO总线比它们蛮。
主机总线适配器将一个或多个磁盘连接到IO总线,两个最常用的磁盘接口是SCSI和SATA,SCSI更快,更贵,可以支持多个磁盘驱动器。
访问磁盘:CPU使用内存映射技术来向IO设备发射命令。在使用内存映射IO的系统中,地址空间中有一块地址是为与IO设备通信保留的。每个这样的地址成为一个IO端口。当一个设备连接到总线时,它被映射到一个或多个端口。比如说假设磁盘控制器映射到端口0xa0,CPU可能通过执行三个对地址0xa0的存储指令,发起磁盘读。磁盘控制器收到来自CPU的指令后将逻辑块号翻译成一个扇区地址,然后将内容直接传送到主存,不需要CPU的干涉。(直接内存访问DMA传送,direct Memory Access)DMA传送完成后,磁盘控制器通过给CPU发送一个中断信号来通知CPU。
SSD封装插到IO总线上标准硬盘插槽(USB或SATA),一个SSD封装由一个或多个闪存芯片和闪存翻译层。闪存芯片替代机械驱动器,闪存翻译层是一个固件/硬件设备,扮演磁盘控制器的角色,将对逻辑块的请求翻译成对地层物理设备的访问。
**读SSD比写SSD要快!**随机读和写的性能差别是由底层闪存基本属性决定的。一个山村由B个块的序列组成,每个块由P页组成,通常页的大小时512字节到4kb,块由32-128页组成,块大小为16kb到512kb。只有在一页所属的块被擦除之后,才能写这一页(通常是指该块中所有位都被设置为1)。不过一旦一个块被擦除后,块中每个页都可以不需要进行擦除就可以写一次。随机写很慢的原因:(1)擦除块需要较长的时间,比访问页高一个数量级;(2)如果写操作试图修改一个包含已有数据的页,那么这个块中所有带有用数据的页都必须被复制到一个新(被擦除过的)块。
存储技术趋势:SRAM级数的成本和心梗以相同的速度改善;DRAM和磁盘变化趋势更大,DRAM每兆字节成本下降了44000倍,DRAM的访问时间只下降了大约10倍。磁盘的成本跌了6个数量级,但访问时间提高得很慢。(事实:增加密度比降低访问时间容易得很多)
DRAM和磁盘的性能滞后于CPU的性能。SRAM滞后于CPU,但性能还是保持增长,DRAM和磁盘性能与CPU性能差距加大。现代计算机频繁地使用基于SRAM的高速缓存,试图弥补处理器-内存之间的差距。
寄存器->L1高速缓存->L2高速缓存->L3高速缓存->主存(DRAM)->本地二级存储(本地磁盘)->远程二级存储(分布式文件系统、Web服务器)
Corei7高速缓存的基本特性:
高速缓存类型 | 访问时间(周期) | 高速缓存大小(C) | 相联度(E) | 块大小(B) | 组数(S) |
---|---|---|---|---|---|
L1 i-cache | 4 | 32KB | 8 | 64B | 64 |
L1 d-cache | 4 | 32KB | 8 | 64B | 64 |
L2统一的高速缓存 | 10 | 256KB | 8 | 64B | 512 |
L3统一的高速缓存 | 40-75 | 8MB | 16 | 64B | 8192 |
影响:
- 较大的高速缓存可能提高命中率,可能增加名和总时间。
- 较大的块能利用空间局部性,不过对于给定的高速缓存大小,块越大意味着高速缓存行数越少,会损害时间局部性比空间局部性更好的程序命中率。较大的块对不命中处罚也有负面影响,因为块越大传送时间越长。
- 较高的相联度降低了抖动的可能性,不过成本会增加,速度降低,标记位更多,需要LRU状态位和额外的控制逻辑。还会增加不命中处罚(牺牲行的选择复杂性增加)
- 直写高速缓存比较容易实现,能使用独立于高速缓存的的写缓冲区。