Widdows 环境下运行的 MASM,是目前编写 x86-64 汇编语言最常用的汇编器。
环境准备
- 64 位版本的 MASM
- 文本编辑器
- 链接器
- 库文件
- C++ 编译器
安装 Visual Studio 后,就已经具备开发环境了。
创建快捷方式
建议使用 everything 或者其它搜索工具搜索 vcvars64.bat
。
可以将它作为一个快捷方式,并且在目标(右键出现菜单后点属性),增加 cmd /k
。
cmd /k "D:\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
/k
指示执行 vcvars64.bat
,并且命令执行完成后保存窗口打开状态。
通常我们不会在 D:\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\
这个目录下编程,所以可以将快捷方式的“起始位置”改成常用目录。
现在双击打开快捷方式:
**********************************************************************
** Visual Studio 2022 Developer Command Prompt v17.10.2
** Copyright (c) 2022 Microsoft Corporation
**********************************************************************
[vcvarsall.bat] Environment initialized for: 'x64'
D:\>
验证
在命令行中,输入 ml64
命令:
D:\>ml64
Microsoft (R) Macro Assembler (x64) Version 14.38.33139.0
Copyright (C) Microsoft Corporation. All rights reserved.
usage: ML64 [ options ] filelist [ /link linkoptions]
Run "ML64 /help" or "ML64 /?" for more info
D:\>
这表明系统已经正确设置好环境变量,可以运行 Microsoft 宏汇编器。
作为最终测试,执行 cl
命令以验证我们是否可以运行MSVC。结果应该获得以下类似的输出:
D:\>cl
用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.38.33139 版
版权所有(C) Microsoft Corporation。保留所有权利。
用法: cl [ 选项... ] 文件名... [ /link 链接选项... ]
D:\>
第一个汇编程序
MASM 要求所有源文件的后缀为 .asm
。因此,使用我们选择的编辑器创建一个名称为 hw64.asm
文件:
; 链接kernel32库
includelib kernel32.lib
; 导入Windows API函数
extrn __imp_GetStdHandle:proc ; 获取标准句柄函数
extrn __imp_WriteFile:proc ; 写文件函数
.CODE
; 定义字符串数据
hwStr byte "Hello World!"
; 计算字符串长度
hwLen = $-hwStr
main PROC
; 准备字符串地址
lea rbx, hwStr ; 将字符串地址加载到rbx
; 为 WriteFile 的 lpNumberOfBytesWritten 参数预留空间
sub rsp, 8 ; 在栈上分配 8 字节空间
mov rdi, rsp ; 将栈指针保存到 rdi(用作输出参数地址)
; 为函数调用预留影子空间(Windows x64调用约定要求)
sub rsp, 030h ; 分配 48 字节影子空间(HEX 30 = DEC 48)
; 调用 GetStdHandle 获取标准输出句柄
mov rcx, -11 ; STD_OUTPUT_HANDLE = -11
call qword ptr __imp_GetStdHandle ; 调用 GetStdHandle,返回值在 rax 中
; 设置 WriteFile 的第 5 个参数 lpOverlapped 为 NULL
mov qword ptr [rsp + 4 * 8], 0 ; 第 5 个参数(lpOverlapped) = NULL
; 准备 WriteFile 函数参数(Windows x64调用约定: rcx, rdx, r8, r9, 栈)
mov r9, rdi ; 第 4 个参数: lpNumberOfBytesWritten 的地址
mov r8d, hwLen ; 第 3 个参数: 要写入的字节数
lea rdx, hwStr ; 第 2 个参数: 缓冲区地址
mov rcx, rax ; 第 1 个参数: 文件句柄(GetStdHandle的返回值)
call qword ptr __imp_WriteFile ; 调用WriteFile写入数据
; 清理栈空间
add rsp, 38h ; 恢复栈指针(48 + 8 = 56 = 0x38字节)
; 返回
ret
main ENDP
; 程序结束,入口点默认为第一个过程
END
为了编译(汇编)该源文件,
ml64 hw64.asm /link/subsystem:console/entry:main kernel32.lib msvcrt.lib
这样就得到了一个 exe 程序。
运行该程序,就可以看到 Hello World!
最基本结构
; 注释是从一个分号 ; 字符开始到行尾的所有文本。
; .code 伪指令指示 MASM该指令后的语句位于保留给机器指令(代码)的内存段(section)中。
.code
main PRO
ret ;返回到调用方
main ENDP
END
C++ 与汇编混合程序
写一个简单的 C++ 程序:
#include <stdio.h>
// 防止 C++ 编译器的名称篡改
// P.S. C++ 编译器为了支持函数重载和命名空间等特性,会对函数名进行编码转换
extern "C"
{
// 汇编语言编写的外部程序
void asmFunc(void);
};
int main(int argc, const char* argv[])
{
printf("Calling asmMain: \n");
asmFunc();
printf("Return from asmMain\n");
}
汇编程序:
.CODE
option casemap:none
public asmFunc
asmFunc PROC
ret
asmFunc ENDP
END
option
语句指示 MASM 将 区分 所有符号的大小写。
这是非常必要的操作,因为在默认情况下,MASM 不区分大小写,并将所有标识符映射为大写(意味着 asmFunc()
函数将转换为 ASMFUNC()
)
D:\asm-x64>ml64 /c asmfunc.asm
Microsoft (R) Macro Assembler (x64) Version 14.38.33139.0
Copyright (C) Microsoft Corporation. All rights reserved.
Assembling: asmfunc.asm
D:\asm-x64>cl main.cc asmfunc.obj
用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.38.33139 版
版权所有(C) Microsoft Corporation。保留所有权利。
main.cc
Microsoft (R) Incremental Linker Version 14.38.33139.0
Copyright (C) Microsoft Corporation. All rights reserved.
/out:main.exe
main.obj
asmfunc.obj
D:\asm-x64>main.exe
Calling asmMain:
Return from asmMain
ml64 命令使用了 /c
选项,该选项表示“仅编译”,并且不尝试运行链接器(如果运行链接器则将失败,因为 asmfunc.asm 不是一个可独立运行的程序)。
cl 命令在 main.cc
文件上运行 MSVC 编译器,并将汇编代码(asmfunc.obj)进行链接。MSVC 编译器的输出是一个可执行文件main.exe,从命令行执行该程序就会产生预期的输出结果。
实现调用 printf
option casemap:none ; 设置符号名称区分大小写
.data
; 定义字符串数据段
; 10 是换行符(ASCII码的LF,Line Feed)
fmtStr byte 'Hello, world!', 10, 0 ; 定义以null结尾的字符串,包含换行符
.CODE
; 声明外部函数printf(来自C运行时库)
externdef printf:proc
; 声明公共函数asmFunc,使其可以被其他模块调用
public asmFunc
asmFunc PROC
; === 函数调用约定准备 ===
; Windows x64调用约定要求栈16字节对齐
; 进入函数时RSP是8的倍数(因为call指令压入了8字节返回地址)
; 需要再减去8的倍数来保持16字节对齐,这里减56字节
sub rsp, 56 ; 为局部变量和参数预留栈空间,保持栈16字节对齐
; === 准备printf函数调用 ===
; Windows x64调用约定:第一个参数通过RCX寄存器传递
lea rcx, fmtStr ; 将字符串地址加载到RCX(printf的第一个参数)
; === 调用printf函数 ===
call printf ; 调用printf函数打印字符串
; === 恢复栈空间 ===
add rsp, 56 ; 恢复栈指针,释放之前分配的空间
; === 函数返回 ===
ret ; 返回调用者
asmFunc ENDP
END ; 汇编程序结束标记