關於入棧、出棧,棧頂棧底之類的分析見
函數調用的壓棧出棧過程分析
下面繼續分析C/C++的記憶體分布。
雖然0x10比一個變數需要的4個地址大了一些,但是0x10應該是規定的最小單位了。假如你要用的空間剛好是它的整數倍,其實是不浪費一分錢棧空間的,下邊做一個數組,證明棧空間大小剛好是所有非靜態變數佔用空間的大小。
這裡可以先無視hello的聲明,它不在棧空間,後面會分析。
x6 #include<stdio.h> x x7 int main(){ x x9 static int hello; x B+>x11 int array[100] = {7,4,1}; x x16 return 0; x x17 }
下邊是彙編代碼:比普通變數情況下複雜些,多用了一些寄存器,多了一些push、pop操作,還有rep指令。
x0x8048394 <main> push %ebp x x0x8048395 <main+1> mov %esp,%ebp x x0x8048397 <main+3> push %edi x x0x8048398 <main+4> push %ebx x x0x8048399 <main+5> sub $0x190,%esp xB+>x0x804839f <main+11> lea -0x198(%ebp),%ebx x x0x80483a5 <main+17> mov $0x0,%eax x x0x80483aa <main+22> mov $0x64,%edx x100個變數(應該還有個標誌類型大小是4的東西吧) x0x80483af <main+27> mov %ebx,%edi x x0x80483b1 <main+29> mov %edx,%ecx x x0x80483b3 <main+31> rep stos %eax,%es:(%edi) x x0x80483b5 <main+33> movl $0x7,-0x198(%ebp) x初始化了三個數。 x0x80483bf <main+43> movl $0x4,-0x194(%ebp) x x0x80483c9 <main+53> movl $0x1,-0x190(%ebp) x x0x80483d3 <main+63> mov $0x0,%eax x x0x80483d8 <main+68> add $0x190,%esp x0x80483de <main+74> pop %ebx x x0x80483df <main+75> pop %edi x x0x80483e0 <main+76> pop %ebp x x0x80483e1 <main+77> ret x x
下邊是棧空間大小。兩者相減,0x190,等於400bytes,剛好是100個int。
(gdb) print $esp$1 = (void *) 0xbffff480(gdb) print $ebp$2 = (void *) 0xbffff618
我們都知道,”加了static就延長了變數的生命週期到程式結束“,或者還知道”加了static的變數地址不太一樣“。
確實,地址變了,靜態變數不在棧裡。
int main(){ static int hello; int top; return 0;}
地址,top在棧中,而hello不在
(gdb) print &top$3 = (int *) 0xbffff614(gdb) print &hello$4 = (int *) 0x8049614
棧大小是0x10
(gdb) print $esp$3 = (void *) 0xbffff608(gdb) print $ebp$4 = (void *) 0xbffff618
去掉int型的聲明,只留一個靜態變數的聲明,會發現一個有意思的現象 也就是說,連語句都被”最佳化“走了,不在main()函數裡邊執行了,不在main()函數裡執行意味著什麼。生命週期不光被向後延長到程式結束,也被向前延長了,它會在main之外,更早的地方被初始化。 那麼如果我在靜態變數聲明語句之前,main()函數之內執行,究竟允不允許呢。可能文法上會做屏蔽,但是按運行順序應該是可以的。 這句話換算成代碼的話,是這樣的:
#include<stdio.h>int main(){ hello = 4; static int hello; return 0;}
連初學者都認為這樣不可能,但是,如果我換一種寫法呢。
#include<stdio.h>static int hello;int main(){ hello = 4; return 0;}
這樣大家就都認可了吧。 但是我覺得這兩個本質上是等價的, 只不過編譯器在碼農層面把這個操作給屏蔽了,可能太奇葩不便於理解。個人覺得是這樣的。不用去試了,確實編譯不過。。。。 換成可編譯的版本運行一次:
x0x8048394 <main> push %ebp x x0x8048395 <main+1> mov %esp,%ebp xB+>x0x8048397 <main+3> movl $0x4,0x8049614 x x0x80483a1 <main+13> mov $0x0,%eax x x0x80483a6 <main+18> pop %ebp x x0x80483a7 <main+19> ret x
main內聲明肯定是看不到的,直接用了一下而已,如果沒有那個指派陳述式,main內部什麼命理你個都看不到,直接就結束了。
我又想到另一個法子,因為編譯後我可以看地址, 有時候(可能吧)不做”變數列表“(也就是所有變數類型和數量的綜合)級的改動,其真實位址都是差不多的,所以可以嘗試先編譯運行,看到地址後,再強行提取變數,C語言的強大。(C語言不是這樣玩的好麼-_-) 這是我的設想:
#include<stdio.h>int main(){ int *ptr = 0x8049614; printf("%d\n",*ptr); static int hello = 100; return 0;}
print *ptr = 0(期望100) 沒成功,但是還有希望,因為這次編譯地址變了:
再來:
#include<stdio.h>//static int hello = 100;int main(){ int *ptr = 0x8049578; printf("%d\n",*ptr); static int hello = 100; return 0;}
不過討厭的是又變了。。。。。。。這個(全域變數區。怎麼叫來著。)地址分配很動態,不如棧的地址那麼穩固。
一定要抓住它,這下試試664。
終於被我抓住了。
靜態變數hello的聲明和定義都在13行,但我在12行就提前並列印了出來。
來來來,再來個普通運行。
然並卵,現實中哪有人運行完了再去看結果,硬把地址寫進去的,我也不知道這麼玩能應用在哪。
這個例子只為證明: 1.靜態變數的儲存地區不在棧,並且也不像棧的地址規則那麼穩定,但是還是能在一定程度上穩定住的,所以能抓住。 2.遇到那個“static”,這句聲明加定義就和放在main外邊沒區別,這種代碼的執行等於在程式正式執行之前就做完了,算初始化吧。 3.所以,無論靜態變數聲明在哪,它的生命週期都貫穿程式始終。 4.明明很早就聲明定義了,之所以你不能直接這樣提取靜態變數的值,應該只是編譯器設的規則,來避免一些不必要的人為錯誤吧。 5. static叫靜態,靜態靜在哪。動態又動在哪。動態當然是動在你運行它才有結果,不運行就沒有。棧雖然也會提前(運行函數體前)分配夠空間,但是最起碼變數的定義賦值是不會有的,而static,是一早就定義賦值完了,所以叫靜態。(所以有充分理由懷疑編譯器就是根據變數類型和數量,假設叫”變數列表“來分配棧空間的,誰叫我學渣沒怎麼學編譯原理呢,只能先猜測一下) 6.不要模仿
擴充訓練: 多加兩層函數呢。
請容許我複習一下C++: C++類靜態成員是否會有所不同。是很早就初始化了,還是從某一個對象運行該函數開始。假如說也是帶定義的形式,比如,static int i = 1;而不是static int i; 首先,第一種形式,ISO C++又不允許了(錯誤:ISO C++ 不允許在類內初始化非常量靜態成員)。 其次,又不允許使用A::i這種形式訪問A的靜態成員變數i。
“奇怪”的發現: 當用一個類連續聲明多個類對象的時候,後邊的類對象被某種最佳化機制放在不一樣的地方了(堆。全域靜態區。)
具體流程如下: 第一次編譯: F f,f2;//對象在棧
第二次編譯:
F f,f2,f3,f4,f5,f6,f7,f8,f9;//只有f和f2在棧 //註:此時esp和ebp沒變,棧沒有擴容,是出於某種“懶惰”,還是“不屑”。
第三次編譯:
F f,f2,f3,f4;
F f5,f6,f7,f8,f9;//對象都在棧
第四次編譯:
F f,f2,f3,f4,f5,f6,f7,f8,f9;//所有對象都在棧。
首先,排除人為錯誤,雖然分配堆的時候F *pF = F();//print &pF指標地址在棧,print pF指標指向的對象在堆。但是此處並不是指標,是實體。。。
編譯器做了某種最佳化,這個最佳化的區分絕對不是代碼使用不使用,f2就沒使用,為什麼和f一樣放到棧了。但是
我的總結就是,這個物件變數上次有地址,這次就還有,因為之前只有 F f,f2;//導致了f2在棧,其他的f3到f8都是後來追加了,
編譯器因為習慣,把f2保留在了棧,其他的扔一邊去了。這次也是,因為拆分成兩句進行聲明時f4,f5們也有了棧地址,所以再合并回去的時候, 編譯器出於習慣,繼續把他們放在棧中。
所以說,編譯器是帶記憶(緩衝)的 。
不過話說回來,一次聲明太多個物件變數,他們為什麼被扔到其他位置了。這是“把未聲明的變數放在靜態變數區”了。
而如果把E e,e2;//突然變成 E e,e2,e3,e33,e333,e3333,e33333,e333333,e3333333,e33333333;//再去編譯
結果所有對象也在棧中,隨機性過強,所以不再糾結這種編譯器的個例特性。浪費時間。
======================================================================================================================= 20160324:補充一個關於函數體與代碼塊的 關於入棧、出棧與棧幀,此文和前邊的一篇關於函數調用的博文已經寫得很清楚(也可能不清楚,兩篇有重合,歸類亂了點~~~~) 總之,關於函數的局部變數,因為是在存在當前棧幀的,所以外部變數在此時是完全屏蔽的,局部變數也活不到外部。 但是代碼塊是個特例(只是從表象知識點來說): 代碼塊可以聲明“局部”變數,而對這個“局部”變數的修改不會影響到上一層的“此”變數,說起來亂,上代碼:
#include<stdio.h>int main(){ int i = 0; { int j = 1; i = 1; printf("i:%d\n",i); int i = 2; printf("inside of block:i:%d\n",i); printf("j:%d\n",j); } printf("outside of block:i:%d\n",i);}
i的列印結果是1,2,1. 這裡i是可以“重複聲明”的,並且聲明的局部的i的修改不會影響外部的。有些人根據表面概念,可能對此有些迷惑。這看似也是局部,代碼塊的局部和函數體的局部有什麼區別呢。
首先確定的是,如果不重複聲明,在代碼塊內的修改,對外部是全部生效的,這點通過for迴圈之類的代碼塊也會有所體會。 那麼如果重複聲明,區別在哪呢。其實這個“重複聲明”,不過是新聲明的一個變數罷了,你把它假設為k,這樣對k的任何修改,當然都不會影響i。
其他的機制啦,協調工作啦,都是編譯器自己映射好了,不用管了。
不過這樣做也有缺點,都要佔用棧空間嘛,又不是你真的去短暫覆蓋原變數過後再去恢複,沒有個記憶體用來記憶怎麼可能覆蓋了再恢複。 這樣的好處可能也就是你常用 for(int i = 0;condition;modify i); 這種東西的話,避免互相干擾吧。 所以,如果有需求,局部乾脆還是換個名字(k)吧,本質一樣免得降低可讀性。
====================================================================================================================== 關於main函數之前的那些彙編,我還不太懂,有空再摸索,想知道靜態變數的初始化,各種類,還有虛函數表的初始化過程。