MASM32绝好资料
Introduction to Assembler (2)
MASM Reference (11)
宏参考MACRO Reference (11)
伪指令参考 (16)
语法参考 (31)
其它参考 (37)
寄存器大全 (37)
标志寄存器Processor Flags (37)
80位数据寄存器Stack of 80-bit Data Registers (38)
Ascii 字符表Ascii Characters (39)
奔腾指令优化参考Pentium Optimisation (40)
调用格式Calling Conventions (43)
命令行工具Command Line Tools (43)
Introduction to Assembler
汇编概述
直线内存模式FLA T Memory Model
32位(二进制位,bit。下同)Windows程序使用“直线内存模式”。这样的程序实际只有一个段,包括程序的所有代码和数据,而且只能在386以上的INTEL处理器上运行。早期的16位程序使用段和偏移来代表地址,每个段的大小不能超过64KB,而“直线内存模式”只有偏移,它的大小是4G,即0XFFFFFFFF,这种地址表示方式使汇编程序更易于开发。
在FLA T模式下,所有的段寄存器都自动设置成为同一个值,32位Windows程序中段/偏移形式的地址在不是必须的。对于DOS程序员来说,32位Windows PE格式的应用程序好象是.COM程序一样,使用单一的段包括了代码和数据,只操作偏移地址,而不是用段/偏移地址。
直线内存模式时,应用程序在4G内存空间内所有的引用都是近程地址(NEAR code addressing)和近程数据(NEAR data addressing)。GS和FS段寄存器在普通的FLA T模式应用程序中并不使用,一般是操作系统用于操作实例(used in instances)。
保护模式内存Protected Mode Memory
DOS是真实地址内存模式,这样应用程序可能改写操作系统的代码,而引起系统瘫痪。例如在编写CGA(注:早期显示模式,显示缓存地址在B800处开始)程序时,使用B800内存区域,如果LOOP循环写的不对,就可能改写高位内存中的DOS代码或BIOS设置,造成死机。设计保护模式就是用来防止这样的事情发生,内存管理器控制并保护应用程序访问的地址,阻止应用程序访问没有权限的内存区域。16位Windows是模拟多任务的操作系统,它的应用程序也可能改写其它应用程序甚至是操作系统的内存。
改写了操作系统代码,应用程序调用某个系统函数时就会引起死机,最常见的就是“蓝屏”(注:Windows95/98常见故障)死机。如果开发的应用程序本身存在逻辑混乱,应该是一个“黑屏”死机。
更改硬件、在多任务基础上使用硬件的时候,保护模式会更加可靠。因为汇编语言编程时允许读写任何地址,所以要留心读写的句柄和地址。如果分配了10K内存来读而却试图读20K 内容,就会引起页错误。也可能因为使用变量或寄存器指向的地址超出了应用程序权限范围,而引起内存读写失败。操作系统会将页异常传递给造成出错的应用程序,如果应用程序没有处理异常,操作系统就会关闭应用程序。
这给应用程序提供一个在保护模式下,得到有读写权限的地址范围的一种方法。
操作数Instruction Operands
操作数被用来做汇编指令助记符(mnemonic)中的参数。一条指令可能有0到3个操作数,有两个操作数的逻辑或数学指令当中,位于右边的是源操作数,左边的是目的操作数。如mov eax,1;mov是汇编代码助记符,eax是目的操作数,1是源操作数。指令的操作结果是将1放入eax。在其它处理器的汇编语言中,操作数使用顺序不同的都会特别注明。
指令码和助记符Opcodes and Mnemonics
Intel极兼容处理器的硬件层面,内建指令(instructions)叫opcodes(注:二进制的指令码),以位(bit)表示,最小的处理单元是字节(BTYE)。这样的二进制的指令码程序可以用16进制的编辑器来编写,但它太繁杂,如果将32位值hex:56 a7 00 fe放入eax寄存器,那就要指令码A1(mov eax),后面跟上fe 00 a7 56。这样的复杂性谁也受不了,于是很早以前发明了助记符,一直用到现在的32位(已经64位了)汇编时代。注意:相同的助记符表示的指令码可能不同,如mov eax,V ar1和mov V ar1,eax两条指令的指令码分别是A1H和A3H。助记符让程序员编写汇编程序时简便易记。
寄存器Registers
处理器内部的高速数据处理单元是寄存器,也是处理器内部的存贮单元。执行指令时,寄存器比内存操作数快的多。INTEL处理器内部的寄存器数量是有限的,普通寄存器8个:EAX,EBX,ECX,EDX,ESI,EDI,ESP和EBP。编程时经常把ESP和EBP单独列出来,因为它们主要在子过程的进入和退出时用来指示堆栈和参数,实际上只剩6个寄存器可以使用。ESI和EDI也可以以字节访问,如用读SI的方式来访问ESI的低字。清楚寄存器本身可以使用什么样宽度类型的数据非常重要。通常带有整数操作数的指令使用三种类型数据:BYTE:单字节8位;WORD:双字节16位;DWORD:四字节32位。例如EAX寄存器,AL或AH 表示使用8位二进制数据,是AX的低字节或高字节,AX表示使用16位数据,它是EAX 的低字,而EAX表示32位数据。EAX寄存器从右到左依次是0位到31位(下同)。访问0到7位用AL表示,8到15位用AH表示,0到15位用AX表示。想访问16到31位要通过ROL eax,16;或ror eax,16;等移位指令,再读AX寄存器。mov eax,cl;这条指令是非法的,两个操作数数据宽度不对。如果必须进行这样的操作,需要进行符号扩展,使用有符号扩展功能的指令:movzx eax,cl;扩展为无符号整型或:movsx eax,cl;扩展为有符号整型。旧指令集中的cbw或cwde指令可以对AL或AX数据进行符号扩展。
寄存器保护Register Preservation
在x86系列处理器中,普通目的寄存器共8个,除ESP和EBP用来管理函数调用和退出之外,还有6个普通目的寄存器。保护寄存器是指程序在使用Call调用子程序或者中断时,应该保护哪些寄存器的数据。
编写32位Windows应用程序时,使用寄存器有一个约定,即访问操作系统函数WIN32API时的一个接口标准:6个自主使用的寄存器中,3个可以自由更改:EAX,ECX,和EDX;另外3个必须保护:EBX,ESI和EDI。如果函数需要使用应该保护的3个寄存器,就必须在使用前将它们的值保存,用后再进行恢复,如:
TestProc proc var1:DWORD,var2:DWORD
push ebx
push esi
push edi
; -------------------------------------
; write code that uses EBX ESI and EDI
; -------------------------------------
pop edi
pop esi
pop ebx
ret
TestProc endp
调用WIN32API函数后,可以自由更改的3个寄存器的值可能被WIN32API更改了,所以这3个寄存器中的有用数据在调用之前要保存好。应用程序使用3个被保护的寄存器,即使调用WIN32API时,这3个寄存器的内容不会因为调用而受影响,如做计数器(或存贮其它有用数据)都是有效的。它们在WIN32API中被保护了,意图是减少在代码中重复进行存贮和恢复。
汇编程序中也可以手工编写函数的入口和出口,这是汇编的一个复杂之处,因为这里的错误非常容易使操作系统死机。所以使用ESP和EBP是非常麻烦的事。下面是保护ESP和EBP 的例子:
call procname
procname:
push ebp ; preserve base pointer
mov ebp,esp ; stack pointer into ebp
; write your assembler code here
mov esp,ebp ; restore stack pointer
pop ebp ; restore base pointer
ret
label:
还有其它保护ESP和EBP寄存器的方式,这看个人喜好和使用约定。需要注意的是使用其它方式或约定时,一定要将这两个寄存器的内容同时保存和恢复。使用个人约定方式,需要手工计算堆栈中每个函数参数和使用局部变量的偏移量,传递的参数开始于[EBP+8],局部变量在堆栈中以EBP为基准按相反的顺序排列,如果想将传递过来的一个参数放入EAX寄存器如mov eax,var1 ,那么实际是这样的指令mov eax,[ebp+8](注:用椎栈传递参数时,第一个参数位于最底端,相对于基指针的位置)。
使用汇编/API混合编程时,如果不知道API用了哪个寄存器,有一对非常有用的汇编指令将所有使用寄存器和标志寄存器保存起来:PUSHAD和POPAD。这对指令在编写应用程序时并不是最优化(指速度)
的,但在开发过程当中相当方便。使用条件转移指令时,使用PUSHFD 和POPFD指令对可以保护标志寄存器。
寄存器的数据类型Data Types In Register
有三种操作数可以放入寄存器:立即数,内存数据,其它寄存器数据。
立即数指数字或ASCII字符。字符的数值是其ASCII码。如:mov al,"a";
内存操作数是某种形式的内存地址。一般习惯将内存操作数两边加[]号,用以区别其它操作数和地址。如:mov al,[esi];再如:mov edx,lpMemvar 将变量的地址放入edx(注:masm32\HELP\Introduction to Assembler。这几处例子指令相互矛盾,内存数据和内存地址没有分开)。
用其它寄存器作为操作数时简单拷贝其内容。如mov ecx,edx;
不能将一个内存操作数用一条指令放入另一个内存操作数。如:mov mV ar,lpMem不合法。如果有空闲的寄存器,可以用两条指令实现这个动作:mov eax,lpMem和mov mV ar,eax。也可以不用寄存器:push lpMem和pop mV ar。
堆栈The Stack
堆栈用于函数存贮数据、返回地址,或者为函数传递参数用的一块临时存贮数据的内存区域。堆栈通常用两条指令来操作:PUSH和POP。堆栈内的数据读写按后进先出的顺序。堆栈的下一个可写的位置叫栈顶(注:ESP总是指向最后一个压入的数据的低位字节,也这是说ESP 的值是这个字节的地址)。压入一个数据,处理器会减小堆栈指针ESP的值,从栈顶读(弹出)一个值,就增加堆栈指针的值,读写几个字节(2或4个)就增减几个字节。
WIN32应用程序保护寄存器有约定,通常用EBP和ESP来控制函数进入和退出的堆栈地址,而3个需要保护的寄存器,进入函数之后要先保存它们,在函数RET之前恢复。
堆栈具有出入平衡的特性,程序保存多少数据,就要弹出多少数据,否则容易引起死机(注:因为函数返回地址等一些重要数据也存贮在堆栈中,函数压入和弹出的数据不等,造成程序跳转混乱,导致崩溃)。
使用堆栈时也有一些技巧。比如一次压栈一个32位数据,可以分两次弹出两个16位数据,堆栈仍是平衡的。但使用堆栈时一定要小心。好的mv
地址和指针Addressing and Pointers
汇编中一定要区分开变量的地址和变量的值。地址是内存位置,值是那个位置存贮了什么数据。从一个
地址获得数据的方法叫“反映射”(dereferencing)。如move eax,[eax];使用了方括号,表示将EAX寄存器指示的内存地址中的数据内容再装入到EAX中,这种方式适用于所有32位寄存器。方括号括起来的寄存器操作数等同于内存操作数。
在高级语言中,经常用指针传递复杂的数据。lea eax,MyV ar;将变量的地址放入EAX寄存器,接下来用指令mov lpMyV ar,EAX;将地址放入变量中,lpMyV ar就成为一个指针。
计算地址表达式Calculating Affective Addresses
数据量相当大或是查表访问的时候,用mov ecx,[eax];等形式的指令显得不太方便。Intel 定义了一个更有力,但十分复杂的机制解决地址计算的问题。X86处理器数组成员的格式定义:[Base Address + Index*Scale + Displacement],使用寄存器表示成:[ebx + ecx*4 +8]。ebx 是基址,ecx是索引,4代表数据类型尺寸,8代表字节偏移。
并不是所有的数据都必须这样表示,如果使用的是字节数组,可以直接使用基址和索引,如:mov al,[ebx+ecx]。在实际的应用程序中如何表示是可选的,包括寄存器和正负符号。
库Libraries
将常用的函数写成库模块,使用起来更加方便。模块代码的编写与普通代码编写类似。应用程序中调用
库的方法是“包含”。写库的时候要注意将不同用途的函数分开,避免用不到的函数编译到应用程序中,这样的原则在库模块的编写中叫“间隔尺寸”。写一个模块非常容易,如: