「大師,程序員不懂匯編能在這個行業混嗎?」
「當然可以了,你在應用層編程,不進入底層就沒問題,但是有句話說得好,『真正的程序員應該理解程序的每個字節』,理解了匯編,會對程序和計算機系統有個透徹的理解,對學習基礎知識有很大的好處!」
「那匯編很難嗎?」
「不難,它比你會的任何一門語言的語法都簡單!」
「哦?那我可以學習一下!你教教我吧!」
「好吧,首先,匯編就是機器語言的助記符,這你肯定知道吧?」
「這我明白,然後呢?」
「然後你要理解CPU的寄存器。」
「這....... 不就是像內存一樣,一個個小格子嗎?為啥要有寄存器? CPU直接操作內存進行運算不就得了?」
「寄存器至少有兩個好處: 1. CPU太快,比內存快100倍,CPU等不及內存。我們把寄存器放到CPU內部,緊鄰ALU(算術邏輯單元),這樣信號幾乎可以立即傳輸了。」
「2. 使用寄存器還有個額外的好處,可以讓指令更短,想想看,如果一條機器指令引用了兩個64位的地址,它該多長啊!」
「明白了,把地址放到寄存器中,指令會短得多,那寄存器都是叫Register 1, Register 2....... 嗎?」
「那肯定不是,按照不同的用途,可以把寄存器分類:」
「暈了暈了,你還說匯編簡單,光是這些莫名其名的寄存器名稱就讓人崩潰。」
「別擔心,我概要地介紹一下,你先有個基本的印象,上圖中的EAX、EBX、ECX、EDX在『大多數情況下』可以認為是『通用寄存器』, 你可以隨便使用。」
「為什麼既有EAX, 還有AX、AH、AL ,他們之間有什麼關?」
「最早的Intel 8086CPU中,寄存器AX、BX、CX、DX等是16位的,16位(AX)又分為高8位字節(AH),和低位字節(AL),後來Intel 推出32位的CPU,寄存器也就擴展(Extend)到了32位,EAX就出現了!」
「那64位CPU是不是得繼續擴展到64位寄存器?」
「孺子可教,x86-64 CPU的寄存器是RAX、RBX、RCX、RDX...... 哎喲,扯遠了,我們接着說ESI、EDI這兩個寄存器,SI是Source Index, DI 是Destination Index,你猜猜他們有什麼用處?」
「Source ? Destination? 好像是複製數據時指定從某個源到某個目的地。」
「對嘍,有些匯編指令是專門複製數據的,可以用上ESI和EDI。還有兩個重要的寄存器EBP和ESP,是專門用來做函數調用的,我們一會兒再說。」
「好吧,大概記住了!」
「匯編確實很簡單,你記住,匯編的指令主要是這三類:數據傳輸類,算術和邏輯運算類,控制類。」
「數據傳輸類就是把數據從一個位置複製到另外一個位置,比如從內存到寄存器,或者從寄存器到內存, 或者從寄存器到寄存器。」
「有意思,都是把右邊的值複製到左邊。」
「這是Intel的匯編格式,在AT&T的匯編格式中,就是把左邊值複製放到右邊。」
「我看到了方括號[3640]、[502c] 這表示一個內存的物理地址嗎?」
「嗯,這真是個好問題,涉及到段寄存器,剛才忘了給你展示了,段寄存器在實模式和保護模式下還不一樣,展開講就太麻煩了,我們先放下,暫時認為這是某個地址吧。再來看看算術和邏輯運算。」
算術和邏輯運算類無非就是加減乘除,AND, OR, 左移,右移
例如:
「非常容易理解,那第三類控制類指令是什麼意思?」
「你想想,用高級語言寫程序是不是有很多分支(if else)、循環(while)?」
「對啊,匯編中有這些指令嗎?」
「沒有,在CPU中實現流程控制的邏輯需要多方配合,在CPU中有很多標誌位,例如著名的ZF(零標誌位),如果最近的操作的結果為零,則ZF= 1,然後你就可以用另外一條語句判斷ZF的值,進行跳轉。」
「我的天啊,搞個跳轉這麼麻煩,還是高級語言好啊!」
「那可不,但是你也要知道,高級語言經過編譯,最終都會變成匯編的形式,它是一切編程的本質!」
「嗯,現在程序可以實現順序執行,按條件跳轉,那函數調用該怎麼實現?」
「終於到了關鍵問題了,函數的調用只使用寄存器是搞不定了,需要內存的配合,在內存中建立一個叫做棧的數據結構。」
「棧我知道,先進後出嘛!」
「在這個棧中,每個元素代表一個運行中的函數,比如有三個函數main ,add,square ,main 調用add,add調用square,那在運行時,函數棧是這樣的:」
「咦,這個棧中每個元素占據的空間不一樣啊?」
「每個函數可能有自己的局部變量和各種參數,那大小肯定不同, 我們把這每個元素稱為「棧幀」,還記得我們剛才提到的EBP寄存器和ESP寄存器嗎?現在就可以派上用場了,用他倆來指向當前棧幀的開始處和結束處。」
「看起來很有道理,但是,只有兩個寄存器,函數調用可能有很多層,棧幀就有很多個,不夠用啊?」
「所以,當main調用add 的時候,需要把main棧幀的開始地址(就是當前EBP的值)保存到add函數的棧幀中,這樣從add返回,就能恢復main的ebp了。」
「明白了,每個棧幀的開始地址相當於一個'門牌號',寫在EBP寄存器中,但是EBP只有一個,所以,需要把上個門牌號暫時保存到下一個函數棧幀中。」
「嗯,你這個比喻很到位,同理,當add 調用square,需要把add的EBP保存到square的棧幀中,以便返回時恢復!」
「如果當前函數執行完,棧幀也就不用了,在廢棄掉之前,把內存中的保存的值恢復到EBP當中,並且移動ESP到上個棧幀的頂部,就OK了!」
「懂了,大師,這樣僅使用兩個寄存器,就能記錄無窮無盡的函數調用了,真是妙啊,這辦法是誰想出來的啊。」
「不知道是誰先想出來的辦法,現在,你覺得匯編很難嗎?」
「看起來似乎不難啊!」
「我今天給你說的只是入門罷了,還有很多細節,尤其是當你讀操作系統源碼的時候,涉及到大量Intel CPU的知識,實模式和保護模式的轉換,頁表的建立......那個時候就真的很麻煩了。」
「在哪兒能學習這些知識?」
「給你推薦一套閃客寫的操作系統教程吧,裡邊把這些知識點都給覆蓋了:」
第一部分 進入內核前的苦力活
開篇詞
第一回 | 最開始的兩行代碼
第二回 | 自己給自己挪個地兒
第三回 | 做好最最基礎的準備工作
第四回 | 把自己在硬盤裡的其他部分也放到內存來
第五回 | 進入保護模式前的最後一次折騰內存
第六回 | 先解決段寄存器的歷史包袱問題
第七回 | 六行代碼就進入了保護模式
第八回 | 煩死了又要重新設置一遍 idt 和 gdt
第九回 | Intel 內存管理兩板斧:分段與分頁
第十回 | 進入 main 函數前的最後一躍!
第一部分完結 進入內核前的苦力活
第二部分 大戰前期的初始化工作
