你好,我是LMOS。
歡迎來到操作系統第一課。在真正打造操作系統前,有一條必經之路:你知道程序是如何運行的嗎?
一個熟練的編程老手只需肉眼看着代碼,就能對其運行的過程了如指掌。但對於初學者來說,這常常是很困難的事,這需要好幾年的程序開發經驗,和在長期的程序開發過程中對編程基本功的積累。
我記得自己最初學習操作系統的時候,面對邏輯稍微復雜的一些程序,在編寫、調試代碼時,就會陷入代碼的迷宮,找不到東南西北。
不知道你現在處在什么階段,是否曾有同樣的感受?我常常說,扎實的基本功就像手里的指南針,你可以一步步強大到不依賴它,但是不能沒有。
因此今天,我將帶領你從“Hello World”起,扎實基本功,探索程序如何運行的所有細節和原理。
一切要從牛人做的牛逼事說起
第一位牛人,是世界級計算機大佬的傳奇——Unix之父Ken Thompson。
在上世紀60年代的一個夏天,Ken Thompson的妻子要回娘家一個月。呆在貝爾實驗室的他,竟然利用這極為孤獨的一個月,開發出了UNiplexed Information and Computing System(UNICS)——即UNIX的雛形,一個全新的操作系統。
要知道,在當時C語言並沒有誕生,從嚴格意義上說,他是用B語言和匯編語言在PDP-7的機器上完成的。
牛人的朋友也是牛人,他的朋友Dennis Ritchie也隨之加入其中,共同創造了大名鼎鼎的C語言,並用C語言寫出了UNIX和后來的類UNIX體系的幾十種操作系統,也寫出了對后世影響深遠的第一版“Hello World”:
#include "stdio.h"
int main(int argc, char const *argv[])
{
printf("Hello World!\n");
return 0;
}
計算機硬件是無法直接運行這個C語言文本程序代碼的,需要C語言編譯器,把這個代碼編譯成具體硬件平台的二進制代碼。再由具體操作系統建立進程,把這個二進制文件裝進其進程的內存空間中,才能運行。
聽起來很復雜?別急,接着往下看。
程序編譯過程
我們暫且不急着摸清操作系統所做的工作,先來研究一下編譯過程和硬件執行程序的過程,約定使用GCC相關的工具鏈。
那么使用命令:gcc HelloWorld.c -o HelloWorld 或者 gcc ./HelloWorld.c -o ./HelloWorld ,就可以編譯這段代碼。其實,GCC只是完成編譯工作的驅動程序,它會根據編譯流程分別調用預處理程序、編譯程序、匯編程序、鏈接程序來完成具體工作。
下圖就是編譯這段代碼的過程:
其實,我們也可以手動控制以上這個編譯流程,從而留下中間文件方便研究:
- gcc HelloWorld.c -E -o HelloWorld.i預處理:加入頭文件,替換宏。
- gcc HelloWorld.c -S -c HelloWorld.s編譯:包含預處理,將C程序轉換成匯編程序。
- gcc HelloWorld.c -c HelloWorld.o匯編:包含預處理和編譯,將匯編程序轉換成可鏈接的二進制程序。
- gcc HelloWorld.c -o HelloWorld鏈接:包含以上所有操作,將可鏈接的二進制程序和其它別的庫鏈接在一起,形成可執行的程序文件。
程序裝載執行
對運行內容有了了解后,我們開始程序的裝載執行。
我們將請出第三位牛人——大名鼎鼎的阿蘭·圖靈。在他的眾多貢獻中,很重要的一個就是提出了一種理想中的機器:圖靈機。
圖靈機是一個抽象的模型,它是這樣的:有一條無限長的紙帶,紙帶上有無限個小格子,小格子中寫有相關的信息,紙帶上有一個讀頭,讀頭能根據紙帶小格子里的信息做相關的操作並能來回移動。
文字敘述還不夠形象,我們來畫一幅插圖:
不理解?下面我再帶你用圖靈機執行一下“1+1=2”的計算,你就明白了。我們定義讀頭讀到“+”之后,就依次移動讀頭兩次並讀取格子中的數據,最后讀頭計算把結果寫入第二個數據的下一個格子里,整個過程如下圖:
這個理想的模型是好,但是理想終歸是理想,想要成為現實,我們得想其它辦法。
於是,第四位牛人來了,他提出了電子計算機使用二進制數制系統和儲存程序,並按照程序順序執行,他叫馮諾依曼,他的電子計算機理論叫馮諾依曼體系結構。
根據馮諾依曼體系結構構成的計算機,必須具有如下功能:
- 把程序和數據裝入到計算機中;
- 必須具有長期記住程序、數據的中間結果及最終運算結果;
- 完成各種算術、邏輯運算和數據傳送等數據加工處理;
- 根據需要控制程序走向,並能根據指令控制機器的各部件協調操作;
- 能夠按照要求將處理的數據結果顯示給用戶。
為了完成上述的功能,計算機必須具備五大基本組成部件:
- 裝載數據和程序的輸入設備;
- 記住程序和數據的存儲器;
- 完成數據加工處理的運算器;
- 控制程序執行的控制器;
- 顯示處理結果的輸出設備。
根據馮諾依曼的理論,我們只要把圖靈機的幾個部件換成電子設備,就可以變成一個最小核心的電子計算機,如下圖:
是不是非常簡單?這次我們發現讀頭不再來回移動了,而是靠地址總線尋找對應的“紙帶格子”。讀取寫入數據由數據總線完成,而動作的控制就是控制總線的職責了。
更形象地將HelloWorld程序裝入原型計算機
下面,我們嘗試將HelloWorld程序裝入這個原型計算機,在裝入之前,我們先要搞清楚HelloWorld程序中有什么。
我們可以通過gcc -c -S HelloWorld得到(只能得到其匯編代碼,而不能得到二進制數據)。我們用objdump -d HelloWorld程序,得到/lesson01/HelloWorld.dump,其中有很多庫代碼(只需關注main函數相關的代碼),如下圖:
以上圖中,分成四列:第一列為地址;第二列為十六進制,表示真正裝入機器中的代碼數據;第三列是對應的匯編代碼;第四列是相關代碼的注釋。這是x86_64體系的代碼,由此可以看出x86 CPU是變長指令集。
接下來,我們把這段代碼數據裝入最小電子計算機,狀態如下圖:
重點回顧
以上,對應圖中的偽代碼你應該明白了:現代電子計算機正是通過內存中的信息(指令和數據)做出相應的操作,並通過內存地址的變化,達到程序讀取數據,控制程序流程(順序、跳轉對應該圖靈機的讀頭來回移動)的功能。
這和圖靈機的核心思想相比,沒有根本性的變化。只要配合一些I/O設備,讓用戶輸入並顯示計算結果給用戶,就是一台現代意義的電子計算機。
到這里,我們理清了程序運行的所有細節和原理。還有一點,你可能有點疑惑,即printf對應的puts函數,到底做了什么?而這正是我們后面的課程要探索的!
這節課的配套代碼,你可以從這里下載。
思考題
為了實現C語言中函數的調用和返回功能,CPU實現了函數調用和返回指令,即上圖匯編代碼中的“call”,“ret”指令,請你思考一下:call和ret指令在邏輯上執行的操作是怎樣的呢?
期待你在留言區跟我交流互動。如果這節課對你有所啟發,也歡迎轉發給你的朋友、同事,跟他們一起學習進步。