Win32Asm 教程(一)
前一章 目录 后一章 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写程序简单多了。你将在下一章中学习更多关于这方面的知识。 Win32Asm 教程 前一章 目录 后一章 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写程序简单多了。你将在下一章中学习更多关于这方面的知识。 Win32Asm 教程 前一章 目录 后一章 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)指针 Win32Asm 教程 前一章 目录 下一章 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 位的最大优点。所以,你现在可以忘了段寄存器并把精神集中在其他的寄存器上。 Win32Asm 教程 前一章 目录 下一章 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 Win32Asm 教程 前一章 目录 下一章 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部分被汇编(翻译为原始代码)并放入可执行文件中去(当然载入后在内存中) 。 Win32Asm 教程 前一章 目录 下一章 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。 Win32Asm 教程 前一章 目录 下一章 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大。 只要记住这点: 一个数字是有符号还是无符号取决于你怎样对待这个数。 Win32Asm 教程 前一章 目录 下一章 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函数也是如此。但然,你可以在你的过程使用其他的寄存器,但这是标 准。