我使用 Netwide Assembler 作为我的汇编器。 我正在尝试创建一个打印字符串
Hello World
的程序,但没有成功。
我按照调用约定将所有参数放入堆栈中。 CC 表示第一个参数需要放在
RCX
中,第二个参数放在 RDX
中,第三个参数放在 R8
中,第四个参数放在 R9
中,第五个参数放在 [RSP + 32]
中。
所以我写了这个程序:
section .data
phrase db 'Hello World' ; create new var with size of db(Double World)
charsWritten dd 0 ; set the var up is as things which will be printed out
section .text
global _start
extern ExitProcess ; import the function ExitProcess to return 0
extern WriteConsoleA ; import the command of printing out
extern GetStdHandle ; descriptor
_start:
mov rcx, -10 ; set 1st argument
call GetStdHandle ; call the result was saved in rax
mov r8, rax ; save result
mov rcx, r8 ; set the saved result up as 1st argument
mov rdx, phrase ; set the pointer as 2nd argument
mov r8, 11 ; set r8 as 3rd argument with data 11 (length)
mov r9, charsWritten ; set the pointer of printed words out as 4th arg.
mov qword [rsp + 32], 0 ; set zero into stack as 5th argument
call WriteConsoleA ; call printing out
mov rcx, 0 ; its first argument of returning 0
call ExitProcess ; call return with 0 as first argument of it
然而这个程序似乎什么也没做。没有错误。我尝试设置这个
.exe
文件。退出状态为 0
(成功)。
C:\Users\Мила ПК\Desktop>nasm -f win64 test.asm -o test.o
C:\Users\Мила ПК\Desktop>ld -o test.exe test.o -lkernel32
C:\Users\Мила ПК\Desktop>test.exe
C:\Users\Мила ПК\Desktop>echo %ERRORLEVEL%
0
我认为与问题相关的其他细节:
数据 | 价值 |
---|---|
操作系统 | Windows 11 专业版 |
系统 | 64 位系统 |
处理器 | 英特尔i7 |
Kai Burghardt 不建议使用 Microsoft Windows。此答案可能有错误。
您可能会想从 Hello, World 程序开始,但实际上您用汇编语言编写的第一个 W64 程序应该是这样的:
global _start
section .text
_start:
ret
在类 UNIX 环境中,loader通常
jmp
到入口点。
然而,在 Winblows 中,入口点本质上是 call
ed† 将返回地址放置在您可以 ret
的堆栈顶部。
作为侧边栏,一个无限循环程序可以这样写:
global _start
section .text
_start:
jmp rax ; rax containing the address of the entry point
现在,来自
ret
的 _start
无法保证 能够正常工作,因为它显然没有正式记录。
官方的方式是使用ExitProcess
。 让我们用
ExitProcess
来重写第一个程序。堆栈对齐
call
之前,ABI 希望您 在 16 字节边界上对齐堆栈。 这意味着以十六进制数编写的堆栈指针应以
0
结尾。 由于堆栈向下增长(朝向数值较小的地址),实现 16 字节对齐的直接方法是:
and RSP, 0xFFFFFFFFFFFFFFF0
可以公平地假设“操作系统”遵守自己的调用约定。
当我们的程序被call
ed†时,堆栈也已经对齐了。 但是,
call
† 将返回地址放在堆栈顶部,从而破坏了对齐(由 push(RIP)
隐式
call
)。因此,当我们的程序从
_start
之后的第一条指令开始时,堆栈指针(以十六进制数表示)以
8
结尾(或算术上表示
RSP mod 16 = 8
为
true
)。 因此,上面的
and
实际上与
sub RSP, 8
具有相同的作用。由于我们不打算使用返回地址,因此我们可以简单地通过弹出堆栈来
丢弃返回地址:
pop RAX ; discard return address, re‑align stack to 16 B
黑暗空间Wix64 ABI 要求您(始终)在堆栈上分配4×8 字节的影子空间。
这个空间可能被被调用者(您call
的函数)使用来溢出前四个参数
RCX
、RDX
、R8
和R9
。
溢出意味着将数据存储在其他地方,以便释放寄存器以供其他用途;稍后可以从所述影子空间检索原始参数值。global _start ; export _start
extern ExitProcess ; tell NASM the reference is resolved later
section .text
_start:
pop RAX ; discard return address, re‑align stack to 16 B
exit:
xor ECX, ECX ; ECX ≔ EXIT_SUCCESS
sub RSP, 4 * 8 ; reserve 32 bytes of shadow space
call ExitProcess ; 32 mod 16 = 0, so stack pointer still aligned
获取输出
call
完成后清理堆栈。 虽然
ExitProcess
(理想情况下)不会返回,但 GetStdHandle
会返回,因此我们反转
sub RSP, 32
的效果:global _start
extern GetStdHandle, ExitProcess
STD_OUTPUT_HANDLE equ -11 ; a more meaningful constant instead of −11
section .text
_start:
pop RAX ; discard return address, re‑align stack to 16 B
get_output:
mov ECX, STD_OUTPUT_HANDLE ; retrieve HANDLE for standard output
sub RSP, 4 * 8 ; reserve 32 bytes of shadow space
call GetStdHandle ; RAX ≔ GetStdHandle(STD_OUTPUT_HANDLE)
add RSP, 4 * 8 ; unreserve 32 bytes of shadow space
exit:
xor ECX, ECX ; ECX ≔ EXIT_SUCCESS
sub RSP, 4 * 8 ; reserve 32 bytes of shadow space
call ExitProcess
您可能会注意到一半的指令只是来回移动堆栈指针。 您可以减少杂耍,但我建议推迟该步骤,直到您的算法按预期工作,直到您不想再插入或删除任何内容。
写入输出
,而不是自己计算(→ hello_world_length
)。
global _start
extern GetStdHandle, WriteConsoleA, ExitProcess
default rel ; if not specified use RIP‑relative addressing
STD_OUTPUT_HANDLE equ -11 ; a more meaningful constant instead of −11
; --- initialized data ---------------------------------------------------------
section .data
hello_world:
db 'Hello, world!', `\r\n`
hello_world_length equ $ - hello_world
; --- uninitialized data -------------------------------------------------------
section .bss
result:
resd 1
; --- executable text ----------------------------------------------------------
section .text
_start:
pop RAX ; discard return address, re‑align stack to 16 B
get_output:
mov ECX, STD_OUTPUT_HANDLE ; retrieve HANDLE for standard output
sub RSP, 4 * 8 ; reserve 32 bytes of shadow space
call GetStdHandle ; RAX ≔ GetStdHandle(STD_OUTPUT_HANDLE)
add RSP, 4 * 8 ; unreserve 32 bytes of shadow space
write_hello_world:
add RSP, 1 * 8 ; keep alignment after push
mov ECX, EAX ; write destination
LEA RDX, [hello_world] ; source string start address
mov R8, hello_world_length ; length of source string
LEA R9, [result] ; destination for number of bytes written
push 0 ; reserved, must be zero
add RSP, 4 * 8 ; reserve 32 bytes of shadow space
call WriteConsoleA
sub RSP, 6 * 8 ; reclaim 48 bytes of stack space
exit:
xor ECX, ECX ; ECX ≔ EXIT_SUCCESS
sub RSP, 4 * 8 ; reserve 32 bytes of shadow space
call ExitProcess
; vim: set filetype=nasm:
显然,即使在 Win64 上,
HANDLE
,因此使用 32 位寄存器就足够了。错误
GetStdHandle
可能会返回
INVALID_HANDLE_VALUE
,您不应将其传递给 WriteConsoleA
。†:出于解释目的,我们假设它已被call
编辑。中间的存根
jmp
到 _start
可以忽略。