目录
3.2 CPU 的实模式
实模式是指 8086 CPU 的寻址方式,寄存器大小,指令用法等,是用来反映 CPU 在该环境下如何工作的概念。所以想了解实模式这种抽象的概念,主要就是了解在实模式下 CPU 能做什么。
3.2.1 CPU 的工作原理
CPU 的唯一的任务就是执行指令,在它眼里,指令就是一串 010101 ......
CPU 执行每条指令的流程:
- 控制单元要取下一条待运行的指令,该指令的地址在程序计数器 PC 中,在 x86CPU 上,程序计数器就是 cs:ip,于是读取 ip 寄存器后,将此地址送上地址总线,CPU 根据此地址便得到了指令,并将其存入到指令寄存器 IR 中;
- 这时候轮到指令译码器 ID 上场了,它根据指令格式检查指令寄存器中的指令,先确定操作码是什么,再检查操作数类型,若是在内存中,就将相应操作数从内存中取回放入自己的存储单元,若操作数是在寄存器中就直接用了,免了取操作数这一过程;
- 操作码有了,操作数也齐了,操作控制器 OC 给运算单元下令,开工,于是运算单元便真正开始执行指令了,ip 寄存器的值被加上当前指令的大小,于是 ip 又指向了下一条指令的地址;
- 接着控制单元又要取下一条指令了,流程回到了本段开头,CPU 便开始了日复一日的循环,由于 CPU 特别不容易坏,所以唯怡让它停下来的条件就是停电。
3.2.2 实模式下的寄存器
寄存器是一种物理存储元件,只不过它比一般的存储介质要快,能够跟上 CPU 的步伐,所以在 CPU 内部有好多这样的寄存器用来给 CPU 存取数据。
缓存也是一项非常伟大的发明,成功解决了速度不匹配设备之间的数据传输,并且在一般情况下, IO 是整个系统的瓶颈,缓存的出现,有效减少了低速 IO 设备的访问频率,从而大幅度地提升了速度。如果缓存中恰好有要的内容,这就称为命中 hit ,否则称为缺失 mis。
CPU 中的一级缓存 L1,二级缓存 L2,它们都是 SRAM,即静态随机访问存储器,是最快的存储器。SRAM 是用寄存器来存储数据的,这就是 SRAM 快的原因。而寄存器为什么快呢?原因是寄存器是使用触发器实现的,这也是一种存储电路,工作速度极快,是纳秒级别的,和 CPU 的速度是一个级别。
CPU 中的寄存器大致上分为两大类:
- 一类是其内部使用的,对程序员不可见。 “是否可见”不是说寄存器是否能看得见,是指程序员是否能使用。
- 另一类是对程序员可见的寄存器,我们进行汇编语言程序设计时,能够直接操作的就是这些寄存器,如段存器,通用寄存器。
在实模式下,默认用到的寄存器都是 16 位宽的。(因为所谓的实模式是相对后面出现的保护模式而言的,CPU 最开始的工作模式就是 8086 CPU 的工作模式,即寄存器都是 16 位宽的)
段寄存器:
段寄存器的作用就是指定一片内存的起始地址,故也称为段基址寄存器,尽管段基址在实模式下要乘以 16 ,在保护模式下只是个选择子(保护模式中会讲),但其作用就是指定一片内存的起始地址,而段内偏移地址,顾名思义,就是仅仅相对于此起始地址的偏移量,
由于要指定的是内存中的一段区域的起始地址,所以称之为段基址寄存器,也称段寄存器,无论是在实模式下,还是保护模式下,它们都是 16 位宽。(这是为了更好地兼容嘛?)
代码段:简而言之就是把所有指令都连续排放在一起,形成了一个全部都是指令的区域,里面存储的是指令的操作码及寻址方式等。该区域可以在硬盘上的文件中,也可以是被加载后的内存中,总之是一段指令区域。它们内部都是紧凑挨着的,内容形式完全一样,只是存放的介质不一样而己。代码段寄存器 CS 就是用来指向内存中这段指令区域的起始地址。
数据段:和代码段类似,只是这段区域中的内容不是指令,而是纯粹的数据,也就是说里面存储的是程序运行所需要的数据,属于指令的操作数。数据段寄存器 DS 便是用来指向此数据区域的起始地址。
栈段:其概念只在内存中,硬盘文件中可真没有。一般的栈段是由操作系统分配指定的,所以是属于被加载到内存后才有的。栈段寄存器 SS 就是用来指向此区域的起始地址。
另外三个附加段寄存器是为了方便大家,在 16 位 CPU 中,只有一个附加段寄存器——ES。而 FS,GS 附加段寄存器是在 32 位 CPU 中增加的。我们使用的是 32 位 CPU,并不是说 32 位 CPU 在实模式下的 16 位环境中就不能用 FS,GS 寄存器,32 位的 CPU 兼容 16 位 CPU 的特性。(兼容就是除了有自己额外新增的特性,还包括所有前辈的特性,只强不弱)
IP 寄存器是不可见寄存器,CS 寄存器是可见寄存器。(也就是说,程序员可以操作 CS 寄存器,而不能操作 IP 寄存器,因此程序员无法直接改变程序计数器 PC 来直接改变 CPU 的执行地址,只能通过像 jmp 这样的指令来跳转。)
复习🍊:
x86体系的CPU中:程序计数器 PC 并不是单一的某种寄存器,它是一种寄存器组合,指的段寄存器 cs 和指令寄存器 ip;有专门改变执行流的指令,如 jmp、call、int、ret ,这些指令可以同时修 cs 和 ip,它们在硬件级别上实现了原子操作。
ARM体系的CPU中:程序计数器有个专门的寄存器,名字就叫 PC,想要改变程序流程,直接对该寄存器赋值便可,可以用 mov 指令来修改程序流。(这个 PC 不知道是怎么实现的)
指令在内存中,访问内存就要用 “段:段内偏移” 的形式,所以 CS 寄存器用来存代码段段基址,IP 寄存器用来存储代码段段内偏移地址,同 CS 寄存器一样都是 16 位宽。并不是这两个寄存器真的决定了 CPU 的航向,只是 CPU 的航向被存入了这两个寄存器之中。(在 x86 体系架构中,是内存就应该用 “段基址:段内偏移地址” 的机制来访问,是地址就该有地方存放)
在执行当前指令的同时,在不跨段的情况下,CPU 以“ 当前 IP 寄存器中的值+当前执行指令的机器码长度” 的和作为新的代码段内偏移地址,将其存入 IP 寄存器,再到该新地址处读取指令井执行。 如果下一条指令需要跨段访问,还要加载新的段基址到 CS 寄存器。
flags 寄存器是计算机的窗口,展示了 CPU 内部各项设置、指标,任何一个指令的执行,其执行过程的细节,对计算机造成了哪些影响,都在 flags 寄存器中通过一些标志位反映出来。
通用寄存器:
无论是实模式,还是保护模式,通用寄存器都是有 8 个,分别是 AX BX CX DX SI DI BP SP。
拿 AX 寄存器举例,根据图 3-6 可知, AX 寄存器是由 AH 寄存器和 AL 寄存器组成的,它们都是 8 位寄存器。由于某种原因,16 位 AX 寄存器不够用了,将其扩展 (Extend) 为 32 位,在 AX 原有的基础上,增加 16 位作为高 16 位便是扩展的 AX,即 EAX。所以 EAX 归根结底也是由 AL,AH 组成的,AL,AH 值变了会直接影响 EAX。
通用是说每个寄存器的功能不单一,可以有多种用途,不像段寄存器 SS 那样只能用来放栈段基址,通用寄存器可以用来保存任何数据。虽说通用,但还是约定了它们的惯用法,除了通用的用途外每个寄存器肩负特定的功用。比如一般情况下,cx 寄存器用作循环的次数控制,bx 寄存器用于存储起始地址。这样,在为一些通用的函数传递参数时会方便很多;另外,一些指令已经固定用一些特定的寄存器作为参数了。
各通用寄存器特定的功能描述如下:
3.2.3 实模式下内存分段由来
CPU 中本来是没有实模式这一称呼的,是因为有了保护模式后,为了与老的模式区别开来,所以称老的模式为实模式。
实模式的 “实” 体现在:程序中用到的地址都是真实的物理地址,"段基址:段内偏移 " 产生的逻辑地址就是物理地址,也就是程序员看到的完全是真实的内存。
本节🍊:任何事物发展到今天,都有一段 “合理” 的过程,了解这一过程是怎么来的,有助于理解它今天的形态。
8086 之前的 CPU 对内存的访问比 8086 还要“实诚”,它们没有段的概念,程序中要访问内存,需要把地址写死,也就是所谓的 “硬编码”。由于 “硬编码” 的各种缺点,Intel 早期的工程师发明了“段”,即 CPU 访问内存用 “段+偏移” 的形式。它就是首次在 8086 上出现的,自那之后的 CPU 都是用这类思想访问内存,只是在形式上有小改动。为了支持段机制,CPU 中新增了段寄存器,如 cs 、ds、es 等。
查了一下为什么叫 8086🍊
这个起源可以从8085说起,这个名字的字面意思是:80年代生产的8位5伏电压处理器。后来,对这个进行升级,就简单的在个位上加1,变成了8086,也就出现了那个非常著名的x86鼻祖,于是就延用下来了。其实,后来的32位和64位系统,全名叫做x86-32 和 x86-64。但是,为了方便区分,就变成了x86和x64。
总结一下:8085是8位,8086是16位,x86-32是32位,x86-64是64位。其中,x86就是x86-32的简称,x64就是x86-64的简称。
8086 的地址总线是 20 位宽,也就是其寻址范围是 2^20 次方= IMB 。但其内部寄存器都是 16 位的,若用单一寄存器来寻址的话,只能访问到 2^16 次方等于 64KB 的空间。(地址线位宽和寄存器位宽是没有必然联系)
为了让 16 位的寄存器寻址能够访问 20 位的地址空间 (注意,这里说的是通过寄存器寻址,因为只有通过 16 位的寄存器去寻址才会受到 16 位的限制,立即数寻址不会受到限制),通过先把 16 位的段基址左移 4 位后变成 20 位,再加段内偏移地址,这样便形成了 20 位地址。
段基址为 0xFFFF0,偏移地址应该小于等于 F 就对啦,而这个偏移地址却是 0xFFFF,超出了 0xFFF0 的空间,也就是多出来 64K-16 字节,这部分内存就是传说中的高端内存区( High Memory Area, HMA )。这部分内存是不存在的,也不用处理,8086 只能接收 20 位长的地址(A0~A19 地址线),所以由于超过了 20 位而产生的进位,就会给丢掉,其作用相当于把地址对 IMB 取模了。这是地址回卷的效果,即超过最大范围后,从 0 重新开始计数。
3.2.4 实模式下 CPU 内存寻址方式
🍊其实一切所谓的格式都是一种“协议”、“约定”,“约定” 的原因是为了省事,让大家共同按照某种约定行事,这样服务的提供方就不必为满足各种各样需求方而煞费苦心。
为了 CPU 设计更容易,CPU 访问数据的形式也需要提前“约定”好,这就是所谓的寻址方式。8086 的寻址方式,从大方向来看可以分为三大类:寄存器寻址、立即数寻址、内存寻址。在内存寻址中又分为:直接寻址、基址寻址、变址寻址、基址变址寻址。
从名字上看,寻址就是寻找地址,寻找谁的地址?CPU 眼里只有二进制数,所以这是 CPU 在寻找“数” 的地址。这个“数”可以源操作数,也可以是目的操作数 (顺便提一句,Intel 汇编语言语法是“指令目的操作数,源操作数”。简而言之,寻址就是找到“数”的所在地,从哪来,往哪去。
寄存器寻址:
最直接的寻址方式就是寄存器寻址,它是指“数”在寄存器中,直接从寄存器中拿数据就行了。只要牵扯到寄存器的操作,无论其是源操作数,还是目的操作数,都是寄存器寻址。若指令中有源操作数是立即数,也属于立即数寻址。
立即数寻址:
立即数就是常数。操作数 “直接” 存在指令中,直接拿过来,立即就能用了,为了突显“立即就能用”的高效率,此数便称为立即数,CPU 免去了找数的过程。
内存寻址:
操作数在内存中的寻址方式称为内存寻址。由于访问内存是用 “段基址:偏内偏移地址” 的形式,特别强调一下,此形式只用在内存访问中。默认情况下数据段寄存器是 DS,即段基址已经有了,只要再给出段内偏移地址就可以访问内存了,最终起决定作用的、有效的是段内偏移地址,所以段内偏移地址称为有效地址。
直接寻址:直接寻址,就是将直接在操作数中给出的数字作为内存地址,通过中括号的形式告诉 CPU,取此地址中的值作为操作数。这里给出的数字是段内偏移地址,默认的段地址是 DS,若使用了段跨越前缀,那么段基址就在该前缀的段寄存器中。(书中有例子,更容易理解)
注⚠️:立即数寻址中的数字是直接拿来就用作操作数了,直接寻址中的数字是用来进一步寻址的。
基址寻址:就是在操作数中用 bx 寄存器或 bp 寄存器作为地址的起始,地址的变化以它为基础。在实模式下,限制了只能用 bx,bp 作为基址寄存器,bx 寄存器的默认段寄存器是 DS,而 bp 寄存器的默认段寄存器是 SS。(bp、sp 都是栈的有效地址,有效地址就是指偏移地址)。
实模式下,CPU 字长是 16(字长反映 CPU 一次运算的数字位数,而实模式下都是 16 位的数字),而栈的操作是以字长为单位的,因此实模式下的 push 指令默认情况下是压入 2 字节的数据,pop 指令默认是弹出 2 字节的数据。
其工作原理都可分为两步:(栈是向低地址方向发展的)
假如执行 push ax:
1 sub sp, 2 先将 sp 中的值减去2
2 mov [sp], ax 再将 ax 中的值 mov 到新的 ss:sp 指向的内存
假如执行 pop ax:
1 mov ax, [sp] 先将 ss:sp 指向的值 mov 到 ax
2 add sp, 2 再将 sp 的指针 +2
访问栈有两种方式,一种是把栈当成“栈”来使用,也就是用 push,pop 指令操作栈,但这样我们只能访问到栈顶,即 sp 指向的地址,没有办法直接访问到栈底和栈顶之间的数据。
很多时候,我们需要读写栈中的数据,即需要把栈当成普通数据段那样访问。处理器为了让开发人员方便控制栈中数据,提供了这种把栈当成数据段来访问的方式,可以用寄存器 bp 来给出栈中偏 移量,所以 bp 默认的段寄存器就是 SS,这样就可通过 SS: bp 的方式把栈当成普通的数据段来访问了。
变址寻址:变址寻址其实和基址寻址类似,只是寄存器由 bx,bp 换成了 si,di。si 是指源索引寄存器 (source index),di 是指目的索引寄存器 (destination index),两个寄存器的默认段寄存器也是 ds。变址寻址主要是用于字符搬运方面的指令,这两个寄存器在很多指令中都要成对使用。(配合基址寻址,用来实现基址变址寻址)
基址变址寻址:从名字上看,这是基址寻址和变址寻址的结合,即基址寄存器 bx 或 bp 加一个变址寄存器 si 或 di。
一种寻址方式对应一种电路实现,增加一种寻址方式,会增加硬件电路的复杂性,所以寻址方式是有限的。
🍊上层形式万千的变化,归根结底就是这么几类硬件电路,而它是有限的。