目录
引用与说明
3.1、地址、section、vstart 浅尝辄止
1、什么是地址
2、什么是 section【汇编】
3、什么是 vstart【汇编】
3.2、CPU 的实模式
1、CPU 工作原理【重要】
2、实模式下的寄存器
4、实模式下 CPU 内存寻址方式
5、栈到底是什么玩意儿
6 ~ 8 无条件转移【汇编】
6、实模式下的 ret
7、实模式下的 call
8、实模式下的 jmp
9、标志寄存器 flags
10、有条件转移
其他问题
引用与说明
- 《操作系统真相还原》,作者:郑钢
- 原书讲解得非常详细,但是战线拉得太长,反而容易找不到重点。所以,编写本篇文章旨在让读者抓住重点,也可以作为原章节的导读。3.1 和 3.2 节在讲一些概念和汇编操作,如果读者对这些概念比较熟悉,完全可以跳过本篇,并不影响实操。从 3.3 节开始进行实际操作,将在下一篇讲解。
建议略读章节 |
---|
3.3.3、实模式下内存分段由来 |
3.1、地址、section、vstart 浅尝辄止
1、什么是地址
地址只是数字,描述各种符号在源程序中的位置,是各符号相对于文件开头的偏移量。编译器给各符号分配地址,这些符号在空间上彼此相邻,连续分布。
地址等于上一个地址 + 上一个地址处的内容的长度。例如地址列第二行的 3 等于 “上一个地址 0” + “上一个地址 0 处的内容:B80000 的长度 3”。“B80000” 是一个 6 位的十六进制数。每两个十六进制数字对应一个字节,“B8” 是第一个字节,“00” 是第二个字节,“00” 是第三个字节。“B80000” 的长度是 3 个字节。“长度是 3”这个描述,它通常指的是这个十六进制数在内存中占用的字节数。
2、什么是 section【汇编】
section 称为节,功能是在程序中宣称一个区域,为了让程序员在逻辑上将程序划分成几个部分。
关键字 section 没有对程序中的地址产生任何影响,section 中的数据的地址依然是相对于整个文件的顺延,仅仅是在逻辑上让开发人员梳理程序之用。
3、什么是 vstart【汇编】
vstart 是虚拟起始地址,section 用 vstart = 来修饰后,被赋予一个虚拟起始地址,它被用来计算在该 section 内的所有引用地址。虚拟起始地址 != x86 CPU 开启分页后的虚拟地址。
vstart 按照开发人员的意愿安排新的起始地址,不再以文件开头 0 为起始,其地址若超过文件大小则不会落在文件内,也就是说根据此地址在文件中是找不到相关数据的,文件中的所有符号都不在这个地址上,所以是虚拟的。
用 vstart 的时机是:预先知道我的程序将来被加载到某地址处。第二章代码中 mbr 用 vstart = 0x7c00 来修饰,是开发人员知道 mbr 要被加载器(BIOS)加载到物理地址 0x7c00,mbr 中后续的物理地址都是 0x7c00 +。
3.2、CPU 的实模式
实模式是指 8086 CPU 的寻址方式、寄存器大小、指令用法等,是用来反应 CPU 在该环境下如何工作的概念,并不是单指某一方面的设置。CPU 的唯一任务就是执行指令。
1、CPU 工作原理【重要】
程序被加载到内存后,指令这时都在内存中了。控制单元要取下一条待运行的指令,该指令的地址在程序计数器 PC 中,在 x86 CPU 上,程序计数器就是 cs:ip。控制单元从 cs 寄存器中读取段基址,从 ip 寄存器读取偏移量。读取 ip 寄存器后,将此地址送上地址总线,CPU 根据此地址便得到了指令,并将其存入到指令寄存器 IR 中。
指令译码器根据指令格式检查指令寄存器中的指令,先确定操作码是什么,再检查操作数类型,若是在内存中,就将相应操作数从内存中取回放入自己的存储单元,若操作数是在寄存器中就直接用,免了取操作数这一过程。
操作码有了,操作数也齐了,操作控制器给运算单元下令,开工,于是运算单元便真正开始执行指令了。运算单元执行指令,并将结果存放在指定的寄存器或内存。
在执行当前指令的同时,在不跨段的情况下,CPU 以 ”当前 IP 寄存器中的值 + 当前执行指令的机器码长度“ 的和作为新的代码段内偏移地址,将其存入 IP 寄存器。如果下一条指令需要跨段访问,还要加载新的段基址到 CS 寄存器。这样便得到了下一条指令的地址。接着控制单元又要取下一条指令了,流程回到了本阶段开头。
2、实模式下的寄存器
寄存器是使用触发器实现的,工作速度极快。
CPU 中的寄存器分类 |
---|
对程序员不可见的寄存器,无法直接使用,但部分可以被程序员初始化 |
全局描述符表寄存器 GDTR |
中断描述符表寄存器 IDTR |
局部描述符表寄存器 LDTR |
任务寄存器 TR |
控制寄存器 CR0~3 |
指令指针寄存器 IP |
标志寄存器 flags |
调试寄存器 DR0~7 |
对程序员可见的寄存器,进行汇编语言设计时,能直接操作的就是这些寄存器 |
段寄存器:见书【P74、P75】,这里不用太纠结,讲到寻址方式时就知道运用在哪了 |
通用寄存器:非常详细,需要仔细阅读,见书【P75、P76】 |
【注】:图 3-3 和 表 3-2 中的各个寄存器的名字要留个印象,接下来讲内存寻址时各个寄存器会频繁出现 ,讲到哪个寄存器时请一定回来查找这几张图和表。
4、实模式下 CPU 内存寻址方式
寻址指的是 CPU 在寻找 “数” 的地址,找到 “数” 的所在地,从哪来,往哪去。这个 “数” 可以是源操作数,也可以是目的操作数。Intel 汇编语言语法是 “指令目的操作数,源操作数”。
每一种寻址方式对应一种电路实现,增加一种寻址方式,会增加硬件电路的复杂性,所以寻址方式是有限的。
- 寄存器寻址
寄存器寻址是最直接的寻址方式,它是指 “数” 在寄存器中,直接从寄存器中拿数据就行了。
; 用 mul 指令实现 0x10 * 0x9
mov ax, 0x10 ; 第一条命令是将 0x10 存入 ax 寄存器
mov dx, 0x9 ; 第二条命令是将 0x9 存入 dx
mul dx ; 第三条指令是求 ax 和 dx 的乘积,乘积的高16位在 dx 寄存器,低16位在 ax 寄存器
以上三条指令都是寄存器寻址。只要牵扯到寄存器的操作,无论其是源操作数,还是目的操作数,都是寄存器寻址。上面的第一、二条指令,它们的源操作数都是立即数,所以也属于立即数寻址。
- 立即数寻址
立即数就是常数。指令由操作码和操作数组成,得到一个数往往不容易。这个数要么在寄存器中,要么在内存中,都是间接给出的,所以得到数就要花费一些 CPU 周期。如果操作数 "直接" 存在指令中,直接拿过来,立即就能用了。为了突显 "立即就能用" 的高效率,此数便称为立即数。立即数免去了找数的过程。
mov ax, 0x18 ; 立即数寻址
mov ds, ax ; 寄存器寻址
mov ax, macro_selector ; 立即数寻址
mov ax, label_start ; 立即数寻址
第一条指令中的源操作数 0x18 是立即数,目的操作数 ax 是寄存器,所以它既是立即数寻址,也是寄存器寻址。第二条指令中,源操作数和目的操作数都是寄存器,所以纯粹是寄存器寻址。第三条指令的源操作数 macro_selector 是个宏,第四条指令的源操作数 label_start 是个标号,这两个在编译阶段会转换为数字,最终可执行文件中的依然是立即数。
- 内存寻址
以上两种寻址方式,操作数一个是在寄存器中,一个是在指令中直接给出。它们都不在内存中。操作数在内存中的寻址方式称为内存寻址。
由于访问内存是用 "段基址:段内偏移地址" 的形式,此形式只用在内存访问中。默认情况下数据段寄存器是 DS,即段基址已经有了,只要再给出段内偏移地址就可以访问内存了,最终起决定作用的、有效的是段内偏移地址,所以段内偏移地址称为有效地址。
“[x]” 表示取了 x 的地址,其中 x 通常指的是一个寄存器或一个计算出的地址。
- 直接寻址(属于内存寻址)
直接寻址,就是将直接在操作数中给出的数字作为内存地址,通过中括号的形式告诉CPU,取此地址中的值作为操作数。
mov ax, [0x1234]
mov ax, [fs:0x5678]
0x1234 是段内偏移地址,DS 是数据段寄存器的默认段寄存器【图 3-3】。这条指令是将内存地址 DS:0x1234 处的值写入 ax 寄存器。段基址*16 变成 20 位地址后,再加上段内偏移地址 0x1234,结果还是 20 位地址。
第二条指令中,由于使用了段跨越前缀 fs,这意味着该指令使用 FS 段寄存器而不是默认的 DS。最终的内存地址是 fs 寄存器的值*16 + 0x5678,CPU 到此内存地址取值再存入 ax 寄存器。
【注】:不要和立即数寻址混了,立即数寻址中的数字是直接拿来就用作操作数了,直接寻址中的数字是用来进一步寻址的。
- 基址寻址(属于内存寻址)
基址寻址,就是在操作数中用 bx 寄存器或 bp 寄存器作为地址的起始,地址的变化以它为基础。用寄存器作为内存寻址,在实模式下必须用 bx 或 bp 寄存器。保护模式下,基址寄存器可选择的很多,可以是全部的通用寄存器。
bx 寄存器的默认段寄存器是 DS。例如 "add word[bx], 0x1234" 这条指令将 0x1234 加上内存地址 ds:bx 处的值后再存入内存地址 ds:bx 中。这条指令用到了立即数寻址和内存基址寻址两种方式。
add word[bx], 0x1234
bp 寄存器的默认段寄存器是 SS,即 bp 和 sp 都是栈的偏移地址,即 bp 是用来访问栈的。
【Q】为什么已经有了 sp 寄存器来 "专门" 访问栈,还要再单独准备个 bp 呢?
【A】
访问栈有两种方式,一种是把栈当作 “栈” 来使用,也就是用 push 和 pop 指令操作栈,sp 寄存器作为栈顶指针,相当于栈中数据的游标,这是专门给 push 指令和 pop 指令做导航用的寄存器,push 指令往哪个内存压入数据,pop 将哪个地址的数据弹出栈,都要看 sp 的值是多少。但这样我们只能访问到栈顶,即 sp 指向的地址,没有办法直接访问到栈底和栈顶之间的数据。
; 实模式下,CPU 字长是16,所以实模式下的 push 指令默认情况下是压入2字节的数据。执行 push ax:
sub sp, 2 ; 先将 sp 的值减去
mov sp, ax ; 再将 ax 的值 mov 到新的 sp 指向的内存
; 实模式下 pop 指令。执行 pop ax:
mov ax,[sp] ; 先将 sp 指向的值 mov 到 ax
add sp,2 ; 再将 sp 的指针+2
很多时候,我们需要读写栈中的数据,即需要把栈当成普通数据段那样访问。举个需要直接写栈的例子,比如标志寄存器 eflags 无法直接修改,只能用 pushf 指令把 eflags 寄存器的内容压到栈中,我们在栈中修改完后,再用 popf 把它弹回到 eflags 中。处理器为了让开发人员方便控制栈中数据,提供了这种把栈当成数据段来访问的方式,可以用寄存器 bp 来给出栈中偏移量,所以 bp 默认的段寄存器就是 SS,这样就可以通过 SS:bp 的方式把栈当成普通的数据端来访问了。
【注】: “栈中保存局部变量和函数参数” 的例子非常详细,见书【P80、P81】。
- 变址寻址(属于内存寻址)
变址寻址和基址寻址类似,只是寄存器由 bx、bp 换成了 si 和 di。si 是指源索引寄存器,di 是指目的索引寄存器。两个寄存器的默认段寄存器也是 ds。
mov [di], ax ; 将寄存器 ax 的值存入 ds:di 指向的内存
mov [si+0x1234], ax ; 变址中也可以加个偏移量
变址寻址主要是用于字符搬运方面的指令,这两个寄存器在很多指令中都要成对使用,如 movsb,movsw,movsd 等。
- 基址变址寻址(属于内存寻址)
名字上看,这是基址寻址和变址寻址的结合,即基址寄存器 bx 或 bp 加一个变址寄存器 si 或 di。
了解即可,项目中没用到。
mov [bx+di], ax ; 将 ax 中的值送入以 ds 为段基址,bx+di 为偏移地址的内存
add [bx+si], ax ; 将 ax 与[ds:bx+si]处的值相加后存入内存[ds: bx+si]
【汇编】:mul 指令说明
mul dx:这条指令将 ax 寄存器中的值与 dx 寄存器中的值相乘。mul 指令是无符号乘法指令,它会用 ax 寄存器中的值作为被乘数,用 dx 寄存器中的值作为乘数。乘法结果的低 16 位将存放在 ax 寄存器中。乘法结果的高16位将存放在 dx 寄存器中。
- ax 寄存器的值:0x10 = 16
- dx 寄存器的值:0x9 = 9
- 乘积 = 16 × 9 = 144,十六进制表示是 0x90。
- ax 存放乘积的低 16 位,因此 ax 寄存器中的值为 0x90 (144)。
- dx 存放乘积的高 16 位。由于 144 的乘积在 16 位范围内没有高位,所以 dx 寄存器中的值将为 0x0 (0)。
【汇编】:word 数据类型伪指令【P89】
数据类型伪指令有 byte、word、dword、qword 等,它们用在操作数前,相当于做数据类型强制转换。在汇编语言中,无论操作数是立即数、寄存器,或是内存,都可以用数据类型伪指令。
在 16 位实模式下默认数据宽度是 16 位,关键字 “word” 用来告诉 CPU 一次要读或写 2 字节。
mov word [addr], near_proc
例如使用 “mov word [addr], near_proc” 指令时,addr 是个 4 字节的变量,用来存储函数 near_proc 的地址。 near_proc 是个函数名,本身是个地址,在编译阶段就会被替换为数字,这个数字的宽度是不定的,比如 0x18 是 0x0018,还是 0x00000018 呢?这涉及到读写多少个字节的问题,这也是高级语言中数据类型的作用。由于此时是在 16 位的实模式下,我们要用 16 位的地址,必须得告诉 cpu 在此内存地址处连续读 2 字节就够了,所以用关键字 word。
5、栈到底是什么玩意儿
CPU 中有栈段 SS 寄存器和栈指针 SP 寄存器,用来指定当前使用的物理地址。堆栈是人们常说的栈,和堆没关系。
内存中的栈,是物理上的,把数据结构中的栈的概念用物理硬件来实现。它同数据段、代码段一样,是个内存中的区域,也就是栈段寄存器 SS 和栈指针 SP 所指向的内存区域。我们常听说的栈溢出,指的就是这个内存区域无法容纳数据了。
栈要实现线性结构。内存就是线性结构,要做的就是给栈指定一片内存区域,区域的起始地址作为栈基址,存入栈基址寄存器 SS 中,另一端是动态变化的,用栈指针寄存器 SP 来指定。栈在使用过程中是向下扩展的,所以栈顶地址肯定小于栈底地址。栈中的内存地址也是用 "段基址 SS 的值*16+栈指针 SP(段内偏移地址)形成的 20 位地址"访问到的。
由于是硬件实现的栈,故硬件提供了相应的方法来存取栈,即 push 和 pop 指令。在操作 SP 时要加减字长,CPU的字长指一次可处理的数据的长度。在实模式下的字长是 16,所以实模式下的 push 和 pop 指令默认情况下是压入或弹出 2 字节的数据,SP 加上或减去 2 字节。【注】:之前在讲解基址寻址时有例子。
【注】如图 3-9 所示,虽然栈是向下发展的,但栈也是内存,访问内存依然是从低地址往高地址,假如当前栈顶是 0x1233E,栈顶数据占 2 字节的话,其范围是 0x1233E ~ 0x1233F。
即使是这里的硬件栈,咱们也可以自己维护指针,如 push ax 可以这样代替:
mov bp, sp
sub bp, 2
mov [bp], ax
bp 默认的段寄存器就是 SS,用 bp 的时候直接操作的便是栈,bp 就相当于栈指针。
6 ~ 8 无条件转移【汇编】
IP 寄存器不能通过赋值更改,所以 CPU 中提供了很多可以改变 CPU 执行流的指令,如 call、ret、jmp 等,它们在内部实现上,包含了许多微操作,用于设置相关数据结构,但是表面上看来只是一个指令。它们在原理上是修改寄存器 CS 和 IP 的值,将 CPU 导向新的位置。
“相对” 时操作数的计算:无论是 call,还是 jmp,只要是 "相对" 的形式,操作数都是这样来的,目标地址减去当前指令地址后所得的差,再减去机器码大小。
【注】:这里将几个关键字给总结了一下,这些关键字的组合表示了调用方式的特征,在 call 和 jmp 中都是适用的。我仅在个别调用方法中给出名字的解释,读者可以依葫芦画瓢分析。
【汇编】near、short、far 修饰符
near 的意思同数据类型伪指令 word 一样,指在内存地址处取 2 字节内容,或者将操作数强制转换为 2 字节。可以认为像 near、short、far 这些用在调用或转移中的修饰符,意义就是数据类型转换。far 表示取 4 字节,short 表示取 1 字节。
near 若加在寄存器前面,如 call near ax,表示在 ax 寄存器取 2 字节,相当于给 ax 寄存器中的值做了类型转换。由于 near 的范围可正可负,是个有符号数范围,所以它不等同于数据类型 word。
6、实模式下的 ret
call 指令用来执行一段新的代码,它执行完目标函数后还是要回来的,所以它得提前把回来的路(返回地址)记好。对于 CPU 来说,它是靠程序计数器 PC 来指路的,所以路就在 PC 中。由于随着函数嵌套调用的层数增加,会有更多的返回地址需要保存。内存空间相对是无限的,保存数量未知的返回地址比较理想。利用栈的后进先出的特性,可以保证函数嵌套调用及嵌套返回顺序的一致性,而且栈空间只受限于内存大小。所以 CPU 在栈中保留程序计数器 PC 的值。在 x86 中的程序计数器是 CS:IP,具体保留 IP 部分还是 CS 和 IP 都保留,是要看目标函数的段基址是否和当前段基址一致,是否跨段访问了。
call 指令只会留下返回地址后并踏上新的征程,保留的返回地址是给 ret 或 retf 指令准备的,在目标函数中必须有这两个指令之一,CPU才能回来。
ret (return)把当前栈顶(寄存器 ss: sp 所指向的地址)处的 2 字节 内容弹出栈并用它为 IP 寄存器赋值。ret 指令不管里面的内容是不是地址,内容的正确性由程序员自己控制。ret 只置换了 IP 寄存器,不用换段基址,属于近返回。既然我们称之为弹出栈,也就是说 ret 指令也要负责维护栈顶指针,由于栈是从高地址往低地址发展,所以被回收的栈顶空间应该是使 sp 指针值变大,故 ret 指令会使 sp 指针+2。
retf (return far)是从栈顶取得 4 字节,栈顶处的 2 字节用来替换 IP 寄存器,另外的 2 字节用来替换 CS 寄存器。retf 也不会去检查从栈顶往上的 4 字节内容是不是偏移地址和段基址,由程序员负责栈中数据的正确性。段寄存器都换了,说明这属于远返回。retf 指令也要负责维护栈顶指针,所以 retf 指令会使 sp 指针+4。
【注】call 和 ret 配对,用于近调用和近返回,call far 和 retf 配对,用于远调用和远返回。
7、实模式下的 call
call,意为呼叫、调用。在汇编言中,用 call 命令实现一个函数的调用。在 8086 中 jmp 和 call 两个指令用于改变程序流程。区别是 jmp 属于一去不回头地去执行新的代码,适用环境是 "交接",call 指令用于执行完一段分支后再回来的情况。
- 16位实模式相对近调用
指令格式: “call near 立即数地址”;near 可省略。
此指令是个 3 字节指令,0xe8 是此操作的操作码,占 1 字节,操作数占 2 字节。立即数地址可以是被调用的函数名、标号、立即数,函数名同标号一样,最终会被编译器转换为一个实际数字地址,如 call near prog_name。
- 不直接、不间接: 指令中的操作数是立即数,是 call 指令相对于目标地址的偏移量,是个地址差,不是绝对地址。CPU 在实际执行中还要将此偏移量还原成绝对地址。需要转换,不够直接;操作数不是寄存器或内存,不够间接。
- 相对: “立即数地址” 是 call 指令相对于目标地址的偏移量。
- 近:跳转地址和当前指令在同一个段内,只需给出段内偏移地址。操作数是个有符号数且又占 2 字节,由于段是个 16 位大小的空间,所以,正负数的范围是 -32768~32767。
操作数的计算:操作数=目标地址﹣当前指令地址﹣3
call 相对近调用,此指令机器码是 e8llhh,占用 3 字节。其中 e8 是操作码,表示相对近调用,ll 表示操作数的低位,hh 表示操作数的高位,hhll 表示跳转目标为 4 位地址。由于 x86 平台是小端字节序,故写成了 llhh,即高位在高地址,低位在低地址。这 4 位地址是个相对增量,首先用目标函数的地址减去当前 call 指令的地址,所得的差再减去此 call 指令机器码的大小,最终的结果便是 call 指令中的操作数,即与目标地址的相对地址增量。如 call proc_name,proc_name 是某函数名,假如 call 所在的地址是 0x9,事先知道 proc_name 所在的地址是 0x12,此处的 call 是相对近转移,机器码占 3 字节,最终 call 的操作数是 0x12 - 0x9 - 3 = 0x6,由于是小端字节序的原因,低位在低地址,高位在高地址,最终的操作码是 e80600。这个值 0x6 不需要我们去算,编译器会将函数的地址转换成相对地址。
【注】:只要是 “相对”,就是给出跳转指令对于当前指令的偏移量,偏移量都是这么算的。
- 16位实模式间接绝对近调用
指令格式:"call 寄存器寻址" 或 "call 内存寻址";near 可省略,已省。
如 call ax,call [0x1234]。不同指令形式对应不同的操作码,"call 内存寻址" 对应的操作码是 ff16,机器码是 ff16+16 位内存地址。机器码除了与寻址方式有关外,还和寄存器名称有关,如 "call ax" 的机器码是 ffd0,"call cx" 的机器码是 ffd1。此调用形式也是近调用,near 可以省略,并没有跨段,所以 call 指令只要保留 IP 寄存器的值就好了,将其压入栈后,再用新的偏移地址替换 IP 的值。
- 间接:"call 寄存器寻址" 或 "call 内存寻址"
- 绝对:直接给出目标地址
- 近:只需给出段内偏移地址
- 16位实模式直接绝对远调用
指令格式:“call far 段基址(立即数):段内偏移地址(立即数)”。far 可省略
操作码是 0x9a。机器码是 0x9a + 2 字节的偏移地址+2 字节的段基址,即偏移地址在前,段基址在后,和指令的调用形式是相反的。由于是远调用,所以 CS 和 IP 都要用新的,call 指令将来还是要回来的,所以要在栈中保留回来的路,即先把老的 CS 寄存器压入栈,再把老的 IP 寄存器压入栈后,用新的 CS 和 IP 寄存器替换,从此开启新的旅途。
- 16位实模式间接绝对远调用
这和第 3 种的区别就是 "直接" 变 "间接" 了。也就是说,段基址和段内偏移地址,都不是立即数,要么在内存中,要么在寄存器中。可是,段基址和段内偏移地址都是 16 位地址,用一个寄存器肯定是盛不下了,至少得用两个。寄存器资源还是非常珍贵的,既然要用两个,干脆一个都不用算啦,所以这种间接绝对远调用的形式,不支持寄存器寻址,只支持内存寻址,即段基址和段内偏移地址在内存中。
指令格式:“call far 内存寻址“。far 不可省略,否则就和间接绝对近调用一样了。
如 call far [bx],call far [0x1234],操作码是 ffle。在该内存中的内容大小是 4 字节,此内容便是地址,前(低)2字节是段内偏移地址,后(高)2字节是段基址。
新的段基址和段内偏移既然是在内存中,访问内存的话,也要按照 "段基址:段内偏移地址" 的形式去操作。例如上面的 call far [0x1234],由于没有段跨越前缀,则将默认的段基址寄存器 ds*16 后再与 0x1234 相加,得到的和为物理地址,再到该物理地址处去读取新的偏移地址和段基址,以该物理地址为起始的 2 个字节是段内偏移地址,以(该物理地址+2)为起始的 2 个字节是段基址。既然是段基址和段内偏移地址都要用新的,CPU 为了记得回来的路,先把老的 CS 寄存器压入栈,再把老的 IP 寄存器压入栈保存起来,再用新的段基址替换 CS,新的段内偏移地址替换IP。
8、实模式下的 jmp
无条件跳转,是指 "生硬地" 改变 CPU 航线,将程序流转移到新的位置。jmp 转移指令只要更新 CS:IP 寄存器或只更新 IP 寄存器就好了,不需要保存它们的值。
- 16位实模式相对短转移
指令格式:"jmp short 立即数地址" 。
操作数是个相对增量,是个有符号数。相对短转移的机器码大小是 2 字节,操作码是 0xeb,可知其占 1 字节。操作数也占 1 字节。
- 不直接、不间接: 同相对近调用
- 相对:操作数是个相对增量,是个有符号数。
- 短:跳转地址和当前指令在同一个段内,只需给出段内偏移地址。 "短" 体现在操作数中,即跳转的范围只能是 1 字节有符号数所表示的范围,即 -128~127。
操作数的计算:操作数=目标地址﹣当前指令地址﹣2
CPU 是要用绝对地址来寻址的,目标地址=操作数 + 当前指令地址 + 2,所得的结果便是目标地址的绝对地址,这样的地址 CPU 才能用。CPU 把求得的绝对地址载入 IP 寄存器。短转移目标地址和当前指令在同一个段内,所以 CS 段寄存器不用修改,CPU 就实现了向新位置的转移。
- 16位实模式相对近转移
同 “相对短转移” 相比,操作数范围增大了,由 8 位宽度变成了 16 位宽度,操作数依然是地址相对量,可正可负,范围是 -32768~32767。其他没啥区别。
指令格式:”jmp near 立即数地址”。
操作码是 0xe9。立即数地址也要经过编译器转换为地址偏移量,再变成机器指令中的操作数。
操作数的计算:操作数=目标地址﹣当前指令地址﹣3。
- 16位实模式间接绝对近转移
同 "jmp 相对近转移" 相比,目标地址是绝对地址,未在指令中直接给出,存在寄存器或内存中。
指令格式: “jmp near 寄存器寻址” 或者 “jmp near 内存寻址”;near 可省。
若操作数在内存中,在不使用段跨越前缀的情况下,段基址寄存器是 DS。由于这也是近转移,CS 寄存器的值不用修改,CPU 只要用 16 位寄存器的值或内存中的 2 字节载入 IP 寄存器,CPU 马上就被带到新的地址。
采用寄存器寻址的 jmp 指令,其操作码是 0xff,操作数随寄存器的不同而不同。采用内存寻址的jmp 指令,其操作码还要看段基址寄存器用的是哪个。【P95】样例说明该点。
- 16位实模式直接绝对远转移
直接绝对远转移就是以立即数的形式给出目标地址的段基址和段内偏移地址。
指令格式:“jmp 立即数形式的段基址:立即数形式的段内偏移地址”。
例如 jmp0: 0x900,其中 0 是段基址,0x900 是段内偏移地址。由于是远转移,所以 CPU 用操作数中的段基址载入 CS 寄存器,用操作数中的偏移地址载入 IP 寄存器后才完成转移。
- 16位实模式间接绝对远转移
与 “间接绝对远调用” 一样,由于操作数是两个数,操作数只能放在内存中。为了指示 CPU 在内存中取 4 个字节,需要在指令中用关键字 far,即前两个字节是段内偏移地址,后两个字节是段基址。若不指定,则和第三种的 "间接绝对近转移" 一样,只在内存处取 2 字节。
指令格式:“jmp far 内存寻址”。far 不可省略。
由于操作数在内存中,在不使用段跨越前缀的情况下,段基址寄存器是 DS。此指令的操作数,需要访问内存才能得到,所以需要知道寻址方式。机器码与寻址方式有关,要根据实际使用的数据段寄存器等情况来决定。同样,由于是远转移,CPU 的 CS 寄存器和 IP 寄存器都要修改成操作数中指定的值,从而实现转移。修改方式请参考 “间接绝对远调用” 。
9、标志寄存器 flags
有条件转移指令的条件放在标志寄存器 flags 中。flags 寄存器是 16 位宽,保护模式下对其扩展(extend)成 32 位的 eflags 寄存器。这些用于判断的条件,本质上是上一条指令执行的结果。
flags 寄存器中存储的信息,只是结果的特征,即标志,并不是真正的结果,结果可以存储在内存中。这些标志告诉大家,为了产生这个结果,机器都做了什么。比如有时单纯的一个结果并不能让我们了解数据的全貌,产生结果的过程中是否有溢出,不知道这些,怎么确定结果是正确的呢?
无论逻辑多复杂,都可以通过最简单的判断和转移来实现。判断哪里?判断什么?这个判断的对象就是标志寄存器中的标志位。
第 x 位 | 标志位名称 | 用途 | 置 1 | 置 0 | 应用 |
---|---|---|---|---|---|
以下仅在 8088 以上 CPU 中有效 | |||||
0 | CF,进位 | 记录进位、借位 | 最高位进位、借位 | 检测无符号数加减法是否有溢出 | |
1 | 空着 | ||||
2 | PF,奇偶位 | 标记结果低 8 位中 1 的个数 | 个数为偶数 | 数据传输开始时和结束后对比,判断传输过程中是否出现错误 | |
3 | 空着 | ||||
4 | AF,辅助进位标志 | 记录运算结果低 4 位的进、借位情况, | 低半字节有进、借位 | ||
5 | 空着 | ||||
6 | ZF,零标志位 | 计算结果为 0 | |||
7 | SF,符号标志位 | 运算结果为负 | |||
8 | TF,陷阱标志位 | 让 CPU 进入单步工作方式 | 让 CPU 进入连续工作方式 | 平时我们用的 debug 程序,在单步调试时,原理上就是让 TF 位为1。 | |
9 | IF,中断标志位 | 中断开启,CPU 可以响应外部可屏蔽中断 | 中断关闭,CPU 不再响应来自外部的可屏蔽中断 | ||
10 | DF,方向标志位 | 用于字符串操作指令中,给地址的变化提供个方向。 | 指令中的操作数地址会自动减少一个单位,单位的大小取决于指令 | 指令中的操作数地址会自动增加一个单位,单位的大小取决于指令 | |
11 | OF,溢出标志位 | 标识计算的结果是否超过了数据类型可表示的范围 | 超过,有溢出 | 专门用于检测有符号数运算结果是否有溢出现象 | |
以下仅在 80286 以上 CPU 中有效,相对于8088,它支持特权级和多任务 | |||||
12~13 | IOPL,输入/输出特权级 | 用在有特权级概念的 CPU 中 | 有4个任务特权级,特权级 0、特权级 1、特权级 2 和特权级 3。故 IOPL 要占用 2 位来表示这 4 种特权级 | ||
14 | NT,任务嵌套标志位 | 8088 支持多任务,一个任务就是一个进程 | 当一个任务中又嵌套调用了另一个任务(进程)时 | ||
15 | 空着 | ||||
以下仅在 80386 以上 CPU 中有效 | |||||
16 | RF,恢复标志位 | 用于程序调试,指示是否接受调试故障,它需要与调试寄存器一起使用 | 忽略调试故障 | 接受调试故障 | |
17 | VM,虚拟 8086 模式 | CPU 有了保护模式后,为了兼容实模式下的用户程序 | 可以在保护模式下运行实模式下的程序 | 实模式下的程序不支持多任务,而且程序中的地址就是真实的物理地址。所以在保护模式下每运行一个实模式下的程序,就要为其虚拟一个实模式环境,故称为虚拟模式 | |
以下仅在 80486 以上 CPU 中有效 | |||||
18 | AC,对齐检查 | 检查程序中的数据或指令其内存地址是否是偶数,是否是16、32的整数倍,没有余数 | 进行地址对齐检查 | 不检查 | |
以下仅在 80586(奔腾)以上 CPU 中有效 | |||||
19 | VIF,虚拟中断标志位 | 虚拟模式下的中断标志 | |||
20 | VIP,虚拟中断挂起标志位 | 在多任务情况下,为操作系统提供的虚拟中断挂起信息,需要与VIF 位配合 | |||
21 | ID,识别标志位 | 系统经常要判断 CPU 型号 | 当前CPU支持 CPU id 指令,可以获取 CPU 的型号、厂商等信息 | 表示当前CPU不支持CPU id 指令 | |
22~31 | 没有实际用途,纯粹是占位用,为了将来扩展 |
10、有条件转移
有条件转移是一个指令族,在此简单称 jxx。
指令格式:”jxx 目标地址“。
若条件满足则跳转到目标地址,否则顺序执行下一条指令。目标地址只能是段内偏移地址。在实模式下,由编译器根据当前指令与目标地址的偏移量,自行将其编译成短转移或近转移。在保护模式下,寄存器中宽度已经到了 32 位,32 位的偏移地址可以访问到整个 32 位地线总线的 4GB 内存空间,编译器不再区分转移方式。
条件转移指令一定得在某个能够影响标志位的指令之后进行。每执行一条指令,标志寄存器中的相应位都会记录这条指令所带来的变化。条件转移指令判断的就是上一条指令对标志位的 "影响",这些 "影响" 就是条件,即条件转移指令中所说的条件就是指标志寄存器中的标志位。
【注】:表 3-12 要留个印象,经常用的就两三个。
其他问题
【Q】有的编译器中还支持 segment,segment 与 section 功能类似,那么 segment 与 section 有什么区别【P67】
【A】【P25】
【Q】该页提到”平坦模型“,什么是”平坦模型“【P70】
【A】【P12】
【Q】IA32 指令格式是怎么构成的?【P71】
【A】【P71】
【Q】既然指令是存放在指令寄存器中的,那指令中用到的数据存放在哪里?【P71】
【A】存储单元的介绍【P71】