注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

★ ★ ★ 卡多 - K.D

DELPHI

 
 
 

日志

 
 

第一课 经典汇编教程Win32Asm教程  

2011-01-02 12:34:29|  分类: ‖ 杂文 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

导言 
先来对这个教程做个小小的介绍。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 

网址:www.crackstore.com

描述:用于创建资源(图形,对话框,位图,菜单等)的资源编辑器。 

关于:大多数的编辑器都行。我个人爱好是resource workshop但你可以用你喜欢的。注意由于resource workshop创建的资源文件有时给资源编译带来麻烦,如果你想使用这个编辑器,你应当把tasm一起下下来,他里面包含了用于编译borland式资源的brc32.exe。 

供选择:Symantec资源编辑器,Resource 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(函数列表视图)。 

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 

 

  评论这张
 
阅读(132)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017