0%

32位汇编程序设计 - 基础

Win32汇编本质是对API的封装,到了Win32阶段学习汇编已经和汇编关系不是很大了,学习更多的为了以后和C的混编;

运行环境:

  • 操作系统: Windows 10家庭版
  • 编译器:Windows XP Debug

32位汇编基础

Win32汇编程序结构

C或C++是一种高级语言,在高级语言中,不必为堆栈段、数据段、代码的定义而担心,编译器会把程序中的字符串和代码分别安排到相应的位置,为了找到它们,在DOS汇编中,我们需要自行安排:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;堆栈段
stack segment stack
db 100 dup (?)
stack ends

;数据段
data segmnet
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
code ends
end start

但是在Win32汇编中,程序结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	.386
.model flat,stdcall
option casemap:none
;include文件定义
include C:\\Tools\\RadASM\\masm32\\include\\windows.inc
include C:\\Tools\\RadASM\\masm32\\include\\kernel32.inc
include C:\\Tools\\RadASM\\masm32\\include\\user32.inc
includelib user32.lib
includelib kernel32.lib

;数据段
.data
szCapTion db 'A MessageBox !',0
szText db 'Hello,World',0

;代码段
.data
szCapTion db 'A MessageBox !',0
szText db 'Hello,World',0

模式定义

32位汇编在头部定义模式:

1
2
3
.386			;伪指令,用于告诉编译器在本程序中使用的指令集
.model flat,stdcall
option casemap:none

指令集

在新的8086CPU上,增加了MMX定义.486或.586,为了使用MMX指令,除了定义.586以外,还要加上一句.mmx伪指令

model语句

用于定于程序的工作模式;

1
.model	内存模式[,语言模式][,其他模式]

内存模式影响生成的可执行文件大小,可执行文件的大小,可以有很多种类型,在DOS下,有只用到64KB的.com文件。在Win32下,可以用4GB内存的PE格式可执行文件;

Windows运行在保护模式中,对WIn32程序来说,只有一种内存模型,即flat模式;

option语句

在Win32汇编中,需要的只是定义option casemap:none,这个语句定义了程序中的变量和子程序是否对大小写敏感;

段的定义

数据段

Win32中只有代码和数据的区别;

.data是可读可写的已定义变量,这些数据在程序中已给定初值;

.data?是可读可写的未定义变量,这些变量一般是当作缓冲区或者程序执行后开始使用;

.const中定义常量;它是可读不可写的;

代码段

.code段是代码段,所有指令必须写在代码段中,在可执行文件中。Win32中数据段是不可执行的,.code段是不可写的;

堆栈段

Win32程序中不必定义堆栈段,系统会自动分配堆栈空间,堆栈段的内存属性是可读可写可执行的;

调用API

调用函数

1
2
;调用函数: invoke 函数名 , 参数 , 参数····

API函数返回值只有一种dword,永远放在EAX寄存器中。或提供一个地址。

函数声明

在调用API时,函数原型也必须提前声明;

1
2
函数名 proto [距离] [语言] [参数1]:数据类型,[参数2]:数据类型,···
函数名 proto [参数1]:数据类型,[参数2]:数据类型,···

include语句

对于所用到的API函数,在程序的开始必须初始化;

标号、变量和数据结构

当程序需要跳转到另一个位置的时候,需要有一个标识来指示新的位置,这就是标号;

MASM中变量的定义:

  • 可以使用字母、数字、下划线及符合@,&
  • 第一个符号不可以是数字
  • 长度不能超过240个字符
  • 不能使用指令关键字
  • 在作用域中必须是唯一的

标号

当在程序中使用跳转指令时,可以用标号来标识跳转的目的地。编译器在编译时会把它替换为地址;

1
2
3
标号名:	目的指令

标号名:: 目的指令 ;从一个子程序跳转到另一个子程序的指令

@@

在DOS中,标号起名很麻烦,因为汇编使用的跳转非常多;

全局变量

全局变量定义

全局变量的作用域是整个程序,Win32汇编得全局变量定义在.data或.data?段内,可以同时定义变量的变量的类型和长度:

1
2
变量名	类型	初始值1,初始值2
变量名 类型 重复数量 dup (初始值1,初始值2)

全局变量的初始化值

全局变量在定义中既可以指定初值,也可以只是用问号预留空间,在data?段中,只能用问好预留空间,因为.data?段中不能指定初始值;

局部变量

局部变量出现后,两个以上子程序都要用到的数据才会被定义为全局变量并统一存入数据段中,仅在子程序内部使用的变量则存入堆栈段中;

局部变量定义

1
2
3
4
local	变量名1[重复数据][:类型],变量名2,[重复数据][:类型]
local loc1[1024]:byte ;1024字节长的局部变量loc1
local loc2 ;默认为DWORD类型
local loc3:WNDCLASS ;定义一个位WNDCLASS的结构体

局部变量使用典型例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
	.386
.model flat,stdcall
option casemap:none

include C:\\Tools\\RadASM\\masm32\\include\\windows.inc
include C:\\Tools\\RadASM\\masm32\\include\\kernel32.inc
include C:\\Tools\\RadASM\\masm32\\include\\user32.inc
includelib user32.lib
includelib kernel32.lib


.data
szCapTion db 'A MessageBox !',0
szText db 'Hello,World',0
wHour dw ?
wMinute dw 10
_hWnd dd ?
word_Buffer dd 100 dup (1,2)
szBuffer byte 1024 dup (?)



.code

TestProc proc
local @loc1:DWORD,@loc2:WORD
local @loc3:BYTE
mov eax,@loc1
mov ax,@loc2
mov al,@loc3
ret
TestProc endp
start:
invoke MessageBox,NULL,offset szText,offset szCapTion,MB_OK
invoke ExitProcess,NULL
call TestProc
end start

我们使用OD调试器查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00401000  /.  55            push    ebp
00401001 |. 8BEC mov ebp, esp
00401003 |. 83C4 F8 add esp, -0x8
00401006 |. 8B45 FC mov eax, dword ptr [ebp-0x4]
00401009 |. 66:8B45 FA mov ax, word ptr [ebp-0x6]
0040100D |. 8A45 F9 mov al, byte ptr [ebp-0x7]
00401010 |. C9 leave
00401011 \. C3 retn
00401012 >/$ 6A 00 push 0x0 ; /Style = MB_OK|MB_APPLMODAL
00401014 |. 68 00304000 push 00403000 ; |Title = "A MessageBox",TAB,"!"
00401019 |. 68 0F304000 push 0040300F ; |Text = "Hello,World"
0040101E |. 6A 00 push 0x0 ; |hOwner = NULL
00401020 |. E8 0D000000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA
00401025 |. 6A 00 push 0x0 ; /ExitCode = 0x0
00401027 \. E8 0C000000 call <jmp.&KERNEL32.ExitProcess> ; \ExitProcess

局部变量必须使用的指令,分别用于局部变量的准备和收尾,执行了call指令后,CPU把返回的地址压入堆栈,在转入子程序指令,ESP寄存器在程序指令中随时可以用到,不可能用ESP寄存器来寻址,所以使用EBP做寻址指针,在初始化之前,先push ebp指令把原来的ebp保存起来。然后将ESP存入EBP中,供存取局部变量做指针用;

数据结构

数据结构的本质是多个字段组成的数据”样板”,相当于一种自定义的数据类型;

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _WNDCLASS { 
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS, *PWNDCLASS;

C语言中的WNDCLASS定义;

我们查看汇编中的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
;前提假设结构名为WNDCLASS,结构体变量名为stWndClass,里面有字段lpfnWndProc

;1
mov eax,stWndClass.lpfnWndProc

;2.esi寄存器作指针寻址
mov esi,offset stWndClass
mov eax,[esi + WNDCLASS.lpfnWndProc] ;注意这里是WNDCLASS

;3.用assume伪指令把寄存器预先定义为结构指针
mov esi,offset stWndClass
assume esi:ptr WNDCLASS
mov eax,[esi].lpfnWndProc
...
assume esi:nothing ;注意:不使用esi做指针的时候需要用这句取消定义

;4.结构的定义可以嵌套
NEW_WNDCLASS struct

dwOption word ?
oldWndClass WNDCLASS <>

NEW_WNDCLASS ends

;5.嵌套的引用
mov eax,[esi].oldWndClass.lpfnWndProc

局部变量使用

1
2
3
4
5
szBuffer	db	1024	dup	(?)
mov ax,szBuffer ;操作错误,因为ax寄存器是一个WORD,无法存放DBYTE
mov ax,WORD ptr szBuffer
mov eax,DWORD ptr szBuffer

如果需要用指定类型之外的长度访问变量,必须显式的指出要访问的长度

movzx

把一个字节扩展到一个字或一个字或一个双字再放到ax或eax中,高位保持0而不是越界存取到其他的变量

1
2
3
4
5
6
7
8
9
10
movzx ax,bBest1
movzx eax,bBest1
movzx eax,cl
movzx eax,ax

反汇编代码查看:
00401012 | 66:0FB605 43374000 | movzx ax,byte ptr ds:[403743] |
0040101A | 0FB605 43374000 | movzx eax,byte ptr ds:[403743] |
00401021 | 0FB6C1 | movzx eax,cl |
00401024 | 0FB7C0 | movzx eax,ax |

movzx指令将扩展的数据位用0替代;

movsx指令完成带符号位的扩展,当被扩展数据最高位为0时,效果和movzx相同,最高位为1时,则扩展部分数据全部用1填充;

获取变量长度

1
2
3
4
5
6
7
8
9
10
11
stWndClass	WNDLCASS	<>
szHello db 'HelloWorld!',0
dwTest dd 1,2,3,4

Test02Proc proc
mov eax,sizeof stWndClass
mov ebx,sizeof WNDCLASS
mov ecx,sizeof szhello
mov edx,sizeof DWORD
mov esi,sizeof dwTest
Test02Proc endp

**sizeof 变量名、数据类型或数据结构名 ;取得变量、数据类型或数据结构以字节为单位的长度 **

lengthof 变量名、数据类型或数据结构名 ;取得变量中数据的项数

获取变量地址

获取变量地址的操作对于全局变量和局部变量是不同的;

对全局变量来说,它的地址在编译时已经由编译器确定了,它的用法使用offset

1
mov 寄存器,offset 变量名

offset是取变量地址的伪操作符,和sizeof伪操作符一样;

对于局部变量,它是用于EBP来取值的,所以局部变量需要使用LEA指令来取指针地址;

1
lea	eax,[ebp-4]

如果需要在INVOKE伪指令的参数中需要使用局部变量的地址,使用addr指令;

1
addr	局部变量和全局变量名

当addr后加全局变量名时,编译器则自动按照offset的方法调用;

子程序

当程序中相同的代码使用的较为频繁时,我们可以将它的代码分离为一个子程序,在主程序中使用call指令调用;

子程序定义

1
2
3
4
5
6
子程序名 proc [距离][语言类型][可视区域][USES寄存器列表][,参数:类型]...[VARARG]
local 局部变量列表

指令

子程序名 endp

参数传递和堆栈平衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Sub1	proc	C	_Var1,_Var2
mov eax,_Var1
mov ebx,_Var2
ret
Sub1 endp

Sub2 proc PASCAL _Var1,_Var2
mov eax,_Var1
mov ebx,_Var2
ret
Sub2 endp


Sub3 proc _Var1,_Var2
mov eax,_Var1
mov ebx,_Var2
ret
Sub3 endp
start:
invoke MessageBox,NULL,offset szText,offset szCapTion,MB_OK
invoke Sub1,1,2
invoke Sub2,1,2
invoke Sub3,1,2
invoke ExitProcess,NULL

end start

反汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
;Sub1	C类型
00401000 <subroutine.sub_401000> | 55 | push ebp |
00401001 | 8BEC | mov ebp,esp |
00401003 | 8B45 08 | mov eax,dword ptr ss:[ebp+8] |
00401006 | 8B5D 0C | mov ebx,dword ptr ss:[ebp+C] |
00401009 | C9 | leave |
0040100A | C3 | ret |
;Sub2 PASCAL类型
0040100B <subroutine.sub_40100B> | 55 | push ebp |
0040100C | 8BEC | mov ebp,esp |
0040100E | 8B45 0C | mov eax,dword ptr ss:[ebp+C] |
00401011 | 8B5D 08 | mov ebx,dword ptr ss:[ebp+8] |
00401014 | C9 | leave |
00401015 | C2 0800 | ret 8 |
;Sub3 StdCall
00401018 <subroutine.sub_401018> | 55 | push ebp |
00401019 | 8BEC | mov ebp,esp |
0040101B | 8B45 08 | mov eax,dword ptr ss:[ebp+8] |
0040101E | 8B5D 0C | mov ebx,dword ptr ss:[ebp+C] |
00401021 | C9 | leave |
00401022 | C2 0800 | ret 8 |
;invoke Sub1 C类型
00401025 <subroutine.EntryPoint> | 6A 02 | push 2 |
00401027 | 6A 01 | push 1 |
00401029 | E8 D2FFFFFF | call <subroutine.sub_401000> |
0040102E | 83C4 08 | add esp,8 |
;invoke Sub2 PASCAL类型
00401031 | 6A 01 | push 1 |
00401033 | 6A 02 | push 2 |
00401035 | E8 D1FFFFFF | call <subroutine.sub_40100B> |
;invoke Sub3 Stdcall类型
0040103A | 6A 02 | push 2 |
0040103C | 6A 01 | push 1 |
0040103E | E8 D5FFFFFF | call <subroutine.sub_401018> | |

高级语法

在高级语言中,所有的分支和循环语句都必须使用条件测试;

MASM条件测试的基本表达式:

寄存器或变量 操作符 操作数

1
2
3
4
5
CARRY?        表示Carry位是否置位
OVERFLOW? 表示Overflow位是否置位
PARITY? 表示Parity位是否置位
SIGN? 表示Sign位是否置位
ZERO? 表示Zero位是否置位

分支语句

1
2
3
4
5
6
7
8
9
.if eax && (bx >= dWX) || !(dWY != ecx)
mov esi,1
.elseif edx
mov esi,2
.elseif esi & 1
mov esi,3
.elseif ZERO? && CARRY?
mov esi,4
.endif

循环语句

1
2
3
4
5
6
7
8
9
10
11
.while 条件测试表达式
指令
[.break [.if 退出条件]]
[.continue]
.endw
;或
.repeat
指令
[.break [.if 退出条件]]
[.continue]
.until 条件测试表达式 (或 .untilcxz [条件测试表达式])

代码风格

b 表示byte
w 表示word
dw 表示dword
h 表示句柄
lp 表示指针
sz 表示以0结尾的字符串
lpsz 表示指向0结尾的字符串的指针
f 表示浮点数
st 表示一个数据结构

举例

hWinMain 主窗口的句柄
dwTimeCount 时间计数器,以双字定义
szWelcome 欢迎信息字符串,以0结尾
lpBuffer 指向缓存区的指针
stWndClass WNDCLASS结构