如何在Windows下用汇编语言编写“Hello World”程序?

122

我想在Windows下使用汇编语言写一些基本的程序。我正在使用NASM,但是一直无法让它正常工作。

如何在Windows上编写和编译一个简单的hello world程序,而不需要借助C函数的帮助?


3
请查看 Steve Gibson 的《Small Is Beautiful》Windows 汇编入门工具包:http://www.grc.com/smgassembly.htm。 - Jeremy
不使用C库是一种有点奇怪的限制。在MS-Windows操作系统中,必须调用某个库,可能是kernel32.dll。微软是用C还是Pascal编写的似乎并不重要。这是否意味着只能调用操作系统提供的函数,而在类Unix系统中被称为系统调用? - Albert van der Horst
使用C库,我认为他或她的意思是不使用类似于GCC或MSVC附带的C运行时库。当然,他或她将不得不使用一些标准的Windows DLL,例如kernel32.dll。 - Rudy Velthuis
3
kernel32.dll 和 gcc 运行库之间的区别并不在于它们的格式(它们都是 dll)和语言(它们可能都是 c,但这是隐藏的)。区别在于是否由操作系统提供。 - Albert van der Horst
我也一直在寻找这个,哈哈,没有找到不带包含文件的fasm。 - B''H Bi'ezras -- Boruch Hashem
9个回答

157

这个示例展示了如何直接使用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
   link /subsystem:console /nodefaultlib /entry:main hello.obj 

29
你可能需要包含 kernel32.lib 文件以链接此程序(我已经这样做了)。使用以下命令进行链接:link /subsystem:console /nodefaultlib /entry:main hello.obj kernel32.lib。 - Zach Burlingame
8
如何在MinGW中将obj文件与ld.exe链接? - DarrenVortex
5
用gcc命令编译hello.obj文件。 - towry
4
使用类似 http://sourceforge.net/projects/alink/ 或 http://www.godevtool.com/#linker 这样的免费链接器是否也可行?我不想只为此安装 Visual Studio。 - Redoman
使用我的版本的 link : Microsoft (R) 增量链接器版本 14.29.30133.0,我得到了 unresolved external symbol _GetStdHandle@4 以及所有外部项? - Ben

43

NASM 示例

调用 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中的新手指南,不需要使用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
祝你好运。

41
问题明确提到“不使用C库”。 - Mehrdad Afshari
27
错误。C语言库本身显然可以,所以这是可能的。实际上只是稍微难一些。您只需要使用正确的5个参数调用WriteConsole()函数即可。 - MSalters
13
尽管第二个示例没有调用任何C库函数,但它也不是Windows程序。虚拟DOS机将被启动来运行它。 - Rômulo Ceccon
8
@Alex Hart,他的第二个例子是针对DOS而不是Windows。 在DOS中,小模式下的程序(.COM文件,总代码+数据+堆栈不超过64Kb)从0x100h开始,因为段中的前256个字节被PSP占用(命令行参数等)。 请参见此链接:http://en.wikipedia.org/wiki/Program_Segment_Prefix - Andriy Volkov
10
这不是要求的内容。第一个示例使用了C库,第二个示例是MS-DOS,而不是Windows。 - Paulo Pinto
显示剩余7条评论

30
这些是使用Windows API调用的Win32和Win64示例,适用于MASM而非NASM,但请查看它们。您可以在此文章中找到更多详细信息。
这里使用MessageBox而不是打印到stdout。

Win32 MASM

;---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

Win64 MASM

;---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在调用之前需要保留28h字节的堆栈空间?其中32字节(0x20)是影子空间或主空间,按照调用约定的要求进行。另外8字节用于重新对齐堆栈到16,因为调用约定要求在调用之前RSP必须对齐到16字节边界。(我们的main函数的调用者(在CRT启动代码中)完成了这个操作。8字节的返回地址意味着RSP距离16字节边界还有8字节。) 影子空间可以被函数用来将其寄存器参数转储到任何堆栈参数(如果有的话)旁边。系统调用需要30h(48字节)来额外保留r10和r11的空间,除了之前提到的4个寄存器。但是,DLL调用只是函数调用,即使它们是围绕syscall指令的包装器。
有趣的事实:非Windows平台,即x86-64 System V调用约定(例如Linux),根本不使用影子空间,并且使用高达6个整数/指针寄存器参数,以及高达8个FP参数在XMM寄存器中。

使用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风格的汇编语言。 "invoke" 用于 "stdcall" 或 "fastcall",而 "cinvoke" 用于 "cdecl" 或可变参数的 "fastcall"。汇编器知道该使用哪个。你可以反汇编输出以查看 "invoke" 的扩展方式。

1
请问您能否为Windows on ARM (WOA)添加汇编代码? - Annie
2
为什么rsp需要0x28字节而不是0x20字节?所有关于调用约定的参考资料都说它应该是32,但实际上似乎需要40。 - douggard
1
在你的32位消息框代码中,由于某种原因,当我使用“title”作为标签名称时,会遇到错误。但是,当我使用其他标签名称,例如“mytitle”时,一切都正常工作。 - user3405291
@PhiS,我很好奇是否有关于使用 ml.exe / ml64.exe 的 MASM 学习资源?因为线上的学习资料非常少,而且说我们必须使用 MASM SDK,但出于某种原因它并不适用于我的电脑 :( 。 - Aritro Shome
1
MASM64示例出现语法错误,似乎title是一个指令:https://learn.microsoft.com/en-us/cpp/assembler/masm/title?view=msvc-170。使用其他名称可以正常工作。 - Nicolás Abram
显示剩余4条评论

16

要使用NASM作为汇编器和Visual Studio的链接器生成.exe文件,以下代码可以正常工作:

default rel         ; Use RIP-relative addressing like [rel msg] by default
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      ; reserve shadow space and make RSP%16 == 0
    mov rcx, 0       ; hWnd = HWND_DESKTOP
    lea rdx,[msg]    ; LPCSTR lpText
    lea r8,[title]   ; LPCSTR lpCaption
    mov r9d, 0       ; uType = MB_OK
    call MessageBoxA

    mov  ecx,eax        ; exit status = return value of MessageBoxA
    call ExitProcess

    add rsp, 28h       ; if you were going to ret, restore RSP

    hlt     ; privileged instruction that crashes if ever reached.

如果将此代码保存为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指向你的机器上的link.exe程序所在的位置,例如C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\binpath_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 and mspdb100.dll)。

(使用default rel使lea指令从任何地方工作,包括虚拟地址空间低2GiB之外。但是,call MessageBoxA仍然是直接的call rel32,只能到达距离自己+-2GiB的指令。)


3
我强烈建议在文件顶部使用 default rel,这样就可以使用RIP相对寻址而不是32位绝对寻址来处理那些寻址方式([msg][title])。 - Peter Cordes
谢谢您解释如何链接!你拯救了我的心理健康。我因为“ error LNK2001:unresolved external symbol ExitProcess”和类似的错误开始抓狂…… - Nik

15

Flat Assembler无需额外的链接器,使汇编程序设计变得非常容易。它也可用于Linux。

这是来自Fasm示例的hello.asm

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  version 1.70.03  (1048575 kilobytes memory)
4 passes, 1536 bytes.

这是在 IDA 中显示的程序:

enter image description here

您可以看到三个调用:GetCommandLineMessageBoxExitProcess


这段代码使用了include和GUI,我们如何在没有任何include的情况下仅使用CMD实现它? - B''H Bi'ezras -- Boruch Hashem
试过阅读手册了吗?http://flatassembler.net/docs.php?article=manual#2.4.2 - ceving
你能指向一个不使用任何dll文件的控制台输出部分吗? - B''H Bi'ezras -- Boruch Hashem

6

除非你调用某些函数,否则这并不是一件微不足道的事情。(而且,说实话,在调用printf和调用win32 api函数之间没有真正的复杂度差异。)

即使是DOS int 21h,它实际上也只是一个函数调用,尽管它是不同的API。

如果你想在没有帮助的情况下完成它,你需要直接与视频硬件通信,可能要将“Hello world”字母的位图写入帧缓冲区。即使如此,视频卡也会将这些内存值转换为DisplayPort/HDMI/DVI/VGA信号。

请注意,实际上,从硬件一直到底层都不比C语言更有趣。一个“hello world”程序归结为一个函数调用。ASM的一个好处是你可以相对容易地使用任何ABI;你只需要知道那个ABI是什么。


这是一个很好的观点 --- ASM和C都依赖于操作系统提供的函数(在Windows中为_WriteFile)。那么魔法在哪里?它在视频卡的设备驱动程序代码中。 - Assad Ebrahim
3
完全离题了。海报要求一个可以在“Windows”下运行的汇编程序。这意味着可以使用Windows设施(例如kernel32.dll),但不能使用Cygwin下的libc等其他设施。拜托,海报明确表示不要使用C库。 - Albert van der Horst
3
我不明白为什么kernel32.dll不是C语言(或至少是C++)库。对于这个问题,有合理的解释,即提问者(或其他类似问题的人)实际上想要问什么。 “...例如kernel32.dll”是一个相当好的解释。(“例如int 21h”是我隐含的解释,但显然现在已经过时了,但在2009年,64位Windows还是例外。)其他答案在这里有效地涵盖了这些内容;这个答案的重点是指出这不是完全正确的问题。 - Captain Segfault

6

如果你想使用NASM和Visual Studio的链接器(link.exe)来运行anderstornvig的Hello World例子,你需要手动链接包含printf()函数的C运行时库。

nasm -fwin32 helloworld.asm
link.exe helloworld.obj libcmt.lib

希望这可以帮助到某些人。

提问者想知道如何基于Windows提供的功能编写printf,所以这完全是无关紧要的。 - Albert van der Horst

5
最好的例子是fasm,因为fasm不使用链接器,这样可以避免另一层复杂性的Windows编程隐藏。如果你满足于一个写入GUI窗口的程序,那么在fasm的示例目录中有一个示例。
如果你想要一个控制台程序,允许重定向标准输入和标准输出也是可能的。有一个(可惜非常复杂)的示例程序可用,它不使用GUI,严格地与控制台一起工作,那就是fasm本身。这可以被简化到必要的部分。(我写了一个forth编译器,这是另一个非GUI示例,但它也很不简单)
这样的程序有以下命令来生成一个32位可执行文件的正确头,通常由链接器完成。
FORMAT PE CONSOLE 

一个名为'.idata'的部分包含了一个表格,帮助Windows在启动时将函数名称与运行时地址相匹配。它还包含对Windows操作系统的KERNEL.DLL的引用。
 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

表格格式是由Windows强制执行的,其中包含在程序启动时在系统文件中查找的名称。FASM通过rva关键字隐藏了一些复杂性。所以_ExitProcess@4是一个fasm标签,而_exitProcess是一个被Windows查找的字符串。

你的程序位于'.text'段。如果你声明该段可读写和可执行,它就是你需要添加的唯一段。

    section '.text' code executable readable writable

你可以调用在.idata部分中声明的所有设施。对于控制台程序,您需要使用_GetStdHandle来查找标准输入和标准输出的文件描述符(使用诸如STD_INPUT_HANDLE这样的符号名称,fasm在包含文件win32a.inc中找到)。 一旦您拥有文件描述符,就可以进行WriteFile和ReadFile操作。 所有函数都在kernel32文档中有描述。你可能已经意识到了,否则你不会尝试使用汇编程序设计。
总之:有一个ASCII名称的表与Windows操作系统相关联。 在启动期间,它被转换为可调用地址的表格,您可以在程序中使用它们。

FASM可能不使用链接器,但它仍然必须汇编PE文件。这意味着它实际上不仅汇编代码,还承担了通常由链接器执行的工作,因此,在我看来,称缺少链接器为“隐藏复杂性”是具有误导性的,相反,汇编器的工作是汇编程序,但将其嵌入到可能依赖许多东西的程序映像中则留给链接器。因此,我认为链接器和汇编器之间的分离是一件好事,而您似乎持不同意见。 - Armen Michaeli
@amn 这样想吧。如果您使用连接器创建上述程序,它会让您更清晰地了解程序的功能或组成结构吗?如果我查看 fasm 源代码,我就可以知道程序的完整结构。 - Albert van der Horst
说得好。另一方面,将链接与其他所有内容分开也有其好处。通常您可以访问对象文件(这在很大程度上有助于让人独立于程序图像文件格式检查程序结构),您可以调用不同的首选链接器,并使用不同的选项。这是关于可重用性和组合性。考虑到这一点,FASM之所以做所有这些事情是因为它“方便”,这违反了这些原则。我并不完全反对它 - 我看到了他们的理由 - 但是我自己不需要它。 - Armen Michaeli
在FASM 64位Windows中的顶部行得到非法指令错误。 - B''H Bi'ezras -- Boruch Hashem
@bluejayke,可能你手头没有fasm的文档。FORMAT PE生成32位可执行文件,而64位Windows拒绝运行它。对于64位程序,你需要使用FORMAT PE64。同时确保在你的程序中使用适当的64位指令。 - Albert van der Horst

1

针对ARM版Windows:

AREA    data, DATA

Text    DCB "Hello world(text)", 0x0
Caption DCB "Hello world(caption)", 0x0

    EXPORT  WinMainCRTStartup
    IMPORT  __imp_MessageBoxA
    IMPORT  __imp_ExitProcess

    AREA    text, CODE
WinMainCRTStartup   PROC
            movs        r3,#0
            ldr         r2,Caption_ptr
            ldr         r1,Text_ptr
            movs        r0,#0
            ldr         r4,MessageBoxA_ptr    @ nearby, reachable with PC-relative
            ldr         r4,[r4]
            blx         r4

            movs        r0,#0
            ldr         r4,ExitProcess_ptr
            ldr         r4,[r4]
            blx         r4

MessageBoxA_ptr DCD __imp_MessageBoxA       @ literal pool (constants near code)
ExitProcess_ptr DCD __imp_ExitProcess
Text_ptr    DCD Text
Caption_ptr DCD Caption

    ENDP
    END

这个问题被标记为[x86][nasm],所以这个ARM答案在这里并不完全相关。我不知道有多少未来的读者会发现它,特别是如果你在代码之外的文本中甚至没有提到ARM Windows(我编辑了一下修复了代码格式和这个问题)。一个自问自答的Q&A可能更适合它,但即使这个问题主要是关于[x86]的,把这个答案留在这里也可能没问题。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接