ChatGPT解决这个技术问题 Extra ChatGPT

Windows下如何用汇编编写hello world?

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

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

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

c
caffiend

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

您可能需要包含 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 等免费链接器,这是否也有效?我不想只为此安装视觉工作室?
P
Peter Cordes

NASM examples

调用 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

还有 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。
P
Peter Cordes

这些是使用 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 将其寄存器 args 转储到任何堆栈 args(如果有)所在的位置。除了前面提到的 4 个寄存器之外,system call 还需要 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 用于 stdcallfastcall,而 cinvoke 用于 cdecl 或变量参数 fastcall。汇编器知道使用哪个。

您可以反汇编输出以查看 invoke 的扩展情况。


+1为您的答案。您能否也为 Windows on ARM (WOA) 添加汇编代码?
为什么 rsp 需要 0x28 字节而不是 0x20?调用约定上的所有引用都说它应该是 32,但实际上似乎需要 40。
在您的 32 位消息框代码中,由于某种原因,当我使用 title 作为标签名称时,我遇到了错误。但是,当我使用其他名称作为 mytitle 之类的标签名称时,一切正常。
没有包括怎么办?
MASM64 示例给出了一个语法错误,似乎标题是一个指令:docs.microsoft.com/en-us/cpp/assembler/masm/title?view=msvc-170 使用另一个名称可以正常工作
c
ceving

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 中的程序:

https://i.stack.imgur.com/YkGWh.png

您可以看到三个调用:GetCommandLineMessageBoxExitProcess


这使用了一个包含和 GUI,我们如何只对 CMD 执行此操作而根本不包含任何内容?
你能指出我没有任何 dll 写入控制台的部分吗?
C
Community

要使用 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”和类似错误而大发雷霆……
C
Captain Segfault

除非你调用某个函数,否则这根本不是微不足道的。 (而且,说真的,调用 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 是个例外。)这里的其他答案有效地涵盖了这些;这个答案的重点是指出这不是一个完全正确的问题。
t
tboerman

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

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

希望这可以帮助某人。


问题的发布者想知道,有人将如何根据 Windows 提供的设施编写 printf,所以这又完全无关紧要。
A
Albert van der Horst

最好的例子是那些使用 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 位指令。
P
Peter Cordes

对于 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] 的问题,也可以在这里留下这个答案。

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅