Visual Studio C++ 的函数序言

问题描述

我在VS2019社区版写了一个很简单的C++函数,对应反汇编有个问题。

功能

void manip(char* a,char* b,long* mc) {
    long lena = 0;
    long lenb = 0;
    long lmc;
    long i,j;
    for (; a[lena] != NULL; lena++);
    for (; b[lenb] != NULL; lenb++);
    lmc = lena + lenb + *mc;
    for (i=0; i < lena; i++) a[lena] = a[lena] + lmc;
    for (j=0; j < lenb; j++) b[lenb] = b[lenb] + lmc;
}

拆解(摘录):

void manip(char* a,long* mc) {
00007FF720DE1910  mov         qword ptr [rsp+18h],r8  
00007FF720DE1915  mov         qword ptr [rsp+10h],rdx  
00007FF720DE191A  mov         qword ptr [rsp+8],rcx  
00007FF720DE191F  push        rbp  
00007FF720DE1920  push        rdi  
00007FF720DE1921  sub         rsp,188h  
00007FF720DE1928  lea         rbp,[rsp+20h]  
00007FF720DE192D  mov         rdi,rsp  
00007FF720DE1930  mov         ecx,62h  
00007FF720DE1935  mov         eax,0CCCCCCCCh  
00007FF720DE193A  rep stos    dword ptr [rdi]  

在前三行中,我们将参数放在堆栈中的帧指针之前。在此之后推送帧 rbp 指针。困扰我的是以下三行:

00007FF720DE1921  sub         rsp,rsp

在上面的三行中,据我所知,第一行保留了堆栈上的空间。

问题:

  1. 我不明白为什么要保留这么大的空间(188h),而我们只需要足够保存 5 个 long,即不超过 5*4=20(16h) 字节。
  2. 第二行是新帧指针的计算,但我不明白我们是怎么得到 20h(32) 的。
  3. 我也不明白第三行的意义。

解决方法

这是MSVC调试模式;它保留了额外的空间并使用 0xCC(在罐头中使用 rep stosd 又名 memset,因此使用 mov rdi,rsp 来设置目的地)对其进行毒害,以帮助检测越界访问错误。 (即使没有一个本地人的地址被占用,也没有一个是数组......)

这是一个惊人数量的额外堆栈空间;不知道MSVC是怎么选择预留的。在 Release 模式下(-O2 优化 https://godbolt.org/z/GY7xTYWKq),它当然根本不接触堆栈。

调试模式必须添加一些额外的选项,这些选项不是 MSVC 命令行的默认选项,因为我无法使用 MSVC2015 19.10 或 19.28 在 https://godbolt.org/z/nGo9516b7 上重现此代码生成。在将传入的寄存器 args 溢出到阴影空间后,我只是得到 sub rsp,40,甚至没有将 RBP 设置为帧指针。 (我猜是因为它是一个叶子函数。)

lea rbp,[rsp+20h] 似乎将 RBP 设置为大概的帧指针,但它并不指向返回地址正下方保存的 RBP。通过一些显示它如何使用它的代码,也许我们可以弄清楚。 (看它的 asm 输出,而不是反汇编,这样你就可以获得局部变量的符号名称)。


顺便说一句,如果您想实际了解循环逻辑是如何工作的,那么针对循环优化的 asm 更具可读性。

您的代码充满了从堆栈中重新加载的指针和 movsxd 符号扩展,因为您使用有符号整数作为不是指针宽度的数组索引,并且编译器没有优化为指针- 增量或至少为 64 位整数。 (Signed-overflow being UB allows this optimization。)

How to remove "noise" from GCC/clang assembly output? 的大部分内容都适用于编写优化时有趣的函数。