Win32汇编--使⽤MASM
经过上⼀讲的准备⼯作,相信⼤家已经搭建好了 Win32 汇编的⼯作环境,并已经知道编译、链接⼀个程序的过程和原理了。现在,我们让例⼦回归到经典:
include <stdio.h>
int main(void)
{
Printf(“Hello, world\n”);
} // 事实上想想,这不正是初⽣的婴⼉?!
⿇雀虽⼩,五脏俱全。刚刚那个C语⾔的”Hello, world”程序包含了C语⾔中的最基本的格式。
在C语⾔的源程序中,我们不需要为堆栈段、数据段和代码段的定义⽽烦恼,编译器会⾃⼰解决。
回顾⼀下,在DOS 下的汇编这段代码会变成什么样? Follow me!;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 堆栈段;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
stack segment
db 100 dup (?) ;定义100个字节的内存存储单元空间,默认值为?
stack ends ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
data segment
szHello db ‘Hello, world’,0dh,0ah,’$’
data ends ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
code segment
assume cs:code,ds:data,ss:stack
start:
mov ax,data
mov ds,ax
mov ah,9
mov dx,offset szHello
int 21h
mov ah,4ch
int 21h
code ends
end start
在例⼦中我们看到,stack、data、code都到了⾃⼰的⼩窝。
回归主题,在Win32 汇编语⾔下,⼩⿇雀”Hello World” ⼜会变成什么样⼦呢? Follow me!;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 3rd Edition>
; by 罗云彬,
; 视频讲座 by ⼩甲鱼, ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Hello.asm
; 使⽤ Win32ASM 写的 Hello, world 程序;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 使⽤ nmake 或下列命令进⾏编译和链接:
; ml /c /coff Hello.asm
; Link /subsystem:windows Hello.obj
;
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.386
.model flat,stdcall
option casemap:none ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include ⽂件定义;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>
; 数据段;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data
szCaption db '来⾃鱼C⼯作室', 0
szText db 'Hello, World !', 0 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
start:
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
invoke ExitProcess,NULL ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
是不是⼜不同了?但是,我们怎么就发觉Win32 汇编其实是前边两种形态的集⼤成者?!
接下来,⼩甲鱼带⼤家逐段来理解和接受这个新先的语⾔!
模式定义
程序的第⼀部分是模式和源程序格式的定义语句
.386
.model flat,stdcall
option casemap:none
这些指令定义了程序使⽤的指令集、⼯作模式和格式。
1)指定使⽤的指令集
.386语句是汇编语句的伪指令,类似的指令还有:.8086、.186、.286、.386/.386p、.486/.486p和.586/.586p等,⽤于告诉编译器在本程序中使⽤的指令集。
在DOS的汇编中默认使⽤的是8086指令集,那时候如果在源程序中写⼊80386所特有的指令或使⽤32位的寄存器就会报错。
Win32环境⼯作在80386及以上的处理器中,所以这⼀句.386是必不可少的。
另外,后⾯带p的伪指令则表⽰程序中可以使⽤特权指令,如:mov cr0,eax
这⼀类指令必须在特权级0上运⾏,如果只指定.386,那么使⽤普通的指令是可以的,编译时到这⼀句就会报错。
如果我们要写的程序是VxD等驱动程序,中间要⽤到特权指令,那么必须定义.386p,在应⽤程序级别的Win32编程中,程序都是运⾏在优先级3上,不会⽤到特权指令,只需定义.386就够了。
80486和Pentium处理器指令是80386处理器指令的超集,同样道理,如果程序中要⽤80486处理器或Pentium处理器的指令,则必须定义.486或.586。
另外,Intel公司的80x86系列处理器从Pentium MMX开始增加了MMX指令集,为了使⽤MMX指令,除了定义.586之外,还要加上⼀句.mmx 伪指令:
.386
.mmx
2)model语句
.model语句在低版本的宏汇编中已经存在,⽤来定义程序⼯作的模式,它的使⽤⽅法是:
.model 内存模式 [,语⾔模式] [,其他模式]
内存模式的定义影响最后⽣成的可执⾏⽂件,可执⾏⽂件的规模从⼩到⼤,可以有很多种类型。详见下表:
Windows 程序运⾏在保护模式下,系统把每⼀个Win32应⽤程序都放到分开的虚拟地址空间中去运⾏,也就是说,每⼀个应⽤程序都拥有其相互独⽴的4GB地址空间。
对Win32程序来说,只有⼀种内存模式,即flat(平坦)模式,意思是内存是很平坦地从0延伸到4GB,再没有64KB段⼤⼩限制。
对⽐⼀下DOS的Hello World和Win32的Hello World开始部分的不同,DOS程序中有这样语句
mov ax,data
mov ds,ax
意思是把数据段寄存器DS指向data数据段,data数据段在前⾯已经⽤data segment语句定义,只要DS不重新设置,那么从此以后指令中涉及的数据默认将从data数据段中取得。
所以下⾯的语句是从data数据段取出szHello字符串的地址后再显⽰:
mov ah,9
mov dx,offset szHello
int 21h
纵观Win32汇编的源程序,没有⼀处可以到ds或es等段寄存器的使⽤,因为所有的4GB空间⽤32位的寄存器全部都能访问到了,不必在头脑中随时记着当前使⽤的是哪个数据段,这就是平坦内存模式带来的好处。
如果定义了.model flat,MASM⾃动为各种段寄存器做了如下定义:
ASSUME cs:FLAT, ds:FLAT, ss:FLAT, es:FLAT, fs:ERROR, gs:ERROR
也就是说,CS,DS,SS和ES段全部使⽤平坦模式,FS和GS寄存默认不使⽤,这时若在源程序中使⽤FS或GS,在编译时会报错。如果有必要使⽤它们,只需在使⽤前⽤下⾯的语句声明⼀下就可以了:
assume fs:nothing, gs:nothing 或者
assume fs:flat, gs:flat
在Win32汇编中,.model语句中还应该指定语⾔模式,即⼦程序和调⽤⽅式,例⼦中⽤的是stdcall,它指出了调⽤⼦程序或Win32 API时参数传递的次序和堆栈平衡的⽅法。
相对于stdcall,不同的语⾔类型还有C, SysCall, BASIC, FORTRAN 和PASCALL,虽然各种⾼级语⾔在调⽤⼦程序时都是使⽤堆栈来传递参数。
Windows的API调⽤使⽤是的stdcall格式,所以在Win32汇编中没有选择,必须在.model中加上stdcall参数。
(1) _stdcall调⽤
_stdcall是Pascal程序的缺省调⽤⽅式,参数采⽤从右到左的压栈⽅式,被调函数⾃⾝在返回前清空堆栈。WIN32 Api都采⽤_stdcall调⽤⽅式。
(2) _cdecl调⽤
_cdecl是C/C++的缺省调⽤⽅式,参数采⽤从右到左的压栈⽅式,传送参数的内存栈由调⽤者维护。_cedcl约定的函数只能被C/C++调⽤,每⼀个调⽤它的函数都包含清空堆栈的代码,所以产⽣的可执⾏⽂件⼤⼩会⽐调⽤_stdcall函数的⼤。
3)option 语句
如例⼦中,我们定义了 option casemap:none 的意义是告诉编译器程序中的变量名和⼦程序名是否对⼤⼩写敏感。
由于Win32 API 的API函数名称本质是区分⼤⼩写的,所以必须指定这个选项,否则调⽤API函数就会出现问题。
段的定义
把上⾯的Win32的Hello World源程序中的语句归纳精简⼀下,再列在下⾯:
.386
.model flat,stdcall
option casemap:none
<⼀些include语句>
.data
<⼀些字符串、变量定义>
.code
<;代码>
<;开始标号>
<;其他语句>
end 开始标号
模式定义中的模式、选项等定义并不会在编译好的可执⾏程序中产⽣什么东西,它们只是说明。
⽽真正的数据和代码是定义在各个段中的,如上⾯的.data段和.code段,考虑到不同的数据类型,还可以有其他种类的数据段,下⾯是包含全部段的源程序结构:
.386
字符串常量存储在哪个区.model flat,stdcall
option casemap:none
<⼀些include语句>
.stack [堆栈段的⼤⼩]
.data
<⼀些初始化过的变量定义>
.data?
<⼀些没有初始化过的变量定义>
.const
<⼀些常量定义>
.code
<;代码>
<;开始标号>
<;其他语句>
end 开始标号
.stack、.data、.data?、.const和.code是分段伪指令,Win32中实际上只有代码和数据之分,
.
data,.data?和.const这些都是指向数据段,.code是指向代码段。
和DOS汇编不同,Win32汇编不必考虑堆栈,系统会为程序分配⼀个向下扩展的、⾜够⼤的段作为堆栈段,所以.stack段定义常常被忽略。
前⾯我们不是说过Win32环境下不⽤段了吗?
是的,这些“段”,实际上并不是DOS汇编中那种意义的段,⽽是内存的“分段”。
上⼀个段的结束就是下⼀个段的开始,所有的分段,合起来,包括系统使⽤的地址空间,就组成了整个可以寻址的4GB线性地址空间。
我们接着往下看会更加容易理解。
Win32汇编的内存管理使⽤了80386处理器的分页机制,每个页(4KB⼤⼩)可以⾃由指定属性,所以上⼀个4KB可能是代码,属性是可执⾏但不可写,下⼀个4KB就有可能是既可读也可写但不可执⾏的数据。
再下⾯呢?有可能是可读不可写也不可执⾏的数据。(主要就看你放的是什么东西……)
嘿嘿,⼤家是否有点理解了?没关系,接着往下!
Win32汇编源程序中“分段”的概念实际上是把不同类型的数据或代码归类,再放到不同属性的内存页(也就是不同的“分段”)中,这中间不涉及使⽤不同的段选择器。(仅仅是配合分页机制搞捣⿎~)
虽然使⽤和DOS汇编同样的.code和.data语句来定义,意思可是完全不同了!
⾄此,相信⼤家和⼩甲鱼⼀样清晰啦,感谢⽼师,感谢拉登,感谢嫦娥_
数据段
.data、.data?和.const定义的是数据段,分别对应不同⽅式的数据定义,在最后⽣成的可执⾏⽂件中也分别放在不同的节区(Section)中。(这个在我们讲解PE结构的时候会很细致描述)
程序中的数据定义⼀段可以归纳为3类:
第⼀类是可读可写的已定义变量。
第⼆类是可读可写的未定义变量。
第三类数据是⼀些常量。
可读可写的已定义变量
这些数据在源程序中已经被定义了初始值,⽽且在程序的执⾏中有可能被更改。
如⼀些标志等,这些数据必须定义在.data段中,.data段是已初始化数据段,其中定义的数据是可读可写的,在程序装⼊完成的时候,这些值就已经在内存中了,.data段存放在可执⾏⽂件的_DATA节区内。
可读可写的未定义变量
这些变量⼀般是当做缓冲区或者在程序执⾏后才开始使⽤的,这些数据可以定义在.data段中,也可以定义在.data?段中,但⼀般把它放
到.data?段中。
虽然定义在这两种段中都可以正常使⽤,但定义在.data?段中不会增⼤.exe⽂件的⼤⼩。
举例说明,如果要⽤到⼀个100KB的缓冲区,可以在数据段中定义:
szBuffer db 100 * 1024 dup (?)
如果放在.data段中,编译器认为这些数据在程序装⼊时就必须有效,所以它在⽣成可执⾏⽂件的时候保留了所有的100KB的内容,即使它们是全零!
如果程序其他部分的⼤⼩是50KB,那么最后的.exe⽂件就会是150KB⼤⼩,如果缓冲区定义为1MB,那么.exe⽂件会增⼤到1050KB。
.data?段则不同,其中的内容编译器会认为程序在开始执⾏后才会⽤到,所以在⽣成可执⾏⽂件的时候只保留了⼤⼩信息,不会为它浪费磁盘空间。
和上⾯同样的情况下,即使缓冲区定义为1MB,可执⾏⽂件同样只有50KB!总之,.data?段是未初始化数据段,其中的数据也是可读可写的,但在可执⾏⽂件中不占空间,.data?段在可执⾏⽂件中存放在_BSS节区中。
数据是⼀些常量
如⼀些要显⽰的字符串信息,它们在程序装⼊的时候也已经有效,但在整个执⾏过程中不需要修改,这些数据可以放在.const段中,.const段是常量段,它是可读不可写的。
⼀般为了⽅便起见,在⼩程序中常常把常量⼀起定义到.data段中,⽽不另外定义⼀个.const段。
在程序中如果不⼩⼼写了对.const段中的数据做写操作的指令,会引起保护错误,Windows会显⽰⼀个提⽰框并结束程序。
代码段
.code段是代码段,所有的指令都必须写在代码段中,在可执⾏⽂件中,代码段是放在_TEXT节区(区块)中的。
Win32环境中的数据段是不可执⾏的,只有代码段有可执⾏的属性。
对于⼯作在特权级3的应⽤程序来说,.code段是不可写的,在编写DOS汇编程序的时候,我们可以为⾮作⽍,如果企图在Win32汇编下做同样的事情,结果就是和上⾯同样 “⾮法操作”!
当然事物总有两⾯性,在Windows95下,在特权级0下运⾏的程序对所有的段都有读写的权利,包括代码段。
另外,在优先级3下运⾏的程序也不是⼀定不能写代码段,代码段的属性是由可执⾏⽂件PE头部中的属性位决定的。
通过编辑磁盘上的.exe⽂件,把代码段属性位改成可写,那么在程序中就允许修改⾃⼰的代码段。
⼀个典型的应⽤就是⼀些针对可执⾏⽂件的压缩软件和加壳软件,如Upx和PeCompact等。
这些软件靠把代码段进⾏变换来达到解压缩和解密的⽬的,被处理过的可执⾏⽂件在执⾏时需要由解压代码来将代码段解压缩。
这就需要写代码段,所以这些软件对可执⾏⽂件代码段的属性预先做修改。
为了带⼤家更好认识这些花花绿绿的“段”到底是什么回事,⼩甲鱼带⼤家看⼀张图……
程序结束和程序⼊⼝
在C语⾔源程序中,程序不必显式地指定程序由哪⾥开始执⾏,编译器已经约定好从main() 函数开始执⾏了。
⽽在汇编程序中,并没有⼀个main函数,程序员可以指定从代码段的任何⼀个地⽅开始执⾏,这个地⽅由程序最后⼀句的end语句来指定:end [开始地址]
这句语句同时表⽰源程序结束,所有的代码必须在end语句之前。
end start
上述语句指定程序从start这个标号开始执⾏。当然,start标号必须在程序的代码段中有所定义。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论