[英]"Hello world" in x64 assembly for Windows - Shadow space / Stack alignment
這是https://codereview.stackexchange.com/questions/278940/hello-world-in-x64-assembly-for-windows-shadow-space-stack-alignment/的轉貼,有人建議我 stackoverflow 可能有更合適的答案/評論。
我目前正在嘗試使用 NASM 深入研究 windows 下的 x64 程序集,並創建了一個簡約的“Hello World”應用程序。
它主要是作為我和可能其他人的教育資源,因此是繁重的文檔風格。
帶有構建說明和代碼的完整倉庫位於hello_kernel32 ,這是相關的源文件:
;; Resources:
;; https://sonictk.github.io/asm_tutorial/
;; https://gist.github.com/mcandre/b3664ffbeb4f5764b36a397fafb04f1c
;; https://retroscience.net/x64-assembly.html
;; Make clear this file contains 64bit assembly
bits 64
;; Use rip-relative addressing
default rel
;; Export entry symbol (this is specified in the call to link.exe)
global _start
;; Import external symbols
;; (all of them exist in kernel32.lib, which gets passed to link.exe in addition to the programs object file hello.obj)
;;
;; Why import symbols/functions from kernel32.lib?
;;
;; In windows, the "low level API stack" is: Kernel < Syscalls < ntdll.dll < kernel32.dll (and others like user32.dll)
;;
;; * The kernel itself cannot be accessed by user programs for obvious reasons (CPU ring protection modes)
;; * Syscalls could be performed, but are undocumented and evidently unstable between different versions of windows
;; * ntdll.dll is only partially documented and not intended for external use
;; * kernel32.dll (and friends) are the "official" low-level entry points to the windows API
extern GetStdHandle
extern WriteFile
extern ExitProcess
;; This section contains read-only data
section .rodata
;; Store the output string followed by CRLF as a sequence of bytes, at address 'msg'
msg db "Hello World!", 0x0d, 0x0a
;; The length will be needed by the output function, and can be statically calculated at assembly time by using 'equ'
;; It is actually a nifty trick that calculates the offset between the current address '$', and the address of 'msg'
;; See https://nasm.us/doc/nasmdoc3.html#section-3.2.4
msg_len equ $ - msg
;; This section contains the code
section .text
_start:
;; For being able to print text, we first need to acquire a HANDLE to STDOUT
;; This HANDLE is a required parameter for the call to WriteFile
;; HANDLE = GetStdHandle(-11)
;;
;; See https://docs.microsoft.com/en-us/windows/console/getstdhandle
;;
;; Parameter 1 (rcx): requests the type of HANDLE, -11 is the constant for STDOUT
;; Return value (rax): HANDLE (an address with some type of meaning) is stored in rax, as per calling conventions
mov rcx, -11
sub rsp, 40 ;; Allocate 32 bytes of shadow space and 8 bytes to keep the stack 16-byte aligned
call GetStdHandle
add rsp, 40 ;; Undo shadow space + alignment padding
;; code = WriteFile(HANDLE, msg, msg_len, NULL, NULL)
;;
;; See https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile
;;
;; Parameter 1 (rcx): HANDLE to write to
;; Parameter 2 (rdx): Address of message to print
;; Parameter 3 (r8): Length of message
;; Parameter 4 (r9): Write amount of written bytes to this address, null pointer
;; (Required according to docs when parameter 5 is null, but passing null seems to work just fine)
;; Parameter 5 (on stack): Unused optional parameter, null pointer
;; Return value (rax): Nonzero on success
mov rcx, rax
mov rdx, msg
mov r8, msg_len
mov r9, 0
push qword 0
sub rsp, 32 ;; The previous push aligned us to 16 bytes, only allocate shadow space
call WriteFile
add rsp, 40 ;; Undo shadow space + push
;; ExitProcess(code)
;;
;; See https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-exitprocess
;;
;; Parameter 1 (rcx): Exit code
mov rcx, rax
sub rsp, 40 ;; Shadow space + 8 byte alignment
call ExitProcess
;; add rsp, 40 ;; Not necessary because the previous function call will end the program anyway
我的主要問題是:
在執行簡單的“hello world”程序時,我發現的大多數資源似乎完全忽略了這一點。 刪除所有sub rsp
/ add rsp
語句時,我的程序似乎也運行良好。
所以我想知道不遵循這些約定對代碼正確性的真正含義是什么。
我目前的理解是GetStdHandle
/ WriteFile
/ ExitProcess
根本不使用影子空間,也不執行任何需要在當前實現中對齊堆棧的操作,如我的機器上存在的那樣。
但是,對kernel32.dll
的任何更新都可以以依賴於存在影子空間和/或對齊堆棧的方式自由更改這些實現。
因此,對於外部調用忽略影子空間/堆棧 alignment 的代碼在與外部 API 不正確接口的一般意義上是不正確的,即使許多 API 可能以它們可以容忍稍微不正確的訪問的方式構建(但這種容忍度是一個實現細節可能隨時改變)。
->這個定義/理解正確嗎? / 有什么要補充/澄清的嗎?
代碼或注釋中是否還有其他明顯的錯誤/誤解?
不,這些函數可能正在寫入它們的影子空間,您的程序只是不依賴於 [rsp+0..31] 在call
中未被修改
您call ExitProcess
而不是ret
(因為 _start 甚至可能在堆棧上沒有返回地址?IDK 如果 Windows 有)。
而且您不會在自己的堆棧空間中保留任何本地變量,這些變量會成為被調用者的影子空間,因此它們不會踩到您的本地變量。
但是,是的,如果您的_start
被稱為 function,即使用RSP%16 == 8
,它們可以容忍錯位。 與 Linux 不同,其中RSP%16 == 0
在_start
入口點,其中 RSP 指向argc
、 argv[]
、 envp[]
,而不是返回地址。
Alignment is usually only a correctness problem in code that uses movaps
/ movdqa
to copy 16 bytes at a time, like glibc scanf/printf being common examples on Linux ( glibc scanf Segmentation faults when called from a function that doesn't align RSP ). 在 Windows 上,也許他們的 DLL 函數不使用 SSE 來復制 16 字節塊,或者他們使用movups
/ movdqu
(MSVC 通常不發出對齊要求的mov
指令,即使對於像_mm_store_si128
這樣的對齊要求的內在_mm_storeu_si128
,在 K10 或 Core2 等古老 CPU 上運行速度較慢的代碼)。
通常,您不會在每個 function 周圍添加/添加 RSP 。 您可以在 function 的頂部添加 rsp sub rsp, 40
以保留陰影空間 + alignment。 並在最后add rsp, 40
一次,或者如果您通過調用 noreturn function 而不是使用ret
結束。 除了 function 序言/尾聲之外,根本不移動 RSP 是正常的,尤其是在具有陰影空間的調用約定中。
要存儲堆棧參數,您可以將push 0
替換為mov qword [rsp+32], 0
。 或者你實際上用push 0
/ sub rsp, 32
啟動你的 function 以便陰影空間上方的0
一直存在,並且可以作為第一個 function 調用以獲取堆棧 arg 的 arg。 沒有堆棧參數的函數必須不要觸摸它,因為它不是它們的參數,只是調用者堆棧框架的一部分。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.