《深入理解计算机系统》笔记
本篇主要以《深入理解计算机系统》黑皮书阅读笔记做主要内容,以及笔者只对感兴趣章节做笔记。
以及图片选至CMU15-213课程PPT和课本。
tag: #ComputerScience/basic
抽象基础:[[数据结构]]
导论
操作系统的概念
本章用于介绍本书的内容大致,其中若有概念不清楚,需要自己去网上搜集资料弄明白。
信息和上下文
不管什么文件终究被翻译为二进制文件供计算机执行。
系统接口(也就是有时候常说的C标准库)(或者说是"系统调用")
编译系统
分四个阶段:预处理阶段 —— 编译阶段 ——汇编阶段 —— 链接阶段
预处理阶段
由预处理器主导,将代码中那些引用类型的代码替换为目标代码,在C语言中主要是将宏处理(比如将宏代码,用起目标文件或代码块插入源代码中的所需位置,完成后,会生成后缀名为“.i”的相比于源文件是一个扩展的文本文件。
g++ -E
编译阶段
由编译器主导,将上一阶段的“.i”文本文件翻译成汇编程序(也是文本文件):后缀为".s"
g++ -S
汇编阶段
将上一阶段的汇编程序,翻译成对应的机器语言指令(二进制文件“.o”),并打包成“可重定位目标程序”格式,传递给下一个阶段。
g++ -C
链接阶段
将上一阶段传递的二进制文件,且将它所需的标准库的函数以及其他,所对应的提前预编译好的二进制文件,通过链接器进行合并,生成可执行文件“.exe"。
g++ -O
系统硬件组成
总线
传输定长的字节块(信息字节)并负责各部件之间的信息的传递。且传输信息的单位被称为字。
字
被设计为传输信息的定长字节块。
字中的二进制位数被称为字长。
通常来说就是处理器一次性能处理数据的长度,例如: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}^{n}2^i\)
正数的补码是本身。
负数
负数通常在计算机中以补码形式(或者无符号数形式)表示。
负数在补码形式下计算方式:\(-x_{w-1}·2^{w-1}为最高位符号位+剩余位数(\sum_{i=0}^{w-2}x_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=0}x_i2^i\),w为字长。且与\(U2B_w\),互为逆函数。
\(UMin = 0\) , \(UMax = 2^w -1\)
\(UMax= 2|Tmax|+1\)
补码编码
\(B2T_w=-x_{w-1}2^{w-1}+\sum^{w-2}_{i=0}x_i2^i\),其与\(T2B_w\)互为逆函数
\(TMin = -2^{w-1}\),\(TMax = 2^{w-1}-1\)
\(|Tmin| = |TMax| + 1\)
反码
\(B2O_w(\vec{x})=-x_{w-1}(2^{w-1}-1)+\sum^{w-2}_{0}x_i2^i\)
原码
其作用是确定乘下位应该取正还负值。
\(B2S_w(\vec{x})=(-1)^{x_{w-1}}·\sum^{w-2}_{0}x_i2^i\)
补码与无符号数的相互转换
补码与无符号的转换实际就是有符号数转换为无符号数
补码转无符号数
满足\(TMin\le x \le TMax\)的x有:
(ps:\(Tmin = -2^{w-1},TMax=2^{w-1}-1\),w为字长)
\(T2U_w(x) = \left \{ \begin{array}{ccc} x+2^w, & x < 0 \\ x, &x\ge0 \end{array} \right.\)
无符号数转补码
\(U2T_w(u)= \left \{ \begin{array}{ccc} u-2^w,&U>TMax \\ u, &U\le TMax \end{array} \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 \{ \begin{array}{ccc} x+y,& x+y < 2^w&正常 \\ x+y-2^w, &2^{w}\le x+y<2^{w+1}& 溢出 \end{array} \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 \{ \begin{array}{ccc} x,&x=0 \\ 2^w-x, &x>0 \end{array} \right.\)
检测无符号数加法中的溢出
补码加法
因为补码表示的是有符号位范围(\(-2^{w-1}\backsim 2^{w-1}-1\)),此处我们也使用了和的截断方法(也就是\(+2^w或者-2^w\)).
\(x+_w^ty=\left \{ \begin{array}{ccc} 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}&负溢出(情况一) \end{array} \right.\)
该表达式图示如下:
检测补码加法中的溢出
补码的非
也就是补码的位数取反,那也就是,当\(x\ge TMin_w\)时,x的相反数。(补码的加法逆元)
表达式:(\(Tmin_w\le x\le TMax_w\))
\(-_u^tx=\left\{\begin{array}{cc}TMin_w,&x=TMin_w\\ -x,& x\le TMin_w \end{array}\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 |
浮点数的分数形式转浮点数的二进制:
自我总结:
\(\frac{a}{b}为浮点数分数\)
\(当转换二进制前,先将a\div\ b=k(取整数部分),a\mod b=d(剩余需要表示的小数部分)\)
\(处理后\frac{a}{b} \Rightarrow k\frac{d}{b},以下是根据\frac{d}{b}写出小数部分:\)
\(b:作为小数部分的二进制一共位数,d直接算二进制,注意此时的d,b都为无符号数二进制。\)
解释:因为我们进行处理后,d、b都是在同一表示维度下,d、b都为无符号数,b只是限制了小数二进制有多少位。
例:\(\frac{25}{16}\Rightarrow 1\frac{9}{16}\)
\(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的作用是使指数能够被表示为一个无符号整数,也就是最终阶码E为无符号数。
提示:
在不同的书中,\(E=e+bias或者E=e-bias\),其实不管哪一个,最终阶码E都得是无符号数,当计算出不是无符号数时我们需要转换。E=e+bias更常用!E=e-bias在特殊场景下使用:更高精度的浮点数
例子:
当单精度下e = -2时,\(E=e+bias=-2+127=125\)
\(或者E=e-bias=-2-127=-129+(2^8-1)=126\)
如果是考试的话,就使用E=e+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: 10000...(共23位) |
---|
规格化形式的浮点数表示为:
\((-1)^s \times 1.f \times 2^{(e - bias)}\)
其中,s 为符号位,f 为尾数,e 为指数,bias 为偏置值。(\(E=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,本章默认汇编代码使用AT&T格式
程序编码
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汇编格式与AT&T格式不同
x86 汇编语言有两种常见的格式,分别是 AT&T 格式和 Intel 格式。
#使用该命令生成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,用来指明运行时栈的结束位置。
操作数指示符
目的操作数时常量。
操作数被分为三个类型:
1.立即数(immediate),用来表示常数值。在ATT格式的汇编代码中,立即数的书写格式是‘$'后面跟一个用标准C表示法表示的整数,比如,$-577
或$0x1F
。不同的指令允许的立即数值范围不同,汇编器会自动选择最紧凑的方式进行数值编码。立即数通常用于执行算术、逻辑和数据传输操作。
直接根据$后的地址访问值。
2.寄存器(register),它表示某个寄存器的内容,16个寄存器的低位1字节、2字节、4字节或8字节中的一个作为操作数,这些字节数分别对应于8位、16位、32位或64位。在下图中,我们用符号\(r_a\)。来表示任意寄存器a,用引用\(R[r_a]\)来表示它的值,这是将寄存器集合看成一个数组R,用寄存器标识符作为索引。
\(r_a\):表示是寄存器;(\(r_a\))表示间接引用寄存器,访问(寄存器的值)作地址的值;
3.内存引用,它会根据计算出来的地址(通常称为有效地址)访问某个内存位置。因为将内存看成一个很大的字节数组,我们用符号\(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\)。
下图中的:各类寻址方式也要熟悉
没看懂的话做一下练习题。
练习题
内存地址和寄存器的值:
地址 | 值 |
---|---|
0x100 | 0xFF |
0x104 | 0xAB |
0x108 | 0x13 |
0x10C | 0x11 |
寄存器 | 值 |
---|---|
%rax | 0x100 |
%rcx | 0x1 |
%rdx | 0x3 |
问题:
操作数 | 值 |
---|---|
%rax | 寄存器寻址:寄存器的值:0x100 |
0x104 | 绝对寻址:在表中地址表0x104的值:0xAB |
$0x108 | 立即数寻址:在表中地址表0x108的值:0x13 |
(%rax) | 间接寻址:将寄存器的值作为地址找值:0xFF |
4(%rax) | (基址+偏移量)寻址:在基地址之上+偏移量的结果作地址找值:0xAB |
9(%rax,%rdx) | 同理:0x11 |
260(%rcx,%rdx) | 同理,只不过偏移量要换成十六进制:0x13 |
0xFC(,%rcx,4) | 比例变址寻址+偏移量寻址:0+%rcx地址*4+0xFC:0xFF |
(%rax,%rcx,4) | 比例变址寻址:0x100+0x001*4:0xAB |
(重要)数据传送指令
将一个数据从一个位置复制到另一个位置的指令。
指令类:每个类中的指令执行相同的操作,但操作数大小不同。
MOV
下表为MOV类:
指令 | 效果 | 描述 |
---|---|---|
MOV S,D | D<--S 将数据从S传送到D | 传送 |
movb | 传送字节 | 传送字节 |
movw | 传送字 | 传送字 |
movl | 传送双字 | 传送双字 |
movq | 传送四字 | 传送四字 |
movabsq I,R | R<--I,将数据从I传送到R | 传送绝对的四字 |
上表的字为16位,但通常来说字为4个字节或8个字节,早期的字才为16位。
以下指令格式为:指令类 源操作,目的操作
指令 | 类型 | 处理字节 |
---|---|---|
movl $0x4050,%eax | Immediate--Register(立即数到寄存器) | 4 bytes |
movw %bp,%sp | Register--Register(寄存器到寄存器) | 2 bytes |
movb (%rdi,%rcx),%al | Memory--Register(内存到寄存器) | 1 byte |
movb $-17, (%rsp) | Immediate--Memory(立即数到内存) | 1 byte |
movq %rax,-12(%rbp) | Register--Memory(寄存器到内存) | 8 bytes |
movl $0x4050,%eax,表示将0x4050立即数存储到寄存eax中.
源操作数指定的值是一个立即数,存储在寄存器中或者内存中。
目的操作数指定一个位置,要么是一个寄存器或者,要么是一个内存地址。
其中内存到寄存器的汇编为 movq (%rax),%rdx。C代码风格为(temp=*t);
寄存器到内存的汇编为 movq %rax,(%rdx)。C代码风格为(*t=temp);
并且在x86-64中,传送指令无法是源地址直接传送到目的地址,而是需要经过寄存器,通过寄存器复制到目的位置。
常规的movq指令只能以表示为32位补码数字的立即数作为源操作数,然后把这个值符号扩展得到64位的值,放到目的位置。movabsq指令能够以任意64位立即数值作为源操作数,并且只能以寄存器作为目的。
MOVZ类中指令把目的中剩余的字节填充为0.
movzbl:其实不仅会把%eax的高3个字节清零,还会把%rax的高4个字节清零。(后续会使用)
MOVS类中指令把目的中剩余的字节填充为符号。
cltq指令= movslq %eax,%rax,cltq指令的源操作为寄存器eax,目的操作为寄存器rax。
tips:
x86架构中常见的寄存器及其作用:
1.通用寄存器:
- EAX:累加器寄存器,用于存储函数的返回值、算术运算的结果等。
- EBX:基址寄存器,通常用于存储指针、数组、字符串等。
- ECX:计数器寄存器,通常用于循环计数、字符串处理等。
- EDX:数据寄存器,通常用于存储除法、乘法等指令的中间结果。
- ESP:栈指针寄存器,用于指向栈顶。
- EBP:基址指针寄存器,用于存储栈帧的基地址。
- ESI:源索引寄存器,通常用于字符串复制、字符串比较等。
- EDI:目标索引寄存器,通常用于字符串复制、字符串比较等。
- RAX: 函数返回值的存储寄存器。
2.段寄存器:
- CS:代码段寄存器,用于存储代码段的基地址。
- DS:数据段寄存器,用于存储数据段的基地址。
- SS:堆栈段寄存器,用于存储堆栈段的基地址。
- ES:附加段寄存器,通常用于存储附加的数据段基地址。
- FS、GS:扩展段寄存器,通常用于存储附加的数据段基地址,比ES寄存器更灵活。
3.指针寄存器:
- EIP:指令指针寄存器,用于存储下一条指令的地址。
4.标志寄存器:
- FLAGS:标志寄存器,用于存储程序的运行状态和结果,包括进位、溢出、零、符号、调试、中断等标志。
压入和弹出栈数据
此处的程序栈跟数据结构中的栈相同,“先进后出”原则,
入栈和出栈指令:
栈指针%rsp,用于标记栈顶所在位置。
例:将4个字节(字)数据入栈:
#入栈
pushq %rdx =
subq $8,%rsp #将栈顶指针值减8
movq %rdx,(%rsp)#将寄存器rdx的值,于程序栈的寄存器rsp的内存中
#出栈
popq %rax =
movq (%rsp),%rax #将寄存器rax中的值,覆盖为栈顶指针的地址
addq $8,%rsp #将栈顶指针向上移动
书中所给示例是倒向的栈,并且其栈顶是向下(向地址低端)移动。
(ps:这里的图像我自己理解是:栈顶和栈底地址是相同的,当入栈的时候,数据进入后,栈顶向下移动;出栈,栈顶会向上移动,但原出栈数据仍保留原地,直到被新入栈数据覆盖,才算真正消失)
因为栈和程序代码以及其他形式的程序数据都是放在同一内存中,所以程序可以用标准的内存寻址方法访问栈内任意位置。(就是说,在数据结构中,栈这个结构的特点是只会访问到栈顶元素,但是在计算机系统层次中,因为使用地址对应数据,那么我们可以通过逻辑上的连续,从而推导其他数据地址,从而可以通过地址直接访问栈内数据。)
(重要)算术逻辑操作
从上至下:加载有效地址、一元操作、二元操作和位移。下图中的所有指令都会设置条件码。
\(>>_L和>>_A\),来区分逻辑右移和算术右移。
加载有效地址(leaq)
leaq
实际上是movq指令的变形。它的指令形式是从内存读取数据到寄存器,但实际没有引用内存。
目的操作数必须是寄存器,并且该指令的计算使用的是另外一个单独的ALU。
一般来说,此指令是为后面的内存引用产生指针。
但是也可以进行简要的加法和有限的乘法计算,如下所示:
x = 5x+7>>
leaq 7(%rdx,rdx,4),%rdx
能进行如此的原因:lea在取寄存器的值时,里面不一定是地址,而是值,所以刚才的式子的汇编就起到算术运算的效果。
当然,若寄存器的值为地址时,lea指令的计算的结果就是地址了。
具体来说,leaq
指令的使用形式如下:
leaq <source>, <destination>
其中,<source>
是一个内存操作数,可以是一个内存地址、寄存器加偏移量、或者是某个全局符号的地址等。<destination>
是一个目标寄存器,用于存储计算结果。
mov和lea的区别
movq
指令用于传送数据的值,可以传送立即数、寄存器或内存位置的值。leaq
指令用于传送内存位置的有效地址,不能传送具体的数据值。
一元和二元操作
一元操作
指的是源和目的为同一个操作数。
二元操作
源为第一个,目的为第二个,齐次进行算术运算,往往是将第二个作为被算术的那一个。
移位操作
移位量,移位数。shift left/right
左移指令:SAL和SHL。
右移指令:SAR(算术移位)和SHR(逻辑移位)。
控制
(重要)条件码
CPU通过一组单个位的条件码寄存器,来表示最近进行的算术或逻辑运算的运算结果。
例如:
条件码 | 作用 |
---|---|
CF | 表示最近的运算操作最高位是否产生了进位。(用于检查无符号运算的溢出) |
ZF | 最近的运算结果是否为0 |
SF | 最近的运算结果为正或为负。 |
OF | 最近的运算结果是否溢出(正溢出[两负数相加]或负溢出(两正数相加)) |
使用高级语言表示:
通过条件码可以对条件分支指令进行判断。
例如:
- 在XOR操作中,会设置进位标志和溢出标志为0.
- 在移位操作中,进位标志将设置为最后一个被移除的位。溢出标志设置为0。
- 自增和自减操作会设置溢出和零标志位,但不改变进位标志。
set条件码:
通过set条件码,可以访问比较指令后CPU设置的标记位(ZF、SF、OF、CF)。
下图通过其b-a的结果进行理解:
//假设 asm{
cmp a,b;
}== b - a;
//通过b-a的结果来判断,其是设置的标志位。
//比如说:当b-a<=0时,说明其比较的结果为SF=1,OF=1或0 以及ZF=1.
//再详细一点:就是说a为正还是负时的情况
比较指令:
比较指令不会修改任何操作数,仅是CPU设置对应结果的符号位(比如:S2-S1为0,则符号位ZF设置为1)。
CMP S1,S2.基于S2-S1。
TEST S1, S1.基于S1&S1。
test指令是对操作数自身的对应位进行“AND”操作。
当你只有一个操作数时,可以通过test指令来设置条件标志,从而通过测试指令,来得到操作数是为正数、负数还是0。
举例
cmpq %rsi, %rdi #cmpq表示按四字长度比较,结果就是设置标志位。
jle .L4#.L4表示标签
.L4: #.L4:表示标签跳转处
#此两句汇编代码表示,先将rsi和rdi寄存器的值进行比较(实际是将两数相减(rdi)-(rsi)),然后根据计算结果,处理器设置的相应符号位(ZF或者CF等)。
#jle .L4表示将前面比较的结果来进行跳转。jle表示的是两数是否小于等于。
(重要)条件传送指令
需先cmp进行比较后,通过标志位的判断,决定下列指令是否执行。
下列指令的操作数顺序:source, destination。如果指令条件成立则从source移动到destination。
指令如下:
例如,cmovl
指令的示例:
cmovl eax, ebx ; 如果标志位表示小于条件成立,将 将eax 的值移动到 ebx
Conditional Moves条件移动指令
此方法是提前计算分支的条件表达式,然后通过该结果传递对应条件的语句块,利用了分支预测技术,保证了分支预测击中的概率。因为处理器流水线技术,会提前传输邻近指令的原因,当分支预测技术错误后,会暂停整个CPU的执行,然后重新选择分支执行,但此情况增加了CPU执行的时钟周期,降低了执行效率。而此方法将代码的执行去近似为CPU执行指令的顺序,提高预测成功率,减少了处理器执行的时钟周期,从而达到优化性能的问题。
但是此方法不是适用所有情况!书中提示,当分值计算量过大和空指针进行条件判断时,前者会浪费资源和时间,后者会引发引用空指针错误。
分支预测错误的时间计算
假设预测错误的概率为\(p\),执行代码的时间为\(t\)。而预测错误的处罚时间为\(f\)(也就是当代码执行途中,发现条件错误,则需要进行装载代码重新执行另外一个分支代码所浪费的时间)。
则代码执行平均时间\(avg=(1-p)t+p(t+f)=t+fp\).这也就是分支预测错误的处罚时间。
条件控制转移
以下使用了C风格代码显示源代码转为汇编后的控制流格式。
在经过书上习题后总结得出,在源代码进行编译后,其汇编代码更喜欢将条件反转把分支结构的else优先级提前。
test = test_expr;//先提前计算条件结果
if(!test)//通过判断,利用goto进行跳转
goto false;
then-statement//条件为真时的指令执行
goto done;
false://条件为假时的指令执行
else-statement
done;
跳转指令
jmp Label为直接跳转。而条件跳转只能为直接跳转。
举例
将寄存器中的值作为跳转目标。
jmp *%rax
将寄存器中的值作为读地址,从内存中读出跳转目标。
jmp *(%rax)
指令表:
指令是否跳转,通过检查CPU对上一个运算结果的符号位(上表的跳转条件),得出。
需要注意的是,在条件跳转的后续语句中,只会指向对应部分。
例如:下面这个不严谨的例子,当我们满足je时,会跳转到标签.L2指向对应语句,而不满足时,会顺序执行,到标签.L2前,然后直接执行ret指令返回结果。
testq %rdi,%rdi
je .L2
shrq $2, %rdi
,L2
popq %rbx
ret
汇编格式下指令跳转
通过(重要)阅读反汇编代码汇总部分进行学习。
循环形式的抽象理解
本节将循环以c语言形式理解,不同的循环方法的汇编形式的控制流。
do...while循环
do{
//Body
}while(condition);
>>transform assembly by C code
loop:
//Body
if(x) goto loop;
while循环
在简单的循环执行中,可使用此方法优化循环。
while(condition)
{
//Body
};
>> transform assembly by C code
goto test;
loop:
//Body
test:
if(x) goto loop;
我们由此可看出,do...while循环和while循环的区别:就是执行循环条件的顺序改变了。
在do..while中,循环条件在语句块后执行。在while中,使用goto先执行循环条件,再后执行语句块。
gcc 代码优化级别:O1
利用条件移动方式,先判断一次条件是否满足循环,不满足就直接跳出(避免进入循环)。
while汇编的优化理解:
在gcc使用 -g0 优化选项,编译器进行编译源代码后,汇编代码中使用了guarded-do变换。
这个变换就是将while循环的相反条件先进行判断(用于某些条件下不进入循环),后续循环中的条件转移指令是在前一个循环条件的再相反的限制补充。
比如:我在源代码中while(n<1),在汇编先判断n>=1,若不满足,则再判断n!=1,从间接建立循环n<1的情况。
for循环
也就是常见的for循环转while循环。
for(init ; test; update){
body;
}
>>transform to while forms
init;
while(test){
body;
update;
}
>>transform assembly by C code
init;
if(!x)
goto done;
goto test;
loop:
Body
update
test:
if(x) goto loop;
done:
for循环中的优化,大部分是转换成while后,使用跟while相同的优化。
switch选择语句
实际执行形式:
从c语言的switch形式-->编译器构建跳转表,将每个case分块组成序列,并返回序列首地址。以及对于每个case块创建索引。
跳转表利用case作为索引,其Targ[i]链接的是case下的实际代码块。从而使case的跳转执行时间为O(1).
举例
上图中指令ja表示的是执行default代码块。而jmp才是根据序列地址进行跳转到所需代码块。
跳转表详解:
上图中可看出,switch-case代码块序列起始地址标签为.L4。每个代码块所占空间8个字节。default部分的地址标签为.L8。
在switch的汇编形式中,常用ja,jb这类无符号数之间比较的指令,因为这样会把负数约束到更高的位置。
跳转表中每个标号分别对应顺序对应着x的要求各个值,如
.section .rodata
.align 8
.b4
表示大小 | 标号 | 对应的x值 |
---|---|---|
.quad | .L8 | x=0 |
.quad | .L5 | x=1 |
.quad | .L5 | x=2 |
上表只是个简单起演示作用,解释一下吧:当x=1或2时,其case执行语句相同。而当x=0时,是执行的.L8下的操作。
书中有要求复原C代码,需要根据跳转表跟汇编中对应的标号和指令进行结合。(建议:在做题时,如果跳转表没有标记出各个x,记得写上,这样更快捷)。
*编译器对switch的创建汇编的三种形式:
我们需要知道的是:编译器使用的是switch的case值,在编译器编译为汇编时,case的值都是常量,一般建立跳表的形式进行查找和执行,而有时候case过多时,会通过平衡二叉树的建立,可用来进行查找执行时跳转case的位置(这也是if-else的条件表达式建立执行和查询方式)。
-
编译器会对switch代码进行创建跳表(如果case的范围不是过大),并交由汇编程序管理。其汇编代码通过跳转表访问代码块的时间复杂度为O(1)。
-
当switch的case值相对稀疏时,编译器会根据case值建立一个条件树(平衡二叉树),来提升索引速度(索引在对数时间内完成)。
-
当查询值为线性时(即挨个挨个查找),则会是线性时间复杂度(二分折半查找)。
当然以上形式都优于把switch-case转换为if-else组的运行速度。
前六小节总结
通过对前六小节的学习,明白了如何使用gcc生成各个阶段的代码或可执行文件。
在汇编语言中,熟悉数据传送指令、set标志位指令,算术逻辑操作和条件传送指令等等,能够看懂汇编代码,以及通过习题练习后,能够通过源代码进行手译伪汇编代码。
自认为最重要的是,看懂了反汇编中一行代码的结构:(重要)阅读反汇编代码。以及对于常见的控制结构:while,for,switch case结构能够看懂,手写伪汇编,了解其中GCC编译器优化-O1常用方法——条件转移控制方法(将能够提前结束循环的条件先进行判断),把源代码转为汇编代码。以及CPU使用的一个分支预测技术和符合现代处理器的编译技术——条件移动方法(将分支需要处理的数据提前进行处理,最后通过条件判断和条件传送指令选择哪部分指令执行),以及使用跳转表建立O(1)实现查询。
机器级程序运行过程
ABI——应用程序二进制接口 application binary interface
函数的调用和返回恰好符合栈的LIFO规则,所以常见的函数创建和调用都使用的是栈结构进行。
栈
下如图为X86-64的栈结构:
上图解释:
一般来说,栈是我们在数据结构一课中理解的相反方向。即栈顶在下和栈底在上,通过对栈顶的地址递减来增加栈的栈帧,也就是”栈向下扩展“。在操作系统中,编制方向一般是小端法,从而栈的地址编制也是栈底BP在地址大的方向,而栈顶SP在地址小的方向。
每一个部分叫一个栈帧,栈帧由返回地址,保存的寄存器,函数参数,局部变量,临时变量和临时数据组成。
- 返回地址(Return Address):返回地址指示函数执行完毕后的下一条指令地址,用于指示程序在函数调用后继续执行的位置。
- 保存的寄存器(Saved Registers):栈帧通常会保存一些寄存器的值,以便在函数调用完成后能够正确地恢复(当时调用者)寄存器的状态。这些寄存器的值包括被调用函数可能会修改的寄存器。
- 函数参数(Function Arguments):如果函数有参数,它们通常会被复制到栈帧中的特定位置,以便被调用函数访问和使用。
- 局部变量(Local Variables):函数内部定义的局部变量在栈帧中分配空间。这些变量的生命周期与函数的执行周期相对应。
- 临时变量和临时数据(Temporary Variables and Data):在函数执行过程中产生的临时变量和临时数据也可以存储在栈帧中。
栈的读写
我们通过栈顶指针SP的移动来管理栈内数据帧的增加和删除。
当我们将SP的地址递减时,增加了栈的容量;相反当我们将SP地址递增时,减少了栈的容量。
需要注意的是,我们在移除栈内数据帧(或元素)时,是指逻辑上被移除的元素不再属于栈,但其物理上仍存在内存中,只有新数据覆盖在此片数据时,才是真正的删除元素。
调用和返回指令
调用
call Label#Label为直接调用
call *operand #*operand表示的是读取寄存器的地址,并访问该地址。属于间接调用
ret/retq #从过程调用中返回
运行时的栈
当执行调用前,调用的标记地址(比如下图的mult2函数的首地址)存放在%rip
寄存器中。与此同时的是,将返回地址(为 call指令的下一条指令地址)压入栈中,当一段调用结束后,能够正确的返回到对应位置继续执行。
而在调用执行过程中%rip
寄存器(程序计数器,用户无法修改)存放的是正在执行指令的地址(即如下图的mutl2函数的指令)。
需要注意在X86_64中的地址长度为8字节。并且注意区分栈内的虚拟地址和%rsp实际指向的程序栈地址。
数据传送
通过固定传参寄存器,用于保存参数。
例如X86-64的整形传递的寄存器:
%rax
寄存器用于保存返回值。
参数1~6复制到对应的寄存器,而参数7 ~ n放在栈上,而参数7位于栈顶。通过栈传递参数时,所有的数据大小都向8的倍数对齐。
一般来说超过的寄存器存储在栈的参数构造区中。
%dl
是指%edx
数据通用寄存器的低八位数据。
局部数据管理
栈的局部数据存储:在栈中一般将局部数据根据所属栈帧,存放在对应位置处。
寄存器的局部数据存储:适用于保存过程共享的资源。被调用者必须保存的寄存器(%rbp,%rbx,%r12~%r15)
递归函数的栈理解
当进行函数递归调用时,若具有重复使用的寄存器值。我们在进行递归调用前,会将需要的寄存器当前值进行压栈保存,当递归调用返回时,出栈恢复寄存器在此刻的值。
一般来说,将rbp在递归调用之前进行压栈,然后保存值或地址。
总结:
x86-64/Linux Stack Frame:
例:
总结
本节前面部分分别讲述返回地址、构造参数、保存的寄存器集、局部变量是为什么要设置的,后面我觉得重要的是,讲述了寄存器和栈的局部空间如何判断(习题3.34)。以及递归函数的汇编代码看出其循环变量是谁,以及对应条件判断是怎样的。
(*须记住)常见的寄存器:
以i结尾的是变址寄存器,以x结尾为通用寄存器。变址和堆栈寄存器只能为32位或者64位,而通用寄存器可以指定8bit。
- 保存返回值
%rax
.
%rax
是累加器寄存器(Accumulator Register),也称为返回值寄存器。它在函数调用过程中用于存储函数的返回值,也常用于存储中间结果、临时数据以及一些算术和逻辑运算。在书中也表明%rax
为指令的目的寄存器。
- 保存参数
%rdi
,%rsi
,%rdx
,%rcx
,%r8
,%r9
.
%rsi
是源索引寄存器(Source Index Register),也称为源操作数寄存器。它常用于存储源数据的地址,用于数据传输、字符串操作、循环等场景。例如,movl (%rsi), %eax
将从%rsi
寄存器指定的内存地址中读取数据并存储到%eax
寄存器中。%rdi
是目标索引寄存器(Destination Index Register),也称为目标操作数寄存器。它常用于存储目标数据的地址,用于数据传输、字符串操作、循环等场景。例如,movl %eax, (%rdi)
将%eax
寄存器中的数据存储到%rdi
寄存器指定的内存地址中。
-
调用函数的临时数据保存
%r10
,%r11
. -
被调用函数临时数据保存
%rbx
,%r12
,%r13
,%r14
. -
指令指针寄存器
%rip
(或者说是属于程序计数器)。
-
%rip
存储了当前正在执行的指令的地址,也就是下一条将要执行的指令的地址。它在程序执行期间不断更新,指向下一条将要执行的指令。rip寄存器的内容有CPU自动维护,用户无法更改。 -
程序寄存器是处理器中的一组寄存器,用于存储与程序执行相关的信息,例如指令地址、指令计数器和程序状态等。这些寄存器用于跟踪和控制指令的执行,以及处理分支、函数调用、返回等操作。
特殊寄存器:
基指针寄存器%rbp
,基指针提供了一个固定的参考点,使得每个栈帧的相对位置可以被正确计算。有时候rbp用于保存调用函数的基指针值或返回地址。
栈顶指针寄存器%rsp
,而栈顶指针则控制着当前栈空间的动态分配和释放,保证栈帧的正确创建和销毁。
(重要)阅读反汇编代码:
阅读反汇编代码时,跳转指令的编码方式一般是相对寻址法,其中代码结构如下。
在跳转指令后,一般紧跟的是指定指令的相对地址。
#本节截选CMU的CSAPP课程的Second lab: <Boom>——phase_1
0000000000400ee0 <phase_1>:#本段汇编代码的起始地址,以及标记位
#单条反汇编代码格式:(基于X86_64指令集)
#虚拟地址 机器指令(操作码,操作数) 指令 源操作码,目的操作码
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal>#调用两字符串是否相等函数
400eee: 85 c0 test %eax,%eax#test指令,对自身进行and逻辑运算
400ef0: 74 05 je 400ef7 <phase_1+0x17>
#跳转指令:相对寻址计算:
#1.虚拟地址形式:400ef7,<phase_1+0x17>:标志+偏移量
#2.在执行跳转指令时,PC(程序计数器)的值是下一条指令的虚拟地址+操作数=400ef2+05=400ef7
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb>
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq
#指令地址间隔数=每个指令的机器指令大小=操作码+操作数大小
需要注意的地方
-
有时候在跳转指令后,紧跟一个rep(repz) retq指令,其中rep(repz)仅表示为空指令用于捕获条件跳转指令目标,防止不属于分支语句的return指令被执行。
-
retq = ret,都是调用后返回指令。
-
我们需要知道的是,在机器程序级的代码中,我们无法通过内存直接访问内存,而是通过内存-寄存器-内存的形式来访问。
-
有时候我们会看见使用的是rbx的64位寄存器,但指令中使用的是它的32位形式ebx,则种情况下的意思就是,rbx的高32位为0,不参与运算。
例如:
movq %rdi, %rbx
andl $1, %ebx
#ebx是rbx的低32位形式
#在与运算过程中只有rbx的低32位参与了运算
#以及立即数1在运算过程中也是32位,并且用0补全剩下的31位
- 由4注意点可知寄存器e开头和r开头的区别就是寄存器空间是否为64位。
计算指令执行过程的提示
在计算%rsp时,我们只需要知道PC保存的是当前执行指令的下一条指令。而%rsp实际地址的计算,因为在X86_64中,函数返回地址长度为64位=8字节,所以当进行函数调用时,在&rsp已知存储的地址上减-8即可(压栈),当然执行ret后%rsp+8(出栈)。
例如:当前已知%rsp指向的程序栈地址为:0x7fffffffe820
,在调用函数test后,其%rsp向下扩展(别忘记小端法存储格式),所以%rsp指向的程序栈地址变为0x7fffffffe820-8=0x7fffffffe818
。
浮点数的指令格式
浮点数的操作与整数型同理,只不过是指令的名称不同。
存储浮点数的寄存器有16个,32字节的名称为ymmn(n:0 ~ 15),16字节的名称为xmmn(n:0 ~15)
各类操作
用于传输操作:
转换操作
算术运算操作
逻辑运算操作
在X86中:
and
:位与操作,将两个操作数的对应位进行逻辑与操作。or
:位或操作,将两个操作数的对应位进行逻辑或操作。xor
:位异或操作,将两个操作数的对应位进行逻辑异或操作。not
:位取反操作,对操作数的每一位进行逻辑取反操作。test
:执行逻辑与操作,但不保存结果,只更新标志位。shl
(或sal
):逻辑左移操作,将操作数的位向左移动,高位用零填充。shr
:逻辑右移操作,将操作数的位向右移动,低位用零填充。sar
:算术右移操作,将操作数的位向右移动,保持符号位不变。rol
:循环左移操作,将操作数的位向左循环移动,旧的高位移动到低位。ror
:循环右移操作,将操作数的位向右循环移动,旧的低位移动到高位
比较操作
扩展:RISC-V指令
指令格式
- opcode(操作码):指令的基本操作,这个缩写是它的惯用名称
- rd:目的操作数寄存器,用来存放操作结果。
- funct3 :一个另外的操作码字段。
- rs1:第一个源操作数寄存器。
- rs2:第二个源操作数寄存器。
- funct7:一个另外的操作码字段。
部分汇编操作码:
- ld:取双字数据
- lui:用于加载很大的立即数
- addi 寄存器数据与立即数相加
- sd存储数据
指令格式转变为机器指令格式:
其地址为16进制,偏移量为10进制,将它们转换成二进制并放在所在位置中,就组合成机器指令。
其中opcode需要根据ISA而定。
如:
当前计算机构建基于两个关键原则
-
指令由数字形式表示
-
程序和数据一样保存在存储器中进行读写。
由此,联想“存储程序”结构可知,就是符合这两个原则。
逻辑操作
按位取反相当于跟全为1的二进制数进行异或操作。
用于决策的指令
分支
beq rs1, rs2, L1
beq操作码,比较两值是否相等。如用于判断寄存器rs1和rs2的值是否相等,当相等则跳转到L1的语句执行。
bne rs1,rs2,L1
bne操作码,比较两值是否不相等。如用于判断寄存器rs1和rs2的值是否不相等,当不相等则跳转到L1的语句执行。
*beq x0,x0,L1
此汇编语句表示无条件分支,表示指令在遇到该指令时,程序必须跳转到L1。
例:
>> 高级语言形式:
if( i == j ) f = g+h ; else f = g-h;
>>汇编语言形式:
//1,4,5汇编语句表示if(i!=j) ,f = g-h和结束分支执行
1 bne x22,x23,Else
//2,3,5汇编语句表示为if(i==j) ,f = g+h和结束分支执行
2 add x19,x20,x21
3 beq x0,x0,Exit //此汇编语句表示无条件分支,表示指令在遇到该指令时,程序必须分支。
4 Else:sub x19,x20,x21
5 Exit:
//tip:一般来说我们常用不相等来作分支语句的条件判断,因为使用相反条件来进行跳转,代码更有效率。
循环
Loop: 标号(Lable),用于标识特定的位置或代码块,常用于跳转指令目标位置。
>>高级语言形式:
while( save[i] == k)
i+=k;
>>汇编语言形式
1 Loop: //标号
2 slli x10,x22,3//先计算数组下标i相对于基址的偏移量
3 add x10,x10,x25//将偏移量加基址上
4 ld x9,0(x10)//将save[i]临时保存一个寄存器中
5 bne x9,x24,Exit//进行循环条件比较
6 addi x22,x22,1//循环的语句块
7 beq x0,x0,Loop//将PC指向Loop继续循环
8 Exit://退出循环
附:用于条件判断的操作码
b:branch,t:than,e:equal,l:less,g:greater.
操作码 | 作用 |
---|---|
bge | 大于等于 |
blt | 小于 |
bgeu | 无符号数的大于等于判断 |
bltu | 无符号数的小于 |
ble | 小于等于 |
bgt | 大于 |
case/switch语句
一般的实现是将switch语句转换成if-then-else语句。
另一种是通过编码的形成指令序列的地址表(分支地址表或分支表)。即程序通过表进行索引,能够跳转到合适的指令序列。
jal和jalr:跳转-链接指令
jal
指令的格式如下:
jal rd, offset
其中:
jal
是指令助记符(mnemonic),表示无条件跳转和链接。rd
是目标寄存器(destination register),用于保存下一条指令的地址。offset
是一个立即数(immediate),表示相对于当前指令的偏移量。它是一个 20 位的有符号整数,可以表示 -2^19 到 2^19-1 的范围。
jalr
指令的操作数格式为 jalr rd, rs1, imm
rd
是目标寄存器,用于保存下一条指令的地址。rs1
是源寄存器,包含了偏移量的计算结果。imm
是立即数字段,表示一个 12 位的有符号立即数。
tip:jal x0, Lable//无条件跳转至引用位。
在 RISC-V 中,约定上,寄存器 x1(ra)被用作链接寄存器(Link Register)。它通常用于保存调用指令的返回地址,以便在函数调用结束后能够正确返回到调用点。而返回地址一般存储在调用指令的下一条指令的地址,即调用指令的 PC(程序计数器)加上 4。在函数调用时,将返回地址保存到寄存器 x1 中,函数执行完后通过跳转到寄存器 x1 的值来实现返回。
jal和jalr的区别:
两者都是跳转到目标地址指令。
但是:
jal
指令的目标地址是相对于当前指令的偏移量,可以是一个常量或标签。
jalr
指令用于实现函数调用中的间接跳转,例如通过使用寄存器中保存的函数地址进行跳转。
程序栈使用
栈按照从高到底的地址顺序增长。将栈指针SP地址增加就是弹栈,减少就是压栈。
动态数据和静态数据的保存
栈分配空间
常用寄存器:
0x13是BIOS读磁盘扇区的中断: ah=Ox02-读磁盘,al=扇区数量(SETUPLEN=4),ch=柱面号,c=开始扇区,dh=磁头号,dI=驱动器号,es:bx=内存地址。
操作系统上运行的程序
链接
异常控制流
虚拟内存
程序间的交流和通信
系统级I/O
网络编程
并发编程
注:笔者要是还没跟新,那就是看概念去了~haha。