程序是如何跑起來的
內存和磁盤的親密關系
- 磁盤緩存是指:從磁盤中讀出的數(shù)據(jù)在內存中,當該數(shù)據(jù)再次被讀取時革答,不是從磁盤而是直接從內存中高速讀出,借助虛擬內存,哪怕是內存容量不足的計算機效览,也可以運行很大的程序
從都具有存儲程序命令和數(shù)據(jù)這點來看,內存和磁盤的功能是相同的荡短,在計算機的5大部件(輸入裝置丐枉,輸出裝置,存儲器掘托,運算器瘦锹,控制器)中,內存和磁盤都被歸類為存儲設備闪盔。內存使用電流實現(xiàn)存儲弯院,磁盤使用磁效應存儲,內存主要是是指主內存(負責存儲 CPU 中運行的程序指令和數(shù)據(jù)內存)泪掀,磁盤主要是指硬盤
不讀入內存就無法運行
程序保存在存儲設備中听绳,通過有序地讀出來實現(xiàn)運行,這一機制稱為存儲程序方式异赫,計算機中主要的存儲部件是內存和磁盤椅挣,磁盤中存儲的程序头岔,必須要加載到內存中才能運行,在磁盤中保存的原始程序是無法直接運行的鼠证,這是因為峡竣,負責解析和運行程序內容的 CPU, 需要通過內部程序計數(shù)器來指定內存地址,然后才能讀出程序量九,
磁盤緩存加快了磁盤當文速度
磁盤緩存 指的是把從磁盤中讀出的數(shù)據(jù)存儲到內存空間中的方式适掰,這樣一來,當接下來需要讀取同一數(shù)據(jù)時娩鹉,就不用通過實際的磁盤攻谁,而是從磁盤緩存中把內容讀出來,使用磁盤緩存可以大大改善磁盤數(shù)據(jù)的訪問數(shù)據(jù)弯予。
虛擬內存把磁盤作為部分內存來使用
虛擬內存是指把磁盤的一部分作為假想的內存來來使用戚宦,借助虛擬內存,在內存不足時也可以運行程序锈嫩,例如在只剩下 5MB 內存空間的情況下也能運行 10MB 大小的程序受楼,cpu 只能執(zhí)行加載到內存中的程序,虛擬內存雖說是把磁盤作為內存的一部分來使用呼寸,但實際上正在運行的程序部分艳汽,在這個時間點上必須存在在內存中,也就是說对雪,為了實現(xiàn)虛擬內存河狐,就必須把實際內存的內容和磁盤上的虛擬內存的內容進行部分置換(swap),并同時運行程序。
為了實現(xiàn)虛擬內存功能瑟捣, Windows 在磁盤上提供了虛擬內存用的文件(page file, 頁文件)馋艺,該文件由 Windows 自動做成和管理,文件的大小也就是虛擬內存的大小迈套,通常是實際內存的相同程度至兩倍程度捐祠,通過 windows 的控制面板,可以查看或變更當前虛擬內存的設定桑李。
節(jié)約內存的編程方法
為了從根本上解決內存不足的問題踱蛀,需要增加內存的容量,或者盡量把運行的應用文件變小贵白。
通過 DLL 文件實現(xiàn)函數(shù)共有
DLL(Dynamic Link Library) 文件率拒,顧命思義是在程序運行時可以動態(tài)加載 Library(函數(shù)和數(shù)據(jù)的集合),多個應用可以共有同一個 DLL 文件禁荒,通過一個 DLL 文件則可以達到節(jié)約內存的效果俏橘。
通過調用 _stdcall
來減小程序文件的大小
_Stdcall
是standard call
的略稱, windows 提供的 DLL 文件內的函數(shù)圈浇,基本上都是_stdcall
的方式調用的寥掐,另一方面,用 C 語言編寫的程序內的函數(shù)磷蜀,默認設置都不是_StdCall
, c 語言特有的調用方式是 C 調用召耘,因為 C 所對應的函數(shù)的傳入?yún)?shù)是可變的,只有函數(shù)調用方才能知道到底有多少個參數(shù)褐隆,在這種情況下污它,棧的清理作業(yè)便無法進行
c 語言函數(shù)調用后,需要執(zhí)行棧清理處理指令庶弃,棧清理處理是指:把不需要的數(shù)據(jù)從接收和傳遞函數(shù)的參數(shù)時使用的的內存上的棧區(qū)域中清理出去衫贬,該命令不是程序記述的,而是在程序編譯時由編譯器自動附加到程序中歇攻,編譯器默認將該處理附加在函數(shù)調用方固惯。
實例:
// 函數(shù)調用方
void main()
{
int a;
a = MyFunc(123, 456);
}
// 被調用方
int MyFunc(int a,int b)
{
return a+b;
}
從函數(shù) main()
中調用了函數(shù) MyFunc()
. 按照默認設定,棧的清理處理會附加在函數(shù) main() 這一方缴守,在同一個程序中葬毫,同樣的函數(shù)可能會被多次反復調用,兒如果是同樣的函數(shù)屡穗,棧清理的內容也是一樣的贴捡,由于該處理是在調用函數(shù)的一方,因此就會導致同一處理被反復進行村砂,造成了內存的浪費
C 語言通過棧來傳遞函數(shù)的參數(shù)烂斋,push
是往棧中存入數(shù)據(jù)的指令, 32 位 cpu 中础废, 1次 push 指令可以存儲 4 個字節(jié)的數(shù)據(jù)汛骂,由于使用了兩次 push 指令把兩個參數(shù)存入到棧中,因此總的來說就是存儲了8字節(jié)的數(shù)據(jù)色迂,通過 call 指令調用函數(shù) MyFunc()
后香缺,棧中存儲的數(shù)據(jù)就不再需要了,于是這時就通過 add esp, 8
這個指令存儲這個棧數(shù)據(jù)的 esp 寄存器前進8位歇僧,來進行數(shù)據(jù)清理图张。c 語言中,函數(shù)的返回值是通過寄存器而非棧來返回的诈悍。
棧清理處理祸轮,比起在函數(shù)調用方進行,在反復被調用的函數(shù)一方進行侥钳,程序整體要小一些适袜,這時所使用的就是 _stdcall
, 在函數(shù)前面加上 stdcall
,就可以把棧清理處理變?yōu)樵诒徽{用函數(shù)一方進行舷夺。
磁盤的物理結構
磁盤是通過把其物理表面劃分成多個空間來使用的苦酱,劃分的方式有扇區(qū)方式和可變長方式售貌,前者指將磁盤劃分為固定長度的空間,后者則是把磁盤劃分為長度可變的空間疫萤,扇區(qū)方式中颂跨,把磁盤的表面劃分成若干個同心圓空間的就是磁道,把磁道按照固定大小(能存儲的數(shù)據(jù)長度相同)劃分而成的空間就是扇面扯饶。
程序是在何種環(huán)境中運行的
運行環(huán)境
運行環(huán)境 = 操作系統(tǒng) + 硬件
CPU 只能解釋其自身固有的機器語言恒削,不同的 CPU 能解釋的機器語言種類也不同,機器語言的程序稱為本地代碼尾序,程序員使用 C 語言等編寫的程序钓丰,在編寫階段僅僅是文本文件,文本文件在任何環(huán)境下都能顯示和編輯每币,我們稱之為源代碼携丁,通過對源代碼進行編譯就可以得到本地代碼。
之所以需要根據(jù)不同的操作系統(tǒng)類型來專門開發(fā)應用軟件脯爪,是因為操作系統(tǒng)的類型不同则北,應用程序向操作系統(tǒng)傳遞指令的途徑也不是相同的。
FreeBSD
unix 系列操作系統(tǒng) FreeBSD 中痕慢,存在一種名為 Ports 的機制尚揣,該機制能夠結合當前運行的硬件環(huán)境來編譯應用的源代碼,進而得到可以運行的本地代碼系統(tǒng)掖举。
BIOS 和引導
BIOS 存儲在 ROM 中快骗,是預先內置在計算機主機內部的程序,BIOS 除了鍵盤塔次,磁盤方篮,顯卡等基本控制程序外,還有啟動“引導程序”的功能励负,引導程序是存儲在啟動驅動器其實區(qū)域的小程序藕溅,操作系統(tǒng)的啟動驅動器一般是硬盤。
從源文件到可執(zhí)行文件
通過編譯源代碼得到本地代碼继榆,通過編譯和鏈接巾表,得到 exe 文件,鏈接器會從庫文件中抽取出必要的目標文件并將其結合到 Exe 文件中略吨,把導入庫信息結合到 exe 文件中集币,這樣程序在運行時就可以利用DLL 內的函數(shù)了。堆的內存空間會根據(jù)程序的命令進行申請和釋放
源代碼完成后翠忠,就可以編譯生成可執(zhí)行文件了鞠苟,負責實現(xiàn)該功能的是編譯器
編譯器負責轉換源代碼
能夠把 C 語言等高級編程語言編寫的源代碼轉換成本地代碼的程序稱為編譯器,每個編寫源代碼的編程語言都需要器專用的編譯器,將 C 語言編寫的源代碼轉換成本地代碼的編譯器稱為 C 編譯器
編譯器首先讀入代碼的內容当娱,然后再把源代碼轉換成本地代碼吃既,編譯器中就好像有一個源代碼同本地代碼的對應表,讀入的源代碼還需要經(jīng)過語法解析趾访,語句解析态秧,語義解析等,才能生成本地代碼
僅靠編譯是無法得到可執(zhí)行文件的
編譯器轉換源代碼后扼鞋,就會生成本地文件,不過愤诱,本地文件是無法直接運行的云头,為了得到可以運行的 EXE 文件,編譯之后還需要進行“鏈接”處理淫半。'
把多個目標文件結合溃槐,生成1個 exe 文件的處理就是鏈接,運行鏈接的程序就成為鏈接器
可執(zhí)行文件運行時的必要條件
本地代碼在對曾許中記述的變量進行讀寫時科吭,是參照數(shù)據(jù)存儲的地址來運行命令的昏滴,在調用函數(shù)時,程序的處理流程就會跳轉至存儲著函數(shù)處理內容的內存地址上对人, exe 文件作為本地代碼的程序谣殊,并沒有指定變量及函數(shù)的實際內存地址。那么在 exe 文件中牺弄,變量和函數(shù)的內存地址的值姻几,是如何來的呢?
答案是:給 exe 文件中的變量及函數(shù)分配了虛擬的內存地址势告,在程序運行時蛇捌,虛擬的內存地址會轉換成實際的內存地址,連接器會在 exe 文件的開頭咱台,追加轉換內存地址所需的必要信息络拌,這個信息被稱為再配置信息,exe 文件的再配置信息,就成了變量和函數(shù)的相對地址回溺,相對地址表示的是相對于基點地址的偏移量春贸,也就是相對距離,實現(xiàn)相對地址馅而,也是需要花費一番心思的祥诽,在源代碼中,雖然變量及函數(shù)是在不同位置分散記述的瓮恭,但在鏈接后的 exe 文件中雄坪,變量及函數(shù)就會變成一個連續(xù)排列的組,這樣一來,各變量的內存地址就可以用相對變量組起始位置這一基點的偏移量來表示维哈,同樣绳姨,各函數(shù)的內存地址也可以相對于函數(shù)組起始位置這一基點的偏移量來表示,而各組基點的內存地址是在程序運行時被分配的阔挠。
/Users/qianbaofeng/Desktop/屏幕快照 2017-07-08 下午3.06.08.png
加載程序時會生成棧和堆
EXE 文件的內容分為再配置信息飘庄,變量組和函數(shù)組,不過當程序加載到內存后购撼,初次之外還會額外生成兩個組跪削,那就是堆和棧,棧是用來存儲函數(shù)內部臨時使用的變量(局部變量)以及函數(shù)調用時所用的參數(shù)的內存區(qū)域迂求,堆是用來存儲程序運行時的任意數(shù)據(jù)及對象的內存領域
個人理解碾盐,所謂的 c++ 面向對象編程,最終都將會被轉換成c 語言的函數(shù)揩局,只不過再調用的時候調用的是獲取和設置函數(shù)毫玖,每個對象的屬性棧及時一堆函數(shù)?凌盯?付枫?
exe 文件中并不存在棧及堆的組,堆和棧需要在內存空間是在 exe 文件加載到內存后開始運行時得到分配的驰怎,因而阐滩,內存中的程序,就是由用于變量的內存空間砸西,用于函數(shù)的內存空間叶眉,用于棧的內存空間,用于堆的內存空間這四個部分構成芹枷。
堆和棧的相似之處在于衅疙,他們的內存空間都是在程序運行時得到申請分配的。不同點在于: 棧中對數(shù)據(jù)進行存儲和舍棄(清理處理)的代碼鸳慈,由編譯器自動生成饱溢,使用棧的數(shù)據(jù)內存空間,每當函數(shù)被調用時都會得到申請分配走芋,并在函數(shù)處理完畢后自動釋放绩郎,與此相對,堆的內存空間翁逞,則要根據(jù)程序員編寫的程序肋杖,來明確進行申請分配或釋放。
不管什么程序挖函,程序的內容都是有處理和數(shù)據(jù)構成的状植,大多數(shù)編程語言都是由函數(shù)來表示處理,變量來表示數(shù)據(jù)。
一些有難度的 Q&A
- Q: 編譯器和解釋器有什么不同
A: 編譯器是在運行前對所有源代碼進行解釋處理的津畸,而解釋器則是在運行時對源代碼的內容一行一行的進行解釋處理的 - Q: 分割編譯是什么
A: 將整個程序分為多個源代碼編寫振定,然后分別進行編寫,最后鏈接成一個 EXE 文件肉拓,這樣每個源代碼都相對變短后频,便于程序管理 - Q: "build" 指的是什么
A: build 指的是連續(xù)執(zhí)行編譯和鏈接 - Q: DLL 的好處?
A: DLL 文件中的函數(shù)可以被多個程序共用,可以節(jié)約內存和磁盤
匯編語言
匯編語言和本地代碼一一對應
計算機 CPU 能直接解釋運行的只有本地代碼程序暖途,用 C 語言等編寫的代碼卑惜,需要通過各自的編譯器編譯后,轉換成本地代碼丧肴,
對棧進行 push 和 pop
程序運行時残揉,會在內存上申請分配一個稱為棧的數(shù)據(jù)空間,數(shù)據(jù)在存儲時是從內存的下層(大號地址)逐漸網(wǎng)上層累加的芋浮,讀出時是按照從上往下的順序進行的。
棧是存儲臨時數(shù)據(jù)的區(qū)域壳快,它的特點是通過 push 指令和 pop 指令進行數(shù)據(jù)的存儲和讀出纸巷,往棧中存儲數(shù)據(jù)稱為 "入棧",從"棧"中讀出數(shù)據(jù)稱為 "出棧"眶痰。對棧盡享讀寫的內存地址是由 esp 寄存器(棧指針)進行管理的瘤旨, push 指令和 pop 指令運行后,esp 寄存器值回自動進行更新.
函數(shù)調用機制
代碼如下:
在匯編語言中,函數(shù)名表示的是函數(shù)所在的內存地址竖伯,當call 調用的函數(shù)運行后存哲, 程序流程必須返回編號(6)的這一行, call 指令運行后七婴, call 指令的下一行的內存地址(6的這一行)回自動 push 入棧祟偷,該值會在 AddNum 函數(shù)處理的最后通過 ret 指令 pop 出棧,然后流程回到 6 這一行
函數(shù)的內部調用
ebp 寄存器的值在 1 中入棧打厘,在 5 中出棧修肠,主要是為了把函數(shù)中用到的 ebp 寄存器的內容,恢復到函數(shù)調用之前的狀態(tài)户盯,在進入函數(shù)處理之前嵌施,無法確定 ebp 寄存器用到了什么地方,但由于函數(shù)內部
(2) 中把負責管理地址的 esp 寄存器的值賦到了 ebp 寄存器中莽鸭,這時因為吗伤, mov 指令中方括號的參數(shù),是不允許指令 esp 寄存器的硫眨,
函數(shù)是的參數(shù)是通過棧來傳遞的足淆,返回值是通過寄存器來返回的。
始終確保全局變量用的內存空間
c 語言中,在函數(shù)外部定義的變量稱為全局變量缸浦,在函數(shù)內部定義的變量稱為局部變量夕冲,
全局變量可以參閱源代碼的任意部分,而局部變量只能在定義該變量的函數(shù)內進行參閱裂逐。
正如之前所說的歹鱼,編譯后的程序,會被歸類到名為段定義的組卜高,初始化的全局變量弥姻,會被定義到名為 _DATA 的段定義中,沒有初始化的全局變量會被匯總到 _BSS
的段定義中掺涛,指令會被匯總到名為 _TEXT
的段定義中
臨時確保局部部變量用的內存空間
局部變量是臨時保存在寄存器和棧中的庭敦,導致局部變量只能在定義該函數(shù)的內部進行參閱。函數(shù)內部利用棧薪缆,在函數(shù)處理完畢后會恢復到初始狀態(tài)秧廉,因此局部變量的值也就被銷毀了,而寄存器也可能會被用于其他目的拣帽,因此疼电,局部變量只是在函數(shù)處理運行期間臨時存儲在寄存器和棧上。