我想在 Windows 下编写一些基本的汇编程序,我正在使用 NASM,但我什么都做不了。
如何在 Windows 上不借助 C 函数编写和编译 hello world?
这个例子展示了如何直接进入 Windows API 而不是 C 标准库中的链接。
global _main
extern _GetStdHandle@4
extern _WriteFile@20
extern _ExitProcess@4
section .text
_main:
; DWORD bytes;
mov ebp, esp
sub esp, 4
; hStdOut = GetstdHandle( STD_OUTPUT_HANDLE)
push -11
call _GetStdHandle@4
mov ebx, eax
; WriteFile( hstdOut, message, length(message), &bytes, 0);
push 0
lea eax, [ebp-4]
push eax
push (message_end - message)
push message
push ebx
call _WriteFile@20
; ExitProcess(0)
push 0
call _ExitProcess@4
; never here
hlt
message:
db 'Hello, World', 10
message_end:
要编译,您需要 NASM 和 LINK.EXE(来自 Visual Studio 标准版)
nasm -fwin32 hello.asm 链接 /subsystem:console /nodefaultlib /entry:main hello.obj
调用 libc stdio printf
,实现int main(){ return printf(message); }
; ----------------------------------------------------------------------------
; helloworld.asm
;
; This is a Win32 console program that writes "Hello, World" on one line and
; then exits. It needs to be linked with a C library.
; ----------------------------------------------------------------------------
global _main
extern _printf
section .text
_main:
push message
call _printf
add esp, 4
ret
message:
db 'Hello, World', 10, 0
然后运行
nasm -fwin32 helloworld.asm
gcc helloworld.obj
a
Nasm 中还有The Clueless Newbies Guide to Hello World ,无需使用 C 库。然后代码看起来像这样。
带有 MS-DOS 系统调用的 16 位代码:在 DOS 仿真器或支持 NTVDM 的 32 位 Windows 中工作。不能在任何 64 位 Windows 下“直接”(透明地)运行,因为 x86-64 内核不能使用 vm86 模式。
org 100h
mov dx,msg
mov ah,9
int 21h
mov ah,4Ch
int 21h
msg db 'Hello, World!',0Dh,0Ah,'$'
将其构建为可执行文件,以便在所有段寄存器彼此相等的.com
情况下加载它(微型内存模型)。cs:100h
祝你好运。
这些是使用 Windows API 调用的 Win32 和 Win64 示例。它们适用于 MASM 而不是 NASM,但请查看它们。您可以在本文中找到更多详细信息。
这使用 MessageBox 而不是打印到标准输出。
;---ASM Hello World Win32 MessageBox
.386
.model flat, stdcall
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib
.data
title db 'Win32', 0
msg db 'Hello World', 0
.code
Main:
push 0 ; uType = MB_OK
push offset title ; LPCSTR lpCaption
push offset msg ; LPCSTR lpText
push 0 ; hWnd = HWND_DESKTOP
call MessageBoxA
push eax ; uExitCode = MessageBox(...)
call ExitProcess
End Main
;---ASM Hello World Win64 MessageBox
extrn MessageBoxA: PROC
extrn ExitProcess: PROC
.data
title db 'Win64', 0
msg db 'Hello World!', 0
.code
main proc
sub rsp, 28h
mov rcx, 0 ; hWnd = HWND_DESKTOP
lea rdx, msg ; LPCSTR lpText
lea r8, title ; LPCSTR lpCaption
mov r9d, 0 ; uType = MB_OK
call MessageBoxA
add rsp, 28h
mov ecx, eax ; uExitCode = MessageBox(...)
call ExitProcess
main endp
End
要使用 MASM 组装和链接这些,请将其用于 32 位可执行文件:
ml.exe [filename] /link /subsystem:windows
/defaultlib:kernel32.lib /defaultlib:user32.lib /entry:Main
或者对于 64 位可执行文件:
ml64.exe [filename] /link /subsystem:windows
/defaultlib:kernel32.lib /defaultlib:user32.lib /entry:main
为什么 x64 Windows 需要在 a 之前保留 28h 字节的堆栈空间call
? 这是调用约定所要求的 32 字节 (0x20) 的影子空间(也称为主空间)。另外 8 个字节将堆栈重新对齐 16,因为调用约定要求RSP在call
. (我们main
的调用者(在 CRT 启动代码中)这样做了。8 字节的返回地址意味着 RSP 距离函数入口处的 16 字节边界有 8 个字节。)
函数可以使用影子空间将其寄存器参数转储到任何堆栈参数(如果有)所在的位置。system call
除了前面提到的 4 个寄存器之外,A还需要 30h(48 个字节)来为 r10 和 r11 保留空间。但是 DLL 调用只是函数调用,即使它们是syscall
指令的包装器。
有趣的事实:非 Windows,即 x86-64 System V 调用约定(例如在 Linux 上)根本不使用影子空间,并且在 XMM 寄存器中使用多达 6 个整数/指针寄存器参数和多达 8 个 FP 参数.
使用 MASM 的invoke
指令(它知道调用约定),您可以使用一个 ifdef 来制作一个可以构建为 32 位或 64 位的版本。
ifdef rax
extrn MessageBoxA: PROC
extrn ExitProcess: PROC
else
.386
.model flat, stdcall
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib
endif
.data
caption db 'WinAPI', 0
text db 'Hello World', 0
.code
main proc
invoke MessageBoxA, 0, offset text, offset caption, 0
invoke ExitProcess, eax
main endp
end
两者的宏变体相同,但您不会以这种方式学习汇编。您将改为学习 C 风格的 asm。invoke
is for stdcall
or fastcall
while cinvoke
is for cdecl
or variable argument fastcall
。汇编器知道使用哪个。
您可以反汇编输出以查看invoke
扩展程度。
Flat Assembler不需要额外的链接器。这使得汇编程序编程非常容易。它也可用于 Linux。
这是hello.asm
来自 Fasm 的例子:
include 'win32ax.inc'
.code
start:
invoke MessageBox,HWND_DESKTOP,"Hi! I'm the example program!",invoke GetCommandLine,MB_OK
invoke ExitProcess,0
.end start
Fasm 创建一个可执行文件:
>fasm hello.asm flat assembler 版本 1.70.03(1048575 KB 内存) 4 遍,1536 字节。
这是IDA中的程序:
您可以看到三个调用GetCommandLine
:MessageBox
和ExitProcess
。
要使用 NASM'compiler 和 Visual Studio 的链接器获取 .exe,此代码可以正常工作:
global WinMain
extern ExitProcess ; external functions in system libraries
extern MessageBoxA
section .data
title: db 'Win64', 0
msg: db 'Hello world!', 0
section .text
WinMain:
sub rsp, 28h
mov rcx, 0 ; hWnd = HWND_DESKTOP
lea rdx,[msg] ; LPCSTR lpText
lea r8,[title] ; LPCSTR lpCaption
mov r9d, 0 ; uType = MB_OK
call MessageBoxA
add rsp, 28h
mov ecx,eax
call ExitProcess
hlt ; never here
如果此代码保存在例如“test64.asm”上,则编译:
nasm -f win64 test64.asm
产生“test64.obj”然后从命令提示符链接:
path_to_link\link.exe test64.obj /subsystem:windows /entry:WinMain /libpath:path_to_libs /nodefaultlib kernel32.lib user32.lib /largeaddressaware:no
其中path_to_link可以是C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin或您机器中的 link.exe 程序的任何位置, path_to_libs可以是C:\Program Files (x86)\Windows Kits\8.1\ Lib\winv6.3\um\x64或任何你的库(在这种情况下 kernel32.lib 和 user32.lib 都在同一个地方,否则为你需要的每个路径使用一个选项)并且/largeaddressaware:no选项是有必要避免链接器抱怨地址太长(在这种情况下是 user32.lib)。此外,正如这里所做的那样,如果从命令提示符调用 Visual 的链接器,则必须事先设置环境(运行一次 vcvarsall.bat 和/或查看MS C++ 2010 和 mspdb100.dll)。
除非你调用某个函数,否则这根本不是微不足道的。(而且,说真的,调用 printf 和调用 win32 api 函数的复杂度并没有真正的区别。)
即使 DOS int 21h 实际上只是一个函数调用,即使它是一个不同的 API。
如果您想在没有帮助的情况下执行此操作,则需要直接与视频硬件对话,可能会将“Hello world”字母的位图写入帧缓冲区。即便如此,显卡仍在将这些内存值转换为 DisplayPort/HDMI/DVI/VGA 信号。
请注意,实际上,这些东西在 ASM 中一直到硬件都没有比在 C 中更有趣的了。“hello world”程序归结为函数调用。ASM 的一个优点是您可以相当轻松地使用任何您想要的 ABI。你只需要知道 ABI 是什么。
如果要将 NASM 和 Visual Studio 的链接器 (link.exe) 与 anderstornvig 的 Hello World 示例一起使用,则必须手动链接包含 printf() 函数的 C 运行时库。
nasm -fwin32 helloworld.asm
link.exe helloworld.obj libcmt.lib
希望这可以帮助某人。
最好的例子是那些使用 fasm 的,因为 fasm 不使用链接器,它通过另一个不透明的复杂层隐藏了 windows 编程的复杂性。如果您对写入 gui 窗口的程序感到满意,那么 fasm 的示例目录中有一个示例。
如果你想要一个控制台程序,它允许标准输入和标准输出的重定向也是可能的。有一个(helas 非常重要的)示例程序可用,它不使用 gui,并且严格与控制台一起工作,这就是 fasm 本身。这可以精简到基本要素。(我编写了第四个编译器,这是另一个非 gui 示例,但它也很重要)。
这样的程序具有以下命令来为 32 位可执行文件生成适当的标头,通常由链接器完成。
FORMAT PE CONSOLE
名为“.idata”的部分包含一个表,该表可帮助窗口在启动期间将函数名称与运行时地址耦合。它还包含对 KERNEL.DLL 的引用,它是 Windows 操作系统。
section '.idata' import data readable writeable
dd 0,0,0,rva kernel_name,rva kernel_table
dd 0,0,0,0,0
kernel_table:
_ExitProcess@4 DD rva _ExitProcess
CreateFile DD rva _CreateFileA
...
...
_GetStdHandle@4 DD rva _GetStdHandle
DD 0
表格格式由窗口强加,并包含程序启动时在系统文件中查找的名称。FASM 隐藏了 rva 关键字背后的一些复杂性。所以 _ExitProcess@4 是一个 fasm 标签,_exitProcess 是一个由 Windows 查找的字符串。
您的程序位于“.text”部分。如果您声明该部分可读可写和可执行,则它是您需要添加的唯一部分。
section '.text' code executable readable writable
您可以调用您在 .idata 部分中声明的所有工具。对于控制台程序,您需要 _GetStdHandle 来查找标准输入和标准输出的文件描述符(使用符号名称,如 fasm 在包含文件 win32a.inc 中找到的 STD_INPUT_HANDLE)。一旦你有了文件描述符,你就可以执行 WriteFile 和 ReadFile。所有函数都在 kernel32 文档中描述。您可能已经意识到这一点,否则您不会尝试汇编程序编程。
总结:有一个带有 asci 名称的表与 windows 操作系统耦合。在启动期间,这将转换为您在程序中使用的可调用地址表。