windows下32位汇编语言学习笔记 第三章  使用MASM

本章讲述的是masm 汇编的程序结构,基本语法,定义等,本章这些内容只是汇编指令里比较常用的,在下面的章节将要用到的指令。实际上汇编指令远不止这些。感兴趣可以参照其他的汇编书籍了解一下。不过对于本书下面的章节来说,这些指令基本上够用了。


Win32汇编程序的基本结构

从例子可以看出来,Win32汇编的结构很简单,下面简单分析下。

模式定义
.386
.model falt,stdcall
option casemap:none

这个地方书上已经将的很清楚了。关于.386 .486 .586 .686 之类的指令集,我没找到资料,试验了一下写成.686也没什么问题。

include includelib语句
include windows.inc
includelib kernel32.lib

这里的include 和C语言里的include 头文件一个道理,都是导入预先声明好的函数,包括定义好的各种结构。

includelib 就是指定连接的时候告诉连接器从那个lib里找你通过include引入并使用的函数,win32API都是以动态链接库的形式提供的,所以这里就需要对你使用的winAPI包含在那个dll里做到心中有数,不知道的就查msdn,每个API说明后面都有这个API包含在那个头文件中,比如:
Header: Declared in Winuser.h; include Windows.h. 
winAPI是C语言写的,所以头文件都是.h的,汇编的头文件声明是.inc的,打开kernel32.inc 找找Exitprocess 的申明 ExitProcess PROTO :DWORD
你也可以不用预定义的.inc头文件,自己定义。

如果你使用了函数确没有包含对应的.lib,比如使用了ExitProcess函数,没有includelib kernel32.lib,连接时就会报错:
error LNK2001: 无法解析的外部符号 __imp__ExitProcess@4
这个外部符号名就是你要调用的函数,名字很诡异吧,这里先有个了解,讲到调用约定的时候再详细说明。


段定义,程序结束和入口.
.data ;全局变量段
szTest    db '消息窗内容',0
szCaption db '消息窗标题',0
.code  代码段
start: 
  invoke  MessageBox,NULL,offset szText,offset szCaption,MB_OK
end start

这里的段就是PE格式里的Section (区块),一个PE文件(可执行)最少包含2个区块,代码块和数据块,区块的名只是方面记忆,对于系统来说是无关紧要的。区块是按内存页对齐的(0x1000 4K)。区块的类型很多,比如.idata包含导入表,.rsrc,包含资源文件等等。但是,任何时候不要通过区块名字来定位区块,从PE结构的IMAGE_SECTION_HEADER来定位区块才是正确的做法,因为区块名字是可以任意的。关于PE结构的说明,我见过的最详细的就是“加密与解密第三版”第10章的介绍,大家可以去看看。

编译本章的hello.asm 用OD打开exe可以看见有3个区段,.text .rdata 和 .data ,.text 就是代码里的.code段,.data就是代码里的.data段,.rdata没人定义怎么自己冒出来了,其实这就是hello.exe的导入表,因为程序里用到了2个外部dll函数,MessageBox,ExitProcess。这个段就是编译器自动生成的。至于为什么叫.rdata,刚才说了,名字不是重要的,只是帮助记忆,导入表区段有的名字可能就是.idata。

另外还需要注意,程序的入口必须自己指定,汇编里没有Main这样的程序执行起点,这点别忘了。

变量名,变量,数据结构
这个地方没啥好说的,多看,多写,慢慢就习惯了,值得注意的地方就是,变量的命名方式一定要按照后面代码风格所说,按照匈牙利表示法来命名,从一开始就养成一个好习惯。

子程序,函数的定义和使用

调用约定和名称修饰符

除了书上将的_cdecl,_stdcall等,还有一种c++builder里常用的_fastcall调用,__fastcall调用也是被调用的函数负责清栈,参数的传递规则是,从左边开始不大于4字节的参数分别用edx,ecx传递,其他参数遵循从又右到左的顺序通过堆栈传递。

c c++在内部是通过函数修饰符来识别函数的,由编译器在编译时生成函数名称修饰符,而且,不同的调用约定不同的语言生成的修饰符定义名称不同,所以有必要了解一下函数的名称修饰符。

例子函数:int max(int,int);

对于C语言
_cdecl调用
名称修饰符是在函数前加一个下划线:_max
_fastcall调用
名称修饰符在函数前加一个@后面加一个@紧跟参数字节数:@max@8
_stdcall调用
名称修饰符在函数前加一个_后面加一个@紧跟参数字节数:_max@8

对于C++语言,不管任何调用约定,描述符都以?开头后边更函数名,然后是根据参数表查出的返回值类型,然后是参数类型,最有以@Z结束
?+函数名+调用规则名+返回类型+参数类型(从左到右)+@Z

其中调用规则名表:_cdecl:@@YA,_stdcall:@@YG,_fastcall:YI

标示符:参数类型
X:void,D:char,E:unsigned char,F:short,H:int,I:unsigned int,J:long,K:unsigned long,M:float,N:double,_N:bool,U:Struct
指针:PA,const指针:PB
对于max函数修饰名称就是:?max@@Y?HHH@Z。这里给了个问号,意思就是不同的调用规则就更具调用规则表变化,其他不变。
很明显,C++的修饰更为详细。
现在回过头看看刚才的错误提示:error LNK2001: 无法解析的外部符号 __imp__ExitProcess@4 
__imp_ 这个是代表函数ExitProcess是从外部导入的,后面的_ExitProcess@4很明显参数是四字节的和ExitProcess(UNIT uExitCode)相符

实际上对于C++的类成员函数,描述符的规则又有不同,但是,如果你写的DLL动态链接库使用自定义类,估计没人会用的,使用类了就不能通用了。


MASM的优化

都知道汇编效率高,但是MASM编译出的EXE真的就是最佳优化的么?让我们看看本章中的hello.exe 用OD反汇编看看是不是这样。
反汇编内容:
00011000 >/$  6A 00         PUSH    0                                ; /Style = MB_OK|MB_APPLMODAL
00011002  |.  68 00300100   PUSH    Hello.00013000                   ; |Title = "A MessageBox !"
00011007  |.  68 0F300100   PUSH    Hello.0001300F                   ; |Text = "Hello, World !"
0001100C  |.  6A 00         PUSH    0                                ; |hOwner = NULL
0001100E  |.  E8 07000000   CALL    <JMP.&user32.MessageBoxA>        ; \MessageBoxA
00011013  |.  6A 00         PUSH    0                                ; /ExitCode = 0
00011015  \.  E8 06000000   CALL    <JMP.&kernel32.ExitProcess>      ; \ExitProcess
0001101A   $- FF25 08200100 JMP     NEAR DWORD PTR DS:[<&user32.Mess>;  user32.MessageBoxA
00011020   .- FF25 00200100 JMP     NEAR DWORD PTR DS:[<&kernel32.Ex>;  kernel32.ExitProcess

看看那2个CALL,一个调用MessageBoxA,一个调用ExitProcess,这个JMP产生了额外的代码,并且增加执行时间,产生这样的代码是因为编译器不知道你调用的函数是从外部导入的。如果编译器预先知道这个函数是从外部引入的,编译器就会把CALL后面的地址直接指向,PE文件的IAT(import_address_table)输入表中的函数地址,当程序运行时由系统加载器更新IAT表(如果需要的话),这样就调用了函数在DLL中的正确地址,避免了这种低效能的调用方式。

高级语言,比如C语言在引入外部DLL函数时,再dll头文件里对于每一个函数都有一个描述 __declspec(dllimport),这就是告诉编译器,这个函数是从外部引入的,从而提高空间和时间效率。
看看C写的,功能呢个同样的代码,编译后的反汇编内容:

00401000  /$  6A 00         PUSH    0                                               ; /Style = MB_OK|MB_APPLMODAL
00401002  |.  68 00304000   PUSH    HelloMsg.00403000                               ; |Title = "HelloMsg"
00401007  |.  68 0C304000   PUSH    HelloMsg.0040300C                               ; |Text = "Hello, Windows 98!"
0040100C  |.  6A 00         PUSH    0                                               ; |hOwner = NULL
0040100E  |.  FF15 AC204000 CALL    NEAR DWORD PTR DS:[<&USER32.MessageBoxA>]       ; \MessageBoxA
00401014  |.  33C0          XOR     EAX, EAX
00401016  \.  C2 1000       RETN    10

这个MessageBoxA的CALL才是效率最高的call!

但是悲剧的是在masm里我们无法用任何描述告诉编译器,当前使用的函数是从外部引入的。结果就是使用效率最高的语言确产生了效率最低的外部函数调用...


有没有办法解决,确实有,我google了一下,发现了一段代码。

比如我们调用ExitProcess函数,可以预先这样写

PROTO@4 TYPEDEF PROTO STDCALL :DWORD                   ;定义一个新的类型proto@4
EXTERNDEF STDCALL _imp__ExitProcess@4:PTR PROTO@4  ;定义一个外部变量,类型为上面定义的类型     
ExitProcess EQU <_imp__ExitProcess@4>                  ;定义一个符号ExitProcess 

把上面3行代码加到 模式定义后面,注释掉include 'kernel32.inc',重新编译,现在看反汇编的内容:

00011000 >/$  6A 00         PUSH    0                                ; /Style = MB_OK|MB_APPLMODAL
00011002  |.  68 00300100   PUSH    Hello.00013000                   ; |Title = "A MessageBox !"
00011007  |.  68 0F300100   PUSH    Hello.0001300F                   ; |Text = "Hello, World !"
0001100C  |.  6A 00         PUSH    0                                ; |hOwner = NULL
0001100E  |.  E8 09000000   CALL    <JMP.&user32.MessageBoxA>        ; \MessageBoxA
00011013  |.  6A 00         PUSH    0                                ; /ExitCode = 0
00011015  \.  FF15 00200100 CALL    NEAR DWORD PTR DS:[<&kernel32.Ex>; \ExitProcess
0001101B      CC            INT3
0001101C   $- FF25 08200100 JMP     NEAR DWORD PTR DS:[<&user32.Mess>;  user32.MessageBoxA

看见没ExitProcess的调用汇编代码成了最佳调用了。



新加的着3行代码,我也是网上抄下来的,请高手看见的帮忙解释下。

我还发现了一个网站 http://www.japheth.de/JWasm.html 这个网站提供了一套自己修改过的.inc文件,而且使用整个代码里只需要include 他们的windows.inc文件。
编译生成后就是优化了的call代码。

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    .686
    .model flat,stdcall
    option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include  windows.inc
includelib  user32.lib
includelib  kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    .data
szCaption  db  'A MessageBox !',0
szText    db  'Hello, World !',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    .code
start:
    invoke  MessageBox,NULL,offset szText,offset szCaption,MB_OK
    invoke  ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    end  start

编译后的反汇编:
00401000 >/$  6A 00         PUSH    0                                ; /Style = MB_OK|MB_APPLMODAL
00401002  |.  68 00304000   PUSH    Hello.00403000                   ; |Title = "A MessageBox !"
00401007  |.  68 0F304000   PUSH    Hello.0040300F                   ; |Text = "Hello, World !"
0040100C  |.  6A 00         PUSH    0                                ; |hOwner = NULL
0040100E  |.  FF15 08204000 CALL    NEAR DWORD PTR DS:[<&user32.Mess>; \MessageBoxA
00401014  |.  6A 00         PUSH    0                                ; /ExitCode = 0
00401016  \.  FF15 00204000 CALL    NEAR DWORD PTR DS:[<&kernel32.Ex>; \ExitProcess

2个call完全优化了。

不过要注意,用他们这个inc,不能使用vs2008自带的ml否则编译就报错,可以用RadASM里自带的masm 编译。之需要把参数/I 指向下载的win32inc的include就可以了。具体功能怎么实现的,我是不理解,还得请高手们帮忙看看。



星期日, 五月 03, 2009