1.首先說C語言轉彙編語言,全局變數的聲明是編譯器自動給變數分配好內存空間然後把數據存進去,還是讓CPU一條指令一條指令的執行,把數據存入到內存中?

第二,C語言的局部變數聲明是如何做的,比如我在main函數中,int t;不賦值的話,會有mov指令嗎?編譯器怎麼給這個變數t分配空間呢?是也像全局變數一樣,在運行之前就分配好空間還是使用mov指令,給這個局部變數分配空間呢?可是,這個mov的操作對象又是什麼呢?我知道int t=1;mov的操作數是1,把1存入到內存中去,可是,不賦值是怎麼樣的呢?

第三,關於數組的全局變數和局部變數的聲明,編譯器是怎麼樣做的呢?彙編代碼是怎麼樣的呢?

第四,像面向對象的高級語言,當new一個對象的時候,彙編語言是怎麼樣的呢?不可能把這個類的內容,全部使用mov指令,複製到新的內存裡面去吧?那這樣要浪費多少時間啊?要是說,只mov類的地址,那也肯定是不可能的,因為每個new的類,都是獨一無二的,不可能共用同一個地址的。

第五,就比如,int a=1;這句代碼,如果是放在函數內部寫,就是局部變數的話,轉為彙編語言就是mov [地址],#1,就是說把1寫入到內存地址為多少的內存裏去,但是,如果是全局變數的話,轉為彙編就是沒有mov什麼指令了,就是在數據段裡面聲明瞭一個變數,我感覺相當於運行程序後,自動把變數分配了一個全局變數空間,並且把值寫進去,不需要指令去把1寫入到內存了。只是編譯器自動執行的。


偶然看到這個問題,而且看到題主還在更新就進來答一下。

這個問題太龐大了……標題的問題其實跟題目描述關係不大,如何轉換更多的是一些工程上的問題,比如說語法分析、編譯器前後端、llvm中間碼之類的東西,不太涉及到細節,就像你建房不會從燒磚開始一樣。而問題的描述是一些很細節的問題,應該這纔是重點,就回答這些吧。

問題1:

問題描述的不是很清楚,說編譯器如何如何,又說cpu指令如何如何,這其實八竿子打不著,前者是你構建程序的時候的事情(比如說生成exe),後者是這個你構建出來的(exe)程序運行時可能會發生的事情,這是不一樣的。

我就從頭說說從你寫出來的代碼到可運行的程序都發生了什麼,這流水賬中與全局變數有關的節選吧。當然,這份流水賬很不詳細很不工程化,但是回答這個問題差不多夠了。

  • 首先你在程序裡面寫了兩個全局變數int a=10;int b=0;,然後在某個子程序裏代碼訪問了他a+=3;b+=5;
  • 然後你編譯了這個程序。編譯器會來來回回看幾遍你的代碼,找出所有的全局變數記錄下來。
  • 編譯器把所有找到的全局變數整合到一起,無論是你的還是你調用的其他庫裏的,都挨著放一起。
  • 然後把他們的名字用他們的位置替代,比如說a是第一個就寫0,b是第二個就寫4(假定int是4位元組),因為a是int佔用了0/1/2/3這4個位置。以此類推。
  • 然後光有了他們本身的位置還不行,編譯器根據目標系統以及其他參數等整理出一個可用的內存起始地址,比如說0x00403000,把這些數字再加上這個數字。
  • 當然,調用這些變數的地方也都改變了,比如說b+=5也會變成 [0x00403004]+=5
  • 最終編譯器就把這些處理後的變數整理到了目標文件中,順便把地址0x00403000也寫進去了。
  • 你運行了這個生成的文件。操作系統的程序載入器查看文件中要求的各種環境,就看到某處寫著「我需要在0x00403000處存儲大小為n的數據,幫我申請到,然後把旁邊這堆數據載入到那裡。
  • 在處理了這些環境之後,程序載入器把控制器給到你自己寫的main函數,此時這些變數當然已經載入完成了。

大致就是這樣,所以其實這些全局變數被放到了一起存儲在生成的目標文件裏,在你啟動這個程序時,操作系統的程序載入器把這一段數據讀到對應的內存處放著,就跟你寫代碼讀個文件到內存裏什麼的沒多大區別。

問題2:

說完了全局變數,這個問題好像是局部變數。

一句話來說的話,就是這些局部變數在各子程序的棧幀中,子程序內部擴充棧空間存儲這些局部變數,並在離開子程序時隨著棧幀被回收。這跟mov沒什麼關係,大致就是由sp寄存器管理著的一個「棧」,通過調整棧的大小來容納這些局部變數,通常來說你會看到sub sp,xxx就是在開局部變數。

至於棧的細節……有點長,如果已知就跳過吧。

棧這個東西……,雖然一般口語都說堆棧,當然堆和棧是兩個東西。棧是什麼呢,抽象的說,棧是一個只有一個口的容器,先放進去的東西會被壓在下面拿不出來,只能後放進去的拿出來了,之前放進去的才能拿出來,就像一個薯片桶一樣。不過裡面的東西雖然拿不出來,但是可以看到,也可以對裡面的東西進行內容上的修改。系統給每個程序(準確的說是線程)配了這樣一個東西,當然實際上就是一段內存模擬出來的,不過談論這個話題的時候先把什麼內存啊自己啊這些細節的東西放一放,只想抽象的部分

這個棧是給程序保存流程(比如說哪個子程序調用了哪個子程序)和一些小的局部變數用的。使用規則是這樣的。

  • 當調用一個子程序的時候,把參數扔進棧裏,然後把返回地址扔進棧裏,返回地址是什麼呢,就是存著你調用完後回到上一層,回到上一層的哪一行呢,存著這個行號。做完之後把控制權給這個子程序。
  • 當離開一個子程序時,從棧裏一件一件往外取東西並扔掉,直到找到返回地址,記下來,然後再看看自己有幾個參數,再把對應數量的東西拿出來扔了。把控制權交給返回地址那邊。
  • 子程序執行的時候,可以在棧裏隨時放幾個盒子存東西,反正要離開子程序時都會扔掉。

大致就是這樣的,舉個例子演示一下實際上棧的工作流程,這部分有點枯燥,不知道怎麼能寫的明白一點。

foo(){
bar(5,10);//我們假定從這裡開始執行
printf(...);
...
}
bar(int aa,int bb){
int x;
int y=100;
x=5;
baz();
return;
}
baz(){
int pi=3;
...
return;
}

  • 程序執行到了一句bar(5,10), 按照上面的規則,先5扔進棧,再10扔進堆棧,然後把回來的地方也就是"返回地址-foo的第二行"扔進堆棧。最後把控制權交給了子程序bar。
  • 此時堆棧內容為["返回地址-foo的第二行",10,5],從左到右看,左邊是入口。
  • =========================
  • 程序執行到了bar的第一行int x,發現了一個沒初始化的局部變數, 所以往堆棧裏扔了一個空盒子
  • 此時堆棧內容為["蓋上寫著x的空盒子","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序執行到了bar的第二行int y=100往堆棧裏扔了一個盒蓋寫著y裡面裝著100的盒子
  • 此時堆棧內容為["蓋上寫著y的盒子 內容物為100","蓋上寫著x的空盒子","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序執行到了bar的第三行x=5找到堆棧裏標著x的盒子,把5扔進去。
  • 此時堆棧內容為["蓋上寫著y的盒子 內容物為100","蓋上寫著x的盒子 內容物為5","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序執行到了bar的第四行baz(),這又是一個子程序調用,不過沒有參數,所以直接把回來的地方扔進去就行了。 然後控制權交給baz。
  • 此時堆棧內容為["返回地址-bar的第五行","蓋上寫著y的盒子 內容物為100","蓋上寫著x的盒子 內容物為5","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序執行到了baz的第一行int pi=3,照常扔進去一個裝著3寫著pi的盒子
  • 此時堆棧內容為["蓋上寫著pi的盒子 內容物為3","返回地址-bar的第五行","蓋上寫著y的盒子 內容物為100","蓋上寫著x的盒子 內容物為5","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序執行到了baz的最後一行return,該離開子程序了,從堆棧裏取出一項,發現是沒用的pi,扔掉,然後再取出一項發現是返回地址,記下來,然後看看自己好像沒有參數,不用再取了。然後細看返回地址寫著bar的第五行,於是把控制權交過去。
  • 此時堆棧內容為["蓋上寫著y的盒子 內容物為100","蓋上寫著x的盒子 內容物為5","返回地址-foo的第二行",10,5]。
  • =========================
  • 程序執行到了bar的第五行return,又是一個返回,繼續離開當前子程序。從棧裏拿出一項,發現上面寫著y,扔掉。再從棧裏拿出來一項,發現上面寫著x,扔掉。再從棧裏拿出來一項,發現是返回地址foo的第二行,記下來。然後看著自己有兩個參數,於是再拿出來兩項扔掉。 控制權交給foo的第二行
  • 此時棧空。
  • =========================
  • 程序執行到了foo的第二行printf(...)下略。

棧就是這樣參與到程序的流程中的,可以讓程序多層級的調用變得很明確,是一個很好的數據結構。

而彙編層面上,棧是用sp寄存器模擬的,sp寄存器裏存儲的內存地址就代表著棧的頂端,如果要往裡存東西,比如說存個int,要把棧頂往上挪4位元組存儲,就把sp減少4,然後把這個int寫到對應的地址就行了,要取出的話就反之,讀取sp中地址的內容,然後再把sp加上4,棧就算複位了。所以不賦值的局部變數開闢,就是直接把sp減少對應的大小就不管了。

另外大部分架構還會用bp寄存器存儲一些信息輔助查找返回地址和參數,不用真正的一個個看過去。

問題3:

如果你理解了前兩個問題,數組的變數聲明也就沒什麼難點了。全局變數的話同樣是存儲在文件中那堆全局變數裏,只不過會有點長而已,載入的時候還是所有全局變數作為一塊數據載入內存。局部變數的話也仍然是sub sp,xx,你經常可以看到一些非常大的數字比如說sub sp,0x3200,這通常都是一些數組或者大的結構體。經常說不要局部變數放很大的東西也是因為sp指向的這塊內存終究是有限的,雖然其實不小。

問題4:

既然你都說了只複製地址是不行的,那當然是全部複製的啦,不過所謂的全部其實也沒多少。首先是類裡面的非靜態成員變數們,這些肯定是要複製一份或者說給他們開新的內存的,然後呢,其實就沒有然後了,其中的方法或者說子程序都是固定代碼,這是不用複製的。所謂的獨一無二其實也就是那些數據部分是獨一無二的。

問題5:

這個沒看出來問什麼,不過提醒下,編譯器只管生成出目標文件,執行這個文件什麼的已經與編譯器無關了。另外局部變數的int a=1一般還是要兩句代碼的,比如說sub sp,4然後mov [sp],1。不過實際應用上還會用到bp來優化,只用指向棧頂的sp有時候會不方便。


差不多也就這樣,上面這些回答裡面省略了大量的細節,還有一些為了簡化理解的小妥協,跟實際應用有一定差距,不過拿來理解思路應該差不多,但不要認為工程上的東西就是這麼做的。


你要明確:

  • 編譯時和運行時。
  • 語言特性的spec與實現。
  • 編譯器優化。

很多詳細的事情你應當看書,比如C專家編程、C pitfalls。

對於1,「編譯器自動給變數分配好內存空間」和「讓CPU一條指令一條指令的執行,把數據存入到內存中」並不矛盾。

具體來講,全局空間是在程序啟動時分配的,但這個空間的初始化(如果有)有可能是需要被執行的。

2:

int t;不賦值的話,會有mov指令嗎?

如果你真的沒用這個變數,那它很可能會被優化掉,在編譯結果里根本不會存在。

編譯器怎麼給這個變數t分配空間呢?

通常來講函數內的局部變數在棧上。整個棧空間是在程序/線程創建的時候預先分配好的,進入一個函數的時候,在這個分配好的棧空間裏使用某一塊。所有對這個變數的訪問,會變成「當前棧+固定偏移量」的形式。

可是,這個mov的操作對象又是什麼呢?

可以是CPU寄存器。

我知道int t=1;mov的操作數是1,把1存入到內存中去,可是,不賦值是怎麼樣的呢?

這裡有幾個要點:

  • 編程語言的語句與其編譯結果並不一一對應,特別是對於編譯到native code的語言,和開了高級別優化選項的時候。很可能一個語句什麼都不幹,或者一個語句幹了很多事情。具體到這件事情:
    • int t可能存在在棧上,也可能被優化掉,變成一系列對寄存器的訪問,也可能完全沒有。
  • 由於1是一個非常小的操作數,它很可能變成CPU指令裏的「立即數」。
  • 不賦值那就是什麼都不幹。但它依然會造成影響:
    • 如果這個變數沒有被優化掉,那它的存在會影響這個函數的棧尺寸的計算。
    • 允許你後續操作訪問這個變數。具體怎麼訪問,取決於它會在棧上,還是被優化掉了。

3:

數組的實現沒有任何特別的地方,也是佔用一定尺寸的空間而已。

4:

對於C++這種非託管的語言,new的時候會幹兩件事情:

  • 在堆上分配對象尺寸那麼大的一塊內存(我們先不考慮自定義分配器);
  • 使用這塊分配好的內存執行構造函數。

不可能把這個類的內容,全部使用mov指令,複製到新的內存裡面去吧?那這樣要浪費多少時間啊?

構造函數裡面幹了什麼,完全取決於構造函數寫了什麼。

另外,構造函數沒有任何神奇的地方。本質上和這樣的C函數沒有區別:

typedef struct Billy
{
int age;
const char* name;
};

void Billy_constructor(Billy* this)
{
this-&>age = 65535;
this-&>name = "Billy Herrington";
}

5:

如果是函數局部變數,不優化有可能是「將內容1放在內存棧頂+a的四位元組處」,優化有可能是什麼都不做。

基本類型的全局變數在程序映像載入時就完成了初始化,基本上就是把可執行文件直接拷貝到內存裏,可執行文件在鏈接時已經搞好了全局區各個位置的值,所以載入完了就已經好了。


你開的這個話題太大了,而且目測你對這部分內容連最基礎的瞭解都沒有。所以,你如果真的有興趣的話,還是建議你係統的學一下編譯原理相關的內容。

這麼東一榔頭西一棒子,往往是你看的時候覺得懂了,但真要上手做點什麼的時候,就會發現毫無頭緒了。

學而不思則罔,思而不學則殆


編譯器究竟為何要下此狠手?年近6旬的操作系統究竟如何管理內存?局部變數來去無蹤,它又究竟是如何分配的?這一切的一切,是人性的泯滅、還是道德的淪喪?

敬請收看新一期 Pearson 在線《深入理解計算機系統》,Randal David 為您講述計算機背後不為人知的祕密!


首先說明,本回答為個人理解,有錯請指出,謝謝。

其實你問的這些問題你自己寫一遍然後看彙編就可以了

1.直接把數據段映射到內存,編譯器已經把初始值寫到了數據段。不過類的還要在main前執行下構造函數,這個編譯器不能幫你。

2.局部變數聲明是通過sub esp在棧上預留空間,如果有初始化當然是mov,不初始化就什麼也不幹,變數值是隨機的。

3.數組沒什麼不一樣,只是局部數組變數初始化好像是一個一個mov過去的,因為沒保存在數據段裏。

4.先分配空間初始化再執行構造函數。

5.同1

還有,編譯完之後就與編譯器沒有關係了。


推薦閱讀:
相關文章