2020-09-09 18:31:39 hawk
实际上这节主要简单介绍一下汇编方面的基础知识,为后面完成MBR程序做铺垫,主要包括汇编指令的规则讲解,会比较枯燥,有基础的或者对于这些没有兴趣的可以直接跳过,把这个当作汇编手册即可。
前面已经分析过了,计算机学科的传统优良传统就是兼容性——实际上实模式指的是8086CPU的工作环境、工作方式以及工作状态等。这里我们简单总结一下相关的知识点。
首先,在实模式下,默认用到的寄存器都是16位宽的(这里指的是当下的CPU会兼容16位CPU的特性,从而确保可以按照16位CPU的特性正常工作)。
CPU中的寄存器,大体上可以分为两大类——程序员可见的;程序员不可见的。对于程序员可见的寄存器,也就是在汇编语言程序设计的时候可以直接操作的寄存器,如段寄存器、通用寄存器等。而对于程序员不可见的寄存器,说的是程序员没有办法直接使用。虽然这些寄存器没有办法是用,但往往需要通过程序员进行初始化,比如全局描述符表寄存器GDTR、中断描述符表寄存器和IDTR局部描述符表寄存器LDTR,都可以通过lgdt指令初始化;任务寄存器TR,可以通过ltr指令初始化。而对于flags寄存器,通过pushf和popf指令,将flags寄存器的内容进行入栈和出栈。
除了上面的分类以外,实际上CPU中的寄存器还可以分为段寄存器、flags寄存器和通用寄存器。
对于段寄存器来说,其产生和CPU的工作模式相关——CPU一般通过分段机制来访问duan内存,即“段基址:段内偏移地址”表示相关的内存中的地址,而这个段基址则是用段寄存器来进行存储的。段寄存器中的段基址就相当于指定的一片内存的起始地址。需要说明的是,无论是在实模式,还是在保护模式(即我们平常使用),段寄存器都是16位的。
实模式下的段寄存器主要是CS代码段寄存器、DS数据段寄存器、ES、FS、GS附加段寄存器和SS栈段寄存器。
对于flags寄存器,其展示了CPU内部各项设置、指标等,会在后面进行介绍。
对于通用寄存器,其在保护模式和实模式下,都为AX、BX、CX、DX、SI、DI、BP、SP这8个。通用指的是每个寄存器的功能不单一,可以有多种用途,但是一般约定了通用寄存器的惯用功能,如下表所示
寄存器 | 助记名称 | 功能描述 |
AX | 累加器 | 常用于算数运算、逻辑运算、保存与外设输入输出的数据 |
BX | 基址寄存器 | 常用于存储内存地址,将其作为基址进行遍历 |
CX | 计数器 | 循环指令中的循环次数 |
DX | 数据寄存器 | 通常用来保存外设控制器的端口号地址 |
SI | 源变址寄存器 | 被操作的数据源地址 |
DI | 目的变址寄存器 | 被操作的数据的目的地址 |
SP | 栈指针寄存器 | 段基址是ss,用来指向栈顶 |
BP | 基址指针 | 通过ss:bp的方式将栈当作普通数据段进行访问 |
这样子,我们基本上完成了对应的寄存器的介绍。下面我们稍微学习和介绍一下实模式下CPU内存寻址方式。
实际上目前看到这,可能很多人会比较困惑,为什么要介绍这些无聊的东西,并且看起来和操作系统没有关系。实际上并不是这样的,我们要实现的MBR中的很多代码到需要对于内存进行访问,而由于我们编写的是较为底层的汇编代码,并且直接运行在CPU上,因此并没有操作系统、编译器等帮助我们管理和完善对于内存的使用,因此我们需要补充从高级语言到直接执行在CPU上这个落差中对于内存的处理,这样我们才能最终没有障碍的实现MBR程序。
下面我们来具体分析一下CPU内存寻址,这里寻址指的是寻找数据的地址。8086下主要分为三大类,而最后一类中又包含四小类。
1. 寄存器寻址
2. 立即数寻址
3. 内存寻址
(1). 直接寻址
(2). 基址寻址
(3). 变址寻址
(4). 基址变址寻址
下面我们将简单分析一下对应的寻址方式。
1. 首先是寄存器寻址,其指的是数据就存储在寄存器中,也就是直接从寄存器中使用数据即可,如下所示
mov ax, 0x10
这就是一条寄存器寻址指令。
2. 其次是立即数寻址,即常数,如下所示
mov ax, 0x18
可以看到,这条指令即是立即数寻址,同样也是寄存器寻址。
3. 直接寻址,这是内存寻址中的一个小类。其将直接在操作数中给出的数字作为内存地址,通过中括号的形式表示取此地址中的值作为操作数,如下所示
mov ax, [0x5678]
可以看到,由于0x5678代表内存地址,而实模式下CPU使用分段内存来访问内存,因此其需要通过段寄存器:段偏移寄存器来表示内存,这里如果没有特殊明确,默认段寄存器为DS段寄存器,这条指令将地址为ds * 16 + 0x5678处的值赋给了ax寄存器,是直接寻址
4. 基址寻址,也是内存寻址中的一个小类。其将bx寄存器或bp寄存器作为基址来寻找地址(注意,在实模式下对于基址寻址来说,仅仅只能用bx寄存器或bp寄存器进行基址寻址)。这里需要说明的是,当bx寄存器或bp寄存器当作地址时,其同样遵循实模式的分段内存原则——即”段基址:段偏移“,这里bx寄存器的默认段寄存器为DS,bp默认的段寄存器为SS,如下所示
mov ax, [bx]
5. 变址寻址,同样是内存寻址中的一个小类。其和基址寻址十分类似,但是理解稍稍不同——其将DS段寄存器所包含的段基址作为基址,而将si寄存器或di寄存器以及可能的立即数运算后的结果作为基址的偏移,从而获取对应的内存中的地址,如下所示
mov [si+0x1234], ax
可以看到,实际上这里的基址为ds * 16,而基址的偏移为si + 0x1234,这样共同组成了一个地址,即ds * 16 + si + 0x1234。
6. 基址变址寻址,同样是内存寻址中的一个小类。根据名字即可知道,这种寻址方式结合了基址寻址和变址寻址,实际上也确实如此。其将bx寄存器和DS段寄存器或bp寄存器和SS段寄存器所生成的地址当作基址,将si寄存器或di寄存器当作基址的偏移,从而确定对应的地址,如下所示
mov [bx+di], ax
可以看到,实际上这里的基址为ds:bx,即ds * 16 + bx,而这里的地址偏移为di,因此其共同组成的地址为ds * 16 + bx + di。
可能有人还是对于基址寻址和变址寻址比较迷惑,实际上我一开始也比较迷惑——这如果变址寻址的立即数为0,那么变址寻址不就是基址寻址么,不过是更换了对应的寄存器和段寄存器而已么?实际上其结果确实是这样的,但并不能因此就将其混为一谈:这就好比数学和物理有部分交集,那么数学和物理是一样的东西么?这里实际上是两种思路,
对于基址寻址来说,其将bx寄存器或者bp寄存器就当作基址,只不过由于实模式下是内存分段,只要是地址就需要通过分段表示,因此bx寄存器或bp寄存器标识基址是默认带上了段寄存器;
而对于变址寻址来说,其将di寄存器或者si寄存器以及可能的立即数当作地址偏移,但同样由于实模式下是内存分段,只要是地址就需要通过分段表示,因此需要带上对应的默认段寄存器,DS段寄存器。需要说明的是,我们一定需要注意一下各个寻址方式的寄存器的要求,否则可能无法正常编译出CPU可执行的指令。
实际上实模式下栈结构和保护模式下栈结构并没有什么大的区别,除了处理数据的字长不同而已。其同样使用push指令和pop指令。这里分别简单介绍一下。
1. push指令,即将数据入栈。由于sp寄存器对应的表明栈顶(同样由于实模式下CPU的分段内存基址,同样需要段寄存器表明地址,这里sp寄存器默认的段寄存器为SS段寄存器),并且由于栈顶处于低地址(内存中栈向下生长),为了避免破坏栈顶的数据,首先将sp寄存器减去一个字长(16位),然后将值写入sp寄存器对应地址的内存中(这里实际上相当于访问了[sp],即类似于基址寻址法,实际上这个命令在实模式下是非法的,因为寄存器并不在上面讲到的基址寻址或变址寻址所给定的寄存器中,这里就简单理解为CPU内部实现的,但外部无法调用即可)。
2. pop指令,即将数据出栈。类似于上面的分析,由于sp寄存器指向栈顶,并且栈顶处于低地址,因此我们直接输出sp寄存器对应地址的内存中的数据即可,但是为了维护栈结构,还需要将sp寄存器减去一个字长(16位)。
实际上这样相当于实现了内存中的栈结构,这里说明一下,实际上栈底相当于SS段寄存器对应的地址,根据分段内存访问基址,也就是SS * 16是栈底地址,其余和普通的栈结构并没有什么太大的区别。
实际上实模式下跳转和保护模式下并没有什么大的区别。同样大体分为两类——无返回的jmp类型和有返回的call类型。
在8086处理器中,决定程序流程的是cs:ip寄存器,因此如果我们能直接修改cs段寄存器或者ip寄存器的话,我们自然也就完成了程序流程的转变。根据这个,实际上call指令中包含了4种方式,其中两种为近调用,即在同一个段中,只需要修改段偏移即可,其返回的话通过ret指令;另外两种为远调用,即跨段调用,需要同时修改段基址和段偏移。
1. 16位实模式相对近调用。其指令形式如下所示
call near near_proc
实际上其转换为机器码为e8llhh,其中e8是操作码,表明是相对近调用,而ll和hh表示一个数值,根据小端序字节,实际上该数值为hhll,等于目标函数的地址-当前相对近调用指令地址-相对近调用地址指令大小(这里是3字节)。
相对近调用指令实现的功能很简单,首先将当前相对近调用指令的下一条指令地址的段偏移入栈(16位),然后修改ip寄存器,将其值修改为当前相对近调用指令地址+相对近调用地址指令大小(这里是3字节)+相对近调用地址指令中的偏移值,即将ip寄存器修改为目标函数地址。
2. 16位实模式间接绝对近调用
实际上,16位实模式间接绝对近调用和前面分析到的16位实模式相对近调用十分相似,都是近调用—即在同一个段中,只需要修改段偏移即可。但是不同点在于16位实模式间接绝对近调用中包含的段偏移地址是绝对地址,并且其是间接的,即通过寄存器或者内存给出来。其指令形式如下所示
call ax call [0x7c00]
实际上这两条指令都是16位实模式间接绝对近调用。对于第一条指令来说,其会调用地址为ax寄存器处的函数;而对于第二条指令来说,其会调用地址为ds * 16 + 0x7c00(实模式CPU分段内存基址)处的函数。如果用内存寻址,该指令的机器码为ff16llhh,其中ff16为操作码,表示使用内存寻址的间接绝对近调用,而ll和hh表示一个数值,根据小端序字节,实际上该数值为hhll,等于目标函数的地址;而如果使用寄存器寻址,该指令的机器码为ff**,其中ff为操作码,表示使用寄存器寻址的间接绝对近调用,而**表示寄存器的表示。
间接绝对近调用指令实现的功能也很简单,首先将当前间接绝对近调用指令的下一条指令地址的段偏移入栈(16位),然后修改ip寄存器,将其值从寄存器或者对应的内存地址处获取,即将ip寄存器修改为对应的目标函数地址。
3. 16位实模式直接绝对远调用
这个不同于前面所介绍的调用,一方面是远调用,即需要同时修改段基址和段偏移;另一方面是直接绝对地址,即直接将绝对地址以立即数形式。其指令形式如下所示
call 0x0000:0x7c00
实际上其转换为机器码为9a007c0000,其中9a是操作码,表明是直接绝对地址远调用,而后紧跟的是32位操作数,其中前16位是段偏移地址,后16位是段基址地址。
直接绝对地址远调用指令实现的功能也很简单,首先将当前直接绝对地址远调用的下一条指令地址的段基址(16位)、段偏移(16位)先后入栈,然后直接将段基址、段偏移修改为直接绝对地址远调用命令中所包含的地址即可。
4. 16位实模式间接绝对远调用
由于是间接地址,因此其地址一般存储于内存中或者寄存器中,而由于是绝对地址远调用,因此其地址需要32位(段地址和段偏移),这也就决定了16位实模式间接绝对地址远调用通过内存寻址,其中数据的地址存储在内存中。其指令形式如下所示
call far [0x7c00]
上述指令便是一个16位实模式间接绝对地址远调用,其机器码ff1ellhh,其中ff1e是间接绝对地址远调用的操作码,而ll和hh表示一个数值,根据小端序字节,实际上该数值为hhll(需要附带段寄存器,默认为ds段寄存器),地址为该值的内存中的值才是函数所在的地址。其中低2个字节是段偏移,高两个字节是段基址。
间接绝对远调用指令实现的功能也很简单,首先将当前直接绝对地址远调用的下一条指令地址的段基址(16位)、段偏移(16位)先后入栈,然后根据指令中包含的地址,将该地址处的内存低2字节作为段偏移,高2字节作为段基址,从而调用该地址处的函数即可。这里需要特别区别一下16位实模式间接绝对地址远调用和16位实模式间接绝对近调用,其调用的方式和保存的值的不同。
实际上对于有返回的调用来说,单单有调用还不行,还需要有返回——即ret和retf指令。这里仅仅简单介绍一下这两个指令——由于call指令分了远调用和近调用,其在栈中的返回地址保存的形式也各不相同,因此需要不同的指令进行返回。对于近调用来说,其通过ret进行返回,即其将将弹出的16位数据写入ip寄存器中,从而完成段偏移的修复;而对于远调用来说,其将弹出的2个16位数据分别写入ip寄存器和cs段寄存器中,从而完成段偏移和段基址的修复。
实际上类似于上面的call类型,jmp类型也同样按照远近来进行划分,主要可以划分为短转移、近转移和远转移这三大类。
1. 16位实模式相对短转移。实际上其有点类似于相对近调用,其指令形式如下所示
jmp short start
实际上相对短转移指令的机器码是ebll,其中eb是相对短转移的操作码,而ll表示偏移,其数值等于目标函数地址-当前相对短转移指令地址-相对短转移指令大小(这里是2字节)。
所以相对短转移指令实现的功能也很简单,就是修改ip寄存器,将其值修改为当前相对短转移指令地址+相对短转移指令大小(这里是2字节)+短转移指令中的偏移值,即将ip寄存器修改为目标函数地址。
2. 16位实模式相对近转移。实际上和16位实模式相对短转移十分类似,仅仅是扩大了偏移的大小,从而可以完成更大范围的跳转,其指令形式如下所示
jmp near short
实际上近转移指令的机器码是e9llhh,其中e9是近转移的操作码,而ll和hh表示一个数值,根据小端序字节,实际上该数值为hhll,其数值等于目标函数地址-当前近转移指令地址-近转移指令大小(这里是3字节)。
所以近转移指令实现的功能也很简单,就是修改ip寄存器,将其值修改为当前近转移指令地址+近转移指令大小(这里是3字节)+近转移指令中的偏移值,即将ip寄存器修改为目标函数地址。
3. 16位实模式间接绝对近转移。
对于16位实模式间接绝对近转移来说,其和16位实模式间接绝对近调用十分地相似,即将段偏移的绝对地址存放在寄存器中或者内存中。指令形式如下所示
jmp near ax jmp near [0x7c00]
实际上这两条指令都属于间接绝对近转移,其将寄存器或者内存中的值直接写入到ip寄存器中,从而完成段基址偏移的修改。对于内存访问方式来说,需要注意CPU的内存分段基址,需要带默认的段寄存器ds段寄存器。
4. 16位实模式直接绝对远转移
类似于call命令的直接绝对远调用,直接将段基址和段偏移位通过立即数进行设置,其指令格式如下所示
jmp 0x0:start
上述即为一个直接绝对远转移,其中start是汇编中的伪指令,用来表示地址,在进行编译的时候会直接转换为对应的立即数。
因此实际上该指令的作用也很简单,就是直接将段基址和段偏移设置为对应的立即数即可。
5. 16位实模式间接绝对远转移
仍然类似于call命令的间接绝对远调用,其需要同时修改段基址和段偏移,因此只能通过内存中保存目标地址。其指令格式如下所示
jmp far [addr]
由于CPU的内存分段基址,其要表示地址的话需要段寄存器,默认为ds段寄存器。这样子,将ds * 16 + addr的低2个字节写入段偏移寄存器,高2个字节写入段基址寄存器,从而完成了转移。
可以看出来,实际上jmp系列和call系列十分相似。
条件跳转,即根据条件情况进行对应的跳转,其中这些条件被存放在标志寄存器中,包括CF位(判断无符号加减法溢出)、PF位(奇偶位)等标志位。大家可以查阅更多资料来了解标志寄存器。下面具体介绍一下条件jmp指令。其是一个指令族,简称为jxx。如果条件满足,jxx将会跳转到指定的位置去执行,否则继续顺序地执行下一条指令。其指令格式如下所示
jxx address
这里需要说明的是,address只能是段内偏移地址,因此可以理解为短转移或近转移。自然不需要额外的段寄存器表明段基址。下面将各个指令具体列出,如下所示
转移指令 | 条件 | 意义 | 英文助记 |
jz/je | ZF=1 | 相减结果为0/相等时转移 | Jump if Zero/Equal |
jnz/jne | ZF=0 | 不等于0/不相等时转移 | Jump if Not Zero/Not Equal |
js | SF=1 | 负数时转移 | Jump if Sign |
jns | SF=0 | 正数时转移 | Jump if Not Sign |
jo | OF=1 | 溢出时转移 | Jump if Overflow |
jno | OF=0 | 未溢出时转移 | Jump if Not Overflow |
jp/jpe | PF=1 | 低字节中有偶数个1时转移 | Jump if Parity/Parity Even |
jnp/jnpe | PF=0 | 低字节中有奇数个1时转移 | Jump if Not Parity/Parity Odd |
jbe/jna | CF=1或ZF=1 | 小于等于/不大于时转移 | Jump if Below or Equal/Not Above |
jnbe/ja | CF=ZF=0 | 不小于等于/大于时转移 | Jump if Not Below or Equal/Above |
jc/jb/jnae | CF=1 | 进位/小于/不大于等于时转移 | Jump if Carry/Below/Not Above Equal |
jnc/jnb/jae | CF=0 | 未进位/不小于/大于等于时转移 | Jump if Not Carry/Not Below/Above Equal |
jl/jnge | SF!=OF | 小于/不大于等于时转移 | Jump Less/Not Great Equal |
jnl/jge | SF=OF | 不小于/大于等于时转移 | Jump if Not Less/Great Equal |
jle/jng | SF!=OF或ZF=1 | 小于等于/不大于时转移 | Jump if Less or Equal/Not Great |
jnle/jg | SF=OF且ZF=0 | 不小于等于/大于时转移 | Jump Not Less Equal/Grea |
jcxz | CX寄存器=0 | cs寄存器值为0时转移 | Jump if register CX‘s value is Zero |
原文:https://www.cnblogs.com/hawkJW/p/13641236.html