影子空间示例

10

编辑:

我已经接受了下面一个答案并添加了我的最终版本的代码。希望它展示给人们实际的影子空间分配示例,而不是更多的文字。

编辑2: 我还找到了一个链接到通话约定PDF的链接,在YouTube视频(所有东西中)的注释中有一些关于Linux上的影子空间和红色区域的有趣信息。它可以在这里找到:http://www.agner.org/optimize/calling_conventions.pdf

原始:

我查看了这里和互联网上的几个其他问题,但似乎找不到在64位Windows汇编中调用子例程/Windows API时分配“Shadow Space”的适当示例。

我的理解是:

  • 调用者应该在call callee之前进行sub rsp,<bytes here>
  • 如果需要保存寄存器(或者不需要保存寄存器,则使用局部变量),则被调用者应该使用它
  • 调用者清理它,例如:add rsp,<bytes here>
  • 分配的数量应该对齐到32字节

考虑到这一点,这是我尝试的内容:

section .text

start:

    sub rsp,0x20 ; <---- Allocate 32 bytes of "Shadow space"

    mov rcx,msg1
    mov rdx,msg1.len
    call write

    add rsp,0x20

    mov rcx,NULL
    call ExitProcess

    ret

write:

    mov [rsp+0x08],rcx      ; <-- use the Shadow space
    mov [rsp+0x10],rdx      ; <-- and again

    mov rcx,STD_OUTPUT_HANDLE   ; Get handle to StdOut
    call GetStdHandle

    mov rcx,rax         ; hConsoleOutput
    mov rdx,[rsp+0x08]      ; lpBuffer
    mov r8,[rsp+0x10]       ; nNumberOfCharsToWrite
    mov r9,empty        ; lpNumberOfCharsWritten
    push NULL           ; lpReserved
    call WriteConsoleA

    ret

我的两个字符串是 "Hello " 和 "World!\n"。这样做可以在崩溃之前打印出 "Hello "。我怀疑我做得很正确...除了我应该以某种方式清理(但我不确定如何)。

我做错了什么?我已经尝试过一些组合大小,也尝试过在调用 WinAPI 之前 "分配 Shadow Space"(我应该这样做吗?)。

应该注意的是,当我根本不关心 Shadow Space 时,这个方法完全可以正常工作。然而,由于我的 write 函数调用 WinAPI(因此不是叶子函数),所以我正在尝试遵守 ABI。


1
也许调用约定的历史,第5部分:amd64会有所帮助?特别要注意被调用函数需要重新对齐堆栈,看起来你没有这样做。 - Harry Johnston
谢谢@HarryJohnston。这个明天早上我要读的事情在我的清单上(现在有点晚了!)。我会回来告诉你我怎么做的 :) - Simon Whitehead
1
除了提到的其他问题外,您还忘记生成解开数据,以便系统在发生异常时可以遍历堆栈。 - Raymond Chen
确实。这是...又一个...我不知道的整个功能部分 :) 我找到了一些NASM示例,基本的要点似乎是,您的解除绑定数据和异常处理程序应该将您的例程返回到它最初调用的状态。这很有道理,因为如果您在整个例程/帧方法中进行了修改,环境如何神奇地知道如何恢复堆栈帧呢?它不能...你必须告诉它。我想我还需要更深入地研究这个问题(因为它已经成为了自己的问题)。谢谢大家。 - Simon Whitehead
1
@RaymondChen:很遗憾,您之前评论中的链接已经失效。https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170 是当前通用调用约定的链接。https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-170 在“x64 ABI 约定”部分是关于“x64 异常处理”的内容。 - Peter Cordes
显示剩余6条评论
2个回答

10

在调用前必须直接提供阴影空间。将阴影空间想象成旧的stdcall / cdecl约定中的遗留物:对于WriteFile,您需要五个push。阴影空间代表最后四个推(前四个参数)。 现在您需要四个寄存器,阴影空间(只需空间,内容不重要)和一个值位于阴影空间之后的堆栈上(实际上是第一个推)。当前返回给调用者(start)的返回地址在WriteFile将用作阴影空间的空间中 ->崩溃。

您可以在函数write内为WinAPI函数(GetStdHandleWriteConsoleA)创建新的阴影空间:

write:
    push rbp
    mov rbp, rsp
    sub rsp, (16 + 32)      ; 5th argument of WriteConsoleA (8) + Shadow space (32)
                            ; plus another 8 to make it a multiple of 16 (to keep stack aligned after one push aligned it after function entry)

    mov [rbp+16],rcx        ; <-- use our Shadow space, provided by `start`
    mov [rbp+24],rdx        ; <-- and again, to save our incoming args

    mov rcx, -11            ; Get handle to StdOut
    call GetStdHandle

    mov rcx,rax             ; hConsoleOutput
    mov rdx, [rbp+16]       ; lpBuffer        ; reloaded saved copy of register arg
    mov r8, [rbp+24]        ; nNumberOfCharsToWrite
    mov r9,empty            ; lpNumberOfCharsWritten
    mov qword [rsp+32],0    ; lpReserved - 5th argument directly behind the shadow space
    call WriteConsoleA

    leave
    ret

1
这让我更加困惑了,因为您已经声明Shadow Space应该在“调用之前直接提供”(我也是这样理解的)。然而,您的示例按照通常的方式在函数内设置本地堆栈(直接在调用之后)。那么到底是哪一个呢?(我的意思不是要表现粗鲁 - 我仍然不确定您的意思) - Simon Whitehead
1
哦,抱歉-我现在明白了。你示例中的影子空间是为调用WinAPI而设置的。你是说我上面的示例是正确的,我只需要为调用WinAPI添加影子空间。是这样吗? - Simon Whitehead
1
@SimonWhitehead:是的。顺便说一句,我碰到了add rsp,0x28这个问题,它与sub rsp,0x20不匹配。这在这里并不重要,但未来你可能会遇到麻烦。 - rkhb
好的,我现在明白了。阴影空间通过方法调用向下级联。我应该在主函数中设置一些阴影空间,然后在写入方法中,我应该为WinAPI调用保留另外32字节的阴影空间,并可以通过从rbp偏移来使用主函数中的阴影空间。这对我很有意义。我明天会尝试一下!非常感谢! - Simon Whitehead
1
@ScienceDiscoverer:如果你想调用遵循Windows x64 ABI / 调用约定的其他函数,你需要在调用之前回到RSP % 16 == 0。如果你的自定义汇编函数没有进行任何调用,那么RSP对齐就不重要了。 - Peter Cordes
显示剩余6条评论

4
为了完整起见,我将这个放在这里,因为这就是我最终得出的结果。这个非常完美,据我所见,除了x64 ASM在Windows上需要UNWIND_INFO/异常处理要求之外,这几乎是完全正确的。评论也有希望是准确的。
编辑:
根据下面Raymond的评论,现在已更新。我删除了rbp的保留,因为它不是必需的,而且使我的堆栈对齐比我想象的更进一步。
; Windows APIs

; GetStdHandle
; ------------
; HANDLE WINAPI GetStdHandle(
;     _In_ DWORD nStdHandle
; ); 
extern GetStdHandle

; WriteFile
; ------------
; BOOL WINAPI WriteFile(
;   _In_        HANDLE       hFile,
;   _In_        LPCVOID      lpBuffer,
;   _In_        DWORD        nNumberOfBytesToWrite,
;   _Out_opt_   LPDWORD      lpNumberOfBytesWritten,
;   _Inout_opt_ LPOVERLAPPED lpOverlapped
; );
extern WriteFile

; ExitProcess
; -----------
; VOID WINAPI ExitProcess(
;     _In_ UINT uExitCode
; );
extern ExitProcess

global start

section .data

    STD_OUTPUT_HANDLE   equ -11
    NULL                equ 0

    msg1                 db "Hello ", 0
    msg1.len             equ $-msg1

    msg2                 db "World!", 10, 0
    msg2.len             equ $-msg2

section .bss

empty               resd 1

section .text

start:

    sub rsp,0x28    ; Allocate 32 bytes of Shadow Space + align it to 16 bytes (8 byte return address already on stack, so 8 + 40 = 16*3)

    mov rcx,msg1
    mov rdx,msg1.len
    call write

    mov rcx,msg2
    mov rdx,msg2.len
    call write

    mov rcx,NULL
    call ExitProcess

    add rsp,0x28    ; Restore the stack pointer before exiting

    ret

write:

    ; Allocate another 40 bytes of stack space (the return address makes 48 total). Its 32
    ; bytes of Shadow Space for the WinAPI calls + 8 more bytes for the fifth argument
    ; to the WriteFile API call.
    sub rsp,0x28

    mov [rsp+0x30],rcx      ; Argument 1 is 48 bytes back in the stack (40 for Shadow Space above, 8 for return address)
    mov [rsp+0x38],rdx      ; Argument 2 is just after Argument 1

    mov rcx,STD_OUTPUT_HANDLE   ; Get handle to StdOut
    call GetStdHandle

    mov rcx,rax             ; hFile
    mov rdx,[rsp+0x30]      ; lpBuffer
    mov r8,[rsp+0x38]       ; nNumberOfBytesToWrite
    mov r9,empty            ; lpNumberOfBytesWritten

    ; Move the 5th argument directly behind the Shadow Space
   mov qword [rsp+0x20],0   ; lpOverlapped, Argument 5 (just after the Shadow Space 32 bytes back)
    call WriteFile

    add rsp,0x28        ; Restore the stack pointer (remove the Shadow Space)

    ret

这导致...:

最终工作!


@RaymondChen 再次请您详细解释一下吗?我正在努力学习,如果有什么偏差,我想了解为什么。我已经思考了一段时间,就我所理解的而言,它是正确的 - 所以如果我错了,那我就有一个大误解。我认为,32字节的影子空间+第五个参数“WriteFile”的8字节使其达到40字节。返回地址在堆栈上使其达到48字节 - 那么添加40字节不会对齐吗?困惑 - Simon Whitehead
整天都在思考...似乎我完全忘记了前言中的 push rbp...这又是另外8个字节,在 sub rsp,0x28 之前发生。所以实际上这将使堆栈增加到56个字节,这不是16的倍数。因此,我应该将其设置为 sub rsp,0x30,将其推到64个字节。 - Simon Whitehead
您的选项包括调整 sub rsp 以考虑 push rbp 使用的内存;或者您可以完全删除 push rbp,然后修改使用 rbp 访问参数的代码,以便使用其他机制。 - Raymond Chen
1
基本上,这一切似乎都是基于这样一个想法:使用push会使SP和堆栈帧之间的关系在代码的不同部分中变得不同,从而使事情变得复杂,因此应该避免使用。虽然这可能过于简单化了。 :-) - Harry Johnston
参数溢出看起来是正确的。对于SysV ABI,write()可能会使用push / pop rbx和rbp,并使用它们在call之间保留函数参数。或者直接溢出参数,在堆栈上保留空间后进行。在现代CPU中,push只有1个字节大小,并且在栈引擎上与mov一样快,只要你小心放置它以避免额外的同步uops。据我所知,我曾经看到clang为了遵守ABI的栈对齐限制而推送/弹出一个虚拟寄存器。 - Peter Cordes
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接