基于Win64的Masm64函数设计
在Win64的API中,函数调用方法非常灵活,所以用户自己编写的函数也应该符合这种要求,特别是编写库函数,如果不符要求,则会给使用者带来很多麻烦。
在Masm64中,调用函数基本方法为以下3种:
(1) Invoke 宏。这是一个完整的自动化调用宏,可以调用任何函数,但编译器处理的速度也是最慢的,不过没什么关系。要注意的是,如果将寄存器RAX作为参数,则只能用在前4个参数中。
(2) rcall 宏。如果函数的参数不超过4个,则可使用该宏调用。这个宏自动化程度比较低,不支持引号表示的文本,也不支持ADDR操作符,其他与Invoke宏的要求相同。编译器处理该宏的速度比较快。
(3) call 指令。使用指令来调用函数,需要调用者自己处理全部参数,如果参数个数少且为整数类参数,则是非常快速的。
这三种调用方法都遵从一条约定,即前4个参数分别由寄存器RCX、RDX、R8、R9来传递,不放入栈单元,但即保留了栈单元空间。但另一方面,每个函数在创建默认栈帧时,会根据情况来处理前4个参数:当函数有参数名时,将前4个参数置入对应的栈单元中;当函数无参数名时,不处理前4个参数。而我们并不一定总是使用默认栈帧,因此在编写函数时常会造成失误,所以有必要建立一些函数模型,以避免错误的发生。
以下为五种常用的函数形式。
1. 通用函数形式
MyFunction1 proc arg1:QWORD,arg2:QWORD,arg3:QWORD,arg4:QWORD,arg5:QWORD
LOCAL ss_a1:QWORD
LOCAL ss_a2:QWORD
; ......
ret
MyFunction1 endp
(1)这种函数形式与Masm32函数形式相同,所不同的是在Masm64中他是通过默认栈帧构建宏来实施的,所以必须要包含STACKFRAME宏。一般我们会使用Masm64系统预制的包含文件masm64rt.inc,该文件中已包含了STACKFRAME宏。如果你自己定制包含文件(*.inc),则也必须添加STACKFRAME宏。STACKFRAME宏是被编译器调用的(见《masm64栈帧结构的详解》)。
(2)这种函数形式的参数个数可以超过4个,也可以不用参数;局部变量根据需要来设置。
(3)可以使用任何一种调用方式。如果使用call指令调用,则形式如下:
mov rcx,111 ;第1个参数
mov rdx,112 ;第2个参数
mov r8, 113 ;第3个参数
mov r9, 114 ;第4个参数
mov rax,115
mov [rsp+4*8],rax ;第5个参数
call MyFunction1
调用者并没有将前4个参数置入栈中,但编译器在构建MyFunction1函数的栈帧时会将前4个参数置入到栈中。
2. 无参数名函数形式
MyFunction2 proc ;arg1:QWORD,arg2:QWORD,arg3:QWORD,arg4:QWORD
LOCAL arg1:QWORD
LOCAL arg2:QWORD
LOCAL arg3:QWORD
LOCAL arg4:QWORD
;---使用调用者为其保留的4个参数的栈空间---
mov [rbp+16],rcx
mov [rbp+24],rdx
mov [rbp+32],r8
mov [rbp+40],r9
;---更为安全的方法---
mov arg1,rcx
mov arg2,rdx
mov arg3,r8
mov arg4,r9
; ......
ret
MyFunction2 endp
(1)这也是使用默认栈帧的函数形式,基本要求与"通用函数形式"相同,但函数参数个数不能超过4个。
(2)这种函数是不使用参数名的,将参数作为注释,以便解读函数。
(3)虽然不使用参数名,但调用者为其保留了4个参数的栈空间供函数使用,这是一种约定。也就是说,凡需要调用其他函数的,调用者必须是有栈帧的。如果调用者没有栈帧,被调用函数就不能使用参数空间了。所以一种更安全的方法,是根据需要将前4个参数保存到局部变量中,这样,不管调用者是否有栈帧均不会有问题,这只是为了容错考虑。
注意,调用者如果没有栈帧,则不能调用API函数,因为API函数没有这类的容错考虑。
(4)可以使用任何一种调用方式。例:
invoke MyFunction2,201,202,203,204
因为MyFunction2函数没有参数名,所以invoke宏会将参数分别赋给rcx、rdx、r8、r9,但不置入栈中。
3. 自定义栈帧函数形式
NOSTACKFRAME ;关闭默认栈帧构建宏
MyFunction3 proc a1:QWORD,a2:QWORD,a3:QWORD,a4:QWORD,a5:QWORD
LOCAL ss_a1:QWORD
LOCAL ss_a2:QWORD
LOCAL ss_a3:QWORD
use_stack 24,16,0 ;构建自定义栈帧
;---保存前4个参数---
mov a1,rcx
mov a2,rdx
mov a3,r8
mov a4,r9
; ......
leave ;释放栈帧并恢复rbp
ret
MyFunction3 endp
STACKFRAME ;开启默认栈帧构建宏(这样就不影响后面的函数)
(1)自定义栈帧的构建原理在《masm64栈帧结构的详解》中已介绍过,这里直接调用一个自定义的栈帧构建宏use_stack。在函数返回前必须用leave指令释放栈帧并恢复RBP指针。
(2)这种函数的前4个参数是空的,如果想使用它,就必须自己将参数值保存到对应的参数名中。
(3)可以使用任何一种调用方式。例:
invoke MyFunction3,301,302,303,304,305
(4)自定义的栈帧构建宏
;======================================
;栈帧构建宏
;入: localsize=局部变量空间尺寸(字节数)
; algn=局部变量内存对齐尺寸
; =0或省略时默认为16字节
; arrn=参数区空间尺寸,以QWORD为单位
; 省略时为16个QWORD为单位(128字节)
;======================================
use_stack MACRO localsize:=<0>,algn:=<16>,arrn:=<16>
LOCAL num,n
num=arrn*8
enter num, 0 ;构建参数栈空间。
n=algn
IFE n
n=16
ENDIF
num = localsize + n - 1
num = num AND -n ;对齐后的长度
sub rsp, num ;局部变量栈空间
ENDM
4. 零栈帧函数形式
NOSTACKFRAME ;关闭默认栈帧构建宏
MyFunction4 proc a1:QWORD,a2:QWORD,a3:QWORD,a4:QWORD,a5:QWORD
ENTER 0,0 ;这样做,函数的参数名就可以使用了。
;---保存前4个参数---
mov a1,rcx
mov a2,rdx
mov a3,r8
mov a4,r9
......
LEAVE ;恢复rbp这是必须的。
ret
MyFunction4 endp
STACKFRAME ;开启默认栈帧构建宏(这样就不影响后面的函数)
(1)这种函数其实是自定义栈帧函数的一种特例,即自定义一个空栈帧,其目的是为了使用函数的参数名。
(2)除了可以使用函数参数名外,与无栈帧函数是一样的,即不能有局部变量,也不能调用其它函数。
(3)可以使用任何一种调用方式。
5. 无栈帧函数形式
NOSTACKFRAME ;关闭默认栈帧构建宏
MyFunction5 proc ;a1:QWORD,a2:QWORD,a3:QWORD,a4:QWORD,a5:QWORD
;---使用调用者为其保留的4个参数的栈空间---
mov [rsp+8+8*0],rcx
mov [rsp+8+8*1],rdx
mov [rsp+8+8*2],r8
mov [rsp+8+8*3],r9
;---访问后面的参数---
mov rax,[rsp+8+8*4] ;这是参数a5
......
ret
MyFunction5 endp
STACKFRAME ;开启默认栈帧构建宏(这样就不影响后面的函数)
(1)无栈帧函数不能有局部变量,也不能调用其它函数。
(2)无栈帧函数的参数名是不能使用的,你可以将它注释掉,不注释掉也没关系,但还是不能使用。这种函数的参数空间必须使用RSP来访问(注意,不能使用RBP),RSP所指的第一个QWORD值为返回地址,[rsp+8]为函数的第一个参数,以次类推。
(3)同样的道理,前4个参数并没有置入栈空间,但这4个QOWRD栈空间可以用于保存参数。
(4)可以使用任何一种调用方式。如果使用call指令调用,则形式如下:
mov rcx,411 ;第1个参数
mov rdx,412 ;第2个参数
mov r8, 413 ;第3个参数
mov r9, 414 ;第4个参数
mov rax,415
mov [rsp+4*8],rax ;第5个参数
call MyFunction5
6. 总结
(1)通用函数形式(MyFunction1)是最常用的一种函数形式,但它不能体现Win64栈帧的优势。
(2)无参数名函数形式(MyFunction2)也是常用的一种函数形式,可以提高编译速度,也可以改善函数的性质。
(3)自定义栈帧函数形式(MyFunction3)主要用于对栈空间内存对齐有特殊要求的函数,例如在局部变量中使用YMM类变量则需要进行32字节栈内存对齐。
(4)零栈帧函数形式(MyFunction4)是编写具有复杂代码但又不需要调用其它函数时的一种很好的函数形式,它不但可以改善函数的性能,而且函数的可读性很高。
(5)无栈帧函数形式(MyFunction5)是性能最高的函数形式,但函数可读性差一些,主要用于编写被调用频率很高的基础类函数。
7. 附: Masm64的下载地址及安装方法
一般的做法是先安装masm32,然后再在同一目录下安装masm64,这样masm32和masm64都可以使用。具体方法如下:
(1)下载masm32并安装。如果你不打算使用Masm32版的话可以不下载。下载网站为:
http://www.masm32.com/download.htm
(2)下载masm64,解压后将其复制到masm32的安装目录下。如果你不安装masm32,则直接解压到Masm64安装目录下就可以了。下载网站为:
http://www.masm32.com/board/index.php
注: 在Microsoft 64 bit MASM标题中。
(3)还需要下载编译工具ml64.exe,并将它复制到Masm64安装目录中的bin64子目录中。
这个编译工具可以在Visual Studio build tools中找到,如果你没有的话可以在网络上搜查。
(4)注意。在使用系统自带的一些例子时,有时无法完成编译与连接,这种情况是因为例子中所使用的编译器和连接器与Masm64系统中bin64子目录下的编译器和连接器名称不一致所造成的,自己修改一下makeit.bat文件中的相应内容就可以了。