close

「大師,程序員不懂匯編能在這個行業混嗎?」

「當然可以了,你在應用層編程,不進入底層就沒問題,但是有句話說得好,『真正的程序員應該理解程序的每個字節』,理解了匯編,會對程序和計算機系統有個透徹的理解,對學習基礎知識有很大的好處!」

「那匯編很難嗎?」

「不難,它比你會的任何一門語言的語法都簡單!」

「哦?那我可以學習一下!你教教我吧!」

「好吧,首先,匯編就是機器語言的助記符,這你肯定知道吧?」

「這我明白,然後呢?」

「然後你要理解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,是專門用來做函數調用的,我們一會兒再說。」

「好吧,大概記住了!」

「匯編確實很簡單,你記住,匯編的指令主要是這三類:數據傳輸類,算術和邏輯運算類,控制類。」

「數據傳輸類就是把數據從一個位置複製到另外一個位置,比如從內存到寄存器,或者從寄存器到內存, 或者從寄存器到寄存器。」


mov ax,3210H ;將0x3210放入寄存器axmov ax,bx ;將bx寄存器的值放入axmov ax,[3640] ;將一個內存單元的值送入axmov [502c],bx ;將bx寄存器的值送入內存單元

「有意思,都是把右邊的值複製到左邊。」

「這是Intel的匯編格式,在AT&T的匯編格式中,就是把左邊值複製放到右邊。」

「我看到了方括號[3640]、[502c] 這表示一個內存的物理地址嗎?」

「嗯,這真是個好問題,涉及到段寄存器,剛才忘了給你展示了,段寄存器在實模式和保護模式下還不一樣,展開講就太麻煩了,我們先放下,暫時認為這是某個地址吧。再來看看算術和邏輯運算。」

算術和邏輯運算類無非就是加減乘除,AND, OR, 左移,右移

例如:

add ax bx ; 把ax和bx的值相加,把結果放入ax寄存器add ax, [37a0] ; 把ax和內存的值相加,結果放到ax寄存器inc bx ; 對bx的內容加1shl bx 1 ; 把bx的值左移一位and al, 11110110b ; and操作,相當於清除位 0 和位 3 ,其他位不變

「非常容易理解,那第三類控制類指令是什麼意思?」

「你想想,用高級語言寫程序是不是有很多分支(if else)、循環(while)?」

「對啊,匯編中有這些指令嗎?」

「沒有,在CPU中實現流程控制的邏輯需要多方配合,在CPU中有很多標誌位,例如著名的ZF(零標誌位),如果最近的操作的結果為零,則ZF= 1,然後你就可以用另外一條語句判斷ZF的值,進行跳轉。」

cmp ax bx ; 比較ax 和 bx ,如果相等,就把ZF標記為1je .L1 ; 如果ZF 為1 ,則跳轉到.L1處......代碼略.......L1 sub ax 10

「我的天啊,搞個跳轉這麼麻煩,還是高級語言好啊!」

「那可不,但是你也要知道,高級語言經過編譯,最終都會變成匯編的形式,它是一切編程的本質!」

「嗯,現在程序可以實現順序執行,按條件跳轉,那函數調用該怎麼實現?」

「終於到了關鍵問題了,函數的調用只使用寄存器是搞不定了,需要內存的配合,在內存中建立一個叫做棧的數據結構。」

「棧我知道,先進後出嘛!」


「在這個棧中,每個元素代表一個運行中的函數,比如有三個函數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 函數前的最後一躍!

第一部分完結 進入內核前的苦力活

第二部分 大戰前期的初始化工作


第11回 | 整個操作系統就 20 幾行代碼
第12回 | 管理內存前先劃分出三個邊界值
第13回 | 主內存初始化 mem_init
第14回 | 中斷初始化 trap_init
第15回 | 塊設備請求項初始化 blk_dev_init
第16回 | 控制台初始化 tty_init
第17回 | 時間初始化 time_init
第18回 | 進程調度初始化 sched_init
第19回 | 緩衝區初始化 buffer_init
第20回 | 硬盤初始化 hd_init

這張圖展示了整個系列的結構

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()