1. 扩展寄存器:
1) IA-32即Intel Architecture, 32-bit,即Intel 32位处理器构架的简称;
2) 该构架下最明显的变化就是地址线采用32根,可访问4GB的线性主存空间;
3) 通用寄存器的扩展:16位构架下的8个通用寄存器ax, bx, cx, dx, si, di, bp, sp都扩展成32位的eax, ebx, ecx, edx, esi, edi, ebp, esp,其中高16位不可单独使用,低16位兼容16位构架;
!8个扩展通用寄存器也可以在16位实模式下正常使用;
4) ip的扩展:当处理器工作在32位模式下时指令地址都是32位的,因此系统默认使用的扩展后的eip(32位)进行取指;
5) 标志寄存器的扩展:flags扩展成32位的eflags,其用法之后会详细讲解;
2. 仍然采用分段模型访问内存:
1) IA-32构架下为了“兼容”的商业模式继续采用分段模型,即使一个eip就可以访问全部的4GB空间;
2) 段寄存器仍然还是原先的16位的,并没有进行扩展,但是还增加了两个段寄存器fs和gs(仅仅是因为字母升序的原因d、e、f、g来命名的);
3) 32位保护模式下采用的段选择子模式进行分段访问的(这之前已经详细地讲过了);
4) 平坦模式:是一种变通的不分段访问全部4GB空间的方式,方法很简单,段基设为0x00000000,则eip就可以访问整个4GB空间了;
3. 基本工作模式:
1) 兼容16位实模式:刚加电时运行在实模式下,此时的80386相当于一个快速的8086,进入32位保护模式后就可以完全利用32位处理器所有强大的功能了;
2) 保护模式:即32位保护模式;
3) 虚拟8086模式:即V86模式,是保护模式的一种,在该模式下80386可以模拟成多个8086处理器,这样就可以并行地运行多个32位程序以及16位程序,比如Windows下的cmd就是运行在该模式下的;
4. 32位寻址方式:
1) 为了兼容16位实模式下的寻址模式,让16位指令和32位指令公用相同的指令码,但通过不同的指令前缀并结合当前处理器运行状态来决定最终的寻址模式;
2) 通过前缀0x66来反转寻址模式:如果当前运行在16位模式下,则有指令前缀0x66就表示使用32位寻址模式,若运行在32位模式下,则有指令前缀0x66就表示使用的是16位寻址模式,如果没有前缀0x66则表示使用和各自运行模式相匹配的方式寻址;
3) 32位寻址格式:
i. 一般描述:[段选择子: 基址寄存器 + 变址寄存器×1/2/4/8 + 8位/32位偏移量]
ii. 段选子就和原先实模式下段超越前缀是一个概念;
iii. 由于是32位寻址模式,因此偏移地址必然都是32位的,所以基址和变址寄存器都是扩展的32位寄存器;
iv. 基址寄存器可以是所有的8个通用扩展寄存器;
v. 变址寄存器可以是7个通用的扩展寄存器(除了esp不能用之外);
vi. 变址寄存器所能乘的系数只能是1或2或4或8,不能是其它数;
vii. 段前缀、基址寄存器、变址寄存器和偏移量都是可选的,但必须至少有一个,这点和16位寻址模式的规矩是一致的;
5. 0x66前缀的模式反转:
1) 在16位模式下所有使用扩展寄存器或者是32位寻址模式的指令时都将会被视为32位指令,因此会为这些指令加上指令前缀0x66,比如:
bits 16 mov cx, dx ; 89 D1 mov eax, edx ; 66 89 D82) 同理在32位模式下所有使用原来实模式下的指令时(基本上都是16位或8位寄存器,内存寻址也都是16位的)就会被视为16位指令,因此会为这些指令加上指令前缀0x66,比如:
[bits 32] mov cx, dx ; 66 89 D1 mov eax, edx ; 89 D83) 其中“bits 16/32”是NASM的伪指令,用于指示接下来编译的指令是按照16位编码还是32位编码,其中前缀编码0x66用于反转编码模式,“bits 16/32”外可以加中括号[ ],但也可以省略,但是建议加中括号以使代码更加清晰;
!如果代码中没有指令编译模式则一律默认是16位形式的;
6. 一般算术指令的扩展:
1) 一般双操作数指令扩展:
add eax, ebx add dword [ecx], 0x5F!这两条指令都会被编译成32位的,第一条很明显是使用了扩展寄存器,而第二条使用了32位的内存寻址模式,因此指令中的立即数会被看做是一个32位的数;
2) 一般单操作数指令扩展:
inc dword [0x2000] dec dword [eax*2+0x08]!一般出现关键字dword都将会被视为是32位指令,因此在16位下会加指令前缀0x66,而在32位下无需加盖前缀;
3) 逻辑移位指令扩展:
shl eax, 1 shl ebx, 9 shl dword [eax*2+0xFF], cl!移位数如果是立即数的话可以是1以上的数字;
!如果移位数用寄存器表示则仍然只能使用cl,因为最多只能移动31位,而cl完全可以保存31这个数字了!
!移位的原理和原来的依然相同,都是先将源操作数和0x1F与一下(即只保留最低5位),然后再进行移位,因为对于32位的数最多只能移动31位,超出这个范围就没任何意义了;
4) loop指令的扩展:在32位模式下(bits 32)循环计数变量存放在ecx中,在16位模式下还是使用cx循环计数;
5) 乘除法指令的扩展:
i. 这里只讲无符号乘法指令mul的扩展,其余都与此相同;
ii. 无符号乘法的扩展:mul r32/m32 ; edx:eax <- eax×r32/m32
7. 栈指令的扩展:这块儿的扩展比较特殊且略微复杂,因此单独细讲
1) 80386芯片允许直接压入立即数:push byte/word/dword imm
i. 牢记:16位模式默认字长是16,而32位模式默认字长是32,因此不管怎么压都只能压入16位数或者32位数,绝不可能压入8位数!
!注意:栈指令只能操作16位或者32位大小的数;
ii. byte:16位下会先将imm带符号扩展至16位后再压入,而32位下会带符号扩展至32位后再压入;
iii. word:16位下直接压入,32位下会先带符号扩展至32位再压入;
iv. dword:16位和32位都直接将该双字压入(sp和esp都先减4);
!小结:
i. 对于压入立即数的情况,16位模式必定压16位数(除非在16位模式下强行压入32位数),32位模式必定压32位数;
ii. 只有在压入16位模式才能压入16位数(不到16位则带符号扩展至16位),32位模式只能压入32位数(不到32位则带符号扩展至32位);
iii. 如果压入的是16位数则必定是sp - 2,如果压入的是32位数则是sp - 4或者esp - 4;
2) 压入的数存在寄存器或者内存中:不管是16位模式还是32位模式,如果压入的是16位数则必定是sp - 2(bits 16)或者esp - 2(bits 32),如果压入的是32位数则必定是sp - 4(bits 16)或者esp - 4(bits32);
[bits 16] push [bx] ; sp - 2 push dword [bp] ; sp - 4 push ax ; sp - 2 push ecx ; sp - 4 [bits 32] push word [ebx + 0x55] ; esp - 2 push dword [ecx] ; esp - 4 push cx ; esp - 2 push edx ; esp - 43) 压入段寄存器:
i. 16位模式下正常,sp - 2后再压;
ii. 32位模式比较奇葩,对于段寄存器只能压32位的,因此会高位填0扩展成32位,接着esp - 4,最后压入;
8. 关于修改段寄存器的小技巧:
1) 考虑到有前缀的指令比没前缀的指令执行的时候会少用一点时钟周期,因此编代码的时候应该尽可能地少让指令携带前缀,即尽量在一个模式下使用该模式的指令(尽量不要在一个模式下使用另一个模式的指令,否则会让指令带一个前缀0x66);
2) 修改段寄存器的时候同样需要注意这个问题,因为程序中会频繁访问内存,因此需要考虑修改段寄存器时的指令编码问题:
[bits 16] mov ds, ax ; 8E D8 mov ds, eax ; 66 83 D8 [bits 32] mov ds, ax ; 66 8E D8 mov ds, eax ; 8E D8!因此从以上看来,在32位模式下应该尽量使用mov sreg, er的形式来修改段寄存器;
!即使很多编译器如NASM已经非常只能了,不管是在bits 16还是bits 32,都会将上述两条指令都翻译成无前缀的形式,但是考略到今后可能会在不同平台不同版本的环境中编程,因此建议还是在16位模式中使用mov sreg, r的形式,在32位模式中使用mov sreg, er的形式;