Skip to content

64 位汇编语言环境设置

发布于  at 01:42 PM更新于  at 11:22 AM

Widdows 环境下运行的 MASM,是目前编写 x86-64 汇编语言最常用的汇编器。

环境准备

安装 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                          ; 汇编程序结束标记
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自小谷的随笔

下一篇
游泳的基础知识和技术