《深入理解计算机系统》笔记
Posted duuuuu17
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《深入理解计算机系统》笔记相关的知识,希望对你有一定的参考价值。
本篇主要以《深入理解计算机系统》黑皮书阅读笔记做主要内容,现在是看书的第一遍:
导论
操作系统的概念
本章用于介绍本书的内容大致,其中若有概念不清楚,需要自己去网上搜集资料弄明白。
信息和上下文
不管什么文件终究被翻译为二进制文件供计算机执行。
系统接口(也就是有时候常说的C标准库)(或者说是"系统调用")
编译系统
分四个阶段:预处理阶段 —— 编译阶段 ——汇编阶段 —— 链接阶段
预处理阶段
由预处理器主导,将代码中那些引用类型的代码替换为目标代码,在C语言中主要是将宏处理(比如将宏代码,用起目标文件或代码块插入源代码中的所需位置,完成后,会生成后缀名为“.i”的相比于源文件是一个扩展的文本文件。
编译阶段
由编译器主导,将上一阶段的“.i”文本文件翻译成汇编程序(也是文本文件):后缀为".s"
汇编阶段
将上一阶段的汇编程序,翻译成对应的机器语言指令(二进制文件“.o”),并打包成“可重定位目标程序”格式,传递给下一个阶段。
链接阶段
将上一阶段传递的二进制文件,且将它所需的标准库的函数以及其他,所对应的提前预编译好的二进制文件,通过链接器进行合并,生成可执行文件“.exe"。
系统硬件组成
总线
传输定长的字节块(信息字节)并负责各部件之间的信息的传递。且传输信息的单位被称为字。
字
被设计为传输信息的定长字节块。
字中的二进制位数被称为字长。
通常来说就是处理器一次性能处理数据的长度,例如:64位计算机指CPU一次性能处理64位的数据,其机器字长为64位(二进制数),对应于8个字节。
I/O设备
系统与外部世界的联系通道。每个I/O设备都通过控制器或适配器与I/O总线相连。
控制器
用于控制和管理外部设备。
是I/O设备本身,或者是主板上的芯片组。
作用
与处理器或主板的其他部分通过总线进行通信。设备控制器负责将处理器发出的指令翻译成外部设备能够理解的命令,并且在必要时将外部设备的数据转换为适合处理器使用的格式。此外,设备控制器还负责管理外部设备的状态、控制数据流以及处理各种错误和异常情况。
适配器
是插在主板插槽上的卡。
作用
提供一个系统与外部设备之间通信的接口
主存
用于临时存放程序和程序需要处理的数据。(内存通常指的是DRAM,CPU寻址的基本单位为字,最小单位为字节)
逻辑上
存储器是一个线性的字节数组,每个字节都由唯一的地址。
实际上
我们使用c语言时,查看一个变量所占内存空间。
其解释为:程序所组成的机器指令其对应的不同数量的字节,来构建在存储器上地址。
处理器
其核心是一个大小为一个字的存储设备(或寄存器)称为PC(程序计数器)。当然,还有ALU(算术/逻辑单元)。指令的执行,围绕以下设备:主存、寄存器文件、ALU。
每个寄存器有唯一的名字,通过ALU计算得到新的数据和地址值。
实际上CPU就是由许多的寄存器组成。
寄存器
由多个触发器或者锁存器组成的电路。N个触发器或者锁存器就可以组成一个N为的寄存器。保存N位的数据。
程序计数器(PC)
一块较小的内存空间,用于存贮下一条指令所在单元的地址。
指令寄存器(IR)
存储当前正在执行指令。
作用
有限存存贮容量的高速存贮部件。可用于暂存指令、数据和地址。
CPU操作
加载
从主存复制一个字节或字到寄存器,用于覆盖寄存器原来的内容。
存储
从寄存器中复制一个字节或字到主存的某个位置,用于覆盖主存对应位置的原来的内容。
操作
把两个寄存器的内容复制到ALU中,ALU并对其进行计算,其结果存放在一个寄存器中并覆盖原有内容。
跳转
从指令中抽取一个字,并将它复制到程序计数器中,用以覆盖PC中原有的值。
处理器的指令架构
处理器所支持的指令集合和指令的执行方式。它定义了处理器如何读取、解释和执行指令,以及支持哪些操作、数据类型和地址模式等。常见的指令架构有 x86、ARM、MIPS 等。不同的指令架构对应不同的硬件平台和操作系统,需要在编写程序时考虑指令集的兼容性和性能。
处理器的微体系结构
描述处理器的实际如何实现和内部结构。
微体系结构的设计对处理器的性能和功耗有着非常大的影响。一些关键的微体系结构特性,比如指令流水线、乱序执行、分支预测、多核处理等,对于处理器的性能和能耗都有着很大的影响。
hello程序的执行
1.键盘上输入./hello
其信息通过I/O设备的输入经过I/O桥,到达于CPU内部的总线接口,传输给寄存器存,寄存器再存储到主存中
2.回车,执行指令
指令解释器对我们输入的指令进行解释,并调用系统指令——加载hello的可执行文件于主存中。
3.CPU处理可执行文件
当可执行文件被加载完成到主存后,CPU将对可执行文件的指令进行执行,并输出到默认的I/O设备中。
cache(高速缓存器)
存放处理器常用指令或者信息,用于解决CPU与主存之间访问速度差异。
L1、L2高速缓存
是SRAM(静态随机访问存储器),L1被访问的速度最快。
存储器
有内存和外存所组成
内存就是主存、外存就是磁盘、固态硬盘等辅助存储器(适合数据存储、不适合频繁读写)。
存储器思想是层次结构:上一层的存储器为低一层存储器的高速缓存,即寄存器文件保存取自L1高速缓存存储器的字。
操作系统
操作系统位于应用程序和底层资源的中间层。操作系统通过抽象的方式管理硬件。
操作系统管理资源方式
通过文件的形式管理I/O设备:
管理I/O设备可以使用设备文件,设备文件是在文件系统中的一种特殊类型的文件,用于与I/O设备进行通信。操作系统通过标准的文件操作函数来进行I/O操作,使用设备驱动程序来控制硬件设备的访问。
在文件I/O中,设备驱动程序是实现文件与硬件设备之间转换的核心部分。设备驱动程序提供了操作系统和硬件设备之间的接口,将文件I/O操作转换为硬件I/O操作,并控制硬件设备的访问。每个设备都有自己的设备驱动程序,不同类型的设备有不同的设备驱动程序。
目的
将I/O设备与应用程序解耦,从而提高系统的灵活性和可维护性。此外,设备文件还可以被权限管理和访问控制所保护,从而增强了系统的安全性。
通过虚拟内存的形式管理主存和I/O设备:
操作系统通过虚拟内存技术可以将主存分成一系列大小相等的页(Page),将磁盘空间分成一系列大小相等的页框(Page Frame),并将它们之间进行映射。当进程需要访问主存中的某一页时,操作系统会将这一页从磁盘中加载到一个空闲的页框中,并建立页表中的映射关系。在进行内存访问时,操作系统会将逻辑地址转换为物理地址,这样进程就可以通过逻辑地址来访问物理内存了。
在访问I/O设备时,操作系统可以将I/O设备的地址空间映射到进程的虚拟地址空间中,这样进程就可以通过和访问主存一样的方式来访问I/O设备了。这种映射方式被称为内存映射I/O(Memory-Mapped I/O)。
通过进程来管理CPU、主存和I/O设备
操作系统通过进程来管理CPU、主存和I/O设备的方式通常称为进程管理。每个进程都是操作系统中的一个独立实体,拥有自己的虚拟地址空间、代码、数据和堆栈。进程管理的目标是为每个进程分配资源,确保进程能够执行,并在必要时提供资源的保护和共享。
在进程管理中,操作系统使用调度算法来决定哪个进程将获得CPU时间片。当一个进程需要访问主存或I/O设备时,它会向操作系统发送请求。操作系统通过相应的调用将请求传递给硬件设备,并在操作完成后将结果返回给进程。
操作系统使用虚拟内存管理机制来管理主存。虚拟内存允许操作系统将进程的虚拟地址空间映射到物理内存上,并根据需要将虚拟地址空间的部分换入或换出物理内存。这种方式使得每个进程都可以访问大于物理内存的地址空间,提高了系统的灵活性和效率。
通过虚拟机形式管理所有软硬件资源
操作系统通过虚拟机监控器(VMM)的形式来实现虚拟化技术,进而实现对所有软硬件资源的管理。VMM是一个在物理机器上运行的软件层,它允许在同一台物理机器上运行多个虚拟机。每个虚拟机拥有自己的操作系统和应用程序,它们之间相互独立,好像在不同的物理机器上运行一样。
在这种模式下,操作系统通过VMM来管理所有虚拟机的资源,包括CPU、主存和I/O设备。VMM会将物理机器的资源虚拟化为多个虚拟机可以使用的资源,并为每个虚拟机提供独立的虚拟环境,使它们可以独立运行,而不会相互影响。
通过虚拟机的形式,操作系统可以更好地管理和利用物理机器的资源,提高资源利用率和效率,同时也提高了系统的可靠性和安全性。
操作系统的基本功能
1)防止硬件被失控的应用程序滥用
2)向应用程序提供简答一致的机制来控制低级硬件设备。
进程
对正在运行的程序的一个抽象。(或者说是一个程序的实例化)
并发运行:
一个进程的指令和另一个进程的指令交错执行。
上下文切换:
CPU交错执行进程的行为。
在现实中,操作系统把CPU控制权转给某个新进程是,就会进行上下文切换。
上下文:
进程运行所需要的状态信息。操作系统保持跟踪所有进程运行状态。
内核
管理进程的进行,它作为操作系统代码常驻主存的部分。是系统管理全部进程所用代码和数据结构的集合。
线程
作为进程的执行单元,比进程更容易共享资源。是程序代码或数据的一段连续片段。
特点
是现今重要的编程模型。
与进程的关系
由一个或多个线程构建成一个进程。
虚拟内存
以一个字来编码。
进程的虚拟地址空间,其模块从低向上为:
程序代码和数据
进程的代码都是同一固定地址开始,然后是存放全局变量。
代码和数据区有可执行目标文件的内容初始化。
堆
动态地扩展和收缩。
共享库
存放共享的代码和数据的区域。
栈
位于用户虚拟地址空间顶部,用于存放函数调用。
内核虚拟内存
只对内核开放的区域。
虚拟内存的基本思想:
将进程的虚拟内存的内容存放在磁盘上,然后主存作为磁盘的高速缓存。
文件
字节序列
系统之间利用网络通信
通过网络,我们将单个电脑孤岛连接起来,从而达到更有用的功能(资源共享,数据通信。
远程登录执行指令:
重要概念
并行和并发
并发:
指同一个时间内具有多个进程需要利用时间片进行交错执行。
并行:
指同一个时间内,有多个进程同时执行,(常用于多核处理器中)。
作用
利用并发使一个系统运行得更快。
线程级并发
构建在进程的基础之上,指多个线程同时执行,每个线程独立执行不同的任务,通过线程之间的协调和同步来实现多任务并发。
作用
在多核处理器上,线程级并发可以通过将线程映射到不同的处理器核心上来实现,从而进一步提高程序的执行效率。线程级并发也是并行计算的基础,通过并行计算可以加速很多计算密集型任务的执行。
问题
需要注意的是,线程级并发也带来了一些问题,如竞态条件、死锁、饥饿等,需要合理的线程调度和同步机制来避免这些问题的出现。
超线程技术:
通过在一个物理处理器内部虚拟出多个逻辑处理器,从而使得一个物理处理器可以同时运行多个线程,提高了处理器的并行度,提升了处理器的性能。
单核处理器系统:
同一个时间只能执行一个任务。
多核处理器系统:
同一个时间能够执行多个任务。
多核处理器结构
指令级并发
使用,达到同时执行多条指令的属性。(标量:处理器一个周期执行一条指令)
技术举例
流水线技术
它可以将指令执行过程分为多个阶段,并且在每个时钟周期内执行一个阶段。超流水线通常包含取指、译码、执行、访存和写回等多个阶段。
超标量执行
它可以同时从指令流中选择多个指令并且并行执行这些指令。超标量执行通常使用多个指令调度单元(Instruction Dispatch Unit,IDU)来选择并行执行的指令,同时使用多个执行单元来执行这些指令。
动态执行
它可以根据程序的运行情况来选择和执行指令。动态执行通常使用分支预测器和数据相关性检测器来帮助选择和执行指令。
单指令、多数据并行(并行计算方法)
指一条指令可对多个数据执行相同的操作
信息的表示和处理
本章研究三种数字表示方式:无符号、补码、浮点数。
信息存储
字节
为最小的可寻址的内存单位。
地址
内存中的每一个字节都由唯一的数字标识。地址的集合称为虚拟地址空间。
地址规则:
对象存储的连续字节序列的常见规则,如下:
程序对象
程序数据、指令和控制信息。
字数据大小
字长*
指明指针数据的标称大小。其虚拟地址按一个字长编码(地址范围:\\(0 \\sim 2^w-1\\))。
32位字长机器、64位字长机器,都表示的是一个字长为:32位、64位。
一个字长为w位的机器,其程序最多可访问\\(2^w\\)个字节。CPU一次性可处理数据位数的大小
表示字符串
十进制数x的ASCII码郑海伟0x3x,而终止字节的十六进制为0x00。
字节顺序与字大小无关。ASCII码作为字符码在任何系统上的结果相同。
不同的机器类型,其使用的指令和编码方式不同。二进制代码很少能以至于不同的操作系统下的机器之间。
布尔代数
1(true) 0(false)
位向量
就是固定长度为w的二进制串。其中位向量可与\\(2^i\\)的状态数进行对应。(类似数组下标对应元素)
布尔环
位向量使用位级运算符后的结果。
加法逆元
\\(a \\wedge a=0\\),注意:此处的\\(\\wedge\\)不是离散中二元运算符的与运算符
利用异或(^)逆元交换两数
异或常用于取两数的二进制形式下不相同处做公共部分,通过再次异或会进行两数交换。
//利用异或(^)先取两数不相同部分,有点类似于特征提取
a = a^b; // sum = a+b
//通过不相同部分与原数的异或,将对方的特征获取到自身上
b = a^b; // b = sum -a
a = a^b; // a = sum -a
//例:
//a = 3 0011
//b = 5 0101
//a = a^b = b-a = 6 0110
//b = a^b = 3 0011
//a = a^b = 5 0101
C语言中的逻辑运算
需要区分逻辑运算符和位运算符
逻辑及运算符:&&,|| ,!
按位逻辑运算符: &(与),|(或),~(取反),^(异或),左移(<<),右移(>>)
tips:
其中按位右移,需要注意逻辑右移还是算术右移。
逻辑右移
最高位往右移动以后,旧最高位现所处位置的左边全补0;
例:(1111 0000) >>4 (0000 1111)
算术右移
左端全部补原符号位。
例:(1000 0000 ) >> 4 ( 1111 1000)
在C中使用$k\\ mod\\ w $来确定位移数。书中提示到,现今所有编译器和机器组合都对符号数使用算术右移。
整数表示
数据类型
C Data Type | Typical 32-bit | Typical 64-bit |
---|---|---|
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 4 | 8 |
float | 4 | 4 |
double | 8 | 8 |
pointer | 4 | 8 |
函数表
后续会使用
正数
\\(\\sum_i=1^n2^i\\)
正数的补码是本身。
负数
负数通常在计算机中以补码形式(或者无符号数形式)表示。
负数在补码形式下计算方式:\\(-x_w-1·2^w-1为最高位符号位+剩余位数(\\sum_i=0^w-2x_i·2^i)=负数\\)
小总结:
正整数和零的补码为本身。
负数的补码是它的相反数(负数的补码的补码是自己):-1 : 1111 1111 1: 0000 0001(两数的二进制相加等于0)
或者说
负数的绝对值与它的相反数之和为\\(2^w\\)。当w=8时,\\(|-128|+128=2^8=256\\)
使用一下代码可进行有符号与无符号的转换调试加深理解
#include<stdio.h>
int main()
unsigned char b = 128; //128U = -128 255U = -1
printf(" % d", (char)b);
//无符号下:0~2^w-1 为正,2^w-1-1 ~ 2^w-1为负
无符号数编码
\\(B2U_w=\\sum^w-1_i=0x_i2^i\\),w为字长。且与\\(U2B_w\\),互为逆函数。
\\(UMin = 0\\) , \\(UMax = 2^w -1\\)
\\(UMax= 2|Tmax|+1\\)
补码编码
\\(B2T_w=-x_w-12^w-1+\\sum^w-2_i=0x_i2^i\\),其与\\(T2B_w\\)互为逆函数
\\(TMin = -2^w-1\\),\\(TMax = 2^w-1-1\\)
\\(|Tmin| = |TMax| + 1\\)
反码
\\(B2O_w(\\vecx)=-x_w-1(2^w-1-1)+\\sum^w-2_0x_i2^i\\)
原码
其作用是确定乘下位应该取正还负值。
\\(B2S_w(\\vecx)=(-1)^x_w-1·\\sum^w-2_0x_i2^i\\)
补码与无符号数的相互转换
补码与无符号的转换实际就是有符号数转换为无符号数
补码转无符号数
满足\\(TMin\\le x \\le TMax\\)的x有:
(ps:\\(Tmin = -2^w-1,TMax=2^w-1-1\\),w为字长)
\\(T2U_w(x) = \\left \\ \\beginarrayccc x+2^w, & x < 0 \\\\ x, &x\\ge0 \\endarray \\right.\\)
无符号数转补码
\\(U2T_w(u)= \\left \\ \\beginarrayccc u-2^w,&U>TMax \\\\ u, &U\\le TMax \\endarray \\right.\\)
总结(重要):
无符号下 \\(0\\backsim2^w-1-1 为正数,\\\\ 2^w-1 \\backsim 2^w-1为负数\\)且正数和负数的表示范围都是根据数从小到大排序
例如:\\(当w=4时,正数:0 \\backsim 7=0\\backsim2^3-1,\\\\负数: -8\\backsim-1=2^3\\backsim2^4-1\\)
负数时
当补码转无符号数时,T为负数,则表示它的无符号数形式应在\\(2^w-1-1\\)之后,又因为无符号数是跟符号数从小到大所对应(或者说所对应编码),所以\\(x+2^w\\)即可。
当无符号数转补码时,当\\(U> TMax\\)时,表示该无符号所对应的符号数为负数,所以\\(u-2^w\\)。
正数时
无论怎么转换都是自身。
tips:
1.
当有一个运算数是无符号时,另一个运算数也被隐式强制转换为无符号。
2.
在C语言的limits头文件的源代码中,其中有符号int的最大值和最小值表示:
\\(INT\\_Max= 2^32-1\\)\\\\int默认4个字节。而\\(INT\\_Min = -INT\\_Max-1\\)
3.应熟知
常见的机器字长下的TMin和TMax的一张表:
机器字长 | TMin | TMax | UMax |
---|---|---|---|
8 | -128 | 127 | 255 |
16 | -32768 | 32767 | 65535 |
32 | -2147483648 | 2147483647 | 4294967295 |
64 | -9223372036854775808 | 9223372036854775807 | 18446744073709551615 |
需要注意的是,这里的TMin和TMax都是针对带符号整数的取值范围。UMax是无符号整数的取值范围,它的取值范围是从0到\\(2^w-1\\),其中w是机器字长。
4.
因为计算机是以无符号数的存储表示数字,那么当我们看到符号位为1时,那么这个位向量为负数,当符号位为0时,那么这个位向量为正数。
二进制位数扩展或缩短
扩展位数
从4位扩展到8位二进制位:0001 --> 0000 0001
无符号数
其符号位为0,直接补全前面拓展位
补码
其符号位为1,直接补全前面拓展位
例子
short sx = -12345;//负数以补码形式表示
//前面说到,负数的补码就是其相反数,也就是说它的相反数取反再加1就是-12345的表示形式
unsigned uy = sx;//(unsigned int)
//uy的赋值过程中:sx先进行拓展为int,然后再转为无符号数
cf c7 --> ff ff cf c7
注:ff ff cf c7
无符号整数表示:
将 ff ff cf c7 按照十六进制转换成十进制,得到的结果为 4,294,836,727。
有符号整数表示:
首先,将 ff ff cf c7 看作是一个补码,即将它的二进制表示形式看作是一个负数的补码。然后,将它转换为有符号整数表示,即将它的补码表示形式转换为十进制表示形式,步骤如下:
1.首先,将 ff ff cf c7 转换成二进制形式,得到 11111111 11111111 11001111 11000111。
2.然后,确定这是一个负数的补码,即最高位为符号位,为 1。
3.将这个二进制数减去 1,得到 11111111 11111111 11001111 11000110。
4.对得到的结果取反,得到 00000000 00000000 00110000 00111001。
5.将得到的二进制数转换为十进制数,得到 -12345。
因此,ff ff cf c7 表示的有符号整数为 -12345。
//负数的表示形式可以看作是它相反数的取反再加1的形式。
截断数字
截断无符号数
当x为十进制数时:
\\(x\\prime = x \\ mod\\ 2^k ,x\\prime为截断后的无符号数,w-k为截断位数\\),而k为截断后剩余位数。
例:十进制数157 的二进制数:1001 1101 截断为6位,157 % 2^6 = 29 截断后的二进制数为:01 1101.
当x为无符号数的二进制形式
截断到k位,左移k位即可。
截断无符号数会丢失高位数值,损失精度。
截断补码数值
当x为十进制数时:
\\(U2T_k(x\\prime=x\\ mod\\ 2^k)\\),截断到k位。
当x为无符号数的二进制形式
假设我们有一个n位的补码数值x,我们希望将它截断为m位。先保留符号位,如果m<n,则将x左移n-m位,然后再右移n-m位,就可以得到x的低m-1位截断结果。如果m>n,则将x右移n位,然后再左移n位,就可以得到x的符号扩展结果。
例子:
- 对于无符号数 0b10101010,如果我们要将它截断为 4 位数,则结果为 0b1010。
- 对于补码数 -10,如果我们要将它截断为 3 位数,则需要先将它转换为补码二进制表示形式 0b11110110,再将其截断为 3 位数得到 0b110,最后将其转换回十进制补码形式为 -2。
- 对于无符号数 0xff,如果我们要将它截断为 7 位数,则结果为 0x7f。
- 对于补码数 -128,在截断为 6 位数时需要考虑到最高位是符号位,所以需要先将其转换为无符号数 128,再将其截断为 6 位得到 0b100000,最后将其转换回十进制补码形式为 -32
有符号数和无符号数编程时会遇到的问题
当我们使用C/C++时,会使用到size_t,其默认为unsigned long类型。
有时候需要整型作循环条件或者判断条件,其返回值,若从理论上,我们是要有负值的,但是因为无符号数运算其结果仍为无符号数,是不会有返回负值的情况,所以这个时候使用size_t或者无符号数需要仔细考虑数据类型的替换。
其次,当我们不需要使用到有关负值时,我们传递给size_t的时候,一定要切记不要传递给他负值,不然会造成意想不到的情况(内存访问越界、无法指定正常大小内存范围等等)
整数运算
\\(+_w^u\\)表示把两数相加的和,截断到w位得到的结果。
无符号数加法
\\(x+_w^uy=\\left \\ \\beginarrayccc x+y,& x+y < 2^w&正常 \\\\ x+y-2^w, &2^w\\le x+y<2^w+1& 溢出 \\endarray \\right.\\)
表达式图示:
当无符号数x,y相加大于表示的整数长度时,我们一般会将其限制在\\(0\\backsim2^w\\)范围内,我们一般有以下几种方法:
1.\\(x+y-2^w\\);
2.\\((x+y)\\ mod\\ 2^w\\);
3.\\(x+y的二进制数,丢弃扩展后大于2^w-1的权值(也就是大于第w-1后的二进制位舍去)\\)
加法逆元
对于w位的无符号数的集合,每个x,必有某个值\\(-_w^ux满足,-_w^ux+\\ _w^ux=0\\)
其计算:
\\(-_w^ux=\\left \\ \\beginarrayccc x,&x=0 \\\\ 2^w-x, &x>0 \\endarray \\right.\\)
检测无符号数加法中的溢出
补码加法
因为补码表示的是有符号位范围(\\(-2^w-1\\backsim 2^w-1-1\\)),此处我们也使用了和的截断方法(也就是\\(+2^w或者-2^w\\)).
\\(x+_w^ty=\\left \\ \\beginarrayccc x+y-2^w,& x+y \\ge 2^w-1&正溢出(情况四) \\\\x+y,&-2^w-1\\le x+y<2^w-1 & 正常(情况三和二)\\\\x+y+2^w, &x+y<-2^w-1&负溢出(情况一) \\endarray \\right.\\)
该表达式图示如下:
检测补码加法中的溢出
补码的非
也就是补码的位数取反,那也就是,当\\(x\\ge TMin_w\\)时,x的相反数。(补码的加法逆元)
表达式:(\\(Tmin_w\\le x\\le TMax_w\\))
\\(-_u^tx=\\left\\\\beginarrayccTMin_w,&x=TMin_w\\\\ -x,& x\\le TMin_w \\endarray\\right.\\)
在位级上,从右往左,第一个1的权值保留标记位位置k,然后位置k的左边全部取反就是补码的非。
无符号乘法
此处使用截断法,截断为w位。即将数与\\(2^w\\)求模。
补码乘法
对无符号数乘除以常数
通常来说,如果被限制在一个范围内(如:\\(2^w\\)),则使用截断法,来限制数所在区域(\\(x\\ mod\\ 2^w\\))
2的幂乘
\\(x\\times2^k= oBx << k\\),也就是说,相当于x的二进制进行左移k位
//举例:
(a<<b)+a = a*b;
//形式A:
(x<<n)+(x<<(n-1))+...+(x<<m);
如:14*x;
∵14=2^3+2^2+2^1 => ∴14*x=(x<<3)+(x<<2)+(x<<1);
//形式B:
(x<<(n+1))-(x<<m) => 14*x=(x<<4)-(x<<1);
(1<<K):表示\\(2^k\\)
2的幂除
\\(x\\div2^k= oBx >> k\\),也就是说,相当于x的二进制进行逻辑右移k位
对补码乘除以常数
2的幂乘
\\(x\\times2^k= oBx << k\\),也就是说,相当于x的二进制进行左移k位
2的幂除
当x为正数时,\\(x\\div2^k= oBx >> k\\),也就是说,相当于x的二进制进行右移k位
当x为负数时,此时进行2的幂除,相当于负数的截断右移(算术右移),也就是先保留符号位,进行右移k位,再补上符号位在最高位。
负数时,向下取整
其\\([x/2^k]\\),也就是算术右移k位。
负数时,向上取整
书中提到了偏移量,是为了将原数+加上偏移量(这个偏移量一般根据位移数k,加上\\(2^k-1\\)即可)后,直接除\\(2^k\\)能整除。
\\((x+y-1)/y\\)对应\\([x/y]\\),x,y为整数。
当\\(y=2^k\\)时。
使用\\((x+(1<<k)-1)>>k\\)公式,x为补码值,k为位移量,对应\\([x/2^k]\\)。
x<0 ? (x+(1<<k)-1 : x) >> k : x;
//x判断是否为补码(负数的存储形式),后面是将公式改写为计算机表达式
浮点数(需要重复看)
常见小数部分,不同指数位下,对应的十进制和二进制值:
指数位 | 小数部分 | 二进制表示 |
---|---|---|
-1 | 0.5 | 0.1 |
-2 | 0.25 | 0.01 |
-3 | 0.125 | 0.001 |
-4 | 0.0625 | 0.0001 |
-5 | 0.03125 | 0.00001 |
-6 | 0.015625 | 0.000001 |
-7 | 0.0078125 | 0.0000001 |
-8 | 0.00390625 | 0.00000001 |
浮点数的分数形式转浮点数的二进制:
自我总结:
\\(\\fracab为浮点数分数\\)
\\(当转换二进制前,先将a\\div\\ b=k(取整数部分),a\\mod b=d(剩余需要表示的小数部分)\\)
\\(处理后\\fracab \\Rightarrow k\\fracdb,以下是根据\\fracdb写出小数部分:\\)
\\(b:作为小数部分的二进制一共位数,d直接算二进制,注意此时的d,b都为无符号数二进制。\\)
解释:因为我们进行处理后,d、b都是在同一表示维度下,d、b都为无符号数,b只是限制了小数二进制有多少位。
例:\\(\\frac2516\\Rightarrow 1\\frac916\\)
\\(b=2^4\\therefore小数二进制位数为4, d=9,二进制表示为1001\\)
\\(故小数部分的二进制表示为:0.1001,总:1.1001\\)
IEEE浮点数表示
表示格式
在内存存储的逻辑形式:
S符号位(1为负,0为正) | E阶码(指数转换而来) | 尾数M |
---|
\\(阶码E = [e(转换进制并使用科学计数法后,阶码的指数e) + bias(偏移值)]_b\\)
也可以说计算机使用移码形式存储阶码
\\(bias= 2 ^ n-1- 1 (n∈阶码总表示位数)\\)
bias的作用是使指数能够被表示为一个无符号整数。
不同精度下:阶码和尾数占位长度不同。
对于单精度浮点数,s 占用 1 位,f 占用 23 位,e 占用 8 位,bias 为 127;
对于双精度浮点数,s 占用 1 位,f 占用 52 位,e 占用 11 位,bias 为 1023。
规定
例:
1.
在单精度下,有一个-0.333333(10),使用IEEE存储形式:
$-0.333333(10) = 0.0101010101.....(2) = 1.010101... * 2^-2 (2) $
S符号位:1(共1位) | E阶码:-2+127 =125(10) 0111 1101(共8位) | 尾数M: 01010101...(共23位) |
---|
M:尾数默认省略小数点前的1,只保存小数点后的。
2.
1.5的二进制表示为1.1,将其规格化为\\(1.1 \\times 2^0\\)形式。
IEEE 754浮点数表示:
S符号位:0(共1位) | E阶码:0+127 =127(10) 0111 1111(共8位) | 尾数M: 00000...(共23位) |
---|
规格化形式的浮点数表示为:
\\((-1)^s \\times 1.f \\times 2^(e - bias)\\)
其中,s 为符号位,f 为尾数,e 为指数,bias 为偏置值。
规格化形式的浮点数中,尾数 f 的第一位默认为 1,因此可以用 23(对于单精度)或 52(对于双精度)位来表示实际的小数部分。
在规格化形式中,指数 e 的范围通常为 1 到 254(对于单精度)或 1 到 2046(对于双精度).
非规格化形式的浮点数表示为:
\\((-1)^s \\times 0.f \\times 2^(1 - bias)\\)
其中,s 为符号位,f 为尾数,bias 为偏置值。
而非规格化形式的浮点数中,尾数 f 的第一位为 0,因此可以用 22(对于单精度)或 51(对于双精度)位来表示实际的小数部分,这些位也包括了小数点前面的 1。
在非规格化形式中,指数 e 的范围为 0(代表 0 或者非常小的数)到 1(代表非规格化的最大值)。
非规格化形式的浮点数通常用于表示接近于 0 的很小的数,例如,对于单精度浮点数,最小的正非规格化值为 \\(2^-149\\),约为 \\(1.4 \\times 10^-45\\)。
总的来说,规格化和非规格化形式都是 IEEE 754 标准中用来表示浮点数的形式,规格化形式的浮点数通常用于表示较大的数,而非规格化形式的浮点数通常用于表示接近于 0 的很小的数。
为什么非规格化的阶码是1-bias?
在IEEE 754标准中,规格化的指数范围是 \\(1\\leq E \\leq 2^k-2\\),其中 \\(k\\) 是指数部分的位数,\\(E\\) 是指数的值。对于非规格化的数,它的指数部分全为0,即 \\(E=0\\),为了避免规格化和非规格化的指数相同,引入了一个偏置值 \\(bias\\),其中规格化数的偏置值为 \\(2^k-1-1\\),而非规格化数的偏置值为 \\(2^k-1-2\\)。
因此,非规格化的指数值是 \\(E=0\\),对应的真实指数是 \\(E- bias= 0-(2^k-1-2)=1-bias\\)。这也是为什么非规格化数的指数部分有一位是0,可以表示比规格化数更小的数,但是相应的精度也会更低。
特殊值
NaN
表示一个不是数字的值,指数全为1,尾数不全为0
无穷大
指数位全为1,尾数位全为0,符号位的正负表示,正负无穷大
非规格化中的0
在非规格化中,除了符号位表正负,其余的指数位和尾数位全为0,其表示\\(-/+0\\)
如何判断规格化和非规格化
规格化的尾数至少有一位为1,且阶码不全为0.
非规格化的尾数至少有一位为0,且阶码全为0.
结论
整数值与单精度浮点值的二进制对比:
舍入
主要记方法。
浮点运算
浮点运算是指对浮点数进行数学运算的过程,包括加、减、乘、除等基本运算,以及开方、对数、三角函数等高级运算。
在计算机中,浮点数通常以二进制表示,因此浮点运算也需要按照二进制进行。在进行浮点运算时,需要先将参与运算的浮点数转换为相同的指数,然后对其进行基本运算,最后对结果进行舍入和规格化。
舍入是指将结果调整为符合浮点数表示精度的最接近值的过程。规格化是指将结果转换为符合规格化浮点数表示要求的形式,即将尾数部分调整为一个1后跟一系列0的形式,并根据情况调整指数部分。
需要注意的是,浮点运算存在精度误差问题,这是由于浮点数的表示精度有限导致的。因此,在进行浮点运算时,需要考虑到精度误差的影响,并采取相应的措施进行处理。
例子:
假设有两个浮点数 a = 2.5 和 b = 1.25,现在要计算 a+b 的值。
首先,需要对 a 和 b 进行对阶。
假设 a 的指数为 0b100,尾数为 0b01000000000000000000000;
b 的指数为 0b011,尾数为 0b01000000000000000000000。
为了让 a 和 b 的指数对齐,需要将 b 的指数增加 1,同时将尾数右移一位,即将 b 转换为指数为 0b100,尾数为 0b00100000000000000000000 的浮点数。
然后,需要将 a 和 b 的尾数相加,得到 0b01100000000000000000000。由于尾数位数有限,需要对结果进行规格化,即将尾数左移一位,同时将指数增加 1,得到指数为 0b101,尾数为 0b10000000000000000000000 的浮点数。
最后,将结果进行舍入,如果舍入后的值不能用浮点数表示,则需要进行溢出处理。
所以,a + b 的结果为 4.
C语言中的浮点数
从 int 转换成 float,数字不会溢出,但是可能被舍入
从 int 或 float 转换成 double,因为 double 有更大的范围(也就是可表示值的范围), 也有更髙的精度(也就是有效位数), 所以能够保留精确的数值。
从 double 转换成 float,因为范围要小一些,所以值可能溢出成\\(+\\infty\\)或者\\(-\\infty\\)另外,由于精确度较小,它还可能被舍人。
从 float 或者 double 转换成 int,值将会向零舍入。-1.999转为-1,1.9999转为1.
家庭作业和练习题需要慢慢做
程序的机器级表示
IA64就是x86-64
程序编码
gcc -Og -o p code.c
-Og指定编译器源代码生成机器代码的优化等级。(通常使用-O1或者-O2)
在汇编阶段,生成的.o文件具有可重定位目标属性,供链接阶段的其他预编译文件或者库文件链接。
机器级代码
第一种指令集体系结构或指令集架构(ISA)来定义机器级程序的格式和行为,它定义了处理器状态、指令的格式,以及每条指令对状态的影响。
第二种抽象是,机器级程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组。
被隐藏的处理器状态:
程序计数器(通常称为“PC”,在 x86-64中用%rip表示)给出将要执行的下一条指令在内存中的地址。
整数寄存器文件包含16个命名的位置,分别存储64 位的值。这些寄存器可以存储地址(对应于C语言的指针)或整数数据。有的寄存器被用来记录某些重要的程序状态,而其他的寄存器用来保存临时数据,例如过程的参数和局部变量,以及函数的返回值。
条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或数据流中的条件变化,比如说用来实现if和while语句。
一组向量寄存器可以存放一个或多个整数或浮点数值。
程序内存包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的内存块(比如说用malloc库函数分配的)。
程序内存用虚拟地址来寻址,例如:x86-64 的虚拟地址是由 64 位的字来表示的,在目前的实现中,这些地址的高 16 位必须设置为0, 所以一个地址实际上能够指定的是 248 或 64TB 范围内 的一个字节。
操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器内存中的物理地址。
代码示例:
使用GCC编译文件
#GCC将mstore.c文件生成汇编文件
duuuuu17> gcc -og -s code.c
#GCC将mstore.c编译并汇编改代码,会生成目标代码文件
duuuuu17> gcc -og -c code.c
#机器执行的程序只是一个字节序列
使用GDB调试文件
用于机器级程序进行分析,查看实际调用过程。
使用objump将机器代码翻译为汇编
#此命令用于反汇编可执行文件,就是将机器码翻译为汇编语言
duuuuu17> objdump -d code.o
code.o: file format pe-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 48 83 ec 28 sub $0x28,%rsp
4: e8 00 00 00 00 callq 9 <main+0x9>
9: 48 8d 0d 00 00 00 00 lea 0x0(%rip),%rcx # 10 <main+0x10>
10: e8 00 00 00 00 callq 15 <main+0x15>
15: b8 00 00 00 00 mov $0x0,%eax
1a: 48 83 c4 28 add $0x28,%rsp
1e: c3 retq
1f: 90 nop
最左边为地址,中间为指令使用到的字节数,右边为机器序列翻译的对应汇编代码。
链接器填上了callq指令调用的main函数需要使用的地址(这也是链接器的作用,将函数调用找到匹配的函数可执行代码的位置)。
地址1f存储的nop指令,其指令对程序没有影响,单纯是对存储器系统能够更好防止下一个代码块。
机器代码和反汇编表示的特性值注意:
- x86-64 的指令长度从 1 到 15 个字节不等。常用的指令以及操作数较少的指令所需 的字节数少,而那些不太常用或操作数较多的指令所需字节数较多。
- 设计指令格式的方式是,从某个给定位置开始,可以将字节唯一地解码成机器指令。例如:$0x28,%rsp是以字节值48开头的。
- 反汇编器只是基于机器代码文件中的字节序列来确定汇编代码。它不需要访问该程序的源代码或汇编代码。
- 反汇编器使用的指令命名规则与 GCC 生成的汇编代码使用的有些细微的差别。在大多数情况下省略了大小指示符\'q\'。
格式的注解
.file "code.c"
.text
.def __main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.LC0:
.ascii "Hello World!\\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
subq $40, %rsp
.seh_stackalloc 40
.seh_endprologue
call __main
leaq .LC0(%rip), %rcx
call printf
movl $0, %eax
addq $40, %rsp
ret
.seh_endproc
.ident "GCC: (x86_64-win32-seh-rev0, Built by MinGW-W64 project) 8.1.0"
.def printf; .scl 2; .type 32; .endef
所有以‘.’开头的行都是指导汇编器和链接器工作的伪指令。
GCC 支持直接在 C 程序中嵌人汇编代码,GCC的内联汇编特性:使用asm伪指令。
intel汇编格式与ATT格式不同
#使用该命令生成intel的汇编格式
duuuuu17>gcc -Og -S -masm=intel code.c
>>内容如下:
.file "code.c"
.intel_syntax noprefix
.text
.def __main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.LC0:
.ascii "Hello World!\\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub rsp, 40
.seh_stackalloc 40
.seh_endprologue
call __main
lea rcx, .LC0[rip]
call printf
mov eax, 0
add rsp, 40
ret
.seh_endproc
.ident "GCC: (x86_64-win32-seh-rev0, Built by MinGW-W64 project) 8.1.0"
.def printf; .scl 2; .type 32; .endef
我们可以发现不同之处:
-
Intel代码省略了指示大小的后缀。我们看到指令push和mov,而不是pushq和movq。
-
Intel代码省略了寄存器名字前面的‘兽\'符号,用的是rbx,而不是%rbx。
-
Intel 代码用不同的方式来描述内存中的位置,例如是‘QWORD PTR[rbx]\'而不是‘(%rbx)’。
-
在带有多个操作数的指令情况下,列出操作数的顺序相反。
数据格式
即C中声明的数据格式在汇编中体现:
c语言数据类型在x86-64中的大小,在64位机器中,指针长8字节。
注意:汇编代码也使用后缀‘l\'来表示4字节整数和8字节双精度浮点数。但这并不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。
访问信息
通用目的寄存器:(x86-64对应的16个存储64位值)
当这些指令以寄存器作为目标时,对于生成小于 8 字节结果的指令,对此有两条规则:
生成 1 字节和 2 字节数字的指令会保持剩下的字节不变;
生成 4 字节数字的指令会把髙位 4 个字节置为 0。
是栈指针%rsp,用来指明运行时栈的结束位置。
操作数指示符
操作数被分为三个类型:
-
立即数(immediate),用来表示常数值。在ATT格式的汇编代码中,立即数的书写格式是‘$\'后面跟一个用标准C表示法表示的整数,比如,
$-577
或$0x1F
。不同的指令允许的立即数值范围不同,汇编器会自动选择最紧凑的方式进行数值编码。 -
寄存器(register),它表示某个寄存器的内容,16个寄存器的低位1字节、2字节、4字节或8字节中的一个作为操作数,这些字节数分别对应于8位、16位、32位或64位。在下图中,我们用符号\\(r_a\\)。来表示任意寄存器a,用引用\\(R[r_a]\\)来表示它的值,这是将寄存器集合看成一个数组R,用寄存器标识符作为索引。
-
内存引用,它会根据计算出来的地址(通常称为有效地址)访问某个内存位置。因为将内存看成一个很大的字节数组,我们用符号\\(M_b[Addr]\\)表示对存储在内存中从地址 Addr开始的b个字节值的引用。为了简便,我们通常省去下标b。
下图的四个组成部分:立即数偏移\\(Imm\\)、基址寄存器\\(r_b\\)、变址寄存器\\(r_i\\)、比例因子\\(s\\),比例因子必须是1、2、4、8.基址和变址寄存器是64位。有效地址的计算:\\(Imma+R[r_b]+R[r_i]\\cdot s\\)。
深入理解计算机操作系统——读书笔记01
以上是关于《深入理解计算机系统》笔记的主要内容,如果未能解决你的问题,请参考以下文章