標籤:

函數的局部變數在棧中是如何分布的?

C++代碼如下(基於Windows7系統使用VS2015進行編譯):

#include &
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
b=002AFB48

所奇怪的是為什麼第一次調用func1() 函數時,變數a在高地址、變數b在低地址,而後面四次調用 func1() 函數時,變數a在低地址、變數b在高地址,這是為什麼呢?


1. 所有的局部變數和右值的存儲位置沒有必然要求,在O2級別優化下,他們可以存放在任何位置,棧,寄存器,x64 GCC在調用函數前還可以把數據臨時放red zone里(毗鄰堆棧指針RSP之外128位元組的空間)。

2. 問題主通過去取地址的方法來測試不可取。因為這樣會逼的編譯器把可能本來放寄存器里的變數,臨時找塊內存去放,從而滿足取地址的需求。

3.正確的方法是使用調試器加斷點加反彙編去觀察變數的行為,這樣可以觀察某個變數的行為來確定他是否被綁定到某個寄存器上。

----------------------------------------------------------------------

這麼多人贊就多說一句:C/C++編譯器關注於你的語義,儘力優化你的行為,不會給你預設「某個pod局部變數或右值需要放在某個位置」這樣的限制;當然反過來,如果你想內嵌彙編和C/C++混合編程,就得小心些:編譯器可能使用任何你認知範圍外的寄存器或者內存。所以需要你通過諸如破壞性描述符之類的提醒編譯器你的彙編中有哪些寫行為,或者默認認為編譯器C部分使用所有存儲,然後看反彙編的結果調整。


具體的原因請參考 @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&<&<& &>
add esp, 8
mov ecx, eax
call std::basic_ostream& &>::operator&<&< push eax call std::endl& &>
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&<&<& &>
add esp, 8
mov ecx, eax
call std::basic_ostream& &>::operator&<&< push eax call std::endl& &>
push eax
call std::endl& &>
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&<&<& &>
add esp, 8
mov ecx, eax
call std::basic_ostream& &>::operator&<&< push eax call std::endl& &>
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&<&<& &>
add esp, 8
mov ecx, eax
call std::basic_ostream& &>::operator&<&< push eax call std::endl& &>
push eax
call std::endl& &>
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&<&<& &>
add esp, 8
mov ecx, eax
call std::basic_ostream& &>::operator&<&< push eax call std::endl& &>
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&<&<& &>
add esp, 8
mov ecx, eax
call std::basic_ostream& &>::operator&<&< push eax call std::endl& &>
push eax
call std::endl& &>
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&<&<& &>
add esp, 8
mov ecx, eax
call std::basic_ostream& &>::operator&<&< push eax call std::endl& &>
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&<&<& &>
add esp, 8
mov ecx, eax
call std::basic_ostream& &>::operator&<&< push eax call std::endl& &>
push eax
call std::endl& &>
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&<&<& &>
add esp, 8
mov ecx, eax
call std::basic_ostream& &>::operator&<&< push eax call std::endl& &>
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&<&<& &>
add esp, 8
mov ecx, eax
call std::basic_ostream& &>::operator&<&< push eax call std::endl& &>
push eax
call std::endl& &>
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])

就這樣嗯。


主要原因R大都回答了,在O2下所有對func1的調用都inline了,a和b都變成了main函數的棧局部變數,這樣5次對func1函數的inline會產生10個int的變數。變數個數增加了不少,但是裡面大部分的life time不重疊,VC++裡面有stack pack專門來把這些life time不重疊的,大小兼容的變數分配到同一個stack slot。下面是具體做法。

下面是被inline後的a和b按定義和使用的時間排序。

{
_b$1
_a$1
}
{
_b$2
_a$2
}
...

第一對大括弧是第一次inline, a和b都是live的而不能共享,分配兩個stack slots。從第二對大括弧開始,第一次inline的兩個stack slot已經空閑可以重用。但是要重用也得挨個檢查前面的stack slot看看是否兼容。那麼答案來了,現在演算法是倒序搜索的,差不多就是下面的代碼, 所以_b$2重用了_a$1,_a$2隻能重用_b$1,類推下去。作為驗證可以在被inline的函數裡面定義更多的變數,看看第二次及以後的調用的變數地址是不是剛好倒過來。

for (i = allocatedStackSlots - 1; i &>= 0; i--)


不要以為你寫的局部變數在編譯之後仍然存在,在一些情況下,有可能直接被優化掉。(連寄存器都不給一個)


重新用 prinf寫了下(cout的彙編太難讀了)

@Thomson (啥破編輯器, @無效)所說的

1. 五個func1 全部inline了

2. a, b開始是 (-8,-4), 後面全部反過來了。。

(ps: vs原來的調試彙編窗口呢??)

pps:

測試的時候發現個好玩的。

賦值

od顯示如下,inline了, 結果還是倒著的。

現在修改原來的 cout版本的

(inline消失了)

不賦值的情況下cout也是inline的

(神奇的優化界限。。。。。)


我覺的你這樣直接看輸出的話應該判斷不了什麼。局部變數可能就在寄存器上並不在棧上。反彙編一下,可以看出來函數的調用和局部變數地址。csapp第三章過程部分講的就是這個,應該完美解決你的疑問。


這裡的棧是指棧幀,在這裡說的函數變數是由一個動態分配的位元組數組所維護的……建議樓主多看看Hotspot虛擬機或者Python虛擬機相關的東西,很有用


推薦閱讀:

能否從編譯原理的角度詳細的描述一下模板編譯的過程?
編譯程序是否有操作系統的參與?
編譯器、解釋器和虛擬機有什麼區別和聯繫?大體原理是什麼?
編譯器處理轉義符?
Android上ARM本地庫是如何運行在其他CPU架構上的?

TAG:C | 編譯原理 | CC |