为啥 MSVC 不在生成的汇编代码中分配 32 字节的影子空间?

Posted

技术标签:

【中文标题】为啥 MSVC 不在生成的汇编代码中分配 32 字节的影子空间?【英文标题】:Why isn't MSVC allocating a 32 byte shadow space in the generated assembly code?为什么 MSVC 不在生成的汇编代码中分配 32 字节的影子空间? 【发布时间】:2020-10-26 17:28:41 【问题描述】:

我试图查看 MSVC 如何分配其 32 字节的影子空间,但它似乎只分配了 8 字节的影子空间。

// Test.c
int main() int var1 = 1;

上面的程序生成以下 .asm 文件:

var1$ = 0

main    PROC
; Test.c
    sub rsp, 24                    ; allocates 24 bytes
    mov DWORD PTR var1$[rsp], 1
    xor eax, eax
    add rsp, 24
    ret 0
main    ENDP

它只分配 24 个字节。当我声明 4 个变量时,它分配了相同的数量,并且由于每个变量都是 4 个字节,这意味着 24 个字节中的 16 个字节用于声明的变量,剩下 8 个字节用于影子空间。 只有在声明 5 个变量时,它才会分配 40 字节的影子空间。为什么它只分配 8 字节的影子空间? 我使用命令CL Test.c /Fa编译了程序

【问题讨论】:

我想你忘了计算调用main的代码推送的返回地址,这意味着堆栈指针在进入main时未对齐 8个字节。 这就解释了! 【参考方案1】:

RSP 中减去 24 与阴影空间没有任何关系。阴影空间仅适用于 main 调用其他一些 64 位 Microsoft ABI 兼容函数时。您的 main 函数是一个叶函数(它不调用任何其他函数),因此不需要为影子空间分配额外空间。如果您修改 main 以调用 C/C++ 库或 WinAPI 中的某些内容,您会发现会为影子空间添加额外的空间来进行此类调用。

鉴于您的函数正在处理 32 位值(并且没有数组)并且不调用任何其他内容,我认为没有理由需要对齐 16 字节边界或添加额外的填充,但这就是它似乎在做。堆栈上的返回地址会使堆栈错位 8。减去 24 会使其在 16 字节边界上对齐,并在变量后填充。

这可能是由于未使用任何优化(如/O1/O2 等)进行编译或编译器将局部变量空间填充到首选数量时代码生成效率低下的结果。理论上,在这种情况下它不必分配任何堆栈空间。它本可以重用由 C/C++ 启动代码为 main 函数分配的返回地址上方的影子空间。

注意:通过优化,除非您将 var1 设置为 volatile 变量,否则代码将被完全消除。编译器应该能识别出你写的代码除了返回给调用者之外什么都不做。


下面的例子调用ExitProcess表示添加了阴影空间; C/C++ 启动代码为局部变量调用main 分配的影子空间的重用;并为它不适合阴影空间的变量使用一些堆栈空间。因为名为@9​​87654332@ 的WinAPI 需要在调用它之前分配32 字节的影子空间。如果你从这个例子中删除它,编译器将不会为它分配额外的空间。

test.c

// Test.c

// Get prototype for ExitProcess
#include <windows.h>

int main() 

    volatile int var1 = 1;
    volatile int var2 = 2;
    volatile int var3 = 3;
    volatile int var4 = 4;
    volatile int var5 = 5;

    // Since this is a WinAPI call it needs shadow space allocated
    ExitProcess(var1+var2+var3+var4+var5);

    // We won't get this far
    return 0;

如果你用/O2 optimizations 编译它以获得最大速度,使用CL Test.c /Fa /O2 你可能会看到类似的东西:

var1$ = 32
var5$ = 64
var4$ = 72
var3$ = 80
var2$ = 88

main    PROC
    sub rsp, 56                 ; 00000038H
    mov DWORD PTR var1$[rsp], 1
    mov DWORD PTR var2$[rsp], 2
    mov DWORD PTR var3$[rsp], 3
    mov DWORD PTR var4$[rsp], 4
    mov DWORD PTR var5$[rsp], 5

    mov edx, DWORD PTR var5$[rsp]
    mov eax, DWORD PTR var4$[rsp]
    add edx, eax
    mov ecx, DWORD PTR var3$[rsp]
    add ecx, edx
    mov edx, DWORD PTR var2$[rsp]
    add edx, ecx
    mov ecx, DWORD PTR var1$[rsp]
    add ecx, edx

    call    QWORD PTR __imp_ExitProcess
    int 3
main    ENDP

var1RSP 的偏移量为 32,因为影子空间是从 RSP 开始调用 ExitProcess 的前 32 个字节。其他变量 var2var3var4var5 都以 >= 64 的偏移量开始。编译器对 RSP 生成了 56 的调整。返回地址在 RSP+56mainRSP+64RSP+96 有影子空间,因此 var2var5 被放置在为 main 分配的影子空间中。

【讨论】:

以上是关于为啥 MSVC 不在生成的汇编代码中分配 32 字节的影子空间?的主要内容,如果未能解决你的问题,请参考以下文章

为啥不在 UIViewController 子类中分配 UIWindow 对象呢?

为啥从 constexpr 引用生成的汇编代码与 constexpr 指针不同?

为啥 C 中没有“memsize”,它返回使用 malloc 在堆中分配的内存块的大小?

将文件加载为字节数组,而不在内存中分配它 C#

在单个代码行 C# 中分配多个变量

如何查找变量是在堆栈还是堆中分配?