无法打印出来

问题描述 投票:0回答:1

我使用 NASM 作为汇编器 所以,我尝试让程序打印出“hello world”这个词,但它没有 我按照调用约定提出了所有参数。正如它所说,第一个参数是 rcx,第二个是 rdx,第三个是 r8,第四个是 r9,第五个是 [rsp + 32]

所以我写了这个脚本:

section .data
    phrase db 'Hello World' ; creating new var with size of db(Double World)
    charsWritten dd 0 ; setting the var up is as things which will be printed out
section .text
    global _start
    extern ExitProcess ; importing the function ExitProcess to return 0
    extern WriteConsoleA ; imporing the command of printing out
    extern GetStdHandle ; descriptor
    _start:
        mov  rcx, -10 ; setting 1-st argument
        call GetStdHandle ; calling the result was saved in rax
        mov r8, rax ; saving result

        mov rcx, r8 ; setting the saved result up as 1-st argument
        mov rdx, phrase ; setting the pointer as 2-st argument
        mov r8, 11 ; setting r8 as 3-rd argument with data 11(lenght)
        mov r9, charsWritten ; setting the pointer of printed words out as 4-st argument
        mov qword [rsp + 32], 0 ; setting zero into stack as 5-th argument
        call WriteConsoleA ; calling printing out

        mov rcx, 0 ; its first argument of returing 0
        call ExitProcess ; calling return with 0 as first argument of it

此脚本不执行任何操作。没有错误。我尝试设置这个 .exe 文件。执行错误级别为0

os: windows 11 pro
system: 64 bit system
processor: intel i7

执行结果: result

assembly nasm
1个回答
0
投票

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 位,因此使用 32 位寄存器就足够了。

错误

使用的 I/O 函数可能会失败,因此您应该检查返回值。

GetStdHandle

可能会返回

INVALID_HANDLE_VALUE

,您不应将其传递给 
WriteConsoleA

:出于解释目的,我们假设它已被call编辑。中间的存根 jmp

_start
 可以忽略。
    

© www.soinside.com 2019 - 2024. All rights reserved.