Win32Asm 教程
这是我的Win32Asm教程。它总是创建中,但我会不停地添加内容。通过上面的next和prev链接,你可以转到后面和前面一页。
介绍
先来对这个教程做个小介绍。Win32Asm不是一个非常流行的编程语言,而且只有为数不多(但很好)的教程。大多数教程都集中在编程的win32部分(例如,winAPI,使用标准Windows编程技术等),而不是汇编语言本身,例如伪代码(opcodes),寄存器(registers)的使用等。虽然你能在其他教程中找到这些,但那些教程通常是解释Dos编程的。它当然可以帮你学汇编语言,但在Windows中编程,你不再需要了解Dos中断(interrupt)和断口(port)In/our函数。在Window中,WindowsAPI提供了你可在你的程序中使用的标准功能(function),后面还会对此有更多内容。这份教程的目标是在解释用汇编编Win32程序的同时学习汇编语言本身。
1.0汇编语言
汇编是创造出来代替原始的由处理器理解的二进制代码的。很久以前,但是尚没有任何高级语言,程序是用汇编写的。汇编代码直接描述处理器可以执行的代码,例如:
add eax,edx
这条指令-add-把两个值加到一起。Eax和edx被称为寄存器,它们可以保存值在处理器内部。这条代码被转换为66 03 c2(16进制)。处理器读这行代码,并执行它所代表的指令,像C等高级语言把它们自己的语言翻译为汇编语言,而汇编程序又把它转换为二进制代码:
C 代码>> C编译器 > >汇编语言>>汇编器>>原始输出(十六进制)
a = a + b;add eax, edx66 03 C2
(注意该处的汇编语言的代码被简化了,实际输出决定于C代码的上下文)
1.1-为什么?(Why?)
既然用Asm写程序更困难,为什么你用Asm而不是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中。而且,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]
链接器
我的选择:微软附加链接器(link.exe)
网址:win32asm.cjb.net(在win32asm包中)
描述:链接器把对象(object)文件和库文件(用与DLL导入)“链接”到一起输出最终的可执行文件。
关于:我会用Iczelion的Win32asm包中的link.exe。但大多数的链接器都可以用。
供选择:Tasm linker[dl]
资源编辑器
我的选择:Borland资源编辑器
网址:www.crackstore.com
描述:用于创建资源(图形,对话框,位图,菜单等)的资源编辑器。
关于:大多数的编辑器都可以。我个人爱好是resource workshop但你可以用你喜欢的。注意由于resource workshop创建的资源文件有时给资源编译带来麻烦,如果你想使用这个编辑器,你应当把tasm一起下下来,他里面包含了用于编译borland风格资源的brc32.exe。
供选择:Symantec资源编辑器,资源创建者(builder)等等
文本编辑器
我的选择:ultraedit
网址:www.ultraedit.com
描述:一个文本编辑器需要说明吗?
关于:文本编辑器的选这是十分个人的。我非常喜欢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(函数列表视图)。
在某个地方创建一个win32文件夹(或其他你喜欢的名字),并为你创建的每一个工程创建一个子文件夹。
3.0 asm基础知识
这章将教你汇编语言的基础知识
3.1伪代码(opcodes)
汇编程序是用伪代码创建的。一个伪代码是一条处理器可以理解的指令。例如:
ADD
Add指令把两个数加到一起。大部分伪代码有参数
ADD eax, edx
ADD有两个参数。在加法的情况下,一个源一个目标。它把源值加到目标值中,并把结果保存在目标中。参数有很多不同的类型:寄存器,内存地址,直接数值(immediate values)如下:
3.2寄存器
有几种大小的寄存器:8位,16位,32位(在MMX处理器中有更多)。在16位程序中,你仅能使用16位和8位的寄存器。在32位的程序中,你可以使用32位的寄存器。
一些寄存器是别的寄存器的一部分:例如,如果EAX保存了值EA7823BBh这里是其他寄存器的值。
EAXEA7823BB
AXEA7823BB
AHEA7823BB
ALEA7823BB
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, 12345678hMov把一个值载入寄存器(注意: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)指针
译者taowen
4.0内存
这部分将解释在Windows中内存是如何管理的。
4.1Dos和win3.xx
在像用于Dos和Win3.xx的16位程序中,内存被分成许多个段。这些段的大小为64kb。为了存储内存,需要一个段指针和一个偏移址指针。段指针标明要使用哪个段,offset指针标明在段本身的位置。看下图:
内存
段 1 (64kb)段 2 (64kb)段 3 (64kb)段 4(64kb)更多
注意下面关于16位程序的解释,后面有更多关于32位的(但不要跳过这部分,要理解32位的内存管理,这部分很重要)上表是全部的那内存,被划分成了64kb的多个段。最多有65536个段。现在取出一段:
段 1(64kb)
Offset 1Offset 2Offset 3Offset 4Offset 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保存了当前栈的offset1,在ss(栈段中)。
4.2 32位Windows
你可能已经注意到了关于段的一切是乏味的。在16位编程中,段是必不可少的。幸运的是,这个问题已经在32位Windows(95及以上)中得到解决。你仍然有段,但不用管他们了因为它们不再是64kb,而是4GB。你如果尝试着改变段寄存器中的一个,windows甚至会崩溃。这称为平坦(flat)内存模式。只有offset而且是32位的,因而范围从0到4,294,967,295。内存中的每一个地址都是用offset表示的。这真是32位胜于16位的最大优点。所以,你现在可以忘了段寄存器并把精神集中在其他的寄存器上。
译者taowen
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中,前一章中有更多信息)你也能从地址的某一个地方获得一个值并把它放入一个寄存器中。下面有一个例子:
offset3435363738393A3B3C3D3E3F404142
data0D0A50324457257A5E72EF7DFFADC7
每一个块代表一个字节
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 目标,源
执行的运算是 目标=目标+源。下面的格式是允许的。
目标源例子
RegisterRegisteradd ecx, edx
RegisterMemoryadd ecx, dword ptr [104h] / add ecx, [edx]
RegisterImmediate valueadd eax, 102
MemoryImmediate valueadd dword ptr [401231h], 80
MemoryRegisteradd 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 / sourceALAH
WORD (16-bits)dx:ax* / sourceAXDX
DWORD (32-bits)edx:eax* / sourceEAXEDX
*:例如,如果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放入目标位中。
指令ANDORXORNOT
源位00110011001101
目标位010101010101XX
输出位00010111011010
如果源和目标均为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 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1 Bit 0
原来 10011011
循环移动,计数=3 100110 1 1 (移出)
结果 01110011
如你在上图所见,位循环了。注意,每个被推出的位又移到了另一边。和Shifting一样,carry位装有最后被移出的位。Rcl和Rcr实际上和Rol,Rcr一样。它们的名字暗示了它们用carry位来表明最后移出的位,但和Rol和Ror干同样的事情。它们没有什么不同。
交换
XCHG指令也非常简单。它同在两个寄存器和内存地址之间交换:
eax = 237h
ecx = 978h
xchg eax, ecx
eax = 978h
ecx = 237h
Tags:
作者:佚名评论内容只代表网友观点,与本站立场无关!
评论摘要(共 0 条,得分 0 分,平均 0 分)
查看完整评论