【问题标题】:How to write hello world in assembler under Windows?Windows下如何用汇编编写hello world?
【发布时间】:2010-11-04 15:32:12
【问题描述】:

我想在 Windows 下用汇编编写一些基本的东西,我正在使用 NASM,但我什么都做不了。

如何在Windows上不借助C函数编写和编译hello world?

【问题讨论】:

  • 还可以查看 Steve Gibson 的 Small Is Beautiful windows 程序集入门工具包。
  • 不使用 c 库是一个有点奇怪的约束。必须调用 MS-Windows 操作系统中的某个库。可能是 kernel32.dll。微软是否用 c 或 Pascal 编写了这个似乎无关紧要。是不是只能调用 OS 提供的函数,在 Unix 类型的系统中什么叫做系统调用?
  • 使用 C 库我假设他或她的意思是不使用像 GCC 或 MSVC 那样的 C 运行时库。当然,他或她将不得不使用一些标准的 Windows DLL,例如 kernel32.dll。
  • kernel32.dll和gcc运行时库的区别不在于格式(都是dll),也不在于语言(可能都是c,但那是隐藏的)。区别在于是否提供操作系统。
  • 我一直在寻找这个也lol找不到任何没有包含的fasm

标签: winapi assembly x86 nasm


【解决方案1】:

这个例子展示了如何直接进入 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

【讨论】:

  • 你可能需要包含 kernel32.lib 来链接它(我做过)。链接 /subsystem:console /nodefaultlib /entry:main hello.obj kernel32.lib
  • 如何将 obj 与 MinGW 的 ld.exe 链接?
  • @DarrenVortex gcc hello.obj
  • 这是否也可以使用免费的链接器,如来自sourceforge.net/projects/alink 的Alink 或来自godevtool.com/#linker 的GoLink?我不想只为此安装 Visual Studio?
【解决方案2】:

NASM examples.

调用libc stdioprintf,实现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

还有 The Clueless Newbies Guide to Hello World in 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 加载,所有段寄存器都彼此相等(微型内存模型)。

祝你好运。

【讨论】:

  • 问题明确提到“不使用 C 库”
  • 错了。 C库本身显然可以,所以它是可能的。事实上,它只是稍微难一点。您只需要使用正确的 5 个参数调用 WriteConsole()。
  • 虽然第二个例子没有调用任何 C 库函数,但它也不是一个 Windows 程序。虚拟 DOS 机器将被触发以运行它。
  • @Alex Hart,他的第二个示例是针对 DOS,而不是针对 Windows。在 DOS 中,微型模式下的程序(.COM 文件,在 64Kb 总代码+数据+堆栈下)从 0x100h 开始,因为该段中的前 256 个字节由 PSP(命令行参数等)占用。见此链接:en.wikipedia.org/wiki/Program_Segment_Prefix
  • 这不是我们所要求的。第一个示例使用 C 库,第二个示例是 MS-DOS,而不是 Windows。
【解决方案3】:

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

这使用 MessageBox 而不是打印到标准输出。

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 需要在 call 之前保留 28h 字节的堆栈空间?这是调用约定所要求的 32 字节 (0x20) 的影子空间(也称为主空间)。另外 8 个字节将堆栈重新对齐 16,因为调用约定要求 RSP 在call 之前 对齐 16 字节。 (我们的main 的调用者(在 CRT 启动代码中)这样做了。8 字节的返回地址意味着 RSP 距离函数入口处的 16 字节边界有 8 个字节。)

Shadow space 可以被函数用来将其寄存器参数转储到任何堆栈参数(如果有)所在的位置旁边。除了前面提到的 4 个寄存器之外,system call 还需要 30h(48 个字节)来为 r10 和 r11 保留空间。但 DLL 调用只是函数调用,即使它们是 syscall 指令的包装器。

有趣的事实:非 Windows,即 x86-64 System V 调用约定(例如在 Linux 上)根本不使用影子空间,并且最多使用 6 个整数/指针寄存器参数,和 em> XMM 寄存器中最多 8 个 FP args。


使用 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 用于stdcallfastcall,而cinvoke 用于cdecl 或变量参数fastcall。汇编器知道使用哪个。

你可以反汇编输出看看invoke是如何扩展的。

【讨论】:

  • +1 为您解答。您能否也为 Windows on ARM (WOA) 添加汇编代码?
  • 为什么 rsp 需要 0x28 字节而不是 0x20?调用约定上的所有引用都说它应该是 32,但实际上似乎需要 40。
  • 在您的 32 位消息框代码中,由于某种原因,当我使用 title 作为标签名称时,我遇到了错误。但是,当我使用其他名称作为标签名称时,例如 mytitle,一切正常。
  • 没有包含怎么办?
  • @douggard 这有点令人困惑,但那是因为 a) 堆栈对齐需要保持在 16,b) 返回地址是通过调用推送的。所以加 0x20 是阴影,+8 是返回地址,+8 是保持对齐。
【解决方案4】:

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 版本 1.70.03(1048575 KB 内存) 4 遍,1536 字节。

这是IDA中的程序:

你可以看到三个调用:GetCommandLineMessageBoxExitProcess

【讨论】:

【解决方案5】:

要使用 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 and mspdb100.dll)。

【讨论】:

  • 我强烈建议在文件顶部使用default rel,以便那些寻址模式([msg][title])使用 RIP 相对寻址而不是 32 位绝对寻址。
  • 感谢您解释如何链接!你拯救了我的心理健康。我开始因“错误 LNK2001:无法解析的外部符号 ExitProcess”和类似错误而大发雷霆……
【解决方案6】:

除非你调用 some 函数,否则这不是微不足道的。 (而且,说真的,调用 printf 和调用 win32 api 函数的复杂度并没有真正的区别。)

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

如果您想在没有帮助的情况下执行此操作,则需要直接与视频硬件对话,可能会将“Hello world”字母的位图写入帧缓冲区。即便如此,视频卡仍在将这些内存值转换为 DisplayPort/HDMI/DVI/VGA 信号。

请注意,真的,这些东西在 ASM 中一直到硬件都没有比在 C 中更有趣的了。“hello world”程序归结为函数调用。 ASM 的一个优点是您可以相当轻松地使用任何您想要的 ABI。你只需要知道 ABI 是什么。

【讨论】:

  • 这是一个很好的观点 --- ASM 和 C 都依赖于操作系统提供的函数(Windows 中的 _WriteFile)。那么魔法在哪里呢?它在显卡的设备驱动程序代码中。
  • 这完全不是重点。海报询问了一个“在 Windows 下”运行的汇编程序。这意味着可以使用 Windows 工具(例如 kernel32.dll),但不能使用其他工具,例如 Cygwin 下的 libc。对于大声喊叫,海报明确表示没有 c-libraries。
  • 我看不出 kernel32.dll 不是 C(或至少是 C++)库。对于这个提问者(或其他提出类似问题的人)的真正意图,有合理的解释。 “......例如kernel32.dll”是一个相当不错的。 (“例如 int 21h”是我隐含的那个,现在显然已经过时了,但在 2009 年 64 位 Windows 是个例外。)这里的其他答案有效地涵盖了这些;这个答案的重点是指出这不是一个完全正确的问题。
【解决方案7】:

如果要将 NASM 和 Visual Studio 的链接器 (link.exe) 与 anderstornvig 的 Hello World 示例一起使用,则必须手动链接包含 printf() 函数的 C 运行时库。

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

希望这对某人有所帮助。

【讨论】:

  • 问题的发帖人想知道,有人会如何根据 Windows 提供的工具编写 printf,所以这又是完全无关紧要的。
【解决方案8】:

最好的例子是 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 操作系统耦合。 在启动过程中,它会转换为可调用地址表,供您在程序中使用。

【讨论】:

  • FASM 可能不使用链接器,但它仍然必须组装 PE 文件。这意味着它实际上不仅是汇编代码,而且还承担了链接器通常会执行的工作,因此,在我看来,将缺少链接器称为“隐藏复杂性”是误导性的,恰恰相反-- 汇编器的工作是汇编程序,但留给链接器将程序嵌入到程序映像中,这可能取决于很多事情。因此,我发现链接器和汇编器之间的分离是一件的事情,你不同意。
  • @amn 这样想。如果您使用链接器来创建上述程序,它是否能让您更深入地了解该程序的功能或它的组成部分?如果我查看 fasm 源代码,我知道程序的完整结构。
  • 公平点。另一方面,将链接与其他所有内容分开也有其好处。您通常可以访问目标文件(这对于让人们检查程序的结构也有很长的路要走,独立于程序图像文件格式),您可以使用不同的选项调用您偏好的不同链接器。这是关于可重用性和可组合性的。考虑到这一点,FASM 所做的一切都是因为它“方便”违反了这些原则。我主要不是反对它——我看到了他们这样做的理由——但我,一方面,不需要它。
  • 在 fasm 64 位窗口顶行获取非法指令错误
  • @bluejayke 可能您手头没有 fasm 的文档。 FORMAT PE 生成一个 32 位可执行文件,64 位窗口拒绝运行。对于 64 位程序,您需要 FORMAT PE64 。还要确保在程序中使用正确的 64 位指令。
【解决方案9】:

对于 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 答案在这里并不完全是主题。 IDK 有多少未来的读者会发现它,特别是如果您甚至没有在代码之外的文本中提及 ARM Windows(我进行了编辑以修复代码格式并修复它)。自我回答的问答可能是一个更好的地方,但即使问题主要是关于 [x86] 的问题,也可以将这个答案留在这里。
猜你喜欢
  • 2023-02-07
  • 2012-08-18
  • 2011-02-08
  • 2013-10-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多