Win32Asm教程
导言
先来对这个教程做个小小的介绍。Win32Asm不是一个非常流行的编程语言,而且只有为数不多(但很好)的教程。大多数教程都集中在编程的win32部分(例如,WinAPI,标准Windows编程技术的使用等),而不是汇编语言本身,例如伪代码(opcodes),寄存器(registers)的使用等。虽然你能在其他教程中找到这些内容,但那些教程通常是解释Dos编程的。它当然可以帮你学习汇编语言,但在Windows中编程,你不再需要了解 Dos中断(interrupt)和端口(port)In/Out函数。在Windows中,WindowsAPI提供了你可在你的程序中使用的标准函数,后面还会对此有更多内容。这份教程的目的是在解释用汇编编Win32程序的同时学习汇编语言本身。
1.0-介绍汇编语言
汇编语言是创造出来代替原始的只能由处理器理解的二进制代码的。很久以前,尚没有任何高级语言,程序都是用汇编写的。汇编代码直接描述处理器可以执行的代码,例如:
add eax,edx
add 这条指令把两个值加到一起。eax和edx被称为寄存器,它们可以在处理器内部保存值。这条代码被转换为66 03 c2(16进制)。处理器阅读这行代码,并执行它所代表的指令。像C这样的高级语言把它们自己的语言翻译为汇编语言,而汇编器又把它转换为二进制代码:
C 代码
a = a + b; >> C编译器 >> 汇编语言
add eax, edx >>汇编器>> 原始输出(十六进制)
66 03 C2
(注意该处的汇编语言的代码被简化了,实际输出决定于C代码的上下文)
1.1-为什么?(Why?)
既然用汇编写程序很困难,那么为什么你用A汇编而不是C或者别的什么??-汇编产生的程序更小而且更快。在像如有人工智能一般的非常高级编程语言中,编译器要产生输出代码比起汇编来更困难。虽然编译器变得越来越好,编译器仍然必须指出最快(或最小)的方式产生汇编代码。而且,你自己来写(汇编)代码(包括可选的代码优化)能生成更小更快的代码。但是,当然,这比使用高级语言难多了。还有另一个与某些使用运行时dll的高级语言不同的地方,它们在大多数时运行良好,但有时由于dll(dll hell)而产生问题,用户总是要安装这些Dll。对于Visual C++,这不是一个问题,它们是与Windows一同安装的。而Visual Basic甚至不把自己的语言转换为汇编语言(虽然5以及更高的版本进行了一些这样的转换,但不完全)。它高度依赖msvbvm50.dll- Visual Baisc虚拟机。由VB产生的exe文件仅仅存在简单的代码和许多对这些dll的调用。这就是vb慢的原因。汇编是所有中最快的。它仅仅用系统的dll 如Kernel32.dll, User32.dll等。
译者注:dll hell是指由于dll新的版本被旧的版本给代替了。由于使用了dll新版本的程序仍然调用新的函数,导致了致命的错误。
另一个误解是许多人认为汇编不可能用来编程。当然,它难,但不是不可能。用汇编创建大的工程的确很难,我只是用它来写小程序,用于需要速度的代码被写在能被其他语言导入的dll中。而且,Dos和Windows还有一个很大的区别。Dos程序把中断当“函数”用。像中断10用于显示,中断13用于文件存储等。在Windows中,API函数只有名字(比如MessageBox, CreateWindowsEx)。你能导入库(DLL)并使用其中的函数。这使得用asm写程序简单多了。你将在下一章中学习更多关于这方面的知识。
2.0-开始前的准备
介绍已经够多了,现在让我们开始吧。要用汇编写程序,你需要一些工具。下面,你能看到我将在本教程中使用哪些工具。我建议你安装同样的工具,因为这样你能跟着教程试验文中的例子。我也给出其他的一些选择,虽然其中的大部分你都可以选择,但是要警告的是在汇编器(masm,tasm和nasm)中有很大的区别。在这个教程中,将使用masm,因为它有许多很有用的功能(例如invoke),它使得编程更容易。当然,你可以自己选择你更喜欢的汇编器,但这将使你跟着教程走难一些而且你不得不把教程中的例子进行转换使它可以在你用的汇编器中运行。
汇编器
我的选择:Masm(在win32asm包中)
网址:win32asm.cjb.net
描述:一个把伪代码(opcodes)翻译为给处理器读的原始输出(object文件)的汇编器
相关内容:Masm,宏(macro)汇编器,是一个有很多有用的特色的汇编器。像“invoke”,它可以简化对API函数的调用并对数据类型进行检查。你将在本教程的后面学习这些。如果你读了上面的文字你就知道本教程推荐使用masm。
供选择:Tasm[dl],nasm[dl]
链接器
我的选择:微软Incremental链接器(link.exe)
网址:win32asm.cjb.net(在win32asm包中)
描述:链接器把目标(object)文件和库文件(用于导入DLL中的函数)“链接”到一起输出最终的可执行文件。
关于:我用Iczelion的Win32asm包中的link.exe。但大多数的链接器都可以用。
供选择:Tasm linker[dl]
资源编辑器
我的选择:Borland Resource Workshop
描述:用于创建资源(图形,对话框,位图,菜单等)的资源编辑器。
关于:大多数的编辑器都行。我个人爱好是resource workshop但你可以用你喜欢的。注意由于resource workshop创建的资源文件有时给资源编译带来麻烦,如果你想使用这个编辑器,你应当把tasm一起下下来,他里面包含了用于编译borland式资源的brc32.exe。
供选择:Symantec资源编辑器,Resource Builder等等
文本编辑器
我的选择:ultraedit
描述:一个文本编辑器需要说明吗?
关于:文本编辑器的选择是十分个性化的。我非常喜欢ultraedit。你可以下载我为ultraedit写的语法文件,它可以使汇编代码语法高亮。但至少,选一个支持语法高亮的文本编辑器(关键字会自动标色)。这非常有用而且它使你的代码更容易读和写。Ultraedit还有一个可以使你在代码中快速跳转到某一个函数的函数列表。
供选择:数百万的文本编辑器中的一个
参考手册
我的选择:win32程序员参考手册
网址:www.crackstore.com(或搜索互联网)
描述:你需要参考一些API函数的用法。最重要的是“win32程序员参考手册”(win32.hlp)。这是个大文件,大约24mb(一些版本是 12mb,但不全)。在这个文件中,对所有系统dll的函数(kernel,user,gdi,shell等)都做了说明。你至少需要这个文件,其他的参考(sock2.hlp, mmedia.hlp, ole.hlp等)也是有帮助的但不一定需要。
供选择:N/A
(译者注:该教程写成较早,现在有极好的MSDN供选择)
2.1-安装工具
现在你已经得到这些工具了,把它们安装到你硬盘的某个角落吧。这有几个值得注意的地方:
把masm包安装到你打算写汇编源程序的那个分区。这保证了包含文件路径的正确性。把masm(和tasm)的bin目录加到autoexec.bat的path中,并重新启动。
如果你用的是ultraedit,使用你可以在前面下载的语法文件并启用function-listview(函数列表视图)。
2.2-为你的源文件准备目录
在某个地方创建一个win32文件夹(或其他你喜欢的名字),并为你创建的每一个工程创建一个子文件夹。
3.0-汇编基础知识
这章将教你汇编语言的基础知识
3.1-伪代码(opcodes)
汇编程序是用伪代码创建的。一个伪代码是一条处理器可以理解的指令。例如:
ADD
Add指令把两个数加到一起。大部分伪代码有参数
ADD eax, edx
ADD有两个参数。在加法的情况下,一个源一个目标。它把源值加到目标值中,并把结果保存在目标中。参数有很多不同的类型:寄存器,内存地址,直接数值(immediate values)参见下文。
3.2-寄存器
有几种大小的寄存器:8位,16位,32位(在MMX处理器中有更多)。在16位程序中,你仅能使用16位和8位的寄存器。在32位的程序中,你可以使用32位的寄存器。
一些寄存器是别的寄存器的一部分:例如,如果EAX保存了值EA7823BBh这里是其他寄存器的值。
EAX EA 78 23 BB
AX EA 78 23 BB
AH EA 78 23 BB
AL EA 78 23 BB
ax,ah,al是eax的一部分。eax是一个32位的寄存器(仅在386以上存在),ax包含了eax的低16位(2字节),ah包含了ax的高字节,而al包含了ax的低字节。因而ax是16位的,al和ax是8位的。在上面的例子中,这些是那些寄存器的值:
eax = EA7823BB (32-bit)
ax = 23BB (16-bit)
ah = 23 (8-bit)
al = BB (8-bit)
使用寄存器的例子(不要管那些伪代码,只看寄存器的说明)
mov eax, 12345678h
;Mov把一个值载入寄存器(注意:12345678h是一个十六进制值,因为h这个后缀。
mov cl, ah
;把ax的高字节移入cl
sub cl, 10
;从cl的值中减去10(十进制)
mov al, cl
;并把cl存入eax的最低字节
让我们来分析上面的代码:
mov指令可以把一个值从寄存器,内存和直接数值移入另一个寄存器。在上面的例子中,eax包含了12345678h,然后ah的值(eax左数第三个字节)被复制入了cl中(ecx寄存器的最低字节)。然后,cl减10并移回al中(eax的最低字节)
寄存器的不同类型:
全功能(General Purpose)
这些32位(它们的组成部分为16/8位)寄存器可以用来做任何事情:
eax (ax/ah/al) 加法器
ebx (bx/bh/bl) 基(base)
ecx (cx/ch/cl) 计数器
edx (dx/dh/dl) 数据
虽然它们有名字,但是你可以用它们做任何事。
段(Segment)寄存器
段寄存器定义了哪一段内存被使用。你可能在win32asm中用不着它们,因为windows有一个平坦(flat)的内存系统。在Dos中,内存被分为 64kb的段,因而如果你想要定一个内存地址。你指定一个段,并用一个offset(偏移址)(像0172:0500(segment: offset))。在windows中,段有4GB的大小,所以你在Windows中不需要段。段总是16位寄存器。
CS 代码段
DS 数据段
SS 栈段
ES 扩展段
FS (only 286+) 全功能段
GS (only 386+) 全功能段
指针寄存器
实际上,你可以把指针寄存器当作全功能寄存器来使用(除了eip),只要你保存并恢复它们的原始值。指针寄存器之所以这么叫是因为它们经常被用来存储内存地址。一些伪代码(movb,scasb等)也要用它们。
esi (si) 源索引
edi (di) 目标索引
eip (ip) 指令指针
eip(在16位编程中为ip)包含了指向处理器将要执行的下一条指令的指针。因而你不能把eip当作全功能寄存器来用。
栈寄存器
有2个栈寄存器:esp和ebp。esp装有内存中当前栈的位置(在下章中,对此有更多的内容)。Ebp在函数中被用成指向局部变量的指针。
esp (sp) 栈指针
ebp (bp) 基(base)指针
4.0-内存
这部分将解释在Windows中内存是如何被管理的。
4.1-Dos和Win3.xx
在运行于Dos和Win3.xx的16位程序中,内存被分成许多个段。这些段的大小为64kb。为了存储内存,需要一个段指针和一个偏移址指针。段指针标明要使用的是哪个段,offset(偏移址)指针标明在段位置。看下图:
内存
段 1 (64kb) 段 2 (64kb) 段 3 (64kb) 段 4(64kb) 更多
注意下面关于16位程序的解释,后面有更多关于32位的内容(但不要跳过这部分,要理解32位的内存管理,这部分很重要)上表是全部的内存,被划分成了多个64kb的段。最多有65536个段。现在取出一段:
段 1(64kb)
Offset 1 Offset 2 Offset 3 Offset 4 Offset 5 更多
为了指向段中的位置,需要使用offset。一个offset是段内部的一个位置。每个段最多有65536个offset。内存中地址的记法是:
SEGMENT:OFFSET
例如:
0030:4012(均为16进制)
它的意思是:段30,offset4012。为了查看那个地址中有什么。你先要到段30,然后到该段的offset4012。在前一章中,你已经学过了段和指针寄存器。例如,段寄存器有:
CS 代码段
DS 数据段
SS 栈段
ES 扩展段
FS (only 286+) 全功能段
GS (only 386+) 全功能段
顾名思义:代码段(CS)包括了当前的代码执行到了哪部分。数据段是用来标明在哪段中取出数据。栈指栈段(后面有更多)。ES,FS, GS是全功能的寄存器,并且可以用于任何段(虽然在Windows中不是如此)。
指针寄存器大多数时装有offset,但全功能寄存器(ax, bx, cx, dx等)也可以这么用。IP标明当前指令执行到了哪个offset。Sp保存了当前栈的在ss(栈段中)的offset。
4.2-32位Windows
你可能已经注意到了关于段的一切是乏味的。在16位编程中,段是必不可少的。幸运的是,这个问题已经在32位Windows(95及以上)中得到解决。你仍然有段,但不用管他们了因为它们不再是64kb,而是4GB。你如果尝试着改变段寄存器中的一个,windows甚至会崩溃。这称为平坦(flat)内存模式。只有offset,而且是32位的,因而范围从0到4,294,967,295。内存中的每一个地址都是用offset表示的。这真是32位胜于 16位的最大优点。所以,你现在可以忘了段寄存器并把精神集中在其他的寄存器上。
5.0-伪代码
伪代码是给处理器的指令,它实际上是原始十六进制代码的可读版。因此,汇编是最低级的编程语言。汇编中的所有东西被直接翻译为十六进制码。换句话说,你没有把高级语言翻译为低级语言的编译器上的烦恼,汇编器仅仅把汇编代码转化为原始数据。
本章将讨论一些用来运算,位操作等的伪代码。还有跳转指令,比较等伪代码在后面介绍。
5.1-一些基本的计算伪代码
MOV
这条指令用来把一个地方移往(事实上是复制到)另一个地方。这个地方可以是寄存器,内存地址或是直接数值(当然只能作为源值)。Mov指令的语法是:
mov 目标,源
你可把一个寄存器移往另一个(注意指令是在复制那个值到目标中,尽管“mov”这个名字是移的意思)
mov edx, ecx
上面的这条指令把ecx的内容复制到了ecx中,源和目标的大小应该一致。例如这个指令是非法的:
mov al, ecx;非法
这条伪代码试图把一个DWORD(32位)值装入一个字节(8位)的寄存器中。这不能个由mov指令来完成(有其他的指令干这事)。但这些指令是允许的因为源和目标在大小上并没有什么不同:
mov al, bl
mov cl, dl
mov cx, dx
mov ecx, ebx
内存地址由offset指示(在win32中,前一章中有更多信息)你也能从地址的某一个地方获得一个值并把它放入一个寄存器中。下面有一个例子:
offset 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42
data 0D 0A 50 32 44 57 25 7A 5E 72 EF 7D FF AD C7
每一个块代表一个字节
offset 的值这里是用字节的形式表示的,但它事实上是32位的值,比如3A(这不是一个常见的offset的值,但如果不这样简写表格装不下),这也是一个32位的值:0000003Ah。只是为了节省空间,使用了一些不常见的低位offset。所有的值均为16进制。
看上表的offset 3A。那个offset的数据是25, 7A, 5E, 72, EF等。例如,要把这个位于3A的值用mov放入寄存器中:
mov eax, dword ptr[0000003Ah]
(h后缀表明这是一个十六进制值)
mov eax, dword ptr[0000003Ah]这条指令的意思是:把位于内存地址3A的DWORD大小的值放入eax寄存器。执行了这条指令后,eax包含了值 725E7A25h。可能你注意到了这是在内存中时的反转结果:25 7A 5E 72。这是因为存储在内存中的值使用了little endian格式。这意味着越靠右的字节位数越高:字节顺序被反转了。我想一些例子可以使你把这个搞清楚。
十六进制dword(32位)值放在内存中时是这样:40, 30, 20, 10(每个值占一个字节(8位))
十六进制word(16位)值放在内存中时是这样:50, 40
回到前面的例子。你也可以对其他大小的值这么做:
mov cl, byte ptr [34h] ; cl得到值0Dh(参考上表)
mov dx, word ptr [3Eh] ; dx将得到值 7DEFh (看上表,记住反序)
大小有时不是必须的。
Mov eax,[00403045h]
因为eax是32位寄存器,编译器假定(也只能这么做)它应该从地址403045(十六进制)取个32位的值。
可以直接使用数值:
mov edx, 5006
这只是使得edx寄存器装有值5006,综括号[和]用来从括号间的内存地址处取值,没有括号就只是这个值。寄存器和内存地址也可以(他应该是32位程序中的32位寄存器):
mov eax,403045h;使eax装有值403045h(十六进制)
mov cx,[eax];把位于内存地址eax的word大小的值(403045)移入cx寄存器。
在mov cx, [eax]中,处理器会先查看eax装有什么值(=内存地址),然后在那个内存地址中有什么值,并把这个word(16位,因为目标-cx-是个16位寄存器)移入cx。
ADD, SUB, MUL, DIV
许多伪代码做计算工作。你可以猜出它们中的大多数的名字:add(加),sub(减),mul(乘),div(除)等。
Add伪代码有如下语法:
Add 目标,源
执行的运算是 目标=目标+源。下面的格式是允许的。
目标 源 例子
Register Register add ecx, edx
Register Memory add ecx, dword ptr [104h] / add ecx, [edx]
Register Immediate value add eax, 102
Memory Immediate value add dword ptr [401231h], 80
Memory Register add dword ptr [401231h], edx
这条指令非常简单。它只是把源值加到目标值中并把结果保存在目标中。其他的数学指令有:
sub 目标,源(目标=目标-源)
mul 目标,源(目标=目标×源)
div 源(eax=eax/源,edx=余数)
减法和加法一样做,乘法是目标=目标×源。除法有一点不同,因为寄存器是整数值(注意,绕回数不是浮点数)除法的结果被分为商和余数。例如:
28/6->商=4,余数=4
30/9->商=3,余数=3
97/10->商=9,余数=7
18/6->商=3,余数=0
现在,取决于源的大小,商(一部分)被存在eax中,余数(一部分)在edx:
源大小 除法 商存于 余数存于
BYTE (8-bits) ax / source AL AH
WORD (16-bits) dx:ax* / source AX DX
DWORD (32-bits) edx:eax* / source EAX EDX
*:例如,如果dx=2030h,而ax=0040h,dx:ax=20300040h。dx:ax是一个双字值。其中高字代表dx,低字代表ax,Edx:eax是个四字值(64位)其高字是edx低字是eax。
Div伪代码的源可以是
an 8-bit register (al, ah, cl,...)
a 16-bit register (ax, dx, ...)
a 32-bit register (eax, edx, ecx...)
an 8-bit memory value (byte ptr [xxxx])
a 16-bit memory value (word ptr [xxxx])
a 32-bit memory value (dword ptr [xxxx])
源不可以是直接数值因为处理器不能决定源参数的大小。
位操作
这些指令都由源和目标,除了“NOT”指令。目标中的每位与源中的每位作比较,并看是那个指令,决定是0还是1放入目标位中。
指令 AND OR XOR NOT
源位 0 0 1 1 0 0 1 1 0 0 1 1 0 1
目标位 0 1 0 1 0 1 0 1 0 1 0 1 X X
输出位 0 0 0 1 0 1 1 1 0 1 1 0 1 0
如果源和目标均为1,AND把输出位设为1。
如果源和目标中有一个为1,OR把输出位设为1。
如果源和目标位不一样,XOR把输出位设为1。
NOT反转源位
一个例子:
mov ax, 3406
mov dx, 13EAh
xor ax,dx
ax=3406(十六进制)是二进制的0000110101001110
dx=13EA(十六进制)是二进制的0001001111101010
对这些位进行xor操作:
源 0001001111101010 (dx)
目标 0000110101001110 (ax)
输出 0001111010100100 (new ax)
新dx是0001111010100100 (十进制的7845, 十六进制的1EA4)
另一个例子:
mov ecx, FFFF0000h
not ecx
FFFF0000在二进制中是11111111111111110000000000000000(16个1,16个0)如果反转每位会得到
00000000000000001111111111111111(16个0,16个1)在十六进制中是0000FFFF。因而执行NOT操作后,ecx是0000FFFFh。
步增/减
有两个很简单的指令,DEC和INC。这些指令使内存地址和寄存器步增或步减,就是这样:
inc reg -> reg = reg + 1
dec reg -> reg = reg - 1
inc dword ptr [103405] -> 位于103405的值步增
dec dword ptr [103405] -> 位于103405的值步减
NOP
这条指令什么都不干。它仅仅占用空间和时间。它用作填充或给代码打补丁的目的。
移位(Bit Rotation 和 shifiting)
注意:下面的大部分例子使用8位数,但这只是为了使目的清楚。
Shifting函数
SHL 目标,计数(count)
SHR 目标,计数(count)
SHL和SHR在寄存器,内存地址中像左或向右移动一定数目(count)的位。
例如:
;这儿al=01011011(二进制)
shr al, 3
它的意思是:把al寄存器中的所有位向右移三个位置。因而al会变成为00001011。左边的字节用0填充,而右边的字节被移出。最后一个被移出的位保存在carry-flag中。Carry-flag是处理器标志寄存器的一位,它不是像eax或ecx一样的,你可以访问的寄存器(虽然有伪代码干这活),但它的值决定于该指令的结构。它(carry-flag)会在后面解释,你要记住的唯一一件事是carry是标志寄存器的一位且它可以被打开或者关闭。这个位等于最后一个移出的位。
shl和shr一样,只不过是向左移。
;这儿bl=11100101(二进制)
shl bl, 2
执行了指令后bl是10010100(二进制)。最后的两个位是由0填充的,carry-flag是1,因为最后移出的位是1。
还有两个伪代码:
SAL 目标, 计数(算术左移)
SAR 目标, 计数(算术右移)
SAL和SHL一样,但SAR不完全和SHR一样。SAR不是用0来填充移出的位而是复制MSB(最高位)例如:
al = 10100110
sar al, 3
al = 11110100
sar al, 2
al = 11101001
bl = 00100110
sar bl, 3
bl = 00000100
Rotation(循环移动) 函数
Rol 目标,计数;循环左移
Ror 目标,计数;循环右移
Rcl 目标,计数;通过carry循环左移
Rcr 目标,计数;通过carry循环右移
循环移动(Rotation)看上去就像移(Shifting),只是移出的位又到了另一边。
例如:ror(循环右移)
Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
移位之前 1 0 0 1 1 0 1 1
循环移位, 计数= 3 1 0 0 1 1 0 1 1 (被移出)
结果 0 1 1 1 0 0 1 1
如你在上图所见,位循环了。注意,每个被推出的位又移到了另一边。和Shifting一样,carry位装有最后被移出的位。Rcl和Rcr实际上和 Rol,Rcr一样。它们的名字暗示了它们用carry位来表明最后移出的位,但和Rol和Ror干同样的事情。它们没有什么不同。
交换
XCHG指令也非常简单。它同在两个寄存器和内存地址之间交换:
eax = 237h
ecx = 978h
xchg eax, ecx
eax = 978h
ecx = 237h
6.0-文件结构
汇编源文件被分成了几个部分。这些部分是code,data,未初始化data,constants,resource和relocations,资源部分是资源文件创建的,后面会有更多的讨论。Relocation部分对我们不重要(它包含了使PE-loader可以在内存的不同的位置装载入程序的信息)。重要的部分是code,data,未初始化data和constants。可能你已经猜到,code部分包含了代码。Data装有数据,并有读写权限。整个data部分被包括在exe文件并可以用数据初始化。
未初始化data在启动时没有内容,甚至没有包括在exe文件本身。它只是由Windows“保留”的一部分内存。这部分也有读写权限。Constants和data部分一样,但只读。虽然这部分可用作常数,但把常数定义在包含文件中更简单也更快捷,并用作直接数值。
6.1-代表各部分的符号
在你的源文件(*.asm)中,你可以用部分标识符定义各部分:
.code;代码部分由此开始
.data;数据部分由此开始
.data?;未初始化数据部分由此开始
.const;常量部分由此开始
可执行文件(*.exe,*.dll和其他)是(在win32中)可移植执行格式(PE),我不会详细的讨论它但是有几点是重要的。部分(Sections)的一些属性定义在PE头中:
Section 名,RVA,offset,原始大小,虚拟大小和标志。Rva(相对虚拟地址)是将要装入的section部分的相对内存地址。这里相对的意思是相对于程序载入的基地址。这个地址也在PE头中,但可以由PE-loader改变(使用relocation部分)。Offset是初始化数据所在的exe文件本身的原始offset。虚拟大小是程序在内存中将达到的大小。标志是读/写/可执行等。
6.2-例子
这有一个示例程序:
.data
Number1 dd 12033h
Number2 dw 100h,200h,300h,400h
Number3 db "blabla",0
.data?
Value dd ?
.code
mov eax, Number1
mov ecx, offset Number2
add ax, word ptr [ecx+4]
mov Value, eax
这个程序不能编译但没关系。
在你的汇编程序中,你放入“部分”中的所有东西都会进入exe文件而且当程序被载入内存时,位于某个内存地址。在上面的data部分,有3个标签: Number1, Number2, Number3。这些标签会保存它们在程序中的offset因而你可以在你的程序中使用它们来指示位置。
DD直接把一个dword放在那,DW是Word而DB是byte。你也可以用db放字符串,因为它实际上是一串byte值。在例子中,data部分会变成内存中的这样:
33,20,01,00,00,01,00,02,00,03,00,04,62,6c,61,62,6c,61,00(均为十六进制值)
(每个值位一byte)
我给其中的一些数字上了色。Number1指向byte 33所在的内存地址,Number2指向红色00的位置,Number3是绿色的62。现在,如果你在你的程序中这么写:
mov eax, Number1
它实际意为:
mov ecx, dword ptr[12033h所在的内存地址]
但这样:
mov ecx, offset Number1
意为:
mov ecx, 12033h所在的内存地址
在第一个例子中,ecx会得到Number1的内存地址的值。在第二个中,ecx会称为内存地址(offset)本身。下面的两个例子有相同的效果:
(1)
mov ecx, Number1
(2)
mov ecx, offset Number1
mov ecx, dword ptr [ecx] ( or mov ecx, [ecx])
现在让我们回到前面的例子中:
.data
Number1 dd 12033h
Number2 dw 100h,200h,300h,400h
Number3 db "blabla",0
.data?
Value dd ?
.code
mov eax, Number1
mov ecx, offset Number2
add ax, word ptr [ecx+4]
mov Value, eax
标签可以使用像Number1,Number2和Number3等值,但它启动时包含0。因为它在未初始化data部分。这样的优点是,你在.data?中定义的所有东西不在可执行文件中而在内存中。
.data?
ManyBytes1 db 5000 dup (?)
.data
ManyBytes2 db 5000 dup (0)
(5000dup意为:5000个副本。值db 4,4,4,4,4,4,4和值db 7dup(4)一样)
ManyBytes1不会在文件本身,只是5000个预分配在内存中的字节。但Manybytes2会在可执行文件中使文件变大5000个字节。虽然你的文件会包含5000个零,但并没有什么用。
Code部分被汇编(翻译为原始代码)并放入可执行文件中去(当然载入后在内存中)。
7.0-条件跳转
在Code部分,你可以看到像这样的标签:
.code
mov eax, edx
sub eax, ecx
cmp eax, 2
jz loc1
xor eax, eax
jmp loc2
loc1:
xor eax, eax
inc eax
loc2:
(xor eax, eax意为:eax=0)
让我们来看看这些代码:
mov eax, edx;把edx放入eax中
sub eax, ecx;eax-ecx
cmp eax, 2
这有一条新指令:cmp。Cmp意为compare(比较)。它能比较两个值(寄存器,内存,直接数值)并设置Z-flag(零标志)。零标志很像carry,也是内部标志寄存器的一位。
Jz loc1
这也是条新的。它是条件跳转指令。Jz=jump if zero(如果设置了零标志就跳转)。Loc1是一个标记指令“xor eax,eax|inc eax”内存开始处offset的标签。因而jz loc1=如果设置了零标志,跳往位于loc1的指令。
Cmp eax, 2;如果eax=2设置零标志
Jz loc1;如果设置了零标志就跳转
=
如果eax等于2,跳往位于loc1的指令
然后有jmp loc2.这也好似一个跳转,但是是一个无条件跳转:它总是执行。上面的代码就是:
if ((edx-ecx)==2)
{
eax = 1;
}
else
{
eax = 0;
}
或者Basic版:
IF (edx-ecx)=2 THEN
EAX = 1
ELSE
EAX = 0
END IF
7.1-标志寄存器
标志寄存器有一套标志。它们设不设置取决于计算或其他时间。我不会讨论它们的全部。只拣几个重要的说:
ZF(零标志) 当计算结果是零时该标志被设置(compare实际上是只设置标志不保存结构的减法)
SF(符号标志) 结果为负就设置
CF(carry标志) Carry标志中存放计算后最右的位。
OF(溢出标志) 标明一个溢出了的计算。如,结构和目标不匹配。
还有更多的标志(Parity, Auxiliary, Trap, Interrupt, Direction, IOPL, Nested Task, Resume, & Virtual Mode)但因为我们不用它们,所以我不解释。
7.2-跳转系列
有一整套的条件跳转,而且它们跳转与否均取决于标志的状态。但由于大部分跳转指令有明白的名字,你甚至无需知道哪个标志要设置,例如:“如果大于等于就跳转”(jge)和“符号标志=溢出标志”一样,而“如果零就跳转”和“如果零标志=1就跳转”一样。
在下表中,“意思”指的是什么样的计算结果该跳转。“如果大于就跳转”意为:
cmp x, y
jmp 如果 x 比 y大
伪代码 意思 条件
JA Jump if above CF=0 & ZF=0
JAE Jump if above or equal CF=0
JB Jump if below CF=1
JBE Jump if below or equal CF=1 or ZF=1
JC Jump if carry CF=1
JCXZ Jump if CX=0 register CX=0
JE (is the same as JZ) Jump if equal ZF=1
JG Jump if greater (signed) ZF=0 & SF=OF
JGE Jump if greater or equal (signed) SF=OF
JL Jump if less (signed) SF != OF
JLE Jump if less or equal (signed) ZF=1 or SF!=OF
JMP Unconditional Jump -
JNA Jump if not above CF=1 or ZF=1
JNAE Jump if not above or equal CF=1
JNB Jump if not below CF=0
JNBE Jump if not below or equal CF=1 & ZF=0
JNC Jump if not carry CF=0
JNE Jump if not equal ZF=0
JNG Jump if not greater (signed) ZF=1 or SF!=OF
JNGE Jump if not greater or equal (signed) SF!=OF
JNL Jump if not less (signed) SF=OF
JNLE Jump if not less or equal (signed) ZF=0 & SF=OF
JNO Jump if not overflow (signed) OF=0
JNP Jump if no parity PF=0
JNS Jump if not signed (signed) SF=0
JNZ Jump if not zero ZF=0
JO Jump if overflow (signed) OF=1
JP Jump if parity PF=1
JPE Jump if parity even PF=1
JPO Jump if paity odd PF=0
JS Jump if signed (signed) SF=1
JZ Jump if zero ZF=1
所有的跳转指令需要一个参数:要跳往的offset。
8.0-关于数的一些事情
在大多数的编程语言中使用整数还是浮点数只取决于变量的声明。在汇编语言中,完全的不同。浮点数的计算是由特别的伪代码和FPU协处理器(浮点单元)完成的。浮点指令将会在后面讨论。先来看看一些关于整数的事情。在c语言中有signed(有符号)整数和unsigned(无符号)整数。Signed是意为数有符号(+或-)。Unsigned总是正。找出下表中的不同(再一次的,这是一个byte的例子,它在其他大小时也同样工作)。
值 00 01 02 03 ... 7F 80 ... FC FD FE FF
无符号意义 00 01 02 03 ... 7F 80 ... FC FD FE FF
有符号意义 00 01 02 03 ... 7F -80 ... -04 -03 -02 -01
因此,在有符号数中,一个byte被分为两段:0~7F用于正值。80~FF用于负值。对于dword值,它也一样:0~7FFFFFFFh为正, 80000000~FFFFFFFFh为负,正如你可能已经注意到的一样,负值的最高位有一个集合,因为它们比80000000h大。这位被称为符号位。
3.1-有符号或无符号?
你和处理器都不能看出一个值是signed还是unsigned。好消息是对于加法和减法来说,一个数是signed还是unsigned没有关系。
计算:-4+9
FFFFFFFC+00000009=00000005(这是对的)
计算:5-(-9)
00000005-FFFFFFF7=0000000E(这也是对的,5――9=4)
坏消息是对于乘法,除法和比较(compare)并不是这样。因此,对于signed数有特殊的乘除伪代码:imul和idiv
Imul也有一个比mul好的地方在于它可以接受直接数值:
imul src
imul src, immed
imul dest,src, 8-bit immed
imul dest,src
idiv src
它们几乎和mul,div一样,只是它们可以计算signed值。比较(compare)可以和unsigned一样用。但标志作不同的设置。因此,对于符号和无符号数字有不同的jump指令:
cmp ax, bx
ja somewhere
ja是一个无符号跳转指令。如果大于就跳转。考虑这个ax=FFFFh(无符号时为FFFFh,有符号时为-1)和bx=0005h(无符号时为5,有符号时为5)。由于FFFFh在无符号时比0005大,ja指令会跳转,但如果用的是jg(指一个有符号跳转):
cmp ax, bx
jg somewhere
jg指令不会跳转,因为-1不比5大。
只要记住这点:
一个数字是有符号还是无符号取决于你怎样对待这个数。
9.0-更多的伪代码
这儿有更多的伪代码
TEST
Test对两个参数(目标,源)执行AND逻辑操作,并根据结果设置标志寄存器。结果本身不会保存。Test用来测试一个位,例如寄存器:
test eax, 100b;b后缀意为二进制
jnz bitset
如果eax右数第三个位被设置了,jnz将会跳转。Test的一个非常普遍的用法是用来测试一方寄存器是否为空:
test ecx, ecx
jz somewhere
如果ecx为零,Jz跳转
关于栈的伪代码
在我讲栈的伪代码之前,我会先解释什么是栈。栈是内存的一个地方,esp为指向栈的指针。栈是用来保存临时数值的地方,有两个指令来放入一个指和再把它取出来:push和pop。Push把一个指压入栈。Pop再把它弹出来。最后一个放入的值最先出来。一个值被放入栈中,栈指针步减,当它移出来的时候,栈指针步增。看这个例子:
(1) mov ecx, 100
(2) mov eax, 200
(3) push ecx ; save ecx
(4) push eax
(5) xor ecx, eax
(6) add ecx, 400
(7) mov edx, ecx
(8) pop ebx
(9) pop ecx
解释
1、 把100放入ecx中
2、 把200放入eax中
3、 把ecx(等于100)压入栈中(第一个压入)
4、 把eax(等于200)压入栈中(最后压入)
5、 /6/7:对ecx执行操作,使ecx的值改变
8、 弹出ebx:ebx成为200(最后压入,最先弹出)
9、 弹出ecx:ecx又成为100(最先压入,最后弹出)
为了说明再压栈和弹栈时,内存中发生了什么,看下图:
Offset 1203 1204 1205 1206 1207 1208 1209 120A 120B
值 00 00 00 00 00 00 00 00 00
ESP
(栈在这里是初始化为0,但实际上并不是这样。ESP表示ESP指向的offset)
mov ax, 4560h
push ax
Offset 1203 1204 1205 1206 1207 1208 1209 120A 120B
值 00 00 60 45 00 00 00 00 00
ESP
mov cx, FFFFh
push cx
Offset 1203 1204 1205 1206 1207 1208 1209 120A 120B
值 FF FF 60 45 00 00 00 00 00
ESP
pop edx
Offset 1203 1204 1205 1206 1207 1208 1209 120A 120B
值 FF FF 60 45 00 00 00 00 00
ESP
edx现在是 4560FFFFh 了.
CALL和RET
Call跳转到某段代码而且一发现RET指令就返回。你可以把它们看成在其他编程语言中的函数或子程序。例如:
……代码……
call 0455659
……更多代码……
455659处的代码:
add eax, 500
mul eax, edx
ret
当执行这条指令时,处理器跳到455659处的代码,执行指令一直到ret为止,并返回到调用处的下一条。Call跳转到的代码被成为过程(procedure)。你可以把你反复使用的代码写进一个过程并在你每次需要它的时候调用。
更深入的细节:call把EIP(指向将要执行指令的指针)压入栈,而ret指令在它返回的时候把它弹出来。你也可以给一个call指定的参数。这是由压栈来完成的:
push something
push something2
call procedure
在一个调用的内部,参数从栈中读出并使用。注意,只在过程中需要的局部变量也储存在栈中。我不会在此深入下去,因为它可以在masm和tasm中很轻易的完称。只要记住你可以写过程,而且它们可以由参数。一个重要的地方:
eax几乎总是用来装一个过程的返回值。
对于windows函数也是如此。但然,你可以在你的过程使用其他的寄存器,但这是标准。
10.0-masm的优点
如果你不在使用masm,你可以跳过这章并尝试着转换所有的例子,或不论如何地读一下,并试着说服自己使用masm。当然,这是你的选择。但masm真的使汇编语言更容易了。
10.1-条件和循环结构
Masm有一些伪高阶的语法来简便地创建条件和循环结构:
.IF, .ELSE, .ELSEIF, .ENDIF
.REPEAT, .UNTIL
.WHILE, .ENDW, .BREAK
.CONTINUE
If
如果你有使用编程语言的经验(你应该有),你可能已经看到了一些像if/else的结构:
.IF eax==1
;eax等于1
.ELSEIF eax=3
; eax等于3
.ELSE
; eax既不是1也不是3
.ENDIF
这种结构非常有用。你不需要和一对跳转搅在一起了,只要一个.IF语句(也不要忘记.IF和.ELSE之前的时期)。嵌套的if是允许的:
.IF eax==1
.IF ecx!=2
; eax= 1 而且 ecx 不是 2
.ENDIF
.ENDIF
但可以更简洁些:
.IF (eax==1 && ecx!=2)
; eax = 1 而且 ecx 不是 2
.ENDIF
这些是你可以使用的操作符:
== 等于
!= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于
& 位测试
! 逻辑非
&& 逻辑与
|| 逻辑或
CARRY? carry bit set
OVERFLOW? overflow bit set
PARITY? parity bit set
SIGN? sign bit set
ZERO? zero bit set
Repeat
这个语句执行一块指令知道条件为真为止:
.REPEAT ;代码在此 .UNTIL eax==1
这块代码反复执行repeat和until之间的代码,知道eax=1。
While
While是repeat语句的反转。它在条件为真时执行代码块:
.WHILE eax==1
;代码在此
.ENDW
你可以使用.BREAK语句来跳出循环
.WHILE edx==1
inc eax
.IF eax==7
.BREAK
.ENDIF
.ENDW
如果Eax==7,while循环将停止
continue指令使repeat或While跳过下面的代码块,重新执行循环。
10.2-invoke
这是胜过tasm和nasm最大的优点。Invoke简化了过程和call的使用。
一般的格式:
push parameter3
push parameter2
push parameter1
call procedure
Invoke 格式:
invoke procedure, parameter1, parameter2, parameter3
汇编后的代码是一摸一样的,但invoke格式更简单而且更可靠。对一个过程使用invoke,你要这样定义prototype:
PROTO STDCALL testprocWORD, WORD, WORD
声明了名为testproc,需三个DWORD大小的参数的过程。现在,如果你这么做……
invoke testproc, 1, 2, 3, 4
……masm会给你一个testproc过程需要三个参数而不是四个的错误。Masm还会做类型检查。它检查参数是否为正确的类型(即大小)
在一个invoke语句中,你可以用ADDR代替offset。这会使地址在汇编时是正确的。
过程这样定义:
testproc PROTO STDCALL WORD, WORD, WORD
.code
testproc proc param1WORD, param2WORD, param3WORD
ret
testproc endp
这会创建一个名为testproc,带三个参数的过程。Prototype是用来调用过程的。
testproc PROTO STDCALL WORD, WORD, WORD
.code
testproc proc param1WORD, param2WORD, param3WORD
mov ecx, param1
mov edx, param2
mov eax, param3
add edx, eax
mul eax, ecx
ret
testproc endp
现在,过程做了一下计算,(param1, param2, param3) = param1 * (param2 + param3).结果(返回值)存放在eax中,局部变量这样定义:
testproc proc param1WORD, param2WORD, param3WORD
LOCAL var1WORD
LOCAL var2:BYTE
mov ecx, param1
mov var2, cl
mov edx, param2
mov eax, param3
mov var1, eax
add edx, eax
mul eax, ecx
mov ebx, var1
.IF bl==var2
xor eax, eax
.ENDIF
ret
testproc endp
你不可以在过程外使用这些变量。它们储存在栈中而且当过程返回时移出。
10.3-宏
现在不解释宏。可能在以后的教程中,但现在它们对我们不重要。
11.0-Windows中的汇编基础
现在你已经有了一些汇编语言的基础知识,你将要学习在Windows中怎样学习汇编。
11.1-API
Windows 编程的根本在于Windows API,应用程序接口。这是由操作系统提供的一套函数。每个Windows程序员都要用这些函数。这些函数在像kernel, user, gdi, shell, advapi等系统dll中。函数有两类:ANSI和Unicode。这和字符串的存储方法有关。Ansi中,每个字节代表一个符号(ASCI码),并用字节0代表一个字符串的结束(null-terminated)。Unicode使用宽字符格式。它的每个字节用2个字节。这允许像中文等多字符的语言的使用。宽字符串由两个0字节结束。Windows通过使用不同的函数名,同时支持Ansi和Unicode。
例如:
MessageBoxA(后缀A意为ansi)
MessageBoxW(后缀W意为宽字符-unicode)
我们只使用ansi型
11.2-导入dll
为了使用来自WindowsAPI的函数,你需要导入dll。这是由导入库(.lib)来完成的。这些库是必需的。因为它们使系统(Windows)能在内存的动态基地址处动态的载入dll。在Win32asm包中(win32asm.cjb.net)提供了大多数标准dll的库。你可以用masm的 includelib语句装载一个库。
译者注:注意,win32asm.cjb.net被中国电信封了ip。访问请使用代理。
Includelib C:\masm32\lib\kernel32.lib
这将载入库kernel32.lib。在例子中,用这种格式:
Includelib \masm32\lib\kernel32.lib
现在你可以看到为什么汇编源文件要和masm在同一个区的原因了。你可以不改动路径为正确的区就能在其他的电脑上编译你的程序。
但你不只是需要包含库。包含文件(.inc)也是必须的。这些可以用l2inc工具由库文件自动生成。包含文件这样装载:
include \masm32\include\kernel32.inc
在包含文件中,定义了dll中函数的原型(prototypes),因而你能使用invoke。
kernel32.inc:
...
MessageBoxA proto stdcall WORD, WORD, WORD, WORD
MessageBox textequ
...
你能看到包含文件内有for Ansi的函数而且没有‘A’的函数名字定义为与真实函数名一样:你可以用MessageBox代替MessageBoxA使用。在你包含了库和包含文件后,你可以使用函数了:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, NULL
11.3-Windows包含文件
这里有一个特别的包含文件。大多数的时候统称为Windows.inc,其中包含了用于Windows API的所有常量和结构的定义。例如,消息框有不同的样式。函数的第四个参数是样式。NULL指的是MB_OK,它只有一个OK按钮。Windows包含文件有这些样式的定义:
> MB_OK equ 0
MB_OKCANCEL equ ...
MB_YESNO equ ...
因此你可以把这些名字当常数来用:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_YESNO
例子将使用masm包中的包含文件:
include \masm32\include\windows.inc
11.4-框架
.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\gdi32.lib
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\gdi32.inc
include \masm32\include\windows.inc
.data
blahblah
.code
start:
blahblah
end start
这是Windows汇编源文件(.asm)的基本框架
.486
告诉汇编器应该生成486处理器(或更高)的伪代码。你可以使用.386,但大多数情况下用.486
.model flat, stdcall
使用平坦内存模式(在前面章节中讨论了)并使用stdcall调用习惯。它的意思是函数的参数从右往左压入(最后的参数最先压入)而且函数在结束时自己清栈。这对于几乎所有的Windows API函数和dll是标准
option casemap:none
控制字符的映射为大写。为了Windows.inc文件能正常工作,这个应该为”none”
includelib
前面讨论了
include
前面也讨论了
.data
开始data部分(看前面章节)
.code
开始code部分(看前面章节)
start:
end start
表示一个程序的开始的标签。它不是非得叫“start”。你可以使用任何和“end”语句后相同的标签:
startofprog:
end startofprog
12.0-第一个程序
是创建你的第一个程序的时候了。本章中的指导将这样组织:
12.1-第一步
如果万事具备,你应该在你的masm同一个区上有一个win32(或win32asm)目录。为每个工程,你应该创建一个子目录。
在win32目录中创建一个名为“Firstprogram“的子目录。创建一个新的文本文件并重命名为“first.asm”。
12.2-第二步
在first.asm中输入一下代码:
.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\windows.inc
因为现在,我们仅需要kernel32和user32两个dll。
12.3-第三步
我们将要创建著名的“Hello World”程序。要显示“hello World”字符串,我们要用消息对话框。消息对话框由MessageBox函数创建。你可以在《win32 程序员参考》(看第二章)中查找这个函数。这是书上说的:
MessageBox函数创建,显示并操作消息对话框。消息对话框包含应用程序定义的消息和标题,加上任何预定义的图标与按钮的组合。
int MessageBox(
HWND hWnd, // handle of owner window
LPCTSTR lpText, // address of text in message box
LPCTSTR lpCaption, // address of title of message box
UINT uType // style of message box
);
Parameters
hWnd
Identifies the owner window of the message box to be created. If this parameter is NULL, the message box has no owner window.
lpText
Points to a null-terminated string containing the message to be displayed.
lpCaption
Points to a null-terminated string used for the dialog box title. If this parameter is NULL, the default title Error is used.
uType
Specifies a set of bit flags that determine the contents and behavior of the dialog box. This parameter can be a combination of flags from the following groups of flags.
[--SNIP--]
在这段文字后有所有常数和标志的列表(他们定义在windows.inc中)。因为它太长了,我没有在这里列出来。通过查看参考,你就知道MessageBox函数要4个参数:父窗口(owner),指向消息串的指针,指向标题串的指针和消息框的类型。
HWnd可以是Null。因为我们的程序没有窗口。
LpText必须是指向我们文本的指针。这仅仅意为参数是文本所在内存地址的offset。
LpCaption 是标题串的offset。
UType 是参考中解释的像MB_OK,MB_OKCANCEL,MB_ICONERROR等值的组合。
让我们先定义两个用于MessageBox的字符串:
在first.asm中加入:
.data
MsgText db "Hello world!",0
MsgTitle db "This is a messagebox",0
.data 指示data部分的开始。用db,字节直接被插入,而且字符串又只是字节的集合,data部分会在包含上面的字符串,附加上结尾的0。MsgText装有第一个字符串的offset。MsgTitle有第二个字符串的offset。现在我们可以使用函数:
invoke MessageBox, NULL, offset MsgText, offset MsgTitle, Null
但因为用的是invoke,你可以使用(更安全)ADDR代替offset:
invoke MessageBox, Null, ADDR MsgText, ADDR MsgTitle, Null
我们还没有看最后一个参数,但这不会有什么问题。因为MB_OK(有一个ok按钮的消息对话框的样式)等于0(NULL)。但你也可以使用其他的任何样式。Utype(第4个参数)的定义是:
指定一系列决定对话框内容与行为的位标志。这个参数可以是下面标志组中标志的组合。
现在以我们要一个有OK按钮与“information”图标的简单消息对话框为例。MB_OK是OK按钮的样式,MB_ICONINFORMATION是 information图标的样式。样式是用“or”操作符联合的。这不是or伪代码。Masm会在汇编前处理or操作。不用or,你可以用+号(加号)代替,但有时对层叠样式有问题(一个样式包含其他一些样式)。但在本例中你也可以用+号。
.code
start:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_OK + MB_ICONINFORMATION
end start
把以上的代码加入到你的first.asm文件中。
我们还加入了一个start标签。如果你现在汇编你的程序并运行它,它将显示一个消息对话框但很有可能在你点OK之后就崩溃了。这是因为程序没有结束,而处理器开始执行MessageBox代码后的任何东西。Windows中程序是用ExitProcess函数结束的:
VOID ExitProcess(
UINT uExitCode //对于所有线程的退出代码 );
我们可以把0用作退出码。
把你的代码改成这样:
.code
start:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_OK + MB_ICONINFORMATION
invoke ExitProcess, NULL
end start
12.4-第4步
因此我们最终的程序是:
.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\windows.inc
.data
MsgText db "Hello world!",0
MsgTitle db "This is a messagebox",0
.code
start:
invoke MessageBox, NULL, ADDR MsgText, ADDR MsgTitle, MB_OK or MB_ICONINFORMATION
invoke ExitProcess, NULL
end start
12.5-第5步
现在我们将从源代码产生可执行文件。
用一下内容新建一个文本文件并命名为make.bat:
@echo off
ml /c /coff first.asm
link /subsystemindows first.obj
pause>nul
解释:
ml /c /coff first.asm
Ml 是宏汇编器(masm)。Masm将从程序创建原始代码。参数的意思是: /c =汇编不链接(因为我们用link.exe来做这项工作) /coff = 产生COFF格式的object(对象)文件,这是Windows可执行文件的标准格式。 first.asm = a汇编first.asm文件
link /subsystemindows first.obj
链接器把object文件和所有导入的dll与库链接起来: /subsystemindows = 创建Windows的可执行文件。 first.obj = 链接 first.obj
如果你把所有的事情都正确的完成了,并运行批处理文件。将产生first.exe。运行它,看看有什么结果。
13.0-Windows中的窗口
在本章中,我们将创建一个有窗口的程序
13.1-窗口
你可能已经猜到了Windows之所以称为Windows的原因了。在Windows中,有两种程序:GUI程序和控制台程序。控制台模式的程序看上去就像 Dos程序,它们在一个似-dos的窗口中运行。你使用的大多数程序是GUI(图形用户界面)程序,它们有一个用于和用户交互的图形界面。这是由创建窗口来完成的。几乎你在Windows中看见的每一件东西都是窗口。首先,你创建一个父窗口,然后是像编辑框,静态控件(文本标签-译者注),按钮等的自窗口(控件)。
13.2-窗口类
每一个窗口都有名字。你为你的父窗口定义你自有的类。对于控件,你可以使用Windows的标准类名(例如,“Edit”,“Static”,“Button”)
13.3-结构
你程序中的窗口类是用“RegisterClassEx“函数注册的。(RegisterClassEx是RegisterClass的扩展版本,后者已经不太使用了)这个函数的声明是:
ATOM RegisterClassEx(
CONST WNDLCASSEX *lpwcx//有类数据的结构之地址
);
lpwcx:指向WNDCLASSEX结构。在把它传递给函数之前,你必须用适当的类属性填写结构。
唯一的参数是指向结构的指针。先来看看一些结构的基本知识:
一个结构是一些变量(数据)的集合。它用STRUCT定义:
SOMESTRUCTURE STRUCT
dword1 dd ?
dword2 dd ?
some_word dw ?
abyte db ?
anotherbyte db ?
SOMESTRUCTURE ENDS
(结构名不一定要大写)
你可以用问号把你的变量定义在未初始化data部分。现在你可以根据定义创建一个结构:
Initialized
Initializedstructure SOMESTRUCTURE <100,200,10,'A',90h>
Uninitialized
UnInitializedstructure SOMESTRUCTURE <>
在第一个例子中,创建了一个新的结构(用初始化了的结构保存它的offset),而且结构的每一个元素用初始化数值填写了。第二个例子只是告诉masm为结构名分配内存,而且每个数据元素用0初始化。在创建了结构之后,你可以在代码中使用它:
mov eax, Initializedstructure.some_word
; eax现在是 10
inc UnInitializedstructure.dword1
; 结构的dword1步增
结构是这样存在内存中的:
内存地址 内容
offset of Initializedstructure 100 (dword, 4 bytes)
offset of Initializedstructure + 4 200 (dword, 4 bytes)
offset of Initializedstructure + 8 10 (word, 2 bytes)
offset of Initializedstructure + 10 65 or 'A' (1 byte)
offset of Initializedstructure + 11 90h (1 byte)
12.3-WNDCLASSEX
现在已经了解了足够多的结构知识,让我们处理RegisterClassEx吧。在《win32程序员参考》中,你可以查找WNDCLASSEX结构的定义。
typedef struct _WNDCLASSEX { //
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX;
解释
cbSize
WNDCLASSEX结构体的大小。用于Windows的认证。你可以用SIZEOF得到它的大小: mov wc.cbsize, SIZEOF WNDCLASSEX
style
为类指定一个样式(如果窗口要有滚动条,加上重画标志。等等)
lpfnWndProc
指向Windows Procedure的指针(本章后面有更多内容)
cbClsExtra
在Windows类结构后本配多少额外内存。对我们不重要
cbWndExtra
在Windows实例后分配多少额外内存。对我们也不重要
hInstance
你程序的实力句柄。你可以用GetMoudleHandle函数得到这个句柄
hIcon
窗口图标资源的句柄
hCursor
窗口光标资源的句柄
hbrBackground
用于填充背景的画刷句柄,或是标准刷子类型中的一个,如 COLOR_WINDOW, COLOR_BTNFACE , COLOR_BACKGROUND.
lpszMenuName
指向一个指定菜单类名的零结尾字符串
lpszClassName
指向一个指定窗口类名的零结尾字符串
hIconSm
一个和窗口类关联的小图标句柄
在你的Win32文件夹中创建一个名为firstWindow的文件夹并在这个文件夹中创建一个名为window.asm的新文件,输入一下内容:
.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\gdi32.lib
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\gdi32.inc
然后创建一个名为make.bat的.bat文件。把这些文本粘贴进去:
@echo off
ml /c /coff window.asm
link /subsystemindows window.obj
pause>nul
从现在开始,为了节省空间,仅显示小段的代码。你可以通过点来显示教程此处的全部代码。完整的代码在新窗口中显示。
译者注:为了方便,我又把这些放回来了。
13.4-注册类
现在我们在名为WinMain的过程中注册类。该过程中完成窗口的初始化。
把这些加入你的汇编文件:
WinMain PROTO STDCALL WORD, WORD, WORD
.data?
hInstance dd ?
.code
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke WinMain, hInstance, NULL, NULL, SW_SHOWNORMAL
end start
这些代码通过GetModuleHandle得到模块句柄,并把模块句柄放入hInstance变量中。这个句柄在Windows API中频繁使用。然后它调用WinMain过程。这不是一个API函数,而是一个我们将要定义的过程。原型是:WinMain PROTO STDCALL WORD, WORD, WORD, WORD,因而是一个带4个参数的函数:
现在把这些代码放在end start:前
WinMain proc hInstWORD, hPrevInstWORD, CmdLineWORD, CmdShowWORD
ret
WinMain endp
你根本就不需要用这个winmain过程,但这是一种十分普遍的处世化你的程序的方法。Visual C自动初始化这个函数的参数,但我们必须自己来做。现在不要管hPrevInst和CmdLine。集中注意在hInst和CmdShow上。Hinst 是实例句柄(=模块句柄),CmdShow是定义窗口该如何显示的标志。(你可以在API参考关于ShowWindows部分发现更多)
在前面代码中的"invoke WinMain, hInstance, NULL, NULL, SW_SHOWNORMAL"用正确的实例句柄和显示标志调用这个函数。现在我们可以在WinMain中写我们的初始化代码了。
WinMain proc hInstWORD, hPrevInstWORD, CmdLineWORD, CmdShowWORD
LOCAL wc:WNDCLASSEX
LOCAL hwndWORD
ret
WinMain endp
这有我们将在过程中要用的两个局部变量
.data
ClassName db "FirstWindowClass",0
.code
WinMain proc hInstWORD, hPrevInstWORD, CmdLineWORD, CmdShowWORD
LOCAL wc:WNDCLASSEX
LOCAL hwndWORD
; now set all the structure members of the WNDCLASSEX structure wc:
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon, eax
mov wc.hIconSm, eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, ADDR wc
ret
WinMain endp
让我们来看看发生了什么:
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
初始化了结构的大小(这是RegisterClassEx要求的)。设置类的样式为”CS_HREDRAW or CS_VREDRAW”,然后设置了窗口过程的offset。你在后面会知道什么是窗口过程,现在你仅需要记住你需要WndProc过程的地址。该地址可以通过“offset WndProc”获得。Cb.ClsExtra和cb.WndExtra我们没有使用因而设它们为Null。
Push hInst
Pop wc.hInstance
Wc.hInstance设为WinMain的hInst参数。为什么我们不用:mov wc.hInstance, hInst?因为mov指令不允许从一个地址移到另一个地址。通过push/pop,值被压入栈,然后又弹入目标中。
mov wc.hbrBackground, COLOR_WINDOW
mov wc.lpszMenuName, NULL
mov wc.lpszClassName, OFFSET ClassName
类的背景色被设为COLOR_WINDOW,没有定义菜单(null)而且lpszClassName设为一个指向零结尾的类名字符串:“FirstWindowClass”它应该是一个在你的程序中定义的唯一名字。
invoke LoadIcon, NULL, IDI_APPLICATION
mov wc.hIcon, eax
mov wc.hIconSm, eax
窗口需要一个图标。但又因为我们要一个指向图标的句柄,我们使用LoadIcon来载入图标并获得句柄。LoadIcon有两个参数:hInstance和 lpIconName。HInstance是包含图标的可执行文件的模块句柄。LpIconName是一个指向图标资源和图标ID的字符串的指针。如果你用NULL为hInstance,你可以从一些标准图表中选这一个(这却是是因为我们在这里还没有图标资源)hIconSm是小图标,你可以对它使用相同的句柄。
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
对光标也一样。NULL作hInstance,并用一个标准光标类型:IDC_ARROW,标准Windows箭头型光标。
invoke RegisterClassEx, ADDR wc
现在,最终用RegisterClassEx来注册类,通过一个指向WNDCLASSEX结构的指针作参数。
13.5-创建窗口
现在,你已经注册了一个类,你可以使用它创建一个窗口:
HWND CreateWindowEx(
DWORD dwExStyle, // extended window style
LPCTSTR lpClassName, // pointer to registered class name
LPCTSTR lpWindowName, // pointer to window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // handle to menu, or child-window identifier
HINSTANCE hInstance, // handle to application instance
LPVOID lpParam // pointer to window-creation data
);
DwExstyle和dwStyle是两个决定窗口样式的参数。
LpClassName 是一个指向你注册了的类名的指针。
LpWindowName 是你窗口的名字(如果有的话,这将成为你窗口的标题)
X, Y, nWidth, nHeight 决定你窗口的位置和大小
HMenu 是菜单窗口的句柄(在后面讨论,现在为空)
HInstance 是程序实例的句柄
LpPararm 是你能在你的程序中使用的扩展值
.data
AppName "FirstWindow",0
.code
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,400,300,NULL,NULL,\
hInst,NULL
mov hwnd, eax
invoke ShowWindow, hwnd, SW_SHOWNORMAL
invoke UpdateWindow, hwnd
(注意\使汇编器读下一行的时候好像还在同一行)
我们的代码将用我们刚刚注册的类名创建一个新的窗口。标题是“FirstWindow”(程序名,AppName),样式是 WS_OVERLAPPEDWINDOW,这是一个创建有标题,系统菜单,可缩放边框和最大化/最小化按钮的窗口样式。CW_USERDEFAULT作为 x和y的位置会使Windows为新窗口使用缺省位置。窗口的(初始)大小是400×300象素。
函数的返回值是窗口句柄,HWND。它储存在局部变量hwnd中。然后窗口用ShowWindow显示。UpdateWindow确保窗口被画出。
13.6-消息循环
窗口可以通过消息和你的程序以及其他窗口通讯。无论何时,一条消息被发送给指定的窗口。它的窗口过程都要被调用。每个窗口都有一个消息循环或消息泵 (pump)。这是一个无止尽的检查是否给有你的窗口的消息的循环。而且如果有,把消息传递给dispatchMessage函数。这个函数会调用你的窗口过程。消息循环和窗口过程是两个完全不同的东西!!!
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShowWORD
LOCAL wc:WNDCLASSEX
LOCAL hwndWORD
LOCAL msg:MSG ;<<
........
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
这是消息循环看上去的样子。.WHILE TRUE, .ENDW循环到eax为0之前都会继续。如果它接到了WM_QUIT消息,GetMessage返回0,这将关闭窗口因而程序应该在不论 GetMessage返回0时退出。如果不是这样(0),消息被传递给TranslateMessage(这个函数把按键翻译为消息)而且消息被 Windows用DispatchMessage函数解包。消息本身在一个消息循环的组成部分MSG结构中(LOCAL msg: MSG被加入过程,增加了一个称为msg的局部消息结构)你可以在你的所有程序中用这个消息循环。
13.7-窗口过程
消息会被发送往窗口过程。一个窗口过程看上去总是这样:
WndProc PROTO STDCALL WORD, WORD, WORD, WORD
.code
WndProc proc hWndWORD, uMsgWORD, wParamWORD, lParamWORD
mov eax, uMsg
.IF eax==XXXX
.ELSEIF eax==XXXX
.ELSE
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
.ENDIF
ret
WndProc endp
窗口过程总是有4个参数
hWnd 包含窗口句柄
uMsg 消息
wParam 消息的第一个参数(由消息定义)
lParam 消息的第二个参数(由消息定义)
窗口不处理的消息应该传递给DefWindowProc,它会处理这些。一个窗口过程的例子:
WndProc proc hWndWORD, uMsgWORD, wParamWORD, lParamWORD
mov eax, uMsg
.IF eax==WM_CREATE
invoke MessageBox, NULL, ADDR AppName, ADDR AppName, NULL
.ELSEIF eax==WM_DESTROY
invoke PostQuitMessage, NULL
.ELSE
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
.ENDIF
ret
WndProc endp
这段代码在窗口初始化时显示程序名称。也要注意我加入了WM_DESTROY消息的处理。这条消息在窗口将要关闭的时候发送。程序要用PostQuitMessage作出反应。
现在看看最终的代码:
.486
.model flat, stdcall
option casemap:none
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\gdi32.lib
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\gdi32.inc
WinMain PROTO STDCALL WORD, WORD, WORD, WORD
WndProc PROTO STDCALL WORD, WORD, WORD, WORD
.data?
hInstance dd ?
.data
ClassName db "FirstWindowClass",0
AppName db "FirstWindow",0
.code
start:
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke WinMain, hInstance, NULL, NULL, SW_SHOWNORMAL
invoke ExitProcess, NULL
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShowWORD
LOCAL wc:WNDCLASSEX
LOCAL hwndWORD
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon, eax
mov wc.hIconSm, eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW-WS_SIZEBOX-WS_MAXIMIZEBOX,CW_USEDEFAULT,\
CW_USEDEFAULT,400,300,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp
WndProc proc hWndWORD, uMsgWORD, wParamWORD, lParamWORD
mov eax, uMsg
.IF eax==WM_CREATE
invoke MessageBox, NULL, ADDR AppName, ADDR AppName, NULL
.ELSEIF eax==WM_DESTROY
invoke PostQuitMessage, NULL
.ELSE
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
.ENDIF
ret
WndProc endp
end start