阴影空间示例

Shadow space example

提问人:Simon Whitehead 提问时间:10/22/2015 最后编辑:Peter CordesSimon Whitehead 更新时间:1/20/2022 访问量:3307

问:

编辑:

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

编辑 2:我还设法在 YouTube 视频(所有内容)的注释中找到了一个指向调用约定 PDF 的链接,该视频在 Linux 上的 Shadow Space 和 Red Zone 上有一些有趣的花絮。可以在这里找到: http://www.agner.org/optimize/calling_conventions.pdf

源语言:

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

我的理解是这样的:

  • 来电者应在sub rsp,<bytes here>call callee
  • 如果需要,被调用方应使用它来存储寄存器(或局部变量,如果不需要保存寄存器)
  • 调用方清理它,例如: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 时,这工作得很好。但是,我正在尝试符合 ABI,因为我的函数调用 WinAPI(因此不是叶函数)。write

Windows 程序集 x86-64 NASM 堆栈内存

评论

1赞 Harry Johnston 10/22/2015
也许调用约定的历史,第 5 部分:amd64 会有所帮助?特别要注意的是,被调用的函数需要重新对齐堆栈,看起来你没有这样做。
0赞 Simon Whitehead 10/22/2015
谢谢@HarryJohnston。这是我明天早上要阅读的内容清单(现在有点晚了!我会回来检查,让你知道我是怎么去的:)
1赞 Raymond Chen 10/22/2015
除了提到的其他问题之外,您还忘记生成展开数据,以便在发生异常时系统可以遍历堆栈。
0赞 Simon Whitehead 10/23/2015
@RaymondChen 你能详细说明吗?“放松数据”对我来说是一个新名词(顺便说一句:这些年来,你的博客对我非常有帮助:))
1赞 Peter Cordes 7/19/2022
@RaymondChen:不幸的是,您之前评论中的链接已失效。learn.microsoft.com/en-us/cpp/build/......是一般调用约定的当前链接。learn.microsoft.com/en-us/cpp/build/......是“x64 ABI 约定”部分中的“x64 异常处理”。

答:

10赞 rkhb 10/22/2015 #1

必须在呼叫之前直接提供影子空间。将阴影空间想象成旧 stdcall/cdecl 约定的遗物:因为您需要五次推送。阴影空间代表最后四次推动(前四个参数)。现在你需要四个寄存器,影子空间(只是空间,内容无关紧要)和影子空间之后堆栈上的一个值(实际上是第一次推送)。当前,调用方 () 的返回地址位于将用作影子空间的空间中,>崩溃。WriteFilestartWriteFile

您可以在函数内为 WinAPI 函数 ( 和 ) 创建一个新的影子空间:GetStdHandleWriteConsoleAwrite

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赞 Simon Whitehead 10/22/2015
这让我现在更加困惑,因为你已经说过影子空间应该在“通话前直接”提供(这也是我的理解)。但是,您的示例会像往常一样在函数中设置本地堆栈(直接在调用之后)。那么到底是哪一个呢?(我并不是要听起来很粗鲁——我仍然不确定你的意思)
1赞 Simon Whitehead 10/22/2015
哦,对不起 - 我现在明白了。示例中的影子空间是为调用 WinAPI 设置的。你是说我上面的例子是正确的,我只需要为对 WinAPI 的调用添加影子空间。这是对的吗?
1赞 rkhb 10/22/2015
@SimonWhitehead:是的。顺便说一句:我绊倒了,这不匹配.这在这里并不重要,但你将来可能会遇到麻烦。add rsp,0x28sub rsp,0x20
0赞 Simon Whitehead 10/22/2015
右。。我想我现在明白了。影子空间通过方法调用向下级联。我应该在 main 中设置一些影子空间,然后在 write 方法中,我应该为 WinAPI 调用保留另一个 32 字节的影子空间,并且可以通过 的偏移量使用 main 中的影子空间。这对我来说是有道理的。我明天会试试!非常感谢!rbp
1赞 Peter Cordes 8/7/2022
@ScienceDiscoverer:如果要按照 Windows x64 ABI / 调用约定调用任何其他函数,则需要在调用前返回 RSP % 16 == 0。如果您的自定义 asm 函数不进行任何调用,则 RSP 对齐无关紧要。
4赞 Simon Whitehead 10/23/2015 #2

为了完整起见,我在这里发布这个,因为这就是我最终的内容。这工作得很好,据我所知,除了 Windows 上 x64 ASM 的 /Exception 处理要求外,这几乎是正确的。希望这些评论也是准确的。UNWIND_INFO

编辑:

在雷蒙兹在下面发表评论后,现在更新了这一点。我删除了保留,因为它不是必需的,并且将我的堆栈对齐方式超出了我的预期。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

这导致...:

Finally working!

评论

0赞 Raymond Chen 10/24/2015
序言未建立 16 字节堆栈对齐。write
0赞 Simon Whitehead 10/24/2015
@RaymondChen再说一遍..你能详细说明一下吗?我正在努力学习,所以如果它以某种方式关闭,我想了解为什么。我想了想,据我所知,它是正确的——所以如果我错了,我有一个很大的误解。我想,Shadow Space 的 32 个字节 + 第五个参数的 8 个字节等于 40。堆栈上的返回地址是 48 - 那么添加 40 个字节不会对齐吗?困惑WriteFile
0赞 Simon Whitehead 10/24/2015
想了一整天......看来我已经完全忘记了序幕中的......这是另一个 8 个字节,并且发生在 .因此,实际上这会将堆栈增加到 56 字节,这不是 16 的倍数。因此,我可能应该将其推送到 64 字节。push rbpsub rsp,0x28sub rsp,0x30
0赞 Simon Whitehead 10/24/2015
如果以上是正确的。.我可以完全避免存储(我猜这是编译器会做的优化)。rbp
1赞 Harry Johnston 10/27/2015
基本上,这一切似乎都基于这样一种想法,即通过使 SP 和堆栈框架之间的关系在代码的不同部分不同而使事情复杂化,因此应该避免。不过,这可能过于简单化了。:-)push