程序是怎樣跑起來(lái)的(二)

程序是如何跑起來(lái)的

內(nèi)存和磁盤(pán)的親密關(guān)系

  • 磁盤(pán)緩存是指:從磁盤(pán)中讀出的數(shù)據(jù)在內(nèi)存中,當(dāng)該數(shù)據(jù)再次被讀取時(shí),不是從磁盤(pán)而是直接從內(nèi)存中高速讀出,借助虛擬內(nèi)存,哪怕是內(nèi)存容量不足的計(jì)算機(jī),也可以運(yùn)行很大的程序

從都具有存儲(chǔ)程序命令和數(shù)據(jù)這點(diǎn)來(lái)看,內(nèi)存和磁盤(pán)的功能是相同的,在計(jì)算機(jī)的5大部件(輸入裝置,輸出裝置,存儲(chǔ)器,運(yùn)算器,控制器)中,內(nèi)存和磁盤(pán)都被歸類(lèi)為存儲(chǔ)設(shè)備。內(nèi)存使用電流實(shí)現(xiàn)存儲(chǔ),磁盤(pán)使用磁效應(yīng)存儲(chǔ),內(nèi)存主要是是指主內(nèi)存(負(fù)責(zé)存儲(chǔ) CPU 中運(yùn)行的程序指令和數(shù)據(jù)內(nèi)存),磁盤(pán)主要是指硬盤(pán)

不讀入內(nèi)存就無(wú)法運(yùn)行

程序保存在存儲(chǔ)設(shè)備中,通過(guò)有序地讀出來(lái)實(shí)現(xiàn)運(yùn)行,這一機(jī)制稱為存儲(chǔ)程序方式,計(jì)算機(jī)中主要的存儲(chǔ)部件是內(nèi)存和磁盤(pán),磁盤(pán)中存儲(chǔ)的程序,必須要加載到內(nèi)存中才能運(yùn)行,在磁盤(pán)中保存的原始程序是無(wú)法直接運(yùn)行的,這是因?yàn)?,?fù)責(zé)解析和運(yùn)行程序內(nèi)容的 CPU, 需要通過(guò)內(nèi)部程序計(jì)數(shù)器來(lái)指定內(nèi)存地址,然后才能讀出程序,

屏幕快照 2017-07-08 上午9.17.30.png

磁盤(pán)緩存加快了磁盤(pán)當(dāng)文速度

磁盤(pán)緩存 指的是把從磁盤(pán)中讀出的數(shù)據(jù)存儲(chǔ)到內(nèi)存空間中的方式,這樣一來(lái),當(dāng)接下來(lái)需要讀取同一數(shù)據(jù)時(shí),就不用通過(guò)實(shí)際的磁盤(pán),而是從磁盤(pán)緩存中把內(nèi)容讀出來(lái),使用磁盤(pán)緩存可以大大改善磁盤(pán)數(shù)據(jù)的訪問(wèn)數(shù)據(jù)。

虛擬內(nèi)存把磁盤(pán)作為部分內(nèi)存來(lái)使用

虛擬內(nèi)存是指把磁盤(pán)的一部分作為假想的內(nèi)存來(lái)來(lái)使用,借助虛擬內(nèi)存,在內(nèi)存不足時(shí)也可以運(yùn)行程序,例如在只剩下 5MB 內(nèi)存空間的情況下也能運(yùn)行 10MB 大小的程序,cpu 只能執(zhí)行加載到內(nèi)存中的程序,虛擬內(nèi)存雖說(shuō)是把磁盤(pán)作為內(nèi)存的一部分來(lái)使用,但實(shí)際上正在運(yùn)行的程序部分,在這個(gè)時(shí)間點(diǎn)上必須存在在內(nèi)存中,也就是說(shuō),為了實(shí)現(xiàn)虛擬內(nèi)存,就必須把實(shí)際內(nèi)存的內(nèi)容和磁盤(pán)上的虛擬內(nèi)存的內(nèi)容進(jìn)行部分置換(swap),并同時(shí)運(yùn)行程序。

為了實(shí)現(xiàn)虛擬內(nèi)存功能, Windows 在磁盤(pán)上提供了虛擬內(nèi)存用的文件(page file, 頁(yè)文件),該文件由 Windows 自動(dòng)做成和管理,文件的大小也就是虛擬內(nèi)存的大小,通常是實(shí)際內(nèi)存的相同程度至兩倍程度,通過(guò) windows 的控制面板,可以查看或變更當(dāng)前虛擬內(nèi)存的設(shè)定。

屏幕快照 2017-07-08 上午10.03.04.png

節(jié)約內(nèi)存的編程方法

為了從根本上解決內(nèi)存不足的問(wèn)題,需要增加內(nèi)存的容量,或者盡量把運(yùn)行的應(yīng)用文件變小。

通過(guò) DLL 文件實(shí)現(xiàn)函數(shù)共有

DLL(Dynamic Link Library) 文件,顧命思義是在程序運(yùn)行時(shí)可以動(dòng)態(tài)加載 Library(函數(shù)和數(shù)據(jù)的集合),多個(gè)應(yīng)用可以共有同一個(gè) DLL 文件,通過(guò)一個(gè) DLL 文件則可以達(dá)到節(jié)約內(nèi)存的效果。

通過(guò)調(diào)用 _stdcall 來(lái)減小程序文件的大小

_Stdcallstandard call 的略稱, windows 提供的 DLL 文件內(nèi)的函數(shù),基本上都是 _stdcall 的方式調(diào)用的,另一方面,用 C 語(yǔ)言編寫(xiě)的程序內(nèi)的函數(shù),默認(rèn)設(shè)置都不是 _StdCall, c 語(yǔ)言特有的調(diào)用方式是 C 調(diào)用,因?yàn)?C 所對(duì)應(yīng)的函數(shù)的傳入?yún)?shù)是可變的,只有函數(shù)調(diào)用方才能知道到底有多少個(gè)參數(shù),在這種情況下,棧的清理作業(yè)便無(wú)法進(jìn)行

c 語(yǔ)言函數(shù)調(diào)用后,需要執(zhí)行棧清理處理指令,棧清理處理是指:把不需要的數(shù)據(jù)從接收和傳遞函數(shù)的參數(shù)時(shí)使用的的內(nèi)存上的棧區(qū)域中清理出去,該命令不是程序記述的,而是在程序編譯時(shí)由編譯器自動(dòng)附加到程序中,編譯器默認(rèn)將該處理附加在函數(shù)調(diào)用方。

實(shí)例:

// 函數(shù)調(diào)用方
void main()
{
int a;
a = MyFunc(123, 456);
}
// 被調(diào)用方
int MyFunc(int a,int b)
{
return a+b;
}

從函數(shù) main() 中調(diào)用了函數(shù) MyFunc(). 按照默認(rèn)設(shè)定,棧的清理處理會(huì)附加在函數(shù) main() 這一方,在同一個(gè)程序中,同樣的函數(shù)可能會(huì)被多次反復(fù)調(diào)用,兒如果是同樣的函數(shù),棧清理的內(nèi)容也是一樣的,由于該處理是在調(diào)用函數(shù)的一方,因此就會(huì)導(dǎo)致同一處理被反復(fù)進(jìn)行,造成了內(nèi)存的浪費(fèi)

屏幕快照 2017-07-08 上午10.33.39.png

C 語(yǔ)言通過(guò)棧來(lái)傳遞函數(shù)的參數(shù),push 是往棧中存入數(shù)據(jù)的指令, 32 位 cpu 中, 1次 push 指令可以存儲(chǔ) 4 個(gè)字節(jié)的數(shù)據(jù),由于使用了兩次 push 指令把兩個(gè)參數(shù)存入到棧中,因此總的來(lái)說(shuō)就是存儲(chǔ)了8字節(jié)的數(shù)據(jù),通過(guò) call 指令調(diào)用函數(shù) MyFunc() 后,棧中存儲(chǔ)的數(shù)據(jù)就不再需要了,于是這時(shí)就通過(guò) add esp, 8 這個(gè)指令存儲(chǔ)這個(gè)棧數(shù)據(jù)的 esp 寄存器前進(jìn)8位,來(lái)進(jìn)行數(shù)據(jù)清理。c 語(yǔ)言中,函數(shù)的返回值是通過(guò)寄存器而非棧來(lái)返回的。

棧清理處理,比起在函數(shù)調(diào)用方進(jìn)行,在反復(fù)被調(diào)用的函數(shù)一方進(jìn)行,程序整體要小一些,這時(shí)所使用的就是 _stdcall, 在函數(shù)前面加上 stdcall,就可以把棧清理處理變?yōu)樵诒徽{(diào)用函數(shù)一方進(jìn)行。

磁盤(pán)的物理結(jié)構(gòu)

磁盤(pán)是通過(guò)把其物理表面劃分成多個(gè)空間來(lái)使用的,劃分的方式有扇區(qū)方式和可變長(zhǎng)方式,前者指將磁盤(pán)劃分為固定長(zhǎng)度的空間,后者則是把磁盤(pán)劃分為長(zhǎng)度可變的空間,扇區(qū)方式中,把磁盤(pán)的表面劃分成若干個(gè)同心圓空間的就是磁道,把磁道按照固定大小(能存儲(chǔ)的數(shù)據(jù)長(zhǎng)度相同)劃分而成的空間就是扇面。

屏幕快照 2017-07-08 上午10.52.48.png

程序是在何種環(huán)境中運(yùn)行的

運(yùn)行環(huán)境

運(yùn)行環(huán)境 = 操作系統(tǒng) + 硬件

CPU 只能解釋其自身固有的機(jī)器語(yǔ)言,不同的 CPU 能解釋的機(jī)器語(yǔ)言種類(lèi)也不同,機(jī)器語(yǔ)言的程序稱為本地代碼,程序員使用 C 語(yǔ)言等編寫(xiě)的程序,在編寫(xiě)階段僅僅是文本文件,文本文件在任何環(huán)境下都能顯示和編輯,我們稱之為源代碼,通過(guò)對(duì)源代碼進(jìn)行編譯就可以得到本地代碼。

之所以需要根據(jù)不同的操作系統(tǒng)類(lèi)型來(lái)專門(mén)開(kāi)發(fā)應(yīng)用軟件,是因?yàn)椴僮飨到y(tǒng)的類(lèi)型不同,應(yīng)用程序向操作系統(tǒng)傳遞指令的途徑也不是相同的。

FreeBSD

unix 系列操作系統(tǒng) FreeBSD 中,存在一種名為 Ports 的機(jī)制,該機(jī)制能夠結(jié)合當(dāng)前運(yùn)行的硬件環(huán)境來(lái)編譯應(yīng)用的源代碼,進(jìn)而得到可以運(yùn)行的本地代碼系統(tǒng)。

BIOS 和引導(dǎo)

BIOS 存儲(chǔ)在 ROM 中,是預(yù)先內(nèi)置在計(jì)算機(jī)主機(jī)內(nèi)部的程序,BIOS 除了鍵盤(pán),磁盤(pán),顯卡等基本控制程序外,還有啟動(dòng)“引導(dǎo)程序”的功能,引導(dǎo)程序是存儲(chǔ)在啟動(dòng)驅(qū)動(dòng)器其實(shí)區(qū)域的小程序,操作系統(tǒng)的啟動(dòng)驅(qū)動(dòng)器一般是硬盤(pán)。

從源文件到可執(zhí)行文件

通過(guò)編譯源代碼得到本地代碼,通過(guò)編譯和鏈接,得到 exe 文件,鏈接器會(huì)從庫(kù)文件中抽取出必要的目標(biāo)文件并將其結(jié)合到 Exe 文件中,把導(dǎo)入庫(kù)信息結(jié)合到 exe 文件中,這樣程序在運(yùn)行時(shí)就可以利用DLL 內(nèi)的函數(shù)了。堆的內(nèi)存空間會(huì)根據(jù)程序的命令進(jìn)行申請(qǐng)和釋放

源代碼完成后,就可以編譯生成可執(zhí)行文件了,負(fù)責(zé)實(shí)現(xiàn)該功能的是編譯器

編譯器負(fù)責(zé)轉(zhuǎn)換源代碼

能夠把 C 語(yǔ)言等高級(jí)編程語(yǔ)言編寫(xiě)的源代碼轉(zhuǎn)換成本地代碼的程序稱為編譯器,每個(gè)編寫(xiě)源代碼的編程語(yǔ)言都需要器專用的編譯器,將 C 語(yǔ)言編寫(xiě)的源代碼轉(zhuǎn)換成本地代碼的編譯器稱為 C 編譯器

編譯器首先讀入代碼的內(nèi)容,然后再把源代碼轉(zhuǎn)換成本地代碼,編譯器中就好像有一個(gè)源代碼同本地代碼的對(duì)應(yīng)表,讀入的源代碼還需要經(jīng)過(guò)語(yǔ)法解析,語(yǔ)句解析,語(yǔ)義解析等,才能生成本地代碼

僅靠編譯是無(wú)法得到可執(zhí)行文件的

編譯器轉(zhuǎn)換源代碼后,就會(huì)生成本地文件,不過(guò),本地文件是無(wú)法直接運(yùn)行的,為了得到可以運(yùn)行的 EXE 文件,編譯之后還需要進(jìn)行“鏈接”處理。'

把多個(gè)目標(biāo)文件結(jié)合,生成1個(gè) exe 文件的處理就是鏈接,運(yùn)行鏈接的程序就成為鏈接器

屏幕快照 2017-07-08 下午2.49.52.png

可執(zhí)行文件運(yùn)行時(shí)的必要條件

本地代碼在對(duì)曾許中記述的變量進(jìn)行讀寫(xiě)時(shí),是參照數(shù)據(jù)存儲(chǔ)的地址來(lái)運(yùn)行命令的,在調(diào)用函數(shù)時(shí),程序的處理流程就會(huì)跳轉(zhuǎn)至存儲(chǔ)著函數(shù)處理內(nèi)容的內(nèi)存地址上, exe 文件作為本地代碼的程序,并沒(méi)有指定變量及函數(shù)的實(shí)際內(nèi)存地址。那么在 exe 文件中,變量和函數(shù)的內(nèi)存地址的值,是如何來(lái)的呢?

答案是:給 exe 文件中的變量及函數(shù)分配了虛擬的內(nèi)存地址,在程序運(yùn)行時(shí),虛擬的內(nèi)存地址會(huì)轉(zhuǎn)換成實(shí)際的內(nèi)存地址,連接器會(huì)在 exe 文件的開(kāi)頭,追加轉(zhuǎn)換內(nèi)存地址所需的必要信息,這個(gè)信息被稱為再配置信息,exe 文件的再配置信息,就成了變量和函數(shù)的相對(duì)地址,相對(duì)地址表示的是相對(duì)于基點(diǎn)地址的偏移量,也就是相對(duì)距離,實(shí)現(xiàn)相對(duì)地址,也是需要花費(fèi)一番心思的,在源代碼中,雖然變量及函數(shù)是在不同位置分散記述的,但在鏈接后的 exe 文件中,變量及函數(shù)就會(huì)變成一個(gè)連續(xù)排列的組,這樣一來(lái),各變量的內(nèi)存地址就可以用相對(duì)變量組起始位置這一基點(diǎn)的偏移量來(lái)表示,同樣,各函數(shù)的內(nèi)存地址也可以相對(duì)于函數(shù)組起始位置這一基點(diǎn)的偏移量來(lái)表示,而各組基點(diǎn)的內(nèi)存地址是在程序運(yùn)行時(shí)被分配的。
/Users/qianbaofeng/Desktop/屏幕快照 2017-07-08 下午3.06.08.png

屏幕快照 2017-07-08 下午3.06.08.png

加載程序時(shí)會(huì)生成棧和堆

EXE 文件的內(nèi)容分為再配置信息,變量組和函數(shù)組,不過(guò)當(dāng)程序加載到內(nèi)存后,初次之外還會(huì)額外生成兩個(gè)組,那就是堆和棧,棧是用來(lái)存儲(chǔ)函數(shù)內(nèi)部臨時(shí)使用的變量(局部變量)以及函數(shù)調(diào)用時(shí)所用的參數(shù)的內(nèi)存區(qū)域,堆是用來(lái)存儲(chǔ)程序運(yùn)行時(shí)的任意數(shù)據(jù)及對(duì)象的內(nèi)存領(lǐng)域

屏幕快照 2017-07-08 下午3.08.58.png

個(gè)人理解,所謂的 c++ 面向?qū)ο缶幊蹋罱K都將會(huì)被轉(zhuǎn)換成c 語(yǔ)言的函數(shù),只不過(guò)再調(diào)用的時(shí)候調(diào)用的是獲取和設(shè)置函數(shù),每個(gè)對(duì)象的屬性棧及時(shí)一堆函數(shù)???

exe 文件中并不存在棧及堆的組,堆和棧需要在內(nèi)存空間是在 exe 文件加載到內(nèi)存后開(kāi)始運(yùn)行時(shí)得到分配的,因而,內(nèi)存中的程序,就是由用于變量的內(nèi)存空間,用于函數(shù)的內(nèi)存空間,用于棧的內(nèi)存空間,用于堆的內(nèi)存空間這四個(gè)部分構(gòu)成。

堆和棧的相似之處在于,他們的內(nèi)存空間都是在程序運(yùn)行時(shí)得到申請(qǐng)分配的。不同點(diǎn)在于: 棧中對(duì)數(shù)據(jù)進(jìn)行存儲(chǔ)和舍棄(清理處理)的代碼,由編譯器自動(dòng)生成,使用棧的數(shù)據(jù)內(nèi)存空間,每當(dāng)函數(shù)被調(diào)用時(shí)都會(huì)得到申請(qǐng)分配,并在函數(shù)處理完畢后自動(dòng)釋放,與此相對(duì),堆的內(nèi)存空間,則要根據(jù)程序員編寫(xiě)的程序,來(lái)明確進(jìn)行申請(qǐng)分配或釋放。

不管什么程序,程序的內(nèi)容都是有處理和數(shù)據(jù)構(gòu)成的,大多數(shù)編程語(yǔ)言都是由函數(shù)來(lái)表示處理,變量來(lái)表示數(shù)據(jù)。

一些有難度的 Q&A

  1. Q: 編譯器和解釋器有什么不同
    A: 編譯器是在運(yùn)行前對(duì)所有源代碼進(jìn)行解釋處理的,而解釋器則是在運(yùn)行時(shí)對(duì)源代碼的內(nèi)容一行一行的進(jìn)行解釋處理的
  2. Q: 分割編譯是什么
    A: 將整個(gè)程序分為多個(gè)源代碼編寫(xiě),然后分別進(jìn)行編寫(xiě),最后鏈接成一個(gè) EXE 文件,這樣每個(gè)源代碼都相對(duì)變短,便于程序管理
  3. Q: "build" 指的是什么
    A: build 指的是連續(xù)執(zhí)行編譯和鏈接
  4. Q: DLL 的好處?
    A: DLL 文件中的函數(shù)可以被多個(gè)程序共用,可以節(jié)約內(nèi)存和磁盤(pán)

匯編語(yǔ)言

匯編語(yǔ)言和本地代碼一一對(duì)應(yīng)

計(jì)算機(jī) CPU 能直接解釋運(yùn)行的只有本地代碼程序,用 C 語(yǔ)言等編寫(xiě)的代碼,需要通過(guò)各自的編譯器編譯后,轉(zhuǎn)換成本地代碼,

對(duì)棧進(jìn)行 push 和 pop

程序運(yùn)行時(shí),會(huì)在內(nèi)存上申請(qǐng)分配一個(gè)稱為棧的數(shù)據(jù)空間,數(shù)據(jù)在存儲(chǔ)時(shí)是從內(nèi)存的下層(大號(hào)地址)逐漸網(wǎng)上層累加的,讀出時(shí)是按照從上往下的順序進(jìn)行的。

屏幕快照 2017-07-08 下午5.54.25.png

棧是存儲(chǔ)臨時(shí)數(shù)據(jù)的區(qū)域,它的特點(diǎn)是通過(guò) push 指令和 pop 指令進(jìn)行數(shù)據(jù)的存儲(chǔ)和讀出,往棧中存儲(chǔ)數(shù)據(jù)稱為 "入棧",從"棧"中讀出數(shù)據(jù)稱為 "出棧"。對(duì)棧盡享讀寫(xiě)的內(nèi)存地址是由 esp 寄存器(棧指針)進(jìn)行管理的, push 指令和 pop 指令運(yùn)行后,esp 寄存器值回自動(dòng)進(jìn)行更新.

函數(shù)調(diào)用機(jī)制

代碼如下:

屏幕快照 2017-07-08 下午6.02.15.png
屏幕快照 2017-07-08 下午6.01.39.png

在匯編語(yǔ)言中,函數(shù)名表示的是函數(shù)所在的內(nèi)存地址,當(dāng)call 調(diào)用的函數(shù)運(yùn)行后, 程序流程必須返回編號(hào)(6)的這一行, call 指令運(yùn)行后, call 指令的下一行的內(nèi)存地址(6的這一行)回自動(dòng) push 入棧,該值會(huì)在 AddNum 函數(shù)處理的最后通過(guò) ret 指令 pop 出棧,然后流程回到 6 這一行

函數(shù)的內(nèi)部調(diào)用

屏幕快照 2017-07-08 下午6.12.11.png
屏幕快照 2017-07-08 下午6.12.40.png

ebp 寄存器的值在 1 中入棧,在 5 中出棧,主要是為了把函數(shù)中用到的 ebp 寄存器的內(nèi)容,恢復(fù)到函數(shù)調(diào)用之前的狀態(tài),在進(jìn)入函數(shù)處理之前,無(wú)法確定 ebp 寄存器用到了什么地方,但由于函數(shù)內(nèi)部
(2) 中把負(fù)責(zé)管理地址的 esp 寄存器的值賦到了 ebp 寄存器中,這時(shí)因?yàn)椋?mov 指令中方括號(hào)的參數(shù),是不允許指令 esp 寄存器的,

函數(shù)是的參數(shù)是通過(guò)棧來(lái)傳遞的,返回值是通過(guò)寄存器來(lái)返回的。

始終確保全局變量用的內(nèi)存空間

c 語(yǔ)言中,在函數(shù)外部定義的變量稱為全局變量,在函數(shù)內(nèi)部定義的變量稱為局部變量,
全局變量可以參閱源代碼的任意部分,而局部變量只能在定義該變量的函數(shù)內(nèi)進(jìn)行參閱。

正如之前所說(shuō)的,編譯后的程序,會(huì)被歸類(lèi)到名為段定義的組,初始化的全局變量,會(huì)被定義到名為 _DATA 的段定義中,沒(méi)有初始化的全局變量會(huì)被匯總到 _BSS 的段定義中,指令會(huì)被匯總到名為 _TEXT 的段定義中

臨時(shí)確保局部部變量用的內(nèi)存空間

局部變量是臨時(shí)保存在寄存器和棧中的,導(dǎo)致局部變量只能在定義該函數(shù)的內(nèi)部進(jìn)行參閱。函數(shù)內(nèi)部利用棧,在函數(shù)處理完畢后會(huì)恢復(fù)到初始狀態(tài),因此局部變量的值也就被銷(xiāo)毀了,而寄存器也可能會(huì)被用于其他目的,因此,局部變量只是在函數(shù)處理運(yùn)行期間臨時(shí)存儲(chǔ)在寄存器和棧上。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容