Win32汇编源程序的结构

  • 通过helloword走进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.指令集

1
2
3
.386
.386p
.......
  • 后面带有p的伪指令,表示可以使用特权指令【0级】

2.互作模式

1
2
.model 内存模式[,语言模式][,其他模式]
内存模式的定义影响最后生成的可执行文件,可执行文件的规模从小到大,可以有很多种类型。
  • windows程序运行在保护模式下,系统把每一个win32应用程序都放到分开的虚拟地址空间中去运行,耶就是说,每一个应用程序都拥有其相互独立的4GB地址空间
  • 对于win32程序来说,只有一种内存模式,也就是flat模式

image-20230421101920721

  • 在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文件 ,把代码段的属性位改成可写的,那么就可以修改自己的代码段了
  • 一个典型的引用就是针对可执行文件的压缩软件和加壳软件【UPX和PoCompact……】

  • 这些软件靠把代码进行变换来达到解压缩和解密的目的,被处理过的可执行文件在执行的时候需要由解压代码来讲代码段解压缩

  • 这就需要写代码段咯

3.程序结束和程序入口

1
2
3
4
end start
这个在一个程序里面的话,就需要写这个,来指明结束和入口地址
但是:
如果有多个模块,需要把这些模块都链接在一起的时候,就只能有一个主模块指定入口地址,不然入口太多会爆炸

4.注释

1
2
用 ;
在字符串里面的 ; 不是注释

5.换行

1
2
用 (\) 做换行符
能提高阅读性罢了

调用API函数

1.首先,说明是API?

1
应用函数接口
1
win32程序是架构在win32api基础上的
1
windows自身的运行也调用这个api函数

2.调用API函数

  • api函数,它实际上就是一种新的方法代替了DOS下的中断

  • DLL【动态链接库文件】是一种windows的可执行文件,采用的是和我们熟悉.exe文件同样的PE约定格式

1
依靠dll文件,程序才能和内核联系

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函数的参数

  • win32api是用堆栈来传递函数的,dll中的函数程序从堆栈中取出参数进行处理,并且在返回之前讲堆栈中已经无用的参数丢去【从右边往左压入栈中 】

  • 参数,参数名字很多,但就只是用于定义这个参数的大小,而不是一定要怎样怎样,参数的名字只是用来描述这个参数的作用罢了,实质上是开辟一个空间的大小

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
//这样是用汇编的格式来表达
  • 在汇编中调用MessageBox函数的方法
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
  • 上面这2种是一样的

6.)include语句

  • 用include来声明某些函数

  • .inc 文件

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 <库文件名>

image-20230421190312235

1
2
咋说呢,我理解的是,函数是存在.dll中
然后我们导入的是.lib,这个里面存放的是.dll的地址信息啥的

3.API参数中的等值定义

这里看书去吧【忘了写笔记】,p62

在masm下,我们可以用.if .elseif .else

标号,变量,数据结构

1.标号

1.)标号的定义

  • 标号–>地址
1
2
1.标号名:目的指令
2.标号名::目的指令
  • 1.只能在某一个子程序中跳
  • 2.可以从一个子程序跳到其他子程序中,不和谐

2.)MASM中的@@

  • @@用于某些时候标号只会使用1.2次
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?

image-20230421204810538

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码

image-20230421205538518

2.)全局变量的初始化

  • 在不想初始化的时候,可以用问好来预留空间
  • 在data?段中只能用?来预留空间
  • 并且未初始化的值都是 0

3.变量–局部变量

  • 也就是用在函数内部的变量

  • 作用域是单个子程序

  • 这些变量就放在堆栈里面,在进入子程序的时候,通过堆栈指针esp来预留出空间,在使用ret指令返回主程序之前,同样通过恢复esp丢弃这些控件,这些变量就变得无效了

  • 由于空间是临时分配的,所以无法定义含有初始化值的变量,对局部变量的初始化一般在子程序中由指令完成。

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
1
类型:也就是占用空间的大小

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
  • 这些指令是必须的,前面的用于局部变量的准备工作,后面的用于扫尾工作
1
2
push ebp
mov ebp,esp
  • 把ebp中的内容保存在栈里 面,然后把esp的内容给ebp,供存取局部变量做指针
  • 在这个例子中一共是7个字节的大小来保存局部变量,所以esp这个指针要往下移动
1
这里就是 add esp,fffffff8
  • 为什么是腾出8个字节的空间呢?
1
因为80386按照dword为界对其,运行速度最快。
1
在子程序中,该esp 而不该 ebp?
  • leave指令的功能
1
2
mov esp,ebp		;在里面使用esp , 原先的esp是放在ebp中的,最后把内容返回给它
pop epb ;在把ebp弹回去

3.)局部变量的初始化值

  • 局部变量不初始化,得到的是一个奇奇怪怪的值【也就是指针指向的自带的值,之前的】

  • 解决这个问题,就先全部都初始化为0

1
RtlZeroMemory 这个API函数就可以做到该功能

4.变量的使用

1.)以不同的类型访问变量

  • 类似和C语言中的数据类型强制转化

  • 但是在masm中,它不会自动帮忙转。

  • 在masm中,变量类型不一样的话就会报错

1
2
3
4
5
比如:
sz db 1024 dup(?)
mov ax,sz

这样就会报错
  • 所以需要使用
1
类型 ptr 变量名
  • 这样就可以把变量转化成你所想要的类型
1
mov ax,word ptr sz
  • 要注意的是,在变量报错的时候,是高放高,低放低
1
2
3
4
5
		.data
a db 12h
b dw 1234h
c dd 12345678h
在这样的在内存中保存的是
1
12 34 12 78 56 34 12
  • 在使用转换类型的时候,其实它不是转换类型,而是把地址的范围改变了,比如之前的是1个字的,转换成2个字,它的内容就会往后延申罢了
1
肯定还是要遵守 高地址放高  低地址放低
  • 它只是用了变量的地址,所需要的变量的大小是取决于其他的东西

  • 2个简单的命令

1
2
3
4
5
6
1.movzx	将变量扩展到应该有的大小
这个是直接在高位放0
2.movsx 将变量扩展到应该有的大小
这个是带有符号位的扩展
最高位是0,就扩展0 //最高位就是符号的标识
最高位是1,就扩展1

image-20230422175319183

2.)变量的尺寸和数量

  • 首先是2个伪指令
1
2
3
4
1.sizeof
对于变量名,数据结构,数据结构名
2.lengthof
对于变量名
  • 1.sizeof去识别某变量的长度
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
mov 寄存器,offset 变量名
  • 1.这个offset就是取地址的伪操作符。【只能用于全局变量,不能用于局部变量】
  • 为什么呢?
1
2
这个地方就要讲究一个先后顺序的操作了,伪指令是在编译器的时候进行,但是指令是在CPU来操作。
局部变量,在编译的时候无从得知它的地址,所以不能用咯
  • 2.所以在获取局部变量的地址的时候,就需要用到 lea 指令【该指令是CPU指令】
  • 对于局部变量,它是用ebp来做指针进行操作的
  • lea的原理:
1
在使用lea的时候,按照ebp的值实际计算出来,然后放在eax中
1
2
就比如:lea	eax,[ebp-4]
这样就能获取一个局部变量的地址
  • 3.在invoke伪指令获取参数的时候,就也不能用lea了….【这个invoke 相当于有参数的call】

  • 哦豁,这下怎么办呢?

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中的值
1
表示参数的使用方式,和堆栈平衡的方式
  • 【可视区域】——有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来调用子程序咯。

  • 在调用子程序的时候,需要首先声明子程序

1
用 proto 来声明子程序
  • 也可以 把子程序写在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就相当于是栈指针【还是我的个人理解,可能会有偏差,但不影响】
  • 堆栈平衡也就是:调用者首先把参数放入栈中,然后调用子程序,在完成后,把栈的指针还原到原来的位置。

image-20230422220257841

  • 这里有一段源程序【P76】我就不写过来了。
  • 其实有点模糊,后面遇到了在来深入理解吧。

数据结构

  • 几乎所有的API所涉及的数据结构在windows.inc文件中都有定义

  • 偷个懒,直接上图片【哈哈哈】

  • 在C语言中

image-20230422222559450

  • 在汇编中定义一个数据结构
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
  • 这里中间这个部分就是嵌套,tmd。嵌套,可以理解成把这个字段啥的,全部都copy过来

  • 假设esi指向了NEW_WNDCLASS,那么要引用oldWndClass里面的某个字段

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 一样