文章目录
0. MASM
ide推荐:radasm
m32lib下是一些常用c程序的asm实现。
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
.const
szCaption db 'welcome',0
szText db 'hahahah哈哈哈哈',0
.code
start:
invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK
invoke ExitProcess, NULL
end start
end
最后的end是用来结束文件的。
windows环境变量设置:
@echo off
set Masm32Dir=F:\masm32
set include=%include%;%Masm32Dir%\include;
set lib=%lib%;%Masm32Dir%\lib
set path=%PATH%;%Masm32Dir%\bin;%Masm32Dir%;
rem set Masm32Dir =
echo on
ml /c /coff [/Cp]
link /subsystem:windows test.obj [xx.res]
Makefile:
EXE = Test.exe
OBJS = Test.obj
RES = Test.res
LINK_FLAG = /subsystem:windows
ML_FLAG = /c /coff
$(EXE): $(OBJS) %(RES)
link $(LINK_FLAG) $(OBJS) $(RES)
.asm.obj:
ml $(ML_FLAG) $<
.rc.res:
rc $<
clean:
del *.obj
del *.res
1. 模式定义
这三行几乎不用变。
.386p
是伪指令,声明可以使用特权指令,如mov cr0, eax
。开发驱动时必须用到。
.model 内存模式[, 语言模式] [, 其它模式]
win32程序只有一种内存模式:flat,意思是内存平坦地从0延伸到4GB,没有64KB段大小限制。这种模式的好处是,win32汇编中不再需要段寄存器,32位寄存器足以访问所以4GB空间。
flat模式寄存器值:
ASSUME cs:FLAT, ds:FLAT,ss:FLAT, es:FLAT
fs:ERROR, gs:ERROR
若要使用以上寄存器,改为cs:flat
或cs:nothing
。
语言模式包括调用约定:
_stdcall
,被调用者负责平衡栈,函数最后为ret xxh
。_cdecl
,调用者负责平衡栈,导致exe比_stdcall
大。
2. 导入库
MessageBox的头文件和导入库:
include <user32.inc>
,尖括号可省略。includelib <user32.lib>
,尖括号也可省略。这一句告诉链接器到指定库文件寻找。
库中有等值定义(可理解为宏定义),如windows.inc的MB_OK equ 0h
win32可以使用if..else
.386
.model flat,stdcall
option casemap:none
include windows.inc
include <user32.inc>
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
.const
szCaption db 'welcome',0
szText db 'hahahah你好',0
szOK db 'OK',0
szCancel db 'cancel',0
.code
start:
invoke MessageBox, \
NULL,\
offset szText, \
offset szCaption,\
MB_OKCANCEL
.if eax == IDOK
invoke MessageBox, \
NULL,\
offset szOK, \
offset szCaption,\
MB_OK
.else
invoke MessageBox, \
NULL,\
offset szCancel, \
offset szCaption,\
MB_OK
.endif
invoke ExitProcess, NULL
end start
end
还有.while .endw
3. 段定义
.code .const .data
等是伪指令,win32实际只有代码和数据之分。.const .data
都指向数据段。win32中堆栈自动分配,所以.stack
自动忽略。
数据段
3类数据定义。
- 可读可写的已定义变量,程序加载时就初始化,位于
.data
段; - 可读可写的未定义变量,一般当作缓冲区,程序执行后使用,一般在
.data
段,占用exe空间,也可以放在.data?
段而不占用exe空间,存放在.bss
节区; - 只读常量,也位于
.data
段。
代码段
属性由PE头部属性字段决定。
标号
除了字母数字下划线,还有@ $ ?
;且长度不能超过240.
标号可代替地址,有两种格式:
标号:指令
标号::指令
,可跨子程序,即作用域为整个程序
很多标号只用一两次,这时要用@@
。
mov cx 1000h
je @F
@@:
loop @B
F:forward, B:before,寻找最匹配的一个。
4. 变量
变量的值可改变,所以必须定义在可写段内,如.data, .data?
或堆栈。
变量命名规范:字母,数字,下划线,以及@, ¥, ?
,不以数字开头。
常量:
.const
IDD_DIALOG1 equ 101
数据定义:
.data
...
未初始化数据定义:
.data?
hInstance dd ?
全局变量也是在.data
里。
除了byte(db), word(dw), dword(dd)
,还有以下类型。
名称 | 表示 | 缩写 |
---|---|---|
三字 | fword | df |
四字 | qword | dq |
十字节BCD码 | tbyte | dt |
有符号字节 | sbyte | |
有符号字 | sword | |
有符号双字 | sdword | |
单精度浮点数 | real4 | |
双精度浮点数 | real8 | |
10字节浮点数 | real10 |
局部变量
LOCAL hDltEDT:HWND
,自动开辟4字节,而且函数结尾自动恢复,相当于:
push ebp
mov ebp, esp
sub esp, 4
...
leave
默认dword,所以可省略。
local loc2
数组要用[]
local arr[1024]:byte
局部变量无法初始化。
注意不要和全局变量重名。
使用时要注意类型:
mov eax, bypeData
,实际上相当于mov eax, dword ptr [byteData地址]
。这里最后加上byte ptr
,类似于c的强制类型转换。
获取变量地址
全局变量的地址编译时确定,offset也是编译时将地址带到指令中:
mov REG, offset G_VAR
而局部变量是不确定的,不能用offset,而要用lea:
lea eax, [@var]
如果要在invoke伪指令的参数中用变量的地址,可以用伪操作符addr VAR
:
- 如果是局部变量,相当于
lea eax,[ebp-xxx]
,push eax
,所以要注意addr和eax不要冲突使用。 - 如果是全局变量,相当于offset。
注意,addr只能在invoke时使用,mov eax, addr VAR
是错误用法。
数据结构
定义类型:
WNDCLASSEXA STRUCT
cbSize DWORD ?
style DWORD ?
lpfnWndProc DWORD ?
...
WNDCLASSEXA ENDS
PWNDCLASS TYPEDEF PTR WNDCLASSEXA
声明及赋值:
.data
stWndClass WNDCLASS <>
stWndClass WNDCLASS <1,2,...>
初始化
局部变量数据结构如果只初始化了部分字段,其余字段的值则随机。最好用RtlZeroMemory API
填0.
访问
3种访问方法:直接访问,寄存器访问,assume伪指令预先定义寄存器访问。
mov eax, stWndClass.lpfnWndProc ; 1
mov esi, offset stWndClass ; 2
mov eax, [esi + WNDCLASS.lpfnWndProc]
mov esi, offset stWndClass ; 3
assume esi:ptr WNDCLASS
mov eax, [esi].lpfnWndProc
assume esi:nothing
数组访问:
person PERSON 100 dup(<>)
lea esi , [person] ;
imul eax , 4 , sizeof(PERSON)
add esi , eax
; eax = &person[4].age
lea eax , [esi + PERSON.age]
变量长度
两个伪指令:
- sizeof: 针对变量、数据类型或数据结构
- lengthof: 针对变量,可以取得数据项数。
szHello db 'hello',0dh,0ah
db 'world',0h
sizeof得到7,但输出的话两行都能显示(越界显示)。
所以如果遇到多行的字符串,要用lstrlen计算。
5. 伪指令
刚刚说了两个伪指令,下面再说下条件伪指令和循环伪指令。
.if ...
.elseif ...
.else
.endif
.while ...
.break .if ...
.continue
.endw
.repeat
.break .if ...
.continue
.until ...
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
.const
szCaption db 'welcome',0
szText db '你爱我吗',0
.code
start:
mov eax, IDNO
.while eax == IDNO
invoke MessageBoxA,\
NULL,\
offset szText, \
offset szCaption,\
MB_YESNO or MB_ICONQUESTION
.endw
end start
end
6. 函数
win32参数只有1种:32位整数。
invoke不是80386处理器指令,而是masmb编译器的伪指令,它会展开成n个push参数入栈和一个call,同时检查参数数量。
返回值永远在eax中。如果eax容纳不了,则eax为返回缓冲区的指针。
函数声明
函数声明总是链接失败,目前还没解决。。。。。而且,一般也不会声明,不然修改的话要修改两处。
函数名 proto [距离][语言][可视区域][USES寄存器] [参数1]:数据类型, [参数2]:数据类型...[VARARG]
proto是伪指令,距离有near, far, near16, near32, far16, far32
,win32可忽略。
语言默认.model
的约定。
可视区域:PRIVATE, PUBLIC, EXPORT
USES的话,倒不如pushad。。。
参数的数据类型永远是dword。
汇编里使用vararg的大概只有printf族吧。
MessageBoxW proto hWnd:dword, lpText:dword, lpCaption:dword, uType:dword
if UNICODE
MessageBox equ <MessageBoxW>
else
MessageBox equ <MessageBoxA>
endif
函数定义
funcname proc hDlg:HWND, dwVar:DWORD
ret
funcname endp
调用约定
__cdecl
c默认的函数调用方法。
- 参数从右到左入栈
- 调用者清栈(手动清栈),
add esp, xxx
所以,参数由调用者维护,可变参数函数只能用此约定。
输出函数名前会加上一个下划线前缀。
crt_
函数开头的都是这种方式。
__stdcall
c++标准调用方式。
- 参数从右到左入栈,成员方法
this
最后入栈; - 被调用者清栈(自动清栈),
retn x
,x是参数占用的字节数
函数编译时必须确定并控制参数个数,否则返回出错。
函数名格式:_funcname@参数字节数
__fastcall
左边开始的两个不大于4字节的(dword)参数分别放在ecx和edx寄存器,,其余参数仍然用栈。
被调用者清栈,retn x
。
函数名格式:@funcname@参数字节数
__thiscall
仅用于c++成员函数。__thiscall
不是关键字,所以不能被程序员指定。
传参和返回与stdcall一样。只是this用ecx传递。
Borland c++编译器使用eax存储this。
代码验证
#include <iostream>
class C
{
public:
void func()
{
std::cout << this << std::endl;
}
};
void func_c(char arg)
{
int a;
}
void __stdcall func_std(char arg0, int arg1)
{
int a;
}
void __fastcall func_fast(char arg0, int arg1, int arg2, int arg3)
{
int a;
}
int main()
{
C c;
c.func();
func_c(0);
func_std(1, 2);
func_fast(3, 4, 5, 6);
return 0;
}
C c;
c.func();
009742D8 8D 4D F7 lea ecx,[c]
009742DB E8 3E CD FF FF call C::func (097101Eh)
func_c(1);
009742E0 6A 01 push 0
009742E2 E8 50 CD FF FF call func_c (0971037h)
009742E7 83 C4 04 add esp,4
func_std(1, 2);
009742EA 6A 02 push 2
009742EC 6A 01 push 1
009742EE E8 87 D0 FF FF call func_std (097137Ah)
func_fast(3, 4, 5, 6);
009742F3 6A 06 push 6
009742F5 6A 05 push 5
009742F7 BA 04 00 00 00 mov edx,4
009742FC B1 03 mov cl,3
009742FE E8 81 D0 FF FF call func_fast (0971384h)
func_std汇编代码:
void __stdcall func_std(char arg0, int arg1)
{
009718D0 55 push ebp
009718D1 8B EC mov ebp,esp
009718D3 81 EC CC 00 00 00 sub esp,0CCh
009718D9 53 push ebx
009718DA 56 push esi
009718DB 57 push edi
009718DC 8D BD 34 FF FF FF lea edi,[ebp-0CCh]
009718E2 B9 33 00 00 00 mov ecx,33h
009718E7 B8 CC CC CC CC mov eax,0CCCCCCCCh
009718EC F3 AB rep stos dword ptr es:[edi]
int a;
}
009718EE 5F pop edi
int a;
}
009718EF 5E pop esi
009718F0 5B pop ebx
009718F1 8B E5 mov esp,ebp
009718F3 5D pop ebp
009718F4 C2 08 00 ret 8
func_fast汇编代码
void __fastcall func_fast(char arg0, int arg1, int arg2, int arg3)
{
009718A0 55 push ebp
009718A1 8B EC mov ebp,esp
009718A3 81 EC E4 00 00 00 sub esp,0E4h
009718A9 53 push ebx
009718AA 56 push esi
009718AB 57 push edi
009718AC 51 push ecx
009718AD 8D BD 1C FF FF FF lea edi,[ebp-0E4h]
009718B3 B9 39 00 00 00 mov ecx,39h
009718B8 B8 CC CC CC CC mov eax,0CCCCCCCCh
009718BD F3 AB rep stos dword ptr es:[edi]
009718BF 59 pop ecx
009718C0 89 55 EC mov dword ptr [arg1],edx
009718C3 88 4D F8 mov byte ptr [arg0],cl
int a;
}
009718C6 5F pop edi
009718C7 5E pop esi
009718C8 5B pop ebx
009718C9 8B E5 mov esp,ebp
009718CB 5D pop ebp
009718CC C2 08 00 ret 8
如果没有arg2和arg3的话,最后依据就是ret,而不是ret 8.
而且可以发现,最终还是把ecx和edx的值传到内存中了。
7. 混合编程
x86混合编程
__asm pushad
__asm{
pushad;
popad;
}
__asm _emit 0x90;
插入二进制数据nop。
_emit
可以用来混淆。
__asm{
jmp sign;
_emit 0x56
_emit 0x1B
sign:
mov eax, 1
}
跳到sign后,指令会改变。
定义一个有多行汇编指令的宏时,一定要交叉使用,否则编译错误。
x64混合编程
可以混合,但不能内联。
vs的x64编译器不支持内联汇编。两种方法:
- 把vs的编译器换成intel的编译器(收费)。
- 链接汇编obj文件
第二种,需要在工程下创建asm文件:
.code
func proc
push rbp
...
pop rbp
ret
func endp
end
项目右键-生成依赖项-生成自定义-masm
asm文件属性-项类型-Microsoft Macro Assembler
asm高亮可以在扩展与更新中下载AsmDude
c文件中声明,extern "C" int func();
.code
myadd proc
mov rax, rcx
add rax, rdx
ret
myadd endp
end
#include <stdio.h>
extern int myadd(int a, int b);
//extern "C" int myadd(int a, int b);
int main()
{
printf("%d\n", myadd(1,2));
return 0;
}
裸函数
除了cc,不含任何编译器生成的汇编指令,所有指令自己写。
void __declspec(naked) func(){__asm ret}
即空函数,在内存中仅仅是一条地址。
int _declspec(naked) fun(int a,int b)
{
_asm
{
push ebp;
mov ebp, esp;
mov eax, [ebp + 8];
add eax, [ebp + 0xc];
mov esp, ebp;
pop ebp;
ret
}
}
int main()
{
int m =fun(10,20);
}
下面的代码,会报错,“不允许使用初始化的 auto 或 register 变量”。
void _declspec(naked) func()
{
int n = 0;
for (int i = 0; i < 10; ++i)
n += i;
}