win32汇编基础

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:flatcs: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),还有以下类型。

名称表示缩写
三字fworddf
四字qworddq
十字节BCD码tbytedt
有符号字节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;
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值