好友
阅读权限 255
听众
最后登录 1970-1-1
zzage
发表于 2009-5-11 11:31
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)。区块的类型很多,比如.IDA ta包含导入表,.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