日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務,提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

 

 

編譯器的任務,是要生成能夠在計算機上運行的代碼,但要生成代碼,我們必須對程序的運行環(huán)境和運行機制有比較透徹的了解。

你要知道,大型的、復雜一點兒的系統(tǒng),比如像淘寶一樣的電商系統(tǒng)、搜索引擎系統(tǒng)等等,都存在一些技術任務,是需要你深入了解底層機制才能解決的。比如淘寶的基礎技術團隊就曾經(jīng)貢獻過,JAVA 虛擬機即時編譯功能中的一個補丁。

這反映出掌握底層技術能力的重要性,所以,如果你想進階成為這個層次的工程師,不能只學學上層的語法,而是要把計算機語言從上層的語法到底層的運行機制都了解透徹。

文本我會對計算機程序如何運行,做一個解密,話題分成兩個部分:

  1. 了解程序運行的環(huán)境,包括 CPU、內(nèi)存和操作系統(tǒng),探知它們跟程序到底有什么關系。
  2. 了解程序運行的過程。比如,一個程序是怎么跑起來的,代碼是怎樣執(zhí)行和跳轉(zhuǎn)的,又是如何管理內(nèi)存的。

首先,我們先來了解一下程序運行的環(huán)境。

程序運行的環(huán)境

程序運行的過程中,主要是跟兩個硬件(CPU 和內(nèi)存)以及一個軟件(操作系統(tǒng))打交道。

細說:程序運行的環(huán)境和運行過程,再看不懂請自行面壁

 

本質(zhì)上,我們的程序只關心 CPU 和內(nèi)存這兩個硬件。你可能說:“不對啊,計算機還有其他硬件,比如顯示器和硬盤啊。”但對我們的程序來說,操作這些硬件,也只是執(zhí)行某些特定的驅(qū)動代碼,跟執(zhí)行其他代碼并沒有什么差異。

1. 關注CPU和內(nèi)存

CPU 的內(nèi)部有很多組成部分,對于本文來說,我們重點關注的是寄存器以及高速緩存,它們跟程序的執(zhí)行機制和優(yōu)化密切相關。

寄存器是 CPU 指令在進行計算的時候,臨時數(shù)據(jù)存儲的地方。CPU 指令一般都會用到寄存器,比如,典型的一個加法計算(c=a+b)的過程是這樣的:

指令 1(mov):從內(nèi)取 a 的值放到寄存器中;指令 2(add):再把內(nèi)存中 b 的值取出來與這個寄存器中的值相加,仍然保存在寄存器中;指令 3(mov):最后再把寄存器中的數(shù)據(jù)寫回內(nèi)存中 c 的地址。

寄存器的速度也很快,所以能用寄存器就別用內(nèi)存。盡量充分利用寄存器,是編譯器做優(yōu)化的內(nèi)容之一。

而高速緩存可以彌補 CPU 的處理速度和內(nèi)存訪問速度之間的差距。所以,我們的指令在內(nèi)存讀一個數(shù)據(jù)的時候,它不是老老實實地只讀進當前指令所需要的數(shù)據(jù),而是把跟這個數(shù)據(jù)相鄰的一組數(shù)據(jù)都讀進高速緩存了。這就相當于外賣小哥送餐的時候,不會為每一單來回跑一趟,而是一次取一批,如果這一批外賣恰好都是同一個寫字樓里的,那小哥的送餐效率就會很高。

內(nèi)存和高速緩存的速度差異差不多是兩個數(shù)量級,也就是一百倍。比如,高速緩存的讀取時間可能是 0.5ns,而內(nèi)存的訪問時間可能是 50ns。不同硬件的參數(shù)可能有差異,但總體來說是幾十倍到上百倍的差異。

你寫程序時,盡量把某個操作所需的數(shù)據(jù)都放在內(nèi)存中的連續(xù)區(qū)域中,不要零零散散地到處放,這樣有利于充分利用高速緩存。這種優(yōu)化思路,叫做數(shù)據(jù)的局部性

這里提一句,在寫系統(tǒng)級的程序時,你要對各種 IO 的時間有基本的概念,比如高速緩存、內(nèi)存、磁盤、網(wǎng)絡的 IO 大致都是什么數(shù)量級的。因為這都影響到系統(tǒng)的整體性能,也影響到你如何做程序優(yōu)化。如果你需要對程序做更多的優(yōu)化,還需要了解更多的 CPU 運行機制,包括流水線機制、并行機制等等,這里就不展開了。

講完 CPU 之后,還有內(nèi)存這個硬件。

程序在運行時,操作系統(tǒng)會給它分配一塊虛擬的內(nèi)存空間,讓它在運行期可以使用。我們目前使用的都是 64 位的機器,你可以用一個 64 位的長整型來表示內(nèi)存地址,它能夠表示的所有地址,我們叫做尋址空間。

64 位機器的尋址空間就有 2 的 64 次方那么大,也就是有很多很多個 T(Terabyte),大到你的程序根本用不完。不過,操作系統(tǒng)一般會給予一定的限制,不會給你這么大的尋址空間,比如給到 100 來個 G,這對一般的程序,也足夠用了。

在存在操作系統(tǒng)的情況下,程序邏輯上可使用的內(nèi)存一般大于實際的物理內(nèi)存。程序在使用內(nèi)存的時候,操作系統(tǒng)會把程序使用的邏輯地址映射到真實的物理內(nèi)存地址。有的物理內(nèi)存區(qū)域會映射進多個進程的地址空間。

細說:程序運行的環(huán)境和運行過程,再看不懂請自行面壁

 

對于不太常用的內(nèi)存數(shù)據(jù),操作系統(tǒng)會寫到磁盤上,以便騰出更多可用的物理內(nèi)存。

當然,也存在沒有操作系統(tǒng)的情況,這個時候你的程序所使用的內(nèi)存就是物理內(nèi)存,我們必須自己做好內(nèi)存的管理。

對于這個內(nèi)存,該怎么用呢

本質(zhì)上來說,你想怎么用就怎么用,并沒有什么特別的限制。一個編譯器的作者,可以決定在哪兒放代碼,在哪兒放數(shù)據(jù),當然了,別的作者也可能采用其他的策略。實際上,C 語言和 Java 虛擬機對內(nèi)存的管理和使用策略就是不同的。

盡管如此,大多數(shù)語言還是會采用一些通用的內(nèi)存管理模式。以 C 語言為例,會把內(nèi)存劃分為代碼區(qū)、靜態(tài)數(shù)據(jù)區(qū)、棧和堆。

細說:程序運行的環(huán)境和運行過程,再看不懂請自行面壁

 

一般來講,代碼區(qū)是在最低的地址區(qū)域,然后是靜態(tài)數(shù)據(jù)區(qū),然后是堆。而棧傳統(tǒng)上是從高地址向低地址延伸,棧的最頂部有一塊區(qū)域,用來保存環(huán)境變量。

代碼區(qū)(也叫文本段)存放編譯完成以后的機器碼。這個內(nèi)存區(qū)域是只讀的,不會再修改,但也不絕對。現(xiàn)代語言的運行時已經(jīng)越來越動態(tài)化,除了保存機器碼,還可以存放中間代碼,并且還可以在運行時把中間代碼編譯成機器碼,寫入代碼區(qū)。

靜態(tài)數(shù)據(jù)區(qū)保存程序中全局的變量和常量。它的地址在編譯期就是確定的,在生成的代碼里直接使用這個地址就可以訪問它們,它們的生存期是從程序啟動一直到程序結(jié)束。它又可以細分為 Data 和 BSS 兩個段。Data 段中的變量是在編譯期就初始化好的,直接從程序裝在進內(nèi)存。BSS 段中是那些沒有聲明初始化值的變量,都會被初始化成 0。

適合管理生存期較長的一些數(shù)據(jù),這些數(shù)據(jù)在退出作用域以后也不會消失。比如,我們在某個方法里創(chuàng)建了一個對象并返回,并希望代表這個對象的數(shù)據(jù)在退出函數(shù)后仍然可以訪問。

而棧適合保存生存期比較短的數(shù)據(jù),比如函數(shù)和方法里的本地變量。它們在進入某個作用域的時候申請內(nèi)存,退出這個作用域的時候就可以釋放掉。

講完了 CPU 和內(nèi)存之后,我們再來看看跟程序打交道的操作系統(tǒng)。

2. 程序和操作系統(tǒng)的關系

程序跟操作系統(tǒng)的關系比較微妙:

一方面我們的程序可以編譯成不需要操作系統(tǒng)也能運行,就像一些物聯(lián)網(wǎng)應用那樣,完全跑在裸設備上。另一方面,有了操作系統(tǒng)的幫助,可以為程序提供便利,比如可以使用超過物理內(nèi)存的存儲空間,操作系統(tǒng)負責進行虛擬內(nèi)存的管理。

在存在操作系統(tǒng)的情況下,因為很多進程共享計算機資源,所以就要遵循一些約定。這就仿佛辦公室是所有同事共享的,那么大家就都要遵守一些約定,如果一個人大聲喧嘩,就會影響到其他人。

程序需要遵守的約定包括:程序文件的二進制格式約定,這樣操作系統(tǒng)才能程序正確地加載進來,并為同一個程序的多個進程共享代碼區(qū)。在使用寄存器和棧的時候也要遵守一些約定,便于操作系統(tǒng)在不同的進程之間切換的時候、在做系統(tǒng)調(diào)用的時候,做好上下文的保護。

所以,我們編譯程序的時候,要知道需要遵守哪些約定。因為就算是使用同樣的 CPU,針對不同的操作系統(tǒng),編譯的結(jié)果也是非常不同的。

好了,我們了解了程序運行時的硬件和操作系統(tǒng)環(huán)境。接下來,我們看看程序運行時,是怎么跟它們互動的。

程序運行的過程

你天天運行程序,可對于程序運行的細節(jié),真的清楚嗎?

1. 程序運行的細節(jié)

首先,可運行的程序一般是由操作系統(tǒng)加載到內(nèi)存的,并且定位到代碼區(qū)里程序的入口開始執(zhí)行。比如,C 語言的 main 函數(shù)的第一行代碼。

每次加載一條代碼,程序都會順序執(zhí)行,碰到跳轉(zhuǎn)語句,才會跳到另一個地址執(zhí)行。CPU里有一個指令寄存器,里面保存了下一條指令的地址。

細說:程序運行的環(huán)境和運行過程,再看不懂請自行面壁

 

假設我們運行這樣一段代碼編譯后形成的程序:

int main(){
    int a = 1;
    foo(3);
    bar();
}
int foo(int c){
    int b = 2;
    return b+c;
}
int bar(){
    return foo(4) + 1;
}

我們首先激活(Activate)main() 函數(shù),main() 函數(shù)又激活 foo() 函數(shù),然后又激活 bar()函數(shù),bar() 函數(shù)還會激活 foo() 函數(shù),其中 foo() 函數(shù)被兩次以不同的路徑激活。

細說:程序運行的環(huán)境和運行過程,再看不懂請自行面壁

 

我們把每次調(diào)用一個函數(shù)的過程,叫做一次活動(Activation)。每個活動都對應一個活動記錄(Activation Record),這個活動記錄里有這個函數(shù)運行所需要的信息,比如參數(shù)、返回值、本地變量等。

目前我們用棧來管理內(nèi)存,所以可以把活動記錄等價于棧楨。棧楨是活動記錄的實現(xiàn)方式,我們可以自由設計活動記錄或棧楨的結(jié)構,下圖是一個常見的設計:

細說:程序運行的環(huán)境和運行過程,再看不懂請自行面壁

 

  • 返回值:一般放在最頂上,這樣它的地址是固定的。foo() 函數(shù)返回以后,它的調(diào)用者可以到這里來取到返回值。在實際情況中,我們會優(yōu)先通過寄存器來傳遞返回值,比通過內(nèi)存?zhèn)鬟f性能更高。
  • 參數(shù):在調(diào)用 foo 函數(shù)時,把參數(shù)寫到這個地址里。同樣,我們也可以通過寄存器來傳遞,而不是內(nèi)存。
  • 控制鏈接:就是上一級棧楨的地址。如果用到了上一級作用域中的變量,就可以順著這個鏈接找到上一級棧楨,并找到變量的值。
  • 返回地址:foo 函數(shù)執(zhí)行完畢以后,繼續(xù)執(zhí)行哪條指令。同樣,我們可以用寄存器來保存這個信息。
  • 本地變量:foo 函數(shù)的本地變量 b 的存儲空間。
  • 寄存器信息:我們還經(jīng)常在棧楨里保存寄存器的數(shù)據(jù)。如果在 foo 函數(shù)里要使用某個寄存器,可能需要先把它的值保存下來,防止破壞了別的代碼保存在這里的數(shù)據(jù)。這種約定叫做被調(diào)用者責任,也就是使用寄存器的人要保護好寄存器里原有的信息。某個函數(shù)如果使用了某個寄存器,但它又要調(diào)用別的函數(shù),為了防止別的函數(shù)把自己放在寄存器中的數(shù)據(jù)覆蓋掉,要自己保存在棧楨中。這種約定叫做調(diào)用者責任
細說:程序運行的環(huán)境和運行過程,再看不懂請自行面壁

 

你可以看到,每個棧楨的長度是不一樣的。

用到的參數(shù)和本地變量多,棧楨就要長一點。但是,棧楨的長度和結(jié)構是在編譯期就能完全確定的。這樣就便于我們計算地址的偏移量,獲取棧楨里某個數(shù)據(jù)。

總的來說,棧楨的設計很自由。但是,你要考慮不同語言編譯形成的模塊要能夠鏈接在一起,所以還是要遵守一些公共的約定的,否則,你寫的函數(shù),別人就沒辦法調(diào)用了。

在之前的文章中我提到過棧楨,這次我們用了更加貼近具體實現(xiàn)的描述:棧楨就是一塊確定的內(nèi)存,變量就是這塊內(nèi)存里的地址。在下一講,我會帶你動手實現(xiàn)我們的棧楨。

2.從全局角度看整個運行過程

了解了棧楨的實現(xiàn)之后,我們再來看一個更大的場景,從全局的角度看看整個運行過程中都發(fā)生了什么。

細說:程序運行的環(huán)境和運行過程,再看不懂請自行面壁

 

代碼區(qū)里存儲了一些代碼,main 函數(shù)、bar 函數(shù)和 foo 函數(shù)各自有一段連續(xù)的區(qū)域來存儲代碼,我用了一些匯編指令來表示這些代碼(實際運行時這里其實是機器碼)。

假設我們執(zhí)行到 foo 函數(shù)中的一段指令,來計算“b+c”的值,并返回。這里用到了mov、add、jmp 這三個指令。mov 是把某個值從一個地方拷貝到另一個地方,add 是往

某個地方加一個值,jmp 是改變代碼執(zhí)行的順序,跳轉(zhuǎn)到另一個地方去執(zhí)行(匯編命令的細節(jié),我們下節(jié)再講,你現(xiàn)在簡單了解一下就行了)。

mov b的地址寄存器1
add c的地址寄存器1
mov寄存器1 foo的返回值地址
jmp返回地址//或ret指令

執(zhí)行完這幾個指令以后,foo 的返回值位置就寫入了 6,并跳轉(zhuǎn)到 bar 函數(shù)中執(zhí)行 foo 之后的代碼。

這時,foo 的棧楨就沒用了,新的棧頂是 bar 的棧楨的頂部。理論上講,操作系統(tǒng)這時可以把 foo 的棧楨所占的內(nèi)存收回了。比如,可以映射到另一個程序的尋址空間,讓另一個程序使用。但是在這個例子中你會看到,即使返回了 bar 函數(shù),我們?nèi)砸L問棧頂之外的一個內(nèi)存地址,也就是返回值的地址。

所以,目前的調(diào)用約定都規(guī)定,程序的棧頂之外,仍然會有一小塊內(nèi)存(比如 128K)是可以由程序訪問的,比如我們可以拿來存儲返回值。這一小段內(nèi)存操作系統(tǒng)并不會回收。

我們目前只講了棧,堆的使用也類似,只不過是要手工進行申請和釋放,比棧要多一些維護工作。

總結(jié)

本文帶你了解了程序運行的環(huán)境和過程,我們的程序主要跟 CPU、內(nèi)存,以及操作系統(tǒng)打交道。你需要了解的重點如下:

  • CPU 上運行程序的指令,運行過程中要用到寄存器、高速緩存來提高指令和數(shù)據(jù)的存取效率。
  • 內(nèi)存可以劃分成不同的區(qū)域保存代碼、靜態(tài)數(shù)據(jù),并用棧和堆來存放運行時產(chǎn)生的動態(tài)數(shù)據(jù)。
  • 操作系統(tǒng)會把物理的內(nèi)存映射成進程的尋址空間,同一份代碼會被映射進多個進程的內(nèi)存空間,操作系統(tǒng)的公共庫也會被映射進進程的內(nèi)存空間,操作系統(tǒng)還會自動維護棧。
  • 程序在運行時順序執(zhí)行代碼,可以根據(jù)跳轉(zhuǎn)指令來跳轉(zhuǎn);棧被劃分成棧楨,棧楨的設計有一定的自由度,但通常也要遵守一些約定;棧楨的大小和結(jié)構在編譯時就能決定;在運行時,棧楨作為活動記錄,不停地被動態(tài)創(chuàng)建和釋放。

以上這些內(nèi)容就是一個程序運行時的秘密。你再面對代碼時,腦海里就會想象出它是怎樣跟CPU、內(nèi)存和操作系統(tǒng)打交道的了。而且有了這些背景知識,你也可以讓編譯器生成代碼,按照本文所說的模式運行了!

 

分享到:
標簽:運行 程序
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數(shù)有氧達人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定