深入 CPU 談程序的運行機制

概述

計算機的屬性反映的是人類創(chuàng)造者的本性。
其內(nèi)部復雜的系統(tǒng),依賴于底層原理來驅(qū)動,很多計算機的原理是相同的,不同的編程語言,復雜的業(yè)務邏輯等等,都是在講述同一個故事。
本篇從物理硬件上的 CPU 如何支撐源代碼運行的角度來窺探計算機硬件和軟件的合作關系。

首先明確程序的概念。程序是指令和數(shù)據(jù)的組合體。例如,C語言“printf ("你好"); ”這個簡單的程序中,printf是指令,"你好"是數(shù)據(jù)。
你好 和 printf從人的視角是很容易理解的,但計算機只能理解0101 的二進制機器碼。
所以編程代碼是如何運行的呢。

本文分兩層講述:

第一層:概述高級語言如何變成機器碼.

第二層:解釋機器碼如何驅(qū)動 CPU 運行。

兩者結(jié)合就解釋了程序運行的原理。

第一層: 概述高級語言如何變成機器碼

計算機作為被造者,必然會朝向?qū)θ祟愒絹碓接押玫能壽E發(fā)展,所謂的高級語言是從人類的視角出發(fā),編程更友好。

從最開始通過紙帶有孔無孔來表示 0 和 1。
到后來,匯編語言采用助記符(memonic)來編寫程序,每一個原本是0101的機器語言指令都會有一個與其相應的助記符,助記符通常為指令功能的英語單詞的簡寫。
而匯編指令與原有的二進制指令幾乎保持著一一對應的關系。
所以匯編語言的產(chǎn)生,但依賴于 0101 機器碼的年代,是一個很大的生產(chǎn)力提升。
但縱使這樣,匯編編程在大型程序中,晦澀難懂,很為開發(fā)維護。因此后續(xù)衍生了 C 語言,再后來從面向程序的語言進化到更高級的語言。必然從這種趨勢上看,未來肯定會有更好的語言,將編程的門檻降到更低。
但以上是建立在人類的視角,在計算機的視角,尤其是 CPU 和內(nèi)存,縱使你千遍萬換,CPU 能直接識別并使用的語言,對應的指令信息只有二進制的機器碼。
所以要想人類編寫的語言能讓 CPU 真正執(zhí)行,就需要層層剝離對人類友好的抽象,還原本真,變成機器碼。

目前的高級語言可以分為兩類: 解釋型語言編譯型語言,這兩種分別需要通過解釋器和編譯器變成匯編語言。然后借助匯編語言與機器碼的映射關系,變?yōu)?CPU 可識別的程序。

所以總結(jié)下來,第一層就是從高級語言的源碼層變成 CPU 能識別的機器碼。

源代碼到機器碼.png

第二層: 解釋機器碼如何驅(qū)動 CPU 運行

前面講到,程序中包含指令和數(shù)據(jù)。以上兩個概念,在 CPU 中是怎么表示的呢。

這里先對 CPU 的物理結(jié)構(gòu)簡單展開以便于后續(xù)原理解釋。

CPU 的物理結(jié)構(gòu)和運行過程

物理結(jié)構(gòu)上看,CPU 和內(nèi)存都是由一堆具有 ON/OFF 開關功能的晶體管組成的電子器件。
從功能的角度,CPU的內(nèi)部由寄存器、控制器、運算器、時鐘四個部分構(gòu)成,各部分之間由電流信號相互連通。

  • 寄存器可用來暫存指令、數(shù)據(jù)等處理對象,可以將其看作是內(nèi)存的一種。根據(jù)種類的不同,一個CPU內(nèi)部會有20~100個寄存器。
  • 控制器負責把內(nèi)存上的指令、數(shù)據(jù)等讀入寄存器,并根據(jù)指令的執(zhí)行結(jié)果來控制整個計算機。
  • 運算器負責運算從內(nèi)存讀入寄存器的數(shù)據(jù)。
  • 時鐘負責發(fā)出CPU開始計時的時鐘信號。不過,也有些計算機的時鐘位于CPU的外部。
程序運行流程示例
CPU的四個構(gòu)成部分
  • CPU 的運行過程大致可以解釋為:
    指令和數(shù)據(jù)存儲在內(nèi)存中,程序啟動后,根據(jù)時鐘信號,控制器會從內(nèi)存中讀取指令和數(shù)據(jù)。通過對這些指令加以解釋和運行,運算器就會對數(shù)據(jù)進行運算,控制器根據(jù)該運算結(jié)果來控制計算機。

接下來就深入到寄存器內(nèi)部,層層剝開機器碼驅(qū)動 CPU 的運行的神秘面紗。

考慮到寫一堆由 01 組成的機器碼對于閱讀和理解過于晦澀。
而匯編語言采用助記符(memonic)來編寫程序,每一個原本是01的機器語言指令都會有一個與其相應的助記符,助記符通常為指令功能的英語單詞的簡寫。例如,mov和add分別是數(shù)據(jù)的存儲(move)和相加(addition)的簡寫。匯編語言和機器語言基本上是一一對應的。
我們利用匯編語言與機器碼一一對應的關系來說明機器碼驅(qū)動 CPU 運行的原理。

CPU 中的寄存器

寄存器根據(jù)功能的不同,分類也不同。
寄存器就是程序運行時,指令和數(shù)據(jù)的真實物理載荷。

寄存器的種類

這里的數(shù)據(jù),可以分為用于運算的數(shù)值表示內(nèi)存地址的數(shù)值兩種。
當然,數(shù)據(jù)類型不同,存儲該數(shù)值的計數(shù)器也不同。
用于運算的數(shù)值放在累加寄存器中,表示內(nèi)存地址的數(shù)值則放在基址寄存器和變址寄存器中。(所以值類型和引用類型在寄存器的存儲方式就有區(qū)別)

所以綜上能看出,CPU是具有各種功能的寄存器的集合體。其中,程序計數(shù)器、累加寄存器、標志寄存器、指令寄存器和棧寄存器都只有一個,其他的寄存器一般有多個。

程序員嚴重的 CPU

寄存器的運作原理

程序計數(shù)器

程序運行啟動后,操作系統(tǒng)會將硬盤中保存的程序復制到內(nèi)存中,讓 CPU 根據(jù)指令和數(shù)據(jù)信息來執(zhí)行。
程序計數(shù)器也叫做指令計數(shù)器,是用于存放下一條指令所在單元的地址的地方。
當執(zhí)行一條指令時,首先需要根據(jù)程序計數(shù)器中存放的指令地址,將指令由內(nèi)存取到指令寄存器中,此過程稱為取指令。與此同時,PC中的地址或自動加1或由轉(zhuǎn)移指針給出下一條指令的地址。此后經(jīng)過分析指令,執(zhí)行指令。完成第一條指令的執(zhí)行,而后根據(jù)程序計數(shù)器取出第二條指令的地址,如此循環(huán),執(zhí)行每一條指令。

為保證程序能連續(xù)自動執(zhí)行下去,CPU 必須具有某些手段來確定下一條指令的地址,程序計數(shù)器就提供了物理基礎。在程序開始執(zhí)行前,必須將它的起始地址,即程序的第一條指令所在的內(nèi)存單元地址送入程序計數(shù)器,因此程序計數(shù)器的內(nèi)容即是從內(nèi)存提取的一條指令的地址。當執(zhí)行指令時,CPU 將自動修改程序計數(shù)器的內(nèi)容,即每執(zhí)行一條指令程序計數(shù)器增加一個量,這個量等于指令所含的字節(jié)數(shù),以便使其保持的總是將要執(zhí)行的下一條指令的地址。由于大多數(shù)指令都是按順序來執(zhí)行的,所以修改的過程通常只是簡單的對程序計數(shù)器加1。
但是,當遇到轉(zhuǎn)移指令如JMP(跳轉(zhuǎn)、外語全稱:JUMP)指令時,后繼指令的地址(即PC的內(nèi)容)必須從指令寄存器中的地址字段取得。在這種情況下,下一條從內(nèi)存取出的指令將由轉(zhuǎn)移指令來規(guī)定,而不像通常一樣按順序來取得。因此程序計數(shù)器的結(jié)構(gòu)應當是具有寄存信息和計數(shù)兩種功能的結(jié)構(gòu)。

根據(jù)順序,選擇和循環(huán)不同方式,往程序計數(shù)器中送入不同的指令。

函數(shù)調(diào)用機制和函數(shù)調(diào)用堆棧

函數(shù)調(diào)用處理也是通過把程序計數(shù)器的值設定成函數(shù)的存儲地址來實現(xiàn)的。不過,這和條件分支、循環(huán)的機制有所不同,函數(shù)的調(diào)用過程更為復雜,尤其對于嵌套函數(shù)的調(diào)用,單純的跳轉(zhuǎn)指令無法實現(xiàn)函數(shù)的調(diào)用。 函數(shù)的調(diào)用需要在完成函數(shù)內(nèi)部的處理后,處理流程再返回到函數(shù)調(diào)用點(函數(shù)調(diào)用指令的下一個地址)。因此,如果只是跳轉(zhuǎn)到函數(shù)的入口地址,處理流程就不知道應該返回至哪里了.

具體用一段程序講解

其流程是:

int a = 123;
int b = 456;

c = MyFunc(a, b);

d = Nextfunc(a, b);

int Myfunc(int a, int b) {
    if a > b {
        return a - b;
    } else {
        return a + b
    }
}

int Nextfunc(int a, int b) {
    return a - b;
}
函數(shù)調(diào)用原理01.jpg
函數(shù)調(diào)用原理02.jpg

除了程序計數(shù)器來保證指令的調(diào)用之外,對于函數(shù)調(diào)用內(nèi)部的函數(shù),需要通過棧寄存器來記錄并保存返回值。

機器語言的call指令和return指令能夠解決這個問題。建議大家把二者結(jié)合起來來記憶。函數(shù)調(diào)用使用的是call指令,而不是跳轉(zhuǎn)指令。在將函數(shù)的入口地址設定到程序計數(shù)器之前,call指令會把調(diào)用函數(shù)后要執(zhí)行的指令地址存儲在名為棧的主存內(nèi)(此代碼中代碼的是 Nextfunc的指令)。函數(shù)處理完畢后,再通過函數(shù)的出口來執(zhí)行return命令。return命令的功能是把保存在棧中的地址(Nextfunc的指令)設定到程序計數(shù)器中,繼續(xù)執(zhí)行。

簡單來說,函數(shù)的調(diào)用會轉(zhuǎn)化為 call 指令,將該函數(shù)的調(diào)用地址設定到程序計數(shù)器中;
函數(shù)結(jié)束會轉(zhuǎn)換成 return 指令,將返回目的地的地址(下一條指令的地址)設定在程序計數(shù)器上,這樣程序就可以流暢運行了。

至于棧寄存器的運行原理這里不作展開。

匯編語言和機器語言的種類

通過 CPU 的描述不難懂得,其實 CPU 依賴的硬件是有限的。所以任何復雜的邏輯,翻譯成對應的機器之類,也就幾種。

類型 功能
數(shù)據(jù)轉(zhuǎn)送指令 寄存器和內(nèi)存,內(nèi)存和內(nèi)存,寄存器和外圍設備之間的讀寫操作
運算指令 用累加寄存器執(zhí)行算術運算,邏輯運算,比較運算和移位運算
跳轉(zhuǎn)指令 實現(xiàn)條件分治,循環(huán),強制跳轉(zhuǎn)等
call/return 指令 函數(shù)的調(diào)用/返回函數(shù)的地址

總結(jié)

本文主要希望通過對程序的運行機制有一個整體宏觀的描述能讓大家更充分的理解編程,讓抽象的世界不再那么晦澀難懂,給你恍然大悟的感覺。

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

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