一、通用寄存器
AX、BX、CX和DX是四个通用寄存器,通常用来存放一般性的数据,后面的分析都在8086CPU中进行。每个通用寄存器是16位,即一次可以处理一个字(2个字节)的数据,但是为了与8086CPU之前的CPU相容,也支持一个字节的寄存器,即AH与AL,类似的还有BH和BL等,CPU操作时会降AH和AL当作独立的寄存器,运算进位时会直接丢弃,因为CPU认为只有一个8位的寄存器而已:
二、物理地址
CPU访问内存需要知晓存储单元的地址,由于8086CPU地址总线为20位,但是寄存器却只有16位,即一次处理的地址最多只有2的16次幂,远小于2的20次幂。为了弥补这个问题,8086CPU采用了一种特殊的方式通过16位的寄存器来构造20位的访问地址,简单来说,即:段地址*16+偏移地址。从计算的位数上来说,段地址和偏移地址存储在CPU寄存器中,都是16位的;段地址*16,即整体在右边添加4个二进制位,成为了20位;20位与16位相加,得到了20位的实际内存地址。这里虽然是8086CPU中的方法,但是其实确实现在所有CPU中的实际寻址算法,即一个基地址加上一个偏移量得到一个实际地址。由此我们可以得到段的概念,内存本身不分段,但是由于CPU的特殊寻址方式,我们可以将内存看作最大64KB的一个个段(16位的最大值正好是64KB),因此我们可以在汇编中人为的指定段的起始和结束。当然,这里的段地址和偏移量实际上存储在CPU的寄存器中,比如获取指令的CS:IP,其中CS为代码段寄存器,其中存储代码段的基地址,IP寄存器则存储着当前要执行的指令的指针,即一个偏移量,因此CS:IP指定了接下来CPU要执行的指令的位置。这里需要说明的是,内存中的数据对于CPU来说都是二进制位,能够区分数据和指令的唯一标准就是指令曾经或者正在被CS:IP指定;每执行完一条指令,IP会累加上条指令的长度,从而指向下调指令。
我们是否可以修改控制CS:IP的值呢?答案是肯定的,只不过我们不能使用mov等传送指令,而应当使用jmp这类转移指令,基本的用法是:
-1. 修改CS:IP: jmp 2AE3:3 执行后:CS=2AE3H, IP=0003H;
-2. 仅修改IP:jmp ax(ie. move ip, ax)即用寄存器中的值修改IP;
三、内存访问
CPU访问内存除了获取指令,还要获取数据,那么数据部分如何定位的呢?同指令的CS:IP一样,8086CPU使用DS:[...]来获取内存数据地址,其中段寄存器存储内存数据段的基地址,而[...]表示一个内存偏移量指向的内存单元,如[0]表示偏移量为0的内存单元,这里使用时要注意,8086CPU不支持直接对DS传送值,因此mov ds, 1000H是非法的,正确地是通过寄存器来实现,即:mov ax, 1000H; mov ds, ax;
这部分我们要学习基本的汇编指令,如mov、sub、add等,都可以操作寄存器,操作完之后将数据放入第一个参数表示的寄存器中。CPU中的内存数据一种特殊的结果就是栈,即只能从一端读写数据的结构,其基本指令是push ax;将寄存器ax的值入栈;和pop ax;从栈中取出栈顶元素放入寄存器ax中;下面是mov\add\sub命令的一个简单示例:
然后我们来看看PUSH命令的执行过程:
然后是POP命令:
可以看出,栈的操作关键是栈顶位置的确定,因此CPU专门使用SS:SP来获取当前内存中栈顶的位置。值得一提的是,CPU本身并没有对栈的大小进行检查,因此实际中会出现栈顶越界的问题(上限超出-PUSH;下限超出-POP),这也就要求我们必须人为进行检查,否则就会出现程序的漏洞。