在 MSYS2 上开发 GNU 汇编程序
汇编语言是一种最贴近机器硬件、运行效率很高的低级编译型语言。由于它与硬件紧密相关,因此它的开发完全受平台限制,所开发的程序代码基本上不具有移植性。当然,只要硬件(主要是中央处理器)相近,那就有可能在不同平台上采用同一套汇编语言编程工具进行程序开发。MSYS2 是 GCC 工具链在 MS Windows 平台上极为成功的移植!利用 MSYS2 提供的编程工具,就可以通过 C/C++/Python 等编程语言在 MS Windows 平台上进行原生程序开发。这既可以借鉴 GNU Linux 平台上优秀开源软件的成功开发经验,还可以将这些优秀的软件移植到 MS Windows 平台。不过,这里要介绍的是如何利用 MSYS2 中基于 MinGW x64 工具链提供的 GNU as 与 ld 工具进行 64 位 GNU 汇编(也即采用 AT&T 语法)程序的开发。
先来看一个示例,它主要通过指令 call 调用了标准 C 函数(比如 printf、exit 等):
/* test.s */ .section .rodata msg: .asciz "Hello, MSVC Library!" .section .text .global _start _start: # align RSP push %rbp mov %rsp, %rbp sub $0x20, %rsp lea msg(%rip), %rcx call printf xor %eax, %eax call exit
上述汇编源代码在 MSYS2 环境下的编译以及运行过程是这样的:
$ as -o test.o test.s $ ld -o test.exe test.o -lmsvcrt $ ./test.exe
为了正确编写 GNU 汇编源代码,除了熟悉 AT&T 汇编语法与 Amd64 指令集之外,特别需要了解 MS Windows x64 的调用协定(FASTCALL 的一种变体)、MS Windows C/C++ 运行库,请参看 https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170、https://learn.microsoft.com/en-us/cpp/c-runtime-library/crt-library-features?view=msvc-170。有一点要特别强调一下:微软的 C 运行时库中还存在着一些以单下划线开头并跟有标准 C 函数名的接口(比如 _printf、_exit 等),这些接口是微软实现的但并不是标准 C 函数,不同版本的 MS Windows 中的 C 运行时库(例如 msvcrt 等)可能提供了标准 C 接口,也可能提供了微软的扩展接口,或者两者兼有,或者只有其中之一。
再来看如何调用 MS Windows 的应用程序编程接口?
/* test.s */ .section .rodata msg: .asciz "Hello, Win32 ASM World" len = .-msg .section .text .global _start _start: # align RSP push %rbp mov %rsp, %rbp sub $0x20, %rsp # https://learn.microsoft.com/en-us/windows/console/getstdhandle # get handle for standard I/O # GetStdHandle(STD_OUTPUT_HANDLE) mov $-11, %rcx call GetStdHandle # https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile # WriteFile(hstdOut, msg, len, &bytes, 0) mov %rax, %rcx lea msg(%rip), %rdx mov $len, %r8 lea -0x20(%rbp), %r9 push $0 call WriteFile # https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitprocess # exit with zero # ExitProcess(0) xor %ecx, %ecx call ExitProcess
上述源代码在 MSYS2 环境下的编译以及运行过程如下:
$ as -o test.o test.s $ ld test.o -lkernel32 -o test.exe $ ./test.exe
上面的汇编代码中用到了 Windows 应用程序编程接口中最重要的动态链接库:Kernel32.dll (包含那些用于管理内存、进程和线程的函数,例如 GetStdHandle、WriteFile、ExitProcess 等函数)。另一个值得一提的是:User32.dll(包含那些用于执行用户界面任务的函数,例如 MessageBoxA 等 函数)。如果想生成对话框程序,可试试下述汇编代码
/* test.s */ .section .rodata mba_msg: .asciz "Hello ASM world!" mba_title: .asciz "Simple Message Box" .section .text .globl _start _start: # align RSP push %rbp mov %rsp, %rbp sub $0x20, %rsp # https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxa # display a message box # MessageBoxA(NULL, mba_msg, mba_title, MB_ICONINFORMATION) xor %ecx, %ecx lea mba_msg(%rip), %rdx lea mba_title(%rip), %r8 mov $0x40, %r9d call MessageBoxA # ExitProcess(0) xor %ecx, %ecx call ExitProcess
它的编译与执行过程如下:
$ as -o test.o test.s $ ld test.o -lkernel32 -luser32 -o test.exe $ ./test.exe
MS Windows 的应用程序编程接口被分为两种:一种是遵循 Win32 时代的应用程序编程接口,这部分被称为本地应用程序编程(Native API);另一种则是以 .NET Framework 为基础开发的,称为 Managed API。本地应用程序编程接口(https://learn.microsoft.com/en-us/windows/win32/api/) 以二进制方式发布、供 C/C++ 程序直接调用。这些本地应用程序开发接口的名称大多以 Nt/Zw 开头,而所有系统调用都以字母 Nt 开头。换而言之,MS Windows 的系统调用是本地应用程序编程(Native API)的子集。来看一下 Linux 与 MS Windows 的应用程序编程接口对照表(https://www.cnblogs.com/UnGeek/p/2981439.html):
如果 GNU 汇编代码中调用了 Nt/Zw 开头的 MS Windows 应用程序编程接口,那么只需在使用 GNU 链接器时增加 -lntdll 选项。
实际上,GCC 工具链为 GNU 汇编语言提供了第三代系统调用指令 syscall,若想通过它调用 MS Windows 的系统调用,那就需要提前知道相应的系统调用号。然而 MS Windows 并没有提供任何相关细节,好在已有开发者对动态库 NTDLL.dll 进行了逆向工程,得到了有关每个 Windows 内核的部分系统调用号的一些非官方信息:
- https://github.com/ikermit/11Syscalls
- https://j00ru.vexillium.org/syscalls/nt/64/
- http://j00ru.vexillium.org/syscalls/win32k/64/
- https://github.com/j00ru/windows-syscalls
请注意,即使是在 MS Windows 10 的不同版本之间,MS Windows 的同一个系统调用的调用号也会发生变化。
/* NtCreateThreadEx on Windows 10 (1511) */ mov %rcx, %r10 mov $0xB4, %rax syscall
有关 Win32 汇编的基础知识,可参考 https://www.cnblogs.com/LyShark/p/11136319.html。还有一点值得提到的是,通过 MinGW64 制作 MS Windows 上动态库(DLL)的方法,它与 Linux 稍有不同,请参考 https://nullprogram.com/blog/2021/05/31/。