當前位置:
首頁 > 知識 > 彙編語言入門教程

彙編語言入門教程

學習編程其實就是學高級語言,即那些為人類設計的計算機語言。

但是,計算機不理解高級語言,必須通過編譯器轉成二進位代碼,才能運行。學會高級語言,並不等於理解計算機實際的運行步驟。

計算機真正能夠理解的是低級語言,它專門用來控制硬體。彙編語言就是低級語言,直接描述/控制 CPU 的運行。如果你想了解 CPU 到底幹了些什麼,以及代碼的運行步驟,就一定要學習彙編語言。

彙編語言不容易學習,就連簡明扼要的介紹都很難找到。下面我嘗試寫一篇最好懂的彙編語言教程,解釋 CPU 如何執行代碼。

一、彙編語言是什麼?

我們知道,CPU 只負責計算,本身不具備智能。你輸入一條指令(instruction),它就運行一次,然後停下來,等待下一條指令。

這些指令都是二進位的,稱為操作碼(opcode),比如加法指令就是00000011。編譯器的作用,就是將高級語言寫好的程序,翻譯成一條條操作碼。

對於人類來說,二進位程序是不可讀的,根本看不出來機器幹了什麼。為了解決可讀性的問題,以及偶爾的編輯需求,就誕生了彙編語言。

彙編語言是二進位指令的文本形式,與指令是一一對應的關係。比如,加法指令00000011寫成彙編語言就是 ADD。只要還原成二進位,彙編語言就可以被 CPU 直接執行,所以它是最底層的低級語言。

二、來歷

最早的時候,編寫程序就是手寫二進位指令,然後通過各種開關輸入計算機,比如要做加法了,就按一下加法開關。後來,發明了紙帶打孔機,通過在紙帶上打孔,將二進位指令自動輸入計算機。

為了解決二進位指令的可讀性問題,工程師將那些指令寫成了八進位。二進位轉八進位是輕而易舉的,但是八進位的可讀性也不行。很自然地,最後還是用文字表達,加法指令寫成 ADD。內存地址也不再直接引用,而是用標籤表示。

這樣的話,就多出一個步驟,要把這些文字指令翻譯成二進位,這個步驟就稱為 assembling,完成這個步驟的程序就叫做 assembler。它處理的文本,自然就叫做 aseembly code。標準化以後,稱為 assembly language,縮寫為 asm,中文譯為彙編語言。

每一種 CPU 的機器指令都是不一樣的,因此對應的彙編語言也不一樣。本文介紹的是目前最常見的 x86 彙編語言,即 Intel 公司的 CPU 使用的那一種。

三、寄存器

學習彙編語言,首先必須了解兩個知識點:寄存器和內存模型。

先來看寄存器。CPU 本身只負責運算,不負責儲存數據。數據一般都儲存在內存之中,CPU 要用的時候就去內存讀寫數據。但是,CPU 的運算速度遠高於內存的讀寫速度,為了避免被拖慢,CPU 都自帶一級緩存和二級緩存。基本上,CPU 緩存可以看作是讀寫速度較快的內存。

但是,CPU 緩存還是不夠快,另外數據在緩存裡面的地址是不固定的,CPU 每次讀寫都要定址也會拖慢速度。因此,除了緩存之外,CPU 還自帶了寄存器(register),用來儲存最常用的數據。也就是說,那些最頻繁讀寫的數據(比如循環變數),都會放在寄存器裡面,CPU 優先讀寫寄存器,再由寄存器跟內存交換數據。

寄存器不依靠地址區分數據,而依靠名稱。每一個寄存器都有自己的名稱,我們告訴 CPU 去具體的哪一個寄存器拿數據,這樣的速度是最快的。有人比喻寄存器是 CPU 的零級緩存。

四、寄存器的種類

早期的 x86 CPU 只有8個寄存器,而且每個都有不同的用途。現在的寄存器已經有100多個了,都變成通用寄存器,不特別指定用途了,但是早期寄存器的名字都被保存了下來。

EAX

EBX

ECX

EDX

EDI

ESI

EBP

ESP

上面這8個寄存器之中,前面七個都是通用的。ESP 寄存器有特定用途,保存當前 Stack 的地址(詳見下一節)。

我們常常看到 32位 CPU、64位 CPU 這樣的名稱,其實指的就是寄存器的大小。32 位 CPU 的寄存器大小就是4個位元組。

五、內存模型:Heap

寄存器只能存放很少量的數據,大多數時候,CPU 要指揮寄存器,直接跟內存交換數據。所以,除了寄存器,還必須了解內存怎麼儲存數據。

程序運行的時候,操作系統會給它分配一段內存,用來儲存程序和運行產生的數據。這段內存有起始地址和結束地址,比如從0x1000到0x8000,起始地址是較小的那個地址,結束地址是較大的那個地址。

程序運行過程中,對於動態的內存佔用請求(比如新建對象,或者使用malloc命令),系統就會從預先分配好的那段內存之中,划出一部分給用戶,具體規則是從起始地址開始劃分(實際上,起始地址會有一段靜態數據,這裡忽略)。舉例來說,用戶要求得到10個位元組內存,那麼從起始地址0x1000開始給他分配,一直分配到地址0x100A,如果再要求得到22個位元組,那麼就分配到0x1020。

這種因為用戶主動請求而劃分出來的內存區域,叫做 Heap(堆)。它由起始地址開始,從低位(地址)向高位(地址)增長。Heap 的一個重要特點就是不會自動消失,必須手動釋放,或者由垃圾回收機制來回收。

六、內存模型:Stack

除了 Heap 以外,其他的內存佔用叫做 Stack(棧)。簡單說,Stack 是由於函數運行而臨時佔用的內存區域。

請看下面的例子。

上面代碼中,系統開始執行main函數時,會為它在內存裡面建立一個幀(frame),所有main的內部變數(比如a和b)都保存在這個幀裡面。main函數執行結束後,該幀就會被回收,釋放所有的內部變數,不再佔用空間。

如果函數內部調用了其他函數,會發生什麼情況?

上面代碼中,main函數內部調用了add_a_and_b函數。執行到這一行的時候,系統也會為add_a_and_b新建一個幀,用來儲存它的內部變數。也就是說,此時同時存在兩個幀:main和add_a_and_b。一般來說,調用棧有多少層,就有多少幀。

等到add_a_and_b運行結束,它的幀就會被回收,系統會回到函數main剛才中斷執行的地方,繼續往下執行。通過這種機制,就實現了函數的層層調用,並且每一層都能使用自己的本地變數。

所有的幀都存放在 Stack,由於幀是一層層疊加的,所以 Stack 叫做棧。生成新的幀,叫做"入棧",英文是 push;棧的回收叫做"出棧",英文是 pop。Stack 的特點就是,最晚入棧的幀最早出棧(因為最內層的函數調用,最先結束運行),這就叫做"後進先出"的數據結構。每一次函數執行結束,就自動釋放一個幀,所有函數執行結束,整個 Stack 就都釋放了。

Stack 是由內存區域的結束地址開始,從高位(地址)向低位(地址)分配。比如,內存區域的結束地址是0x8000,第一幀假定是16位元組,那麼下一次分配的地址就會從0x7FF0開始;第二幀假定需要64位元組,那麼地址就會移動到0x7FB0。

七、CPU 指令

7.1 一個實例

了解寄存器和內存模型以後,就可以來看彙編語言到底是什麼了。下面是一個簡單的程序example.c。

gcc 將這個程序轉成彙編語言。

上面的命令執行以後,會生成一個文本文件example.s,裡面就是彙編語言,包含了幾十行指令。這麼說吧,一個高級語言的簡單操作,底層可能由幾個,甚至幾十個 CPU 指令構成。CPU 依次執行這些指令,完成這一步操作。

example.s經過簡化以後,大概是下面的樣子。

可以看到,原程序的兩個函數add_a_and_b和main,對應兩個標籤_add_a_and_b和_main。每個標籤裡面是該函數所轉成的 CPU 運行流程。

每一行就是 CPU 執行的一次操作。它又分成兩部分,就以其中一行為例。

這一行裡面,push是 CPU 指令,%ebx是該指令要用到的運運算元。一個 CPU 指令可以有零個到多個運運算元。

下面我就一行一行講解這個彙編程序,建議讀者最好把這個程序,在另一個窗口拷貝一份,省得閱讀的時候再把頁面滾動上來。

7.2 push 指令

根據約定,程序從_main標籤開始執行,這時會在 Stack 上為main建立一個幀,並將 Stack 所指向的地址,寫入 ESP 寄存器。後面如果有數據要寫入main這個幀,就會寫在 ESP 寄存器所保存的地址。

然後,開始執行第一行代碼。

push指令用於將運運算元放入 Stack,這裡就是將3寫入main這個幀。

雖然看上去很簡單,push指令其實有一個前置操作。它會先取出 ESP 寄存器裡面的地址,將其減去4個位元組,然後將新地址寫入 ESP 寄存器。使用減法是因為 Stack 從高位向低位發展,4個位元組則是因為3的類型是int,佔用4個位元組。得到新地址以後, 3 就會寫入這個地址開始的四個位元組。

第二行也是一樣,push指令將2寫入main這個幀,位置緊貼著前面寫入的3。這時,ESP 寄存器會再減去 4個位元組(累計減去8)。

7.3 call 指令

第三行的call指令用來調用函數。

上面的代碼表示調用add_a_and_b函數。這時,程序就會去找_add_a_and_b標籤,並為該函數建立一個新的幀。

下面就開始執行_add_a_and_b的代碼。

這一行表示將 EBX 寄存器裡面的值,寫入_add_a_and_b這個幀。這是因為後面要用到這個寄存器,就先把裡面的值取出來,用完後再寫回去。

這時,push指令會再將 ESP 寄存器裡面的地址減去4個位元組(累計減去12)。

7.4 mov 指令

mov指令用於將一個值寫入某個寄存器。

這一行代碼表示,先將 ESP 寄存器裡面的地址加上8個位元組,得到一個新的地址,然後按照這個地址在 Stack 取出數據。根據前面的步驟,可以推算出這裡取出的是2,再將2寫入 EAX 寄存器。

下一行代碼也是干同樣的事情。

上面的代碼將 ESP 寄存器的值加12個位元組,再按照這個地址在 Stack 取出數據,這次取出的是3,將其寫入 EBX 寄存器。

7.5 add 指令

add指令用於將兩個運運算元相加,並將結果寫入第一個運運算元。

上面的代碼將 EAX 寄存器的值(即2)加上 EBX 寄存器的值(即3),得到結果5,再將這個結果寫入第一個運運算元 EAX 寄存器。

7.6 pop 指令

pop指令用於取出 Stack 最近一個寫入的值(即最低位地址的值),並將這個值寫入運運算元指定的位置。

上面的代碼表示,取出 Stack 最近寫入的值(即 EBX 寄存器的原始值),再將這個值寫回 EBX 寄存器(因為加法已經做完了,EBX 寄存器用不到了)。

注意,pop指令還會將 ESP 寄存器裡面的地址加4,即回收4個位元組。

7.7 ret 指令

ret指令用於終止當前函數的執行,將運行權交還給上層函數。也就是,當前函數的幀將被回收。

可以看到,該指令沒有運運算元。

隨著add_a_and_b函數終止執行,系統就回到剛才main函數中斷的地方,繼續往下執行。

上面的代碼表示,將 ESP 寄存器裡面的地址,手動加上8個位元組,再寫回 ESP 寄存器。這是因為 ESP 寄存器的是 Stack 的寫入開始地址,前面的pop操作已經回收了4個位元組,這裡再回收8個位元組,等於全部回收。

最後,main函數運行結束,ret指令退出程序執行。

八、參考鏈接

http://kakaroto.homelinux.net/2017/11/introduction-to-reverse-engineering-and-assembly/

http://www.cs.virginia.edu/~evans/cs216/guides/x86.html

文章版權歸原作者所有,轉載僅供學習使用,不用於任何商業用途,如有侵權請留言聯繫刪除,感謝合作。

-----這裡是數學思維的聚集地------

超級數學建模」(微信號supermodeling),每天學一點小知識,輕鬆了解各種思維,做個好玩的理性派。50萬數學精英都在關注!


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 超級數學建模 的精彩文章:

21歲就破解困擾人們300年難題的天才,卻一生坎坷,懷才不遇,至死還得不到認可
這樣的數學家,中國有一千個就了不得

TAG:超級數學建模 |