32位x86处理器编程导入——《x86汇编语言:从实模式到保护模式》读书笔记08
在说正题之前,我们先看2个概念。
1.指令集架构(ISA)
ISA 的全称是 instruction set architecture,中文就是指令集架构,是指对程序员实际“可见”的指令集,包含了程序员编写一个能正确运行的二进制机器语言程序的所有信息,涉及到指令、 I/O 设备等。例如 Intel 的 IA-32、Intel 64、ARM 的 ARMv7、ARMv8 等等。
2.微架构
微架构(Microarchitecture)又称为微体系结构/微处理器体系结构。是将一种给定的指令集架构在处理器中执行的方法。一种给定的指令集可以在不同的微架构中执行。 [1]
需要说明的是:
微架构与指令集是两个概念:指令集是CPU选择的语言,而微架构是具体的实现.[2]
ARM公司将自己研发的指令集叫做ARM指令集,同时它还研发具体的微架构(如Cortex系列)并对外授权。但是,一款CPU使用了ARM指令集不等于它就使用了ARM研发的微架构。Intel、高通、苹果、Nvidia等厂商都自行开发了兼容ARM指令集的微架构,同时还有许多厂商使用ARM开发的微架构来制造CPU。通常,业界认为只有具备独立的微架构研发能力的企业才算具备了CPU研发能力,而是否使用自行研发的指令集则无关紧要。厂商研发CPU时并不需要获得指令集授权就可以获得指令集的相关资料与规范,指令集本身的技术含量并不是很高。获得授权主要是为了避免法律问题。然而微架构的设计细节是各家厂商绝对保密的,而且由于其技术复杂,即便获得相应文档也难以山寨。 [2]
如前所述,仅仅从ARM购买微架构来组装芯片的厂商是不能被称作CPU研发企业的,这些芯片也不能被称为“xx厂商研发的CPU”.典型如华为的海思920、三星Exynos 5430,只能说是“使用ARM Cortex-A15核心的芯片”。但是如果一款兼容ARM指令集的芯片使用了厂商自主研发的微架构,情况就不同了。高通骁龙800、苹果A7就是这样的例子——它们分别使用了高通、苹果自主研发的CPU. [2]
3. 32位寄存器
(1)通用寄存器
在16位处理器内,有8个通用寄存器,分别是AX,BX,CX,DX,SI,DI,BP,SP. 其中,前4个还可以拆分成2个独立的寄存器来用。32处理器在16位处理器的基础上,扩展了这8个通用寄存器的长度,使之达到32位。它们的名字分别是EAX,EBX,ECX,EDX,ESI,EDI,ESP,EBP.
注意:
- 在使用这些寄存器的时候,指令的源操作数和目的操作数必须具有相同的长度(个别特殊用途的指令除外)。如果目的操作数是32位寄存器,源操作数是立即数,那么立即数被视为是32位的。
- 32位通用寄存器的高16位是不能独立使用的,但是低16位保持同16位处理器的兼容性(可以拆成8位的来用)。
- 可以在32位的处理器上运行16位处理器上的软件。
(2)指令指针寄存器
在32位模式下,为了生成32位物理地址,处理器需要使用32位的指令指针寄存器,也就是说之前的16位的IP扩展成为32位的EIP。当处理器工作在16位模式下时,依然使用16位的IP;工作在32位模式下时,使用32位的EIP。
注意:和之前一样,EIP也只能由处理器自己使用,程序员无法直接访问。 对IP和EIP的修改通常是通过某些隐式的指令进行的,比如JMP,CALL,RET,IRET等等。
(3)标志寄存器
在32位处理器中,标志寄存器由之前16位的FLAGS扩展为32位的EFLAGS,低16位的每个字段和原先保持一致。
下图摘自《 The Intel Architecture Software Developer’s Manual——Volume 3 》
(4)段寄存器
在32位模式下,对内存的访问从理论上来说不需要分段,因为有32根地址线,可以直接寻址4G的内存。但是,IA-32结构的处理器是基于分段模型的,因此,32位处理器依然需要以段为单位访问内存,即使它工作在32位模式下。
不过,可以采取一个变通的方案,即只分一个段。也就是说段的基地址是0x0000_0000,段的长度是4GB。在这种情况下,相当于不分段,即平坦模型(Flat Mode)。
在32位模式下,处理器要求在加载程序时,先定义该程序拥有的段,然后才可以使用这些段。定义段时,除了起始地址外,还附加了段界限、特权级别、类型等属性。当程序访问一个段时,处理器将通过固件进行各种检查工作,以防止对内存的违规访问。
在32位模式下,传统的段寄存器,如CS,SS,DS,ES保存的不再是16位的段基地址,而是段选择子(到底什么是段选择子,我们以后再说)。另外,32位处理器还增加了两个额外的段寄存器,分别是FS和GS。
4.实模式与保护模式
8086是16位的处理器,可以通过分段来访问1M的内存,段的最大长度是64KB。8086只有一种工作模式,就是我们现在所说的“实模式”。1985年,Intel公司推出了80386,获得了极大的成功。80386以及后续的处理器,都向前兼容,可以运行实模式下的8086程序。而且,在加电时,这些处理器都自动处于实模式下。只有在一番设置之后,才能运行在保护模式下。
5.逻辑地址和线性地址
在8086,我们把“段地址:段内偏移地址”称为逻辑地址。8086CPU内部有一个地址加法器,用来把逻辑地址转换成物理地址。转换规则是:物理地址=段地址×10H+段内偏移量
但是在80386中,情况就不同了。80386的逻辑地址(也叫虚拟地址)构成是“段选择子:段内偏移量”。逻辑地址经过80386CPU内部的分段部件转换后成为线性地址。线性地址再经过分页部件转换就成为物理地址。如果禁用分页机制,那么线性地址就是物理地址。
6.处理器的寻址方式
在16位处理器上,内存寻址方式为:
在32位处理器上,内存寻址方式为:
也是就是说,在指定有效地址的时候,可以使用所有的32位通用寄存器作为基址寄存器。同时,还可以再加上一个除ESP之外的32位通用寄存器作为变址寄存器。另外,变址寄存器还允许乘以一个比例因子(1或2或4或8)。最后,还可以加上一个8位或者32位的偏移量。
举例:
add eax,[0x2008]
sub eax,[eax+0x04]
mov ecx,[edx+esi*8+0x02]
7.汇编器指令 BITS
相同的机器指令,在16位模式和32位模式下的解释和执行效果是不同的。举例来说:
8B5022,这条机器码,在16位模式下,对应的汇编指令是:mov dx,[bx+si+0x02]; 但是,在32位模式下,对应的汇编指令却是 mov edx,[eax+0x02];
NASM汇编器中有一个伪指令——BITS.
'BITS'指令指定 NASM 产生的代码是被设计运行在 16 位模式的处理器上还是运行在32位模式的处理器上。语法是'BITS 16'或'BITS 32'. NASM以.bin格式输出时,默认是16位模式。如果我们需要编译成32位的,则需要加上[bits 32](方括号可以有,也可以没有。)
8.一般指令的扩展
(1)loop指令
在16位处理器上,loop指令的循环次数在寄存器CX中。在32位处理器上,如果当前模式是16位的,那么loop指令执行时,仍然使用CX寄存器;如果运行在32位模式下,则使用ECX;
(2)mul指令
在16位处理器上,无符号数乘法指令mul的格式为
mul r/m8 ; AX <- AL * r/m8
mul r/m16 ; DX:AX <- AX * r/m16
说明:这里的r/m8表示8位的通用寄存器或内存单元, r/m16表示16位的通用寄存器或内存单元,下面的r/m32表示32位的通用寄存器或内存单元
在32位处理器上,除了依然支持上面的的操作,另外还支持以下扩展格式:
mul r/m32 ;EDX:EAX <- EAX * r/m32
有符号数乘法指令imul与此相同。
(3)div指令
在16位处理器上,无符号除法指令div的格式为:
div r/m8 ; AX ÷ r/m8 = AL …… AH
div r/m16 ; DX:AX ÷ r/m16 = AX ……DX
在32位处理器上,除了依然支持上面的操作,还支持以下扩展格式:
div r/m32 ; EDX:EAX ÷ r/m32 = EAX……EDX
有符号数除法指令idiv于此相同;
(4)push和pop指令
操作数是立即数的情况
32处理器的栈操作指令push和pop也有所扩展,允许压入双字操作数。特别是,它支持立即数的压栈操作。指令格式为(imm8/16/32表示8位或16位或32位立即数):
push imm8 ;操作码为6A
push imm16 ; 操作码为68
push imm32; 操作码也为68
还是举书上的例子吧:
- 例1:压入一个字节
push byte 0x55;
这里的关键字byte是给编译器看的,告诉它压入的是字节(毕竟0x55可以解释为字0x0055或者双字0x0000_0055).这条指令的16位形式(用bits 16 编译)和32位形式(用bits 32 编译)是一样的,机器码都是
6A 55
但是,执行的时候就不同了。注意,无论什么时候,处理器都不会压入一个字节。它要么压入字,要么压入双字。
在16位模式下,默认的操作数是16位。于是处理器将0x55作符号扩展,扩展成16位的0x0055,然后压入栈。(压栈时用sp寄存器,且先将sp减去2);
在32位模式下,处理器将0x55扩展成32位的0x0000_0055,然后压入。(压栈时用esp寄存器,且先将esp减去4)
- 例2:压入一个字
压入一个字,必须用word关键字来修饰,如
push word 0xfffb
在16位模式下,默认的操作数是16位的。处理器直接压入该字(压栈时用sp寄存器,且先将sp减去2);
在32位模式下,处理器将0xfffb扩展成32位的0xffff_fffb,然后压入。(压栈时用esp寄存器,且先将esp减去4)
- 例3:压入一个双字
如果压入双字,则必须用关键字dword来修饰。如
push dword 0xfb
在16位模式下,压入的是0x0000_00fb(压栈时用sp寄存器,且先将sp减去4);
在32位模式下,压入的也是0x0000_00fb(压栈时用esp寄存器,且先将esp减去4).
操作数位于通用寄存器或者内存单元的情况
对于操作数位于通用寄存器或者内存单元的情况,只能压入字或者双字。指令格式为:
push r/m16
push r/m32
比如:
push ax
push edx
如果操作数位于内存单元中,则必须用关键字word或者dword修饰,如:
push word [0x2000]
push dword [ecx+esi*2+0x02]
无论操作数位于寄存器还是内存单元,
在16位模式下,压入字的时候,将sp的内容减去2;压入双字的时候,将sp的内容减去4;
在32位模式下,压入字的时候,将esp的内容减去2;压入双字的时候,将esp的内容减去4.
操作数是段寄存器的情况
指令格式为:
push cs/ds/es/fs/gs/ss
在16位模式下,将sp的内容减去2,然后直接压入段寄存器的内容;
在32位模式下,先将段寄存器的内容扩展为32位(高16位全为0),然后将esp的内容减去4,再压入扩展后的32位的值。
今天就说到这里,下次我们开启保护模式之旅