函数的局部变量在栈中是如何分布的?

C++代码如下(基于Windows7系统使用VS2015进行编译): #include <iostream> using namespace std; void func1() { int a; int b; cout << "&a=" << &a << endl << "&b=" << &b << endl << endl; } int main() { func1(); func1(); func1(); func1(); func1(); return 0; } 运行结果如下所示: &a=002AFB48 &b=002AFB44 &a=002AFB44 &b=002AFB48 &a=002AFB44 &b=002AFB48 &a=002AFB44 &b=002AFB48 &a=002AFB44…
关注者
176
被浏览
11500
具体的原因请参考 @Thomson 大大的回答:函数的局部变量在栈中是如何分布的? - Thomson 的回答 - 知乎 <- 帮忙顶起来~

这事情很明了:题主用Visual Studio 2015中的C++编译器(“CL”),以Release模式编译了问题描述中的测试程序。

所以呢?Release模式的配置下默认是会打开优化的。而此例中 main() 对 func1() 的5次调用都会被完全内联到 main() 中,内联后的样子大概是这样的:
int main()
{
  // func1(); // 1
  {
    int a;
    int b;
    cout << "&a=" << &a << endl
         << "&b=" << &b << endl
         << endl;
  }

  // func1(); // 2
  {
    int a;
    int b;
    cout << "&a=" << &a << endl
         << "&b=" << &b << endl
         << endl;
  }

  // func1(); // 3
  {
    int a;
    int b;
    cout << "&a=" << &a << endl
         << "&b=" << &b << endl
         << endl;
  }

  // func1(); // 4
  {
    int a;
    int b;
    cout << "&a=" << &a << endl
         << "&b=" << &b << endl
         << endl;
  }

  // func1(); // 5
  {
    int a;
    int b;
    cout << "&a=" << &a << endl
         << "&b=" << &b << endl
         << endl;
  }

  return 0;
}
然后CL编译器决定对这5对a、b局部变量总给只分配2个stack slot给它们,并在作用域不重叠的情况下复用空间。

但是!不知道CL编译器具体为啥有点抽风(但就此测试的结果来说绝对是正确符合C++语义要求的),这5对a、b局部变量的分配,分别是:
_b$1 = -8                                         ; size = 4
_b$2 = -8                                         ; size = 4
_b$3 = -8                                         ; size = 4
_b$4 = -8                                         ; size = 4
_a$5 = -8                                         ; size = 4
_a$6 = -4                                         ; size = 4
_a$7 = -4                                         ; size = 4
_a$8 = -4                                         ; size = 4
_a$9 = -4                                         ; size = 4
_b$10 = -4                                          ; size = 4
这是用CL 19.x系列在x86上/O2优化级别编译出来的结果,-4、-8分别代表分配到的stack slot相对frame pointer的偏移量,即便最终生成的代码省略了frame pointer这里的语义也还是一样。

对应到上面示意的内联后代码,分别是:
// group 1
_a$5 = -8                                         ; size = 4
_b$10 = -4                                          ; size = 4

// group 2
_a$9 = -4                                         ; size = 4
_b$4 = -8                                         ; size = 4

// group 3
_a$8 = -4                                         ; size = 4
_b$3 = -8                                         ; size = 4

// group 4
_a$7 = -4                                         ; size = 4
_b$2 = -8                                         ; size = 4

// group 5
_a$6 = -4                                         ; size = 4
_b$1 = -8                                         ; size = 4
所以是的,CL编译器就是选择了给第一对a、b局部变量反着来分配它们的stack slot。这是规范完全允许的实现,是编译器的自由。咱看不到CL编译器源码也就无谓深究它是如何抽风了…

附录:

编译器版本:Microsoft (R) C/C++ Optimizing Compiler Version 19.10.24629 for x86
编译参数:/O2

以下是我以上述配置做实验所看到的 main() 的汇编:
_b$1 = -8                                         ; size = 4
_b$2 = -8                                         ; size = 4
_b$3 = -8                                         ; size = 4
_b$4 = -8                                         ; size = 4
_a$5 = -8                                         ; size = 4
_a$6 = -4                                         ; size = 4
_a$7 = -4                                         ; size = 4
_a$8 = -4                                         ; size = 4
_a$9 = -4                                         ; size = 4
_b$10 = -4                                          ; size = 4
_main   PROC                                      ; COMDAT
        sub      esp, 8
        lea      eax, DWORD PTR _a$5[esp+8]
        push     eax
        push     OFFSET ??_C@_03HIBPDJKI@?$CGa?$DN?$AA@
        push     OFFSET std::cout
        call     std::operator<<<std::char_traits<char> >
        add      esp, 8
        mov      ecx, eax
        call     std::basic_ostream<char,std::char_traits<char> >::operator<<
        push     eax
        call     std::endl<char,std::char_traits<char> >
        add      esp, 4
        lea      ecx, DWORD PTR _b$10[esp+8]
        push     ecx
        push     OFFSET ??_C@_03HKFJIHPB@?$CGb?$DN?$AA@
        push     eax
        call     std::operator<<<std::char_traits<char> >
        add      esp, 8
        mov      ecx, eax
        call     std::basic_ostream<char,std::char_traits<char> >::operator<<
        push     eax
        call     std::endl<char,std::char_traits<char> >
        push     eax
        call     std::endl<char,std::char_traits<char> >
        add      esp, 8
        lea      eax, DWORD PTR _a$9[esp+8]
        push     eax
        push     OFFSET ??_C@_03HIBPDJKI@?$CGa?$DN?$AA@
        push     OFFSET std::cout
        call     std::operator<<<std::char_traits<char> >
        add      esp, 8
        mov      ecx, eax
        call     std::basic_ostream<char,std::char_traits<char> >::operator<<
        push     eax
        call     std::endl<char,std::char_traits<char> >
        add      esp, 4
        lea      ecx, DWORD PTR _b$4[esp+8]
        push     ecx
        push     OFFSET ??_C@_03HKFJIHPB@?$CGb?$DN?$AA@
        push     eax
        call     std::operator<<<std::char_traits<char> >
        add      esp, 8
        mov      ecx, eax
        call     std::basic_ostream<char,std::char_traits<char> >::operator<<
        push     eax
        call     std::endl<char,std::char_traits<char> >
        push     eax
        call     std::endl<char,std::char_traits<char> >
        add      esp, 8
        lea      eax, DWORD PTR _a$8[esp+8]
        push     eax
        push     OFFSET ??_C@_03HIBPDJKI@?$CGa?$DN?$AA@
        push     OFFSET std::cout
        call     std::operator<<<std::char_traits<char> >
        add      esp, 8
        mov      ecx, eax
        call     std::basic_ostream<char,std::char_traits<char> >::operator<<
        push     eax
        call     std::endl<char,std::char_traits<char> >
        add      esp, 4
        lea      ecx, DWORD PTR _b$3[esp+8]
        push     ecx
        push     OFFSET ??_C@_03HKFJIHPB@?$CGb?$DN?$AA@
        push     eax
        call     std::operator<<<std::char_traits<char> >
        add      esp, 8
        mov      ecx, eax
        call     std::basic_ostream<char,std::char_traits<char> >::operator<<
        push     eax
        call     std::endl<char,std::char_traits<char> >
        push     eax
        call     std::endl<char,std::char_traits<char> >
        add      esp, 8
        lea      eax, DWORD PTR _a$7[esp+8]
        push     eax
        push     OFFSET ??_C@_03HIBPDJKI@?$CGa?$DN?$AA@
        push     OFFSET std::cout
        call     std::operator<<<std::char_traits<char> >
        add      esp, 8
        mov      ecx, eax
        call     std::basic_ostream<char,std::char_traits<char> >::operator<<
        push     eax
        call     std::endl<char,std::char_traits<char> >
        add      esp, 4
        lea      ecx, DWORD PTR _b$2[esp+8]
        push     ecx
        push     OFFSET ??_C@_03HKFJIHPB@?$CGb?$DN?$AA@
        push     eax
        call     std::operator<<<std::char_traits<char> >
        add      esp, 8
        mov      ecx, eax
        call     std::basic_ostream<char,std::char_traits<char> >::operator<<
        push     eax
        call     std::endl<char,std::char_traits<char> >
        push     eax
        call     std::endl<char,std::char_traits<char> >
        add      esp, 8
        lea      eax, DWORD PTR _a$6[esp+8]
        push     eax
        push     OFFSET ??_C@_03HIBPDJKI@?$CGa?$DN?$AA@
        push     OFFSET std::cout
        call     std::operator<<<std::char_traits<char> >
        add      esp, 8
        mov      ecx, eax
        call     std::basic_ostream<char,std::char_traits<char> >::operator<<
        push     eax
        call     std::endl<char,std::char_traits<char> >
        add      esp, 4
        lea      ecx, DWORD PTR _b$1[esp+8]
        push     ecx
        push     OFFSET ??_C@_03HKFJIHPB@?$CGb?$DN?$AA@
        push     eax
        call     std::operator<<<std::char_traits<char> >
        add      esp, 8
        mov      ecx, eax
        call     std::basic_ostream<char,std::char_traits<char> >::operator<<
        push     eax
        call     std::endl<char,std::char_traits<char> >
        push     eax
        call     std::endl<char,std::char_traits<char> >
        xor      eax, eax
        add      esp, 16              ; 00000010H
        ret      0
_main   ENDP
留意一下,以 _a$5[esp+8] 为例,这个意思是以esp+8为stack frame base的话,我们要访问的变量位于 _a$5 也就是 -8 的偏移量上,所以结合起来看这里实际的代码是 [esp] ([esp + 8 + _a$5] => [esp + 8 + -8] => [esp])

就这样嗯。