版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第3章使用MASM经过上一讲的准备工作,相信大家已经搭建好了Win32汇编的工作环境,并已经知道编译、链接一个程序的过程和原理了。现在,我们让例子回归到经典:#include<stdio.h>int
main(void){Printf(“Hello,world\n”);}//事实上想想,这不正是初生的婴儿?!3.1Win32汇编源程序的结构麻雀虽小,五脏俱全。刚刚那个C语言的”Hello,world”程序包含了C语言中的最基本的格式。在C语言的源程序中,我们不需要为堆栈段、数据段和代码3.1.2段的定义而烦恼,编译器会自己解决。回顾一下,在DOS下的汇编这段代码会变成什么样?Followme!在例子中我们看到,stack、data、code都找到了自己的小窝。3.1Win32汇编源程序的结构回归主题,在Win32汇编语言下,小麻雀”HelloWorld”又会变成什么样子呢?Followme!是不是又不同了?但是,我们怎么就发觉Win32汇编其实是前边两种形态的集大成者?!接下来,大家逐段来理解和接受这个新先的语言!3.1.1模式定义程序的第一部分是模式和源程序格式的定义语句.386.modelflat,stdcalloptioncasemap:none这些指令定义了程序使用的指令集、工作模式和格式。3.1.1模式定义1)指定使用的指令集.386语句是汇编语句的伪指令,类似的指令还有:.8086、.186、.286、.386/.386p、.486/.486p和.586/.586p等,用于告诉编译器在本程序中使用的指令集。在DOS的汇编中默认使用的是8086指令集,那时候如果在源程序中写入80386所特有的指令或使用32位的寄存器就会报错。Win32环境工作在80386及以上的处理器中,所以这一句.386是必不可少的。3.1.1模式定义另外,后面带p的伪指令则表示程序中可以使用特权指令,如:movcr0,eax这一类指令必须在特权级0上运行,如果只指定.386,那么使用普通的指令是可以的,编译时到这一句就会报错。如果我们要写的程序是VxD等驱动程序,中间要用到特权指令,那么必须定义.386p,在应用程序级别的Win32编程中,程序都是运行在优先级3上,不会用到特权指令,只需定义.386就够了。3.1.1模式定义80486和Pentium处理器指令是80386处理器指令的超集,同样道理,如果程序中要用80486处理器或Pentium处理器的指令,则必须定义.486或.586。另外,Intel公司的80x86系列处理器从PentiumMMX开始增加了MMX指令集,为了使用MMX指令,除了定义.586之外,还要加上一句.mmx伪指令:.386.mmx3.1.1模式定义2)model语句.model语句在低版本的宏汇编中已经存在,用来定义程序工作的模式,它的使用方法是:.model内存模式[,语言模式][,其他模式]内存模式的定义影响最后生成的可执行文件,可执行文件的规模从小到大,可以有很多种类型。详见下表:3.1.1模式定义内存模式模式内存使用方式tiny用来建立.com文件,所有的代码、数据和堆栈都在同一个64KB段内small建立代码和数据分别用一个64KB段的.exe文件medium2.代码段可以有多个64KB段,数据段只有一个64KB段compact2.代码段只有一个64KB,数据段可以有多个64KB段large2.代码段和数据段都可以有多个64KB段huge同large,并且数据段中的一个数组也可以超过64KBflatWin32程序使用的模式,代码和数据使用同一个4GB段3.1.1模式定义Windows
程序运行在保护模式下,系统把每一个Win32应用程序都放到分开的虚拟地址空间中去运行,也就是说,每一个应用程序都拥有其相互独立的4GB地址空间。对Win32程序来说,只有一种内存模式,即flat(平坦)模式,意思是内存是很平坦地从0延伸到4GB,再没有64KB段大小限制。3.1.1模式定义对比一下DOS的HelloWorld和Win32的HelloWorld开始部分的不同,DOS程序中有这样语句
movax,data
movds,ax意思是把数据段寄存器DS指向data数据段,data数据段在前面已经用datasegment语句定义,只要DS不重新设置,那么从此以后指令中涉及的数据默认将从data数据段中取得。3.1.1模式定义所以下面的语句是从data数据段取出szHello字符串的地址后再显示:movah,9movdx,offsetszHelloint21h纵观Win32汇编的源程序,没有一处可以找到ds或es等段寄存器的使用,因为所有的4GB空间用32位的寄存器全部都能访问到了,不必在头脑中随时记着当前使用的是哪个数据段,这就是平坦内存模式带来的好处。3.1.1模式定义如果定义了.modelflat,MASM自动为各种段寄存器做了如下定义:ASSUMEcs:FLAT,ds:FLAT,ss:FLAT,es:FLAT,fs:ERROR,gs:ERROR也就是说,CS,DS,SS和ES段全部使用平坦模式,FS和GS寄存默认不使用,这时若在源程序中使用FS或GS,在编译时会报错。如果有必要使用它们,只需在使用前用下面的语句声明一下就可以了:assumefs:nothing,gs:nothing或者assumefs:flat,gs:flat3.1.1模式定义在Win32汇编中,.model语句中还应该指定语言模式,即子程序和调用方式,例子中用的是stdcall,它指出了调用子程序或Win32API时参数传递的次序和堆栈平衡的方法。相对于stdcall,不同的语言类型还有C,SysCall,BASIC,FORTRAN和PASCALL,虽然各种高级语言在调用子程序时都是使用堆栈来传递参数。Windows的API调用使用是的stdcall格式,所以在Win32汇编中没有选择,必须在.model中加上stdcall参数。话题:理解stdcall和cdecl(1)_stdcall调用_stdcall是Pascal程序的缺省调用方式,参数采用从右到左的压栈方式,被调函数自身在返回前清空堆栈。WIN32Api都采用_stdcall调用方式。(2)
_cdecl调用_cdecl是C/C++的缺省调用方式,参数采用从右到左的压栈方式,传送参数的内存栈由调用者维护。_cedcl约定的函数只能被C/C++调用,每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。3.1.1模式定义3)option语句如例子中,我们定义了optioncasemap:none的意义是告诉编译器程序中的变量名和子程序名是否对大小写敏感。由于Win32API的API函数名称本质是区分大小写的,所以必须指定这个选项,否则调用API函数就会出现问题。3.1.2段的定义把上面的Win32的HelloWorld源程序中的语句归纳精简一下,再列在下面:
.386.modelflat,stdcalloptioncasemap:none<一些include语句>.data<一些字符串、变量定义>.code<代码><开始标号><其他语句>end开始标号3.1.2段的定义模式定义中的模式、选项等定义并不会在编译好的可执行程序中产生什么东西,它们只是说明。而真正的数据和代码是定义在各个段中的,如上面的.data段和.code段,考虑到不同的数据类型,还可以有其他种类的数据段,下面是包含全部段的源程序结构:3.1.2段的定义
.386
.modelflat,stdcall
optioncasemap:none
<一些include语句>
.stack[堆栈段的大小]
.data
<一些初始化过的变量定义>
.data?
<一些没有初始化过的变量定义>
.const
<一些常量定义>
.code
<代码>
<开始标号>
<其他语句>
end
开始标号3.1.2段的定义.stack、.data、.data?、.const和.code是分段伪指令,Win32中实际上只有代码和数据之分,.data,.data?和.const这些都是指向数据段,.code是指向2.代码段。和DOS汇编不同,Win32汇编不必考虑堆栈,系统会为程序分配一个向下扩展的、足够大的段作为堆栈段,所以.stack段定义常常被忽略。解决之前提出的问题前面我们不是说过Win32环境下不用段了吗?是的,这些“段”,实际上并不是DOS汇编中那种意义的段,而是内存的“分段”。上一个段的结束就是下一个段的开始,所有的分段,合起来,包括系统使用的地址空间,就组成了整个可以寻址的4GB空间。我们接着往下看会更加容易理解。解决之前提出的问题Win32汇编的内存管理使用了80386处理器的分页机制,每个页(4KB大小)可以自由指定属性,所以上一个4KB可能是代码,属性是可执行但不可写,下一个4KB就有可能是既可读也可写但不可执行的数据。再下面呢?有可能是可读不可写也不可执行的数据。(主要就看你放的是什么东西……)嘿嘿,大家是否有点理解了?没关系,接着往下!解决之前提出的问题Win32汇编源程序中“分段”的概念实际上是把不同类型的数据或代码归类,再放到不同属性的内存页(也就是不同的“分段”)中,这中间不涉及使用不同的段选择器。(仅仅是配合分页机制搞捣鼓~)虽然使用和DOS汇编同样的.code和.data语句来定义,意思可是完全不同了!至此,相信大家和小甲鱼一样清晰啦,感谢老师,感谢拉登,感谢嫦娥^_^2.数据段.data、.data?和.const定义的是数据段,分别对应不同方式的数据定义,在最后生成的可执行文件中也分别放在不同的节区(Section)中。(这个在我们讲解PE结构的时候会很细致描述)程序中的数据定义一段可以归纳为3类:第一类是可读可写的已定义变量。第二类是可读可写的未定义变量。第三类数据是一些常量。(1)可读可写的已定义变量这些数据在源程序中已经被定义了初始值,而且在程序的执行中有可能被更改。如一些标志等,这些数据必须定义在.data段中,.data段是已初始化数据段,其中定义的数据是可读可写的,在程序装入完成的时候,这些值就已经在内存中了,.data段存放在可执行文件的_DATA节区内。(2)可读可写的未定义变量这些变量一般是当做缓冲区或者在程序执行后才开始使用的,这些数据可以定义在.data段中,也可以定义在.data?段中,但一般把它放到.data?段中。虽然定义在这两种段中都可以正常使用,但定义在.data?段中不会增大.exe文件的大小。举例说明,如果要用到一个100KB的缓冲区,可以在数据段中定义:
szBuffer
db
100*1024dup(?)(2)可读可写的未定义变量如果放在.data段中,编译器认为这些数据在程序装入时就必须有效,所以它在生成可执行文件的时候保留了所有的100KB的内容,即使它们是全零!如果程序其他部分的大小是50KB,那么最后的.exe文件就会是150KB大小,如果缓冲区定义为1MB,那么.exe文件会增大到1050KB。(2)可读可写的未定义变量.data?段则不同,其中的内容编译器会认为程序在开始执行后才会用到,所以在生成可执行文件的时候只保留了大小信息,不会为它浪费磁盘空间。和上面同样的情况下,即使缓冲区定义为1MB,可执行文件同样只有50KB!总之,.data?段是未初始化数据段,其中的数据也是可读可写的,但在可执行文件中不占空间,.data?段在可执行文件中存放在_BSS节区中。(3)数据是一些常量如一些要显示的字符串信息,它们在程序装入的时候也已经有效,但在整个执行过程中不需要修改,这些数据可以放在.const段中,.const段是常量段,它是可读不可写的。一般为了方便起见,在小程序中常常把常量一起定义到.data段中,而不另外定义一个.const段。在程序中如果不小心写了对.const段中的数据做写操作的指令,会引起保护错误,Windows会显示一个提示框并结束程序。3.代码段.code段是代码段,所有的指令都必须写在代码段中,在可执行文件中,代码段是放在_TEXT节区(区块)中的。Win32环境中的数据段是不可执行的,只有代码段有可执行的属性。对于工作在特权级3的应用程序来说,.code段是不可写的,在编写DOS汇编程序的时候,我们可以为非作歹,如果企图在Win32汇编下做同样的事情,结果就是和上面同样
“非法操作”!3.代码段当然事物总有两面性,在Windows95下,在特权级0下运行的程序对所有的段都有读写的权利,包括代码段。另外,在优先级3下运行的程序也不是一定不能写代码段,代码段的属性是由可执行文件PE头部中的属性位决定的。通过编辑磁盘上的.exe文件,把代码段属性位改成可写,那么在程序中就允许修改自己的代码段。3.代码段一个典型的应用就是一些针对可执行文件的压缩软件和加壳软件,如Upx和PeCompact等。这些软件靠把代码段进行变换来达到解压缩和解密的目的,被处理过的可执行文件在执行时需要由解压代码来将代码段解压缩。这就需要写代码段,所以这些软件对可执行文件代码段的属性预先做修改。3.代码段为了带大家更好认识这些花花绿绿的“段”到底是什么回事,小甲鱼带大家看一张图……/home.php3.1.3程序结束和程序入口在C语言源程序中,程序不必显式地指定程序由哪里开始执行,编译器已经约定好从main()函数开始执行了。而在汇编程序中,并没有一个main函数,程序员可以指定从2.代码段的任何一个地方开始执行,这个地方由程序最后一句的end语句来指定:end[开始地址]这句语句同时表示源程序结束,所有的代码必须在end语句之前。3.1.3程序结束和程序入口
endstart上述语句指定程序从start这个标号开始执行。当然,start标号必须在程序的2.代码段中有所定义。但是,一个源程序不必非要指定入口标号,这时候可以把开始地址忽略不写,这种情况发生在编写多模块程序的单个模块的时候。3.1.3程序结束和程序入口当分开写多个程序模块时,每个模块的源程序中也可以包括.data、.data?、.const和.code段,结构就和上面的Win32HelloWorld一样,只是其他模块最后的end语句必须不带开始地址。当最后把多个模块链接在一起的时候,只能有一个主模块指定入口地址,在多个模块中指定入口地址或者没有一个模块指定了入口地址,链接程序都会报错。3.1.4注释和换行注释是源程序中不可忽略的一部分,汇编源程序的注释以分号(;)开始,注释既可以在一行的头部,也可以在一行的中间,一行中所有在分号之后的字符全部当做注释处理,但在字符串的字义中包含的引号内的分号不当做是注释的开始。
;这里是注释
call
_PrintChar
;这里是注释
szChar
db
‘Hello,world;’,0dh,0ah
3.1.4注释和换行当源程序的某一行过长,不利于阅读的时候,可以分行书写,分行的办法是在一行的最后用反斜杠(\)做换行符,如:
invokeMessageBox,NULL,offsetszText,offsetszCaption,MB_OK
可以写为:
invokeMessageBox,\
NULL,\
;父窗口句柄
offsetszText,\
;消息框中的文字
offsetszCaption,\
;标题文字
MB_OK
3.2调用API函数首先,API是什么?答:Win32程序是构筑在Win32API基础上的。在Win32API中,包括了大量的函数、结构和消息等,它不仅为应用程序所调用,也是Windows自身的一部分,Windows自身的运行也调用这些API函数。在DOS下,操作系统的功能是通过各种软中断来实现的,如大家都知道int21h是DOS中断,int13h和int10h是BIOS中的磁盘中断和视频中断。3.2调用API函数当应用程序要引用系统功能时,要把相应的参数放在各个寄存器中再调用相应的中断,程序控制权转到中断中去执行,完成以后会通过iret中断返回指令回到应用程序中。DOS汇编下的HelloWorld程序中有下列语句:movah,9movdx,offsetszHelloint21h3.2调用API函数解释:这3条语句调用DOS系统模块中的屏幕显示功能,功能号放在ah中,9号功能表示屏幕显示,要输出到屏幕上的内容的地址放在dx中,然后去调用int21h,字符串就会显示到屏幕上。这个例子说明了应用程序调用系统功能的一般过程。首先,系统提供功能模块并约定参数的定义方法,同时约定调用的方式,同时约定调用的方式,应用程序按照这个约定来调用系统功能。3.2调用API函数在这里,ah中放功能号9,dx中放字符串地址就是约定的参数,int21h是约定的调用方式。下面来看看这种方法的不便这处。首先,所有的功能号定义是冷冰冰的数字,int21h的说明文档是这样的:int21h说明文档再进入09号功能看使用方法:
string(Func09)
AH=09h
DS:DX->stringterminatedby“$”3.2调用API函数这就是DOS时代汇编程序员都有一厚本《中断大全》的原因,因为所有的功能编号包括使用的参数定义仅从字面上看,是看不出一点头绪来的。另外,80x86系列处理器能处理的中断最多只能有256个,不同的系统服务程序使用了不同的中断号,这少得可怜的中断数量就显得太少了,结果到最后是中断挂中断,大家抢来抢去的,把好好的一个系统搞得像接力赛跑一样。调用API函数习惯工作于DOS汇编的程序员同志都有一个愿望:如果说,能够以功能名称作为子程序名直接调用,他们愿意以生命中的十年寿命作为交换……随着Win32的到来,他们的愿望实现了!这就是API函数,它事实上就是以一种新的方法代替了DOS下的中断。与DOS中断相比,Win32的系统功能模块放在Windows的动态链接库(DLL)中。调用API函数DLL是一种Windows的可执行文件,采用的是和我们熟悉的.exe文件同样的PE(PortableExecutable)约定格式。我是内存大家可以叫我小内我有2G我是程序A,小A,我有4G!我是程序B,小B,我有4G!我是程序3,小3,我也有4G!我是DLL文件,我是太监总管,他们都靠我才能和内核联系!具体怎么联系?关于DLLDLL事实上只是一个大大的集装箱,装着各种系统的API函数。应用程序在使用的时候由Windows自动载入DLL程序并调用相应的函数。实际上,Win32的基础就是由DLL组成的。Win32API的核心由3个DLL提供,它们是:KERNEL32.DLL——系统服务功能。包括内存管理、任务管理和动态链接等。GDI32.DLL——图形设备接口,处理图形绘制。USER32.DLL——用户接口服务。建立窗口和传送消息等。关于DLL当然,Win32API还包括其他很多函数,这些也是由DLL提供的,不同的DLL提供了不同的系统功能。如使用TCP/IP协议进行网络通信的DLL是Wsock32.dll,它所提供的API称为SocketAPI;专用于电话服务方面的API称为TAPI(TelephonyAPI),包含在Tapi32.dll中,所有的这些DLL提供的函数组成了现在使用的Win32编程环境。我们也经常自己打包自己的“集装箱”!3.2.2调用API--API函数的参数在DOS下,我们演示过无数次,通过中断来调用系统“函数”,其中的“参数”是通过放在寄存器(ah)中。Win32API是用堆栈来传递参数的,调用者把参数一个个压入堆栈,DLL中的函数程序再从堆栈中取出参数处理,并在返回之前将堆栈中已经无用的参数丢弃。在Microsoft发布的《MicrosoftWin32Programmer’sReference》中定义了常用API的参数和函数声明。3.2.2调用API--API函数的参数
intMessageBox(
HWNDhWnd,
//handletoownerwindow
LPCTSTRlpText,
//textinmessagebox
LPCTSTRlpCaption,
//messageboxtitle
UINTuType
//messageboxstyle
);//注意,上边是用C语言表示!调用者调用MessageBox函数hWndlpTextlpCaptionuType栈DLL调用MessageBox函数实现,从栈拿参数3.2.2调用API--API函数的参数上述函数声明说明了MessageBox有4个参数,这些数据类型看起来很复杂,但有一点是很重要的,对于汇编语言来说,Win32环境中的参数实际上只有一种类型,那就是一个32位的整数,所以这些HWND,LPCTSTR和UINT实际上就是汇编中的dword(doubleword,双字型,4个字节,两个字,32位)之所以定义为不同的模样,主要是用来说明了用途。由于Windows是用C写成的,世界上的程序员好像也是用C语言的最多,所以Windows所有编程资料发布的格式也是C格式。3.2.2调用API--API函数的参数上面的声明用汇编的格式来表达就是:MessageBoxProtohWnd:dword,lpText:dword,\lpCaption:dword,uType:dword在汇编中调用MessageBox函数的方法是:
pushuType
pushlpCaption
pushlpText
pushhWnd
callMessageBox3.2.2调用API--API函数的参数在源程序编译链接成可执行文件后,callMessageBox语句中的MessageBox会被换成一个地址,指向可执行文件中的导入表的一个索引(函数名或索引号)。导入表中指向MessageBox函数的实际地址会在程序装入内存的时候,根据User32.dll在内存中的位置由Windows系统动态填入。3.2.2调用API--使用invoke语句API是可以调用了,另一个烦人的问题又出现了,Win32的API动辄就是十几个参数,整个源程序一眼看上去基本上都是把参数压堆栈的push指令,参数的个数和顺序很容易搞错,由此引起的莫名其妙的错误源源不断,源程序的可读性看上去也很差。如果写的时候少写了一句push指令,程序在编译和链接的时候都不会报错,但在执行的时候必定会崩溃,原因是堆栈对不齐了。3.2.2调用API--使用invoke语句有木有解决的办法呢?那是必须得!最好是像C语言一样,能在同一句中打入所有的参数,并在参数使用错误的时候能够提示。好消息又来了,Microsoft终于做了一件好事,在MASM中提供了一个伪指令实现了这个功能,那就是invoke伪指令,它的格式是:invoke
函数名
[,参数1][,参数2]…[,参数n]invokeMessageBox,NULL,offsetszText,\offsetszCaption,MB_OK3.2.2调用API--使用invoke语句注意,invoke并不是80386处理器的指令,而是一个MASM编译器的伪指令,在编译的时候它把上面的指令展开成我们需要的4个push指令和一个call指令,同时,进行参数数量的检查工作,如果带的参数数量和声明时的数量不符,编译器报错:errorA2137:toofewargumentstoINVOKE编译时看到这样的错误报告,首先要检查的是有没有少写一个参数。3.2.2调用API--使用invoke语句对于不带参数的API调用,invoke伪指令的参数检查功能可有可无,所以既可以用callAPI_Name这样的语法,也可以用invokeAPI_Name这样的语法。3.2.2调用API--API函数的返回值有的API函数有返回值,如MessageBox定义的返回值是int类型的数,返回值的类型对汇编程序来说也只有dword一种类型,它永远放在eax中。如果要返回的内容不是一个eax所能容纳的,Win32API采用的方法一般是返回一个指针,或者在调用参数中提供一个缓冲区地址,干脆把数据直接返回到缓冲区中去。3.2.2调用API--函数的声明在调用API函数的时候,函数原型也必须预先声明,否则,编译器会不认这个函数。invoke伪指令也无法检查参数个数。声明函数的格式是:函数名
proto[距离][语言][参数1]:数据类型,[参数2]:数据类型句中的proto是函数声明的伪指令,距离可以是NEAR,FAR,NEAR16,NEAR32,FAR16或FAR32,Win32中只有一个平坦的段,无所谓距离,所以在定义时是忽略的;语言类型就是.model那些类型,如果忽略,则使用.model定义的默认值。3.2.2调用API--函数的声明对Win32汇编来说只存在dword类型的参数,所以所有参数的数据类型永远是dword,另外对于编译器来说,它只关心参数的数量,参数的名称在这里是无用的,仅是为了可读性而设置的,可以省略掉,所以下面两句消息框函数的定义实际上是一样的:MessageBoxProtohWnd:dword,lpText:dword,\lpCaption:dword,uType:dwordMessageBoxProto:dword,:dword,:dword,:dword3.2.2调用API--函数的声明在Win32环境中,和字符串相关的API共有两类,分别对应两个字符集:一类是处理ANSI字符集(1B)的,另一类是处理Unicode字符集(2B)的。前一类函数名字的尾部带一个A字符,处理Unicode的则带一个W字符。我们比较熟悉的ANSI字符串是以NULL结尾的一串字符数组,每一个ANSI字符占一个字节宽。对于欧洲语言体系,ANSI字符集已足够了,但对于有成千上万个不同字符的几种东方语言体系来说,Unicode字符集更有用。3.2.2调用API--函数的声明MessageBox和显示字符串有关,同样它有两个版本,严格地说,系统中有两个定义:
MessageBoxAProtohWnd:dword,lpText:dword,\lpCaption:dword,uType:dword
MessageBoxBProtohWnd:dword,lpText:dword,\lpCaption:dword,uType:dword虽然《MicrosoftWin32Programmer’sReference》中只有一个MessageBox定义,但User32.dll中确确实实没有MessageBox,而只有MessageBoxA和MessageBoxW,那么为什么还是可以使用MessageBox呢?Follow3.2.2调用API--函数的声明由于并不是每个Win32系统都支持W系统的API,例如在Windows9x系列中,对Unicode是不支持的,很多的API只有ANSI版本,只有WindowsNT系列才对Unicode完全支持。为了编写在几个平台中通用的程序,一般应用程序都使用ANSI版本的API函数集。这样的话,为了使程序更有移植性,在源程序中一般不直接指明使用Unicode还是ANSI版本,而是使用宏汇编中的条件汇编功能来统一替换。3.2.2调用API--函数的声明如在源程序中使用MessageBox,但在头文件中定义:if
UNICODE
MessageBox
equ
<MessageBoxW>else
MessageBox
equ
<MessageBoxA>endif所有涉及版本问题的API都可以按此方法定义,然后在源程序的头指定UNICODE=1或UNICODE=0,重新编译后就能产生不同的版本。3.2.2调用API--include语句对于所有要用到的API函数,在程序的开始部分都必须预先声明,但这一个步骤显然是比较麻烦的,为了简化操作,可以采用各种语言通用的解决办法,就是把所有的声明预先放在一个文件中,在用到的时候再用include语句包含进来。现在回到Win32HelloWorld程序,这个程序用到了两个API函数:MessageBox和ExitProcess,它们分别在User32.dll和Kernel32.dll中。3.2.2调用API--include语句在MASM32工具包中已经包括了所有DLL的API函数声明列表,每个DLL对应<DLL名.inc>文件(这些文件就是存放对应的函数声明),在源程序中只要使用include语句包含进来就可以了:
includeuser32.inc
includekernel32.inc当用到其他的API函数时,只需相应增加对应的include语句。3.2.2调用API--include语句编译器对include语句的处理仅是简单地把这一行用指定的文件内容替换掉而而已。include语句的语法是:
include
文件名
或
include<文件名>当遇到要包括的文件名和MASM的关键字同名等可能会引起编译器混淆的情况时,可以用<>将文件名括起来。3.2.2调用API--includelib语句在DOS汇编中,使用中断调用系统功能是不必声明的,处理器自己知道到中断向量表中去取中断地址。在Win32汇编中使用API函数,程序必须知道调用的API函数存在于哪个DLL中,否则,操作系统必须搜索系统中存在的所有DLL,并且无法处理不同DLL中的同名函数,这显然是不现实的,所以,必须有个文件包括DLL库正确的定位信息,这个任务是由导入库来实现的。3.2.2调用API--includelib语句在使用外部函数的时候,DOS下有函数库的概念,那时的函数库实际上是静态库,静态库是一组已经编写好的代码模块,在程序中可以自由引用。在源程序编译成目标文件,最后要链接可执行文件的时候,由link程序从库中找出相应的函数代码,一起链接到最后的可执行文件中。3.2.2调用API--includelib语句DOS下C语言的函数库就是典型的静态库。库的出现为程序员节省了大量的开发时间,缺点就是每个可执行文件中都包括了要用到的相同函数的代码,占用了大量的磁盘空间,在执行的时候,这些代码同样重复占用了宝贵的内存。Win32环境中,程序链接的时候仍然要使用函数库来定位函数信息,只不过由于函数代码放在DLL文件中,库文件中只留有函数的定位信息和参数数目等简单信息,这种库文件叫做导入库。3.2.2调用API--includelib语句一个DLL文件对应一个导入库,如User32.dll文件用于编程的导入库是User32.lib,MASM32工具包中包含了所有DLL的导入库。为了告诉链接程序使用哪个导入库,使用的语句是:
includelib
库文件名
或
includelib
<库文件名>和include的用法一样,在要包括让编译器混淆的文件名时加括号。3.2.2调用API--includelib语句Win32HelloWorld程序用到的两个API函数MessageBox和ExitProcess分别在User32.dll和Kernel32.dll中,那么在源程序使用的相应语句为:
includelib
user32.lib
includelib
kernel32.lib和include语句的处理不同,includelib不会把.lib文件插入到源程序中,它只是告诉链接器在链接的时候到指定的库文件中去找而已。Dll文件中的函数没有包括声明,所以才需要将.inc文件插进去!3.2.3API参数中的等值定义回过头来看显示消息框的语句:invokeMessageBox,NULL,offsetszText,offset\szCaption,MB_OK还是这个函数,不过这次我们关注的焦点有所改变:MB_OK地球人都知道,MB_OK就是使得程序弹出来的时候有个“确定”的选项!我们这次来探索他背后的数字含义。3.2.3API参数中的等值定义回顾一下原型:intMessageBox(
HWNDhWnd, LPCTSTRlpText, LPCTSTRlpCaption, UINTuType);在uType这个参数中使用了MB_OK,这个MB_OK是什么意思?小甲鱼带大家着手来查找文档!实例演练1.MB_OK事实上是12.修改helloworld显示一个问号、一个确定按钮、一个取消按钮3.在以上基础上当按下确定的时候弹出另一个对话框,说”您刚刚按下了确定按钮”,按下取消的时候同样要弹一个对话框提醒4.事实上用je,jmp已经OUT啦,在MASM下,我们可以用if,elseif,else……5.探究.if背后的真相!3.3标号、变量和数据结构当程序中要跳转到另一位置时,需要有一个标识来指示位置,这就是标号,通过在目的地址的前面放上一个标号,可以在指令中使用标号来代替直接使用地址。关于变量的使用是任何编程语言都要遇到的工作,Win32汇编也不例外,在MASM中使用变量也有需要注意的几个问题,错误地使用变量定义或用错误的方法初始化变量会带来难以定位的错误。3.3标号、变量和数据结构变量是计算机内存中已命名的存储位置,在C语言中有很多种类的变量,如整数型、浮点型和字符型等,不同的变量有不同的用途和尺寸,比如说虽然长整数和单精度浮点数都是32位长,但它们的用途不同。变量的值在程序运行中是经常改变的,所以它必须定义在可写的段内,如.data和.data?,或者在堆栈内。按照定义的位置不同,MASM中的变量也分为全局变量和局部变量两种。3.3标号、变量和数据结构在MASM中标号和变量的命名规范是相同的,它们是:1)可以用字母、数字、下划级及符号@、$和?。2)第一个符号不能是数字。3)长度不能超过240个字符。4)不能使用指令名等关键字。5)在作用域内必须是唯一的。这些规则是大部分编程语言约定俗成的!3.3.1标号的定义当在程序中使用一条跳转指令的时候,可以用标号来表示跳转的目的地,编译器在编译的时候会把它替换成地址。标号既可以定义在目的指令同一行的头部,也可以在目的指令前一行单独用一行定义,标号定义的格式是:格式一标号名:
目的指令格式二标号名::目的指令3.3.1标号的定义我们比较常用的方法是使用格式一(一个冒号那个),注意这时候标号的作用域是当前的子程序,在不同子程序中可以存在同样名字的标号,这也就意味着这种格式不能从一个子程序通过标号跳转到另一个子程序中。那如果实在痒,想跳,怎么办?格式二就应运而生了!没错,当我们需要从一个子程序中用指令跳到另一个子程序中的标号位置时候,我们用格式二,但代码并不和谐!@@在DOS时代,为标号起名是个麻烦的事情,因为汇编指令用到跳转指令特别多,任何比较和测试等都要涉及跳转,所以在程序中会有很多标号,在整个程序范围内起个不重名的标号要费一番功夫,结果常常用addr1和addr2之类的标号一直延续下去……@@事实上很多标号会使用一到两次,而且不一定非要起个有意义的名称,如汇编程序中下列代码结构很多:
movcx,1234h
cmpflag,1
jeloc1
movcx,1000h
loc1:
looploc1在别的地方事实上就不会用到loc1了@@对于这种不环保的做法,我们用@@来取而代之!程序改下如下:
movcx,1234h
cmpflag,1
je@F
movcx,1000h
@@:
loop@B//B这里是Before的意思是不是既方便,又美观?!@@当用@@做标号时,@F表示本条指令后的第一个@@标号,@B表示本条指令前的第一个@@标号,注意,当程序中可以有多个@@标号,@B和@F只寻找匹配最近的一个。不要在间隔太远的代码中使用@@标号,因为在以后的修改中@@和@B,@F中间可能会被无意中插入一个新的@@,这样一来,@B或@F就会引用到错误的地方去,距离最好限制在编辑器能够显示的同一屏幕的范围内。3.3.2变量--全局变量全局变量的定义全局变量的作用域是整个程序,Win32汇编的全局变量定义在.data或.data?段内,可以同时定义变量的类型和长度,格式如:变量名
类型
初始值1,
初始值2,…变量名
类型
重复数量
dup(初始值1,初始值2,…)MASM中可以定义的变量类型相当多,也很实在,都是表达占地多少?!3.3.2变量--全局变量3.3.2变量--全局变量注意:所有使用到变量类型的情况中,只有定义全局变量的时候类型才可以用缩写!【举例】
.data
wHour
dw
?
;例1
wMinute
dw
10
;例2
_hWnd
dd
?
;例3
word_Buffer
dw
100dup(1,2)
;例4
szBuffer
byte
1024dup(?)
;例5
szText
db
‘Hello,world!’
;例63.3.2变量--全局变量在byte类型变量的定义中,可以用引号定义字符串和数值定义的方法混用。假设要定义两个字符串Hello,World!和Helloagain,每个字符串后面中回车和换行符,最后以一个0字符结尾,可以定义如下:
szText
db
‘Hello,World!’,0dh,0ah,\ ’Helloagain’,0dh,0ah,0关于CR和LFDos和windows采用回车+换行(CR/LF)表示下一行而UNIX/Linux采用换行符(LF)表示下一行苹果机(MACOS系统)则采用回车符(CR)表示下一行CR用符号'\r'表示,十进制ASCII代码是13,十六进制代码为0x0DLF使用‘\n’符号表示,ASCII代码是10,十六制为0x0A全局变量的初始化值全局变量在定义中既可以指定初值,也可以只用问题预留究竟,在.data?段中,只能用问号预留空间,因为.data?段中不能指定初始值。这里就有一个问题:既然可以用问号预留空间,那么在实际运行的时候,这个未初始化的值是随机的还是确定的呢?答:在全局变量中,这个值就是0,所以用问号指定的全局变量如果要以0为初始值的话,在程序中可以不必为它赋值。3.3.3局部变量局部变量这个名称最早源于高级语言,主要是为了定义一些仅在单个函数里面有用的变量而提出的,使用局部变量能带来一些额外的好处,它使程序的模块化封装变得可能!试想一下,如果要用到的变量必须定义在程序的数据段里面,假设在一个子程序中要用到一些变量,当把这个子程序移植到别的程序时,除了把代码移过去以外,还必须把变量定义移过去。3.3.3局部变量而即使把变量定义移过去了,由于这些变量定义在大家都可以用的数据段中,就无法对别的代码保持透明,别的代码有可能有意无意地修改它们还有,在一个大的工程项目中,存在很多的子程序,所有的子程序要用到的变量全部定义在数据段中,会使数据段变得很大,混在一起的变量也使维护变得非常不方便。3.3.3变量--局部变量局部变量这个概念出现以后,两个以上子程序都要用到的数据才被定义为全局变量统一放在数据段中,仅在子程序内部使用的变量则放在堆栈中,这样子程序可以编成黑匣子的模样,使程序的模块结构更加分明。局部变量的作用域是单个子程序,在进入子程序的时候,通过修改堆栈指针esp来预留出需要的空间,在用ret指令返回主程序之前,同样通过恢复esp丢弃这些空间,这些变量就随之无效了。用C反汇编举例演示!3.3.3局部变量局部变量的缺点就是因为空间是临时分配的,所以无法定义含有初始化值的变量,对局部变量的初始化一般在子程序中由指令完成。全局变量的内存分配是静态的,局部变量的内存分配是动态的,这句话我们现在是否有了真正的理解?!3.3.3局部变量--局部变量的定义MASM用local伪指令提供了对局部变量的支持。定义的格式是:local
变量名1[[重复数量]][:类型],
变量名2[[重复数量]][:类型]……local伪指令必须紧接在子程序定义的伪指令proc后、其他指令开始前,这是因为局部变量的数目必须在子程序开始的时候就确定下来系统才知道怎么分配,在一个local语句定义不下的时候,可以有多个local语句,语法中的数据类型不能用缩写。3.3.3局部变量--局部变量的定义Win32汇编默认的类型是dword,如果定义dword类型的局部变量,则类型可以省略。当定义数组的时候,可以[]括号起来。不能使用定义全局变量的dup伪指令。局部变量不能和已定义的全局变量同名。局部变量的作用域是当前子程序,所以在不同的子程序中可以有同名的局部变量。3.3.3局部变量--局部变量的定义定义局部变量的例子:
local
local[1024]:byte
;例1
local
loc2
;例2
local
loc3:WNDCLASS
;例3例1定义了一个1024字节长的局部变量loc1例2定义了一个名为loc2的局部变量,类型是默认值dword例3定义了一个WNDCLASS数据结构,名为loc3局部变量的使用
TestProc
proc;名为TestProc的子程序
local
@loc1:dword,@loc2:word
local
@loc3:byte;用local语句定义了3个变量
moveax,@loc1;对应类型进行存储,然后返回
mov
ax,@loc2
moval,@loc3
ret
TestProc
endp局部变量的使用
TestProc
proc;名为TestProc的子程序
local
@loc1:dword,@loc2:word
local
@loc3:byte;用local语句定义了3个变量
moveax,@loc1;对应类型进行存储,然后返回
mov
ax,@loc2
moval,@loc3
ret
TestProc
endp局部变量的使用我们来看看它反汇编之后是什么样子的::0040100055
pushebp
:004010018BEC
movebp,esp
:00401003
83C4F8
addesp,FFFFFFF8
:004010068B45FC
moveax,dwordptr[ebp-04]
:00401009668B45FA
movax,wordptr[ebp-06]
:0040100D
8A45F9
moval,byteptr[ebp-07]
:00401010C9
leave
:00401011C3
ret局部变量的使用可以看到,反汇编后的指令比源程序多了前后两段指令,它们是:
:0040100055
pushebp
:004010018BEC
movebp,esp
:00401003
83C4F8
addesp,FFFFFFF8
:00401010C9
leave这些就是使用局部变量所必需的指令,分别用于局部变量的准备工作和扫尾工作。局部变量的使用【分析过程】当调用者执行了callTestProc指令后,CPU把返回的地址(当前地址)压入堆栈,再转移(jmp)到子程序执行。esp在程序的执行过程中可能随时用到,不可能用esp来随时存取局部变量,ebp寄存器(可以理解为小三)是以堆栈段为默认数据段的,所以,可以用ebp做指针指向堆栈替代esp。于是,在初始化前,先用一句pushebp指令把原来的dbp保存起来,然后把esp的值放到ebp中。局部变量的使用(介绍局部变量怎么腾出空间的)再后面就是堆栈中预留空间了,由于堆栈是向下增长的。所以要在esp中加一个负值,FFFFFFF8就是-8。栈低地址高地址esp-8ebp局部变量的使用我们来考虑另一个问题:一个dword加一个word加一个byte不是7吗,为什么刚刚我们在堆栈为局部变量让出了8个字节的空间呢?这是因为在80386处理器中,以dword(32位)为界对齐时存取内存速度最快,所以MASM宁可浪费一个字节,执行了这3句指令后,初始化完成,就可以进行正常的操作了,从指令中可以看出局部变量在堆栈中的位置排列。局部变量的使用在程序退出的时候,必须把正确的esp设置回去,否则,ret指令会从堆栈中取出错误的地址返回,看程序可以发现,ebp就是正确的esp值,因为子程序开始的时候已经有一句movebp,esp,所以要返回的时候只要先movesp,ebp,然后再popebp,堆栈就是正确的了。在80386指令集中有一条指令可以在一句中实现这些功能,就是leave指令,所以,编译器在ret指令之前只使用了一句leave指令。局部变量的使用明白了局部变量使用的原理,就很容易理解使用时的注意点:ebp寄存器是关键(第一次听说小三是关键)因为它起到保存原始esp的作用,并随时用做存取局部变量的指针基址,所以在任何时刻,不要尝试把ebp用于别的用途,否则会带来意想不到的后果。(在任何时候不要做对不起小三的事情,不然后果很严重T_T)闲言碎语Win32汇编中局部变量的使用方法可以解释一个很有趣的现象:在DOS汇编的时候,如果在子程序中的push指令和pop指令不配对,那么返回的时候ret指令从堆栈里得到的肯定是错误的返回地址,程序也就死掉了。但在Win32汇编中,push指令和pop指令不配对可能在逻辑上产生错误,却不会影响子程序正常返回,原因就是在返回的时候esp不是靠相同数量的push和pop指令来保持一致的,而是靠leave指令从保存在ebp中的原始值中取回来的,也就是说,即使把esp改得一塌糊涂也不会影响到子程序的返回,当然,窍门就在ebp,把ebp改掉,程序就玩完了!3.3.3局部变量-局部变量的初始化值显然,局部变量是无法在定义的时候指定初始化值的,因为local伪指令只是简单地把空间给留出来,那么开始使用时它里面是什么值呢?和全局变量不一样,局部变量的初始值是随机的,是其他子程序执行后在堆栈里留下的垃圾(因为我们知道,腾出空间只是改变栈指针esp),所以,对局部变量的值一定要初始化,特别是定义为结构后当参数传递给API函数的时候。局部变量的初始化值在API函数使用的大量数据结构中,往往用0做默认值,如果用局部变量定义数据结构,初始化时只定义了其中的一些字段,那么其余字段的当前值可以是编程者预想不到的数值,传给API函数后,执行的结果可能是意想不到的,这是初学者很容易忽略的一个问题。所以最好的办法是:在赋值前首先将整个数据结构填0,然后再初始化要用的字段,这样其余的字段就不必一个个地去填0了,RtlZeroMemory这个API函数就是实现填0的功能的。3.3.5变量的使用接着上一节的话题,我们继续谈变量,书本原本的数据结构我们后边介绍。这个话题有点像C语言中的数据类型强制转换,C语言中的类型转换指的是把一个变量的内容转换成另外一种类型,转换过程中,数据的内容已经发生了变化,如把浮点数转换成整数后,小数点后的内容就丢失了。在MASM中以不同的类型访问不会对变量造成影响。变量的使用例如,以db方式定义一个缓冲区:
szBuffer
db
1024dup(?)然后
movax,szBuffer编译器会报一个错:
errorA2070:invalidinstructionoperands意思是无效的指令操作,为什么呢?因为szBuffer是用db定义的,而ax的尺寸是一个word,等于两个字节,尺寸不符合。3.3.5变量的使用--以不同的类型访问变量在MASM中,如果要用指定类型之外的长度访问变量,必须显式地指出要访问的长度,这样编译器忽略语法上的长度检验,仅使用变量的地址。使用的方法是:
类型
ptr
变量名类型可以是byte,word,dword,fword,qword,real8和real10。如:
movax,wordptrszBuffer
moveax,dwordptrszBuffer3.3.5变量的使用--以不同的类型访问变量在这里要注意的是,指定类型的参数访问并不会去检测长度是否溢出,看下面一段代码:
.data
bTest1
db
12h
wTest2
dw
1234h
dwTest3
dd
12345678h
……
.code
mov
al,bTest1
mov
ax,wordptrbTest1
mov
eax,dwordptrbTest1
……3.3.5变量的使用--以不同的类型访问变量上面的程序片断,每一句执行后寄存器中的值是什么呢?moval,bTest1这一句很显然使al等于12h,下面的两句呢,ax和eax难道等于0012h和00000012h吗?实际运行结果是3412h和78123412h,为什么呢?(DOS汇编基础不错的同学,应该能理解)现在我们先来看反汇编的内容:Followme!3.3.5变量的使用--以不同的类型访问变量所以说呢,刚才这个例子说明了汇编中用ptr强制覆盖变量长度的时候,实质上是只用了变量的地址而禁止编译器进行检验。编译器并不会考虑定界的问题,程序员在使用的时候必须对内存中的数据排列有个全局概念,以免越界存取到意料之外的数据。如果程序员的本意是类似于C语言的强制类型转换,想把bTest1的一个字节扩展到一个字或一个双字再放到ax或eax中,高位保持0而不是越界存取到其他的变量,要肿么办呢?80386处理器提供的movzx指令可以实现这个功能,例如:
movzx
ax,bTest1
;ax==0012h
movzx
eax,bTest1
;eax==00000012h
movzx
eax,cl
;eax==000000(cl)
movzx
eax,ax
;eax==0000(ax)用movzx指令进行数据长度扩展是Win32汇编中经常用到的技巧。3.3.5变量的使用--以不同的类型访问变量3.3.5变量的使用--变量的尺寸和数量在源程序中用到变量的尺寸和数量的时候,可以用sizeof和lengthof伪指令来实现,格式是:
sizeof
变量名、数据类型或数据结构名
lengthof
变量名他们的区别是:sizeof伪指令可以取得变量、数据类型或数据结构以字节为单位的长度,然而lengthof则可以取得变量中数据的项数。stWndClass
WNDCLASS
<>
szHello
db
‘Hello,world!’,0
dwTest
dd
1,2,3,4
……
.code
……
moveax,sizeofstWndClass
movebx,sizeofWNDCLASS
movecx,sizeofszHello
movedx,sizeofdword
movesi,sizeofdwTest3.3.5变量的使用--变量的尺寸和数量执行后eax的值是stWndClass结构的长度:40ebx同样是:40ecx的值是Hello,world!字符串的长度加上一个字节的0结束符:13edx的值是一个双字的长度:4esi等于4个双字的长度:163.3.5变量的使用--变量的尺寸和数量如果把所有的sizeof换成lengthof,那么eax会等于1,因为只定义了1项WNDCLASS而ecx同样等于13esi则等于4lenghofWNDCLASST和lengthofdword是非法的用法,编译程序会报错。3.3.5变量的使用--变量的尺寸和数量要注意的是,sizeof和lengthof的数值是编译时产生的,由编译器传递到指令中去,上边的指令最后产生的代码就是:
moveax,40
movebx,40
movecx,13
movedx,4
movesi,163.3.5变量的使用--变量的尺寸和数量如果为了把Hello和World分两行定义,szHello是这样定义的:
szHello
db
‘Hello’,odh,oah
db
‘World’,0那么sizeofszHello是多少
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2026福建同安第一中学附属学校校园招聘备考题库附答案
- 2026福建省遴选公务员403人参考题库附答案
- 2026福建福州市司法局行政复议辅助人员招聘3人参考题库附答案
- 2026贵州贵阳市某国有企业招聘2人考试备考题库附答案
- 2026青海海西州格尔木市公安局招聘警务辅助人员46人参考题库附答案
- 中共台州市路桥区委全面深化改革委员会办公室关于公开选聘工作人员1人备考题库附答案
- 常州市武进区前黄实验学校招聘考试备考题库附答案
- 河南省科学院碳基复合材料研究院科研辅助人员招聘备考题库附答案
- 纪检监察基础知识
- 纪检监察培训课件汇编
- 中西医结合诊治妊娠胚物残留专家共识(2024年版)
- 2026年托里国电投发电有限责任公司招聘备考题库及1套完整答案详解
- 2025-2026学年北京市海淀区初二(上期)期末物理试卷(含答案)
- 2025-2026年鲁教版八年级英语上册期末真题试卷(+答案)
- (正式版)DB51∕T 2732-2025 《用材林培育技术规程 杉木》
- 八年级下册 第六单元写作 负责任地表达 教学课件
- 美容院2025年度工作总结与2026年发展规划
- 26年三上语文期末密押卷含答题卡
- 2026届云南省昆明市西山区民中数学高一上期末考试模拟试题含解析
- 2025-2030乌干达基于咖啡的种植行业市场现状供需分析及投资评估规划分析研究报告
- 2026年共青团中央所属单位招聘66人备考题库及答案详解一套
评论
0/150
提交评论