Win32汇编源程序的结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| .386 .model flat,stdcall option casemap:none
include windows.inc include user32.inc includelib user32.lib include kernel32.inc 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
|
1.模式定义
1 2 3
| .386 .model flat,stdcall option casemap:none
|
1.指令集
2.互作模式
1 2
| .model 内存模式[,语言模式][,其他模式] 内存模式的定义影响最后生成的可执行文件,可执行文件的规模从小到大,可以有很多种类型。
|
- windows程序运行在保护模式下,系统把每一个win32应用程序都放到分开的虚拟地址空间中去运行,耶就是说,每一个应用程序都拥有其相互独立的4GB地址空间
- 对于win32程序来说,只有一种内存模式,也就是flat模式
在win32汇编中,.model语句还应该指定语言模式,也就是子程序和调用方法
例子中用的是stdcall,它指出了伊奥用子程序或win32 API时参数传递的次序和堆栈平衡的方法
相对于stdcall,不同的语言类型还有C,syscall,BASIC , FORTRAN和PASCALL,虽然各种高级语言在调用子程序的时候,都是用堆栈来传递参数
windows的api调用使用的是stdcall格式,所以在win32汇编中没有选择,必须在.model中加上stdcall参数
理解stdcall和cdecl
1 2 3 4 5
| _stdcall调用: 是pascal程序的缺省调用方式,参数采用从右到左的压栈方式,被调用函数自身在返回前清空堆栈。win32 api 都采用_stdcall调用方式
_cdecl调用 _cdecl是c/c++的缺省调用方式,参数采用从右往左的压栈方式,传送参数的内存由调用者维护。_cdecl约定的函数只能被c/c++调用,每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小回避_stdcall函数大
|
3.格式
1 2 3 4
| option语句 意义是对大小写是否敏感 由于win32 api函数是分大小写的所以 必须得有这个
|
2.段的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 模式定义 <一些include语句> .stack[堆栈段的大小] .data <一些初始化过的变量定义> .data? <一些没有初始化过的变量的定义> .const <常量的定义> .code <代码> <开始标号> <其他语句> end 开始标号
|
- win32汇编源程序中“分段”的概念实际上是把不同类型的数据或代码归类,在放到不同属性的内存页(耶就是不同的“分段”)中,这中间不涉及使用不同的段寄存器【仅仅是配合分页机制罢了】
1.数据段
- .data .data? .const定义的是数据段,分别对应不同方式放在不同的区域
1 2 3
| .data 是可读可写的数据,定义好的变量 int a=1;
|
1 2 3 4 5
| .data? 可读可写的数据,未定义变量 有空间,但是没有初始化罢了 int a; 定义在data?段中不会增加.exe文件的大小
|
1 2
| .const 只能读的数据,不能写,只要写了就会报错
|
2.代码段
1 2 3 4
| .code段就是代码段,直接操控CPU咯 但是!我们是在等级3下运行程序,所以对于code段是不能写的,不能修改code段中的东西 在优先级为3下运行的程序耶不是一定不能写代码段,代码段的属性是由可执行文件PE头部中的属性位所决定的 通过编译磁盘上的.exe文件 ,把代码段的属性位改成可写的,那么就可以修改自己的代码段了
|
3.程序结束和程序入口
1 2 3 4
| end start 这个在一个程序里面的话,就需要写这个,来指明结束和入口地址 但是: 如果有多个模块,需要把这些模块都链接在一起的时候,就只能有一个主模块指定入口地址,不然入口太多会爆炸
|
4.注释
5.换行
调用API函数
1.首先,说明是API?
2.调用API函数
1.)关于dll
- dll实质上是一个打打的集装箱,装着各种系统的API函数,应用程序在使用的时候由windows自动载入dll程序并且调用相应的函数
- 实在上,win32的基础就是由dll组成的。win32api的核心由3哥dll提供
1 2 3
| 1.kernel32.dll -- 系统服务功能。包含内存管理,任务管理和动态链接 2.gdi32.dll -- 图形设备接口,处理图形绘制 3.user32.dll -- 用户接口服务,建立窗口和传送消息
|
win32api还包含起来的很多的函数,都是由dll提供
比如:TCP/IP协议进行网络通信的dll是wsock32.dll。。。等
所有的DLL提供的函数组成了现在使用的win32编程环境
2.)api函数的参数
1 2 3 4 5 6 7
| int MessageBox( HWND hWnd, //handle to ouner window LPCTSTR lp Text, //text in message box LPCTSTR lpCaption, //message box title UINT uType, //message box style )//这个是c语言来表示的
|
1 2
| MessageBox Proto hWnd:dword,ip Text:dword,ipCaption:dword,uType:dword //这样是用汇编的格式来表达
|
1 2 3 4 5
| push uType push lpCaption push lpText push hWnd call MessageBox
|
- 在源程序编译链接成可执行文件后,call MessageBox语句中的MessageBox会被换成一个地址,指向可执行文件中的导入表的一个索引【函数名或者索引号】
1 2 3
| 导入表:程序会在导入表中写入【将要调用xxx函数】,然后里面就会存放着来自导出表中的函数的地址。。里面会动态的存放着这些将被调用的函数的地址
导出表:就是动态链接库里面所有函数的一一对应的地址,在加载的时候就会把这些地址【将要用到的】给导入表。
|
3.)使用invoke语句
- invoke伪指令【相当于call指令,多了参数个数检查的功能】
1
| invoke 参数名字[,参数1][,参数2]...[,参数n]
|
它能对参数个数进行检查,看是否合格
对于不带参数的api调用,invoke或者call都可以
4.)api函数的返回值
- 返回值各种各样,但是在汇编程序中,只有dword这一种类型,它永远放在eax中
- 如果返回的东西比较大,放不下,就会返回一个指针。把数据放在缓冲区中【指针指向这里】
5.)函数的声明
1
| 函数名 proto [距离][语言][参数1]:数据类型,[参数2]:数据类型
|
- 句中的proto是函数声明的伪指令。
- 距离–种类很多【near far near16….】,但是在win32中不管【因为平坦的段】
- 语言–忽略就使用 .model定义的值
对于汇编来说,只关心参数的数量,名字只是为了好看,所以可以略去
1 2
| MessageBox Proto hWnd:dword,lpText:dowrd,\ lpCaption:dword,uType:dword
|
1
| MessageBox Proto :dword,:dword,:dword,:dword
|
6.)include语句
1 2 3
| include <文件名> include 文件名 都可以
|
7.)includelib语句
在win32汇编种使用api函数,程序必须知道调用的api函数存放在那个dll中,需要有一个文件包括dll库正确的定位信息,这个任务是由导入库来实现的。
在使用外部函数的时候,DOS下有函数库的概念,那时的函数库实际上是静态库,静态库是一组已经编写号的代码模块,在程序中可以自由引用
最后用link 从库中找出相应的函数代码,一起链接到最后的可执行文件中
Win32环境中,程序连接的时候任然要使用函数库定位函数信息,只不过由于函数代码放在dll文件中,库文件中留有函数的定位信息和参数数码等简单信息,这种库文件叫做导入库
一个dll文件对应一个导入库,比如:user32.dll文件用于编程的导入库是user32.lib,masm32互具包中包含了所有dll的导入库
1 2
| includelib 库文件名 includelib <库文件名>
|
1 2
| 咋说呢,我理解的是,函数是存在.dll中 然后我们导入的是.lib,这个里面存放的是.dll的地址信息啥的
|
3.API参数中的等值定义
这里看书去吧【忘了写笔记】,p62
在masm下,我们可以用.if .elseif .else
标号,变量,数据结构
1.标号
1.)标号的定义
- 1.只能在某一个子程序中跳
- 2.可以从一个子程序跳到其他子程序中,不和谐
2.)MASM中的@@
1 2 3 4 5 6 7
| mov cx,1234h cmp flag,1 jz @f ;@f是跳到这个指令后的第一个@@处 mov cx,1000H @@: ... loop @b ;@是跳到本指令前的第一个@@的地方
|
2.变量–全局变量
1.)定义全部变量
- 全部变量的作用域是整个程序,可以用data 或者 data?
1 2 3 4 5
| .data wHour dw ? wMinute dw 10 变量名 变量类型【可以用缩写】 值
|
- 在使用byte类型变量的定义中,可以用引号来定义字符串和数值定义的方法混用
1 2 3 4
| szText db 'Hello,world!',0dh,0ah,'hello again',0dh,0ah,0 用 , 隔开 0dh和0ah其实就是回车和换行符 用的是ascii码
|
2.)全局变量的初始化
- 在不想初始化的时候,可以用问好来预留空间
- 在data?段中只能用?来预留空间
- 并且未初始化的值都是 0
3.变量–局部变量
1.)局部变量的定义
1
| local 变量名1[[重复数量]][:类型],变量名2[[重复数量]][:类型]...
|
- local伪指令必须在子程序定义的伪指令 proc 后,其他指令之前。
- 不能使用缩写
- 默认的类型是dword
- 当定义数组的时候,可以用[]括起来
- 不能使用dup伪指令
- 不能和全局变量同名,但可以和局部变量重名
1 2 3
| local loc1[1024]:byte ;定义了一个1024字节长的局部变量loc1 local loc2 ;定义了名叫loc2的局部变量,类型默认是dword local loc3:WNDCLASS ;定义了一个WNDCLASS数据结构,名loc3
|
2.)局部变量的使用
1 2 3 4 5 6 7 8 9
| TestProc proc local loc1:dword,loc2:word local loc3:byte mov eax,loc1 mov ax,loc2 mov al,loc3 ret TestProc endp
|
- 上面例子在创建局部变量的时候,真正的汇编语句是这样的
1 2 3 4 5 6 7 8 9 10
| push ebp mov ebp,esp ;把esp中的内容放在了ebp中 add esp,fffffff8 ;对esp操作
mov eax,dword ptr [ebp-04] ;win32没有段寄存器的说法 mov ax,word ptr [ebp-06] mov al,byte ptr [ebp-07]
leave ret
|
- 这些指令是必须的,前面的用于局部变量的准备工作,后面的用于扫尾工作
- 把ebp中的内容保存在栈里 面,然后把esp的内容给ebp,供存取局部变量做指针
- 在这个例子中一共是7个字节的大小来保存局部变量,所以esp这个指针要往下移动
1
| 因为80386按照dword为界对其,运行速度最快。
|
1 2
| mov esp,ebp ;在里面使用esp , 原先的esp是放在ebp中的,最后把内容返回给它 pop epb ;在把ebp弹回去
|
3.)局部变量的初始化值
1
| RtlZeroMemory 这个API函数就可以做到该功能
|
4.变量的使用
1.)以不同的类型访问变量
类似和C语言中的数据类型强制转化
但是在masm中,它不会自动帮忙转。
在masm中,变量类型不一样的话就会报错
1 2 3 4 5
| 比如: sz db 1024 dup(?) mov ax,sz 这样就会报错
|
1 2 3 4 5
| .data a db 12h b dw 1234h c dd 12345678h 在这样的在内存中保存的是
|
- 在使用转换类型的时候,其实它不是转换类型,而是把地址的范围改变了,比如之前的是1个字的,转换成2个字,它的内容就会往后延申罢了
1 2 3 4 5 6
| 1.movzx 将变量扩展到应该有的大小 这个是直接在高位放0 2.movsx 将变量扩展到应该有的大小 这个是带有符号位的扩展 最高位是0,就扩展0 //最高位就是符号的标识 最高位是1,就扩展1
|
2.)变量的尺寸和数量
1 2 3 4
| 1.sizeof 对于变量名,数据结构,数据结构名 2.lengthof 对于变量名
|
1 2 3
| sz dd 1,2,3,4
sizeof sz
|
- 这样的话,长度就是16【4*4】
- 在使用sizeof的时候只是去识别该变量的长度,在masm编译器中,变量的定义就是一行
- 所以有:
1 2 3 4
| sz db 'hello',0dh,0ah ;0dh和0ah其实就是换行 db 'word',0 sizeof sz
|
- 这样去识别的时候,sz的长度就是7,而不是13,但是在用函数MessageBox的函数的时候,会显示出所有,因为字符串的结束是按照0所结束的。在没有识别到0的时候,就会继续识别【在地址中存放的时候,他是挨着向下存放的】
- **所以在识别字符串的长度的时候,应该使用lstrlen函数去计算
3.)获取变量地址
- 1.这个offset就是取地址的伪操作符。【只能用于全局变量,不能用于局部变量】
- 为什么呢?
1 2
| 这个地方就要讲究一个先后顺序的操作了,伪指令是在编译器的时候进行,但是指令是在CPU来操作。 局部变量,在编译的时候无从得知它的地址,所以不能用咯
|
- 2.所以在获取局部变量的地址的时候,就需要用到 lea 指令【该指令是CPU指令】
- 对于局部变量,它是用ebp来做指针进行操作的
- lea的原理:
1
| 在使用lea的时候,按照ebp的值实际计算出来,然后放在eax中
|
1 2
| 就比如:lea eax,[ebp-4] 这样就能获取一个局部变量的地址
|
1 2
| 卧槽,肯定有办法啊。 这个时候就是用addr 伪指令
|
- 注意:在使用addr的时候,只能在invoke下使用!!
- addr的原理
1 2
| 当addr后面跟的是全局变量的时候,它的作用就和offset的用法一样。 当addr后面跟的是局部变量的时候,它的作用就和lea一样,会把地址这玩意放在eax中
|
- 注意:在使用addr的时候,前面不能有eax进行传参
1 2
| 比如: invoke text,eax,addr szHello 这样是错误的!!
|
1 2 3 4 5 6
| lea eax,[ebp-4] ;首先是对addr伪指令进行操作 push eax ;这个是放的addr szHello的地址 push eax ;这个放的是eax 第一个参数的地址 call test
这样的话,就会把eax中的值被覆盖掉,就会出现逻辑上的错误。
|
使用子程序
把一段代码封装起来【我咋感觉和函数差不多】
过程和函数,过程无返回值,函数有返回值
化繁为简,在主程序中用call
当然也是要用invoke
1.子程序的定义
1 2 3 4 5 6
| 子程序名 proc [距离][语言类型][可视区域][USES寄存器列表][,参数:类型]...[VARARG] local 局部变量列表 指令 子程序名 endp
|
- 【距离】——有很多种,这里懒得列出来了(P75)win32种肯有无视它
- 【语言类型】——也有很多种(P75)忽略就是使用 .model中的值
- 【可视区域】——有PRIVATE,PUBLIC,EXPORT,三种
1 2 3 4 5
| PRIVATE:表示子程序只对本模块可见 PUBLIC:表示对所有的模块可见(最后编译完成在.exe文件中) EXPORT:表示是到处的函数,当编写DLL的时候要将某个函数到处的时候可以这样使用
默认是用PUBLIC
|
- 【USES寄存器列表】——就是把将用的寄存器push 最后 pop
1
| 不如使用pushad在开头,最后使用popad指令,一次性保存和恢复所有的寄存器
|
- 【参数和类型】——参数指参数的名称,在定义参数名的时候不能和全局变量和局部变量重名。类型不用管,在win32中都是dw
1 2 3
| VARARG:表示在确定的参数后还可以跟多个数量不确定的参数。 在win32中使用VARARG的API只有wsprintf。 类似和printf
|
- 也可以 把子程序写在invoke调用之前,就可以省略proto咯
2.参数传递和堆栈平衡
1
| 栈是共用的,相当于是一个内存空间,直接访问那个内存空间就好啦
|
1
| 也就是说,在进行参数传递的时候,会把参数先放到栈中,这个栈中的内容是现在这个子程序所有的。最后把这个栈的指针重新指回原来的位置,将相当于栈中的内容清空了【其实我理解的是,栈就是一部分内存空间,通过指针来确定这个栈中的内容或者大小,只要指针指回去了,就相当于栈清空了,其实内容是没有被清空的】【在内存空间中,内容只能被覆盖---个人理解罢了】
|
1
| SubRouting(Var1,Var2,Var3)
|
1 2 3 4 5
| push Var3 push Var2 push Var1 call SubRouting add esp,12 ;esp就相当于是栈指针【还是我的个人理解,可能会有偏差,但不影响】
|
- 堆栈平衡也就是:调用者首先把参数放入栈中,然后调用子程序,在完成后,把栈的指针还原到原来的位置。
- 这里有一段源程序【P76】我就不写过来了。
- 其实有点模糊,后面遇到了在来深入理解吧。
数据结构
1 2 3 4 5
| 结构名 struct 字段名1 类型 ? 字段名2 类型 ? ... 结构名 ends
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| WNDCLASSEX STRUCT cbSize DWORD ? style DWORD ? lpfnWndProc DWORD ? cbClsExtra DWORD ? cbWndExtra DWORD ? hInstance DWORD ? hIcon DWORD ? hCursor DWORD ? hbrBackground DWORD ? lpszMenuName DWORD ? lpszClassName DWORD ? hIconSm DWORD ? WNDCLASSEX ENDS
|
1 2 3
| .data? stWndClass WNDCLASS <> ...
|
或者
1 2 3
| .data stWndClass WNDCLASS <1,2,3,4,5,6,7,8,9,10> ...
|
- 这样就能定义按照WNDCLASS为结构的变量stWndClass。
- 如何调用其中的内容呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 1.用点 mov eax,stWndClass.lpfnWndProc 就是变量.字段==>就相当于调用了3【在这里就是】 2.用指针来找到地址 mov esi,offset stWndClass mov eax,[esi+WNDCLASS+lpfnWndProc] //这里要使用WNDCLASS+lpfnWndProc //相当于就是偏移地址
3.用assume伪指令把寄存器预定义为结构指针。 mov esi,offset stWndClass assume esi:ptr WNDCLASS mov eax,[esi].lpfnWndProc ... assume esi:nothing ;当不在使用esi寄存器来做指针的时候,就用这个来取消定义
|
1 2 3 4 5 6
| NEW_WNDCLASS struct
dwOptiond word ? oldWndClass WNDCLASS <>
NEW_WNDCLASS ends
|
1
| mov eax,[esi].oldWndClass.lpfnWndProc
|
高级语法
1.条件测试语句
懒得说了
2.分支语句
1 2 3 4 5
| .if .elseif .else
.endif ;最后需要用这个来表示结束
|
3.循环
1 2 3 4 5
| .while 条件测试表达式 指令 [.break[.if 退出条件]] [.continue] .endw
|
1 2 3 4 5 6 7
| .repeat 指令 [.break[.if退出条件]] [.continue] .until 条件测试表达式(或.untilcxz[条件测试表达式])
;这个和do while 一样
|