以前的系列文章從 CPU 和內存方面簡單介紹了一下彙編語言,可是尚未系統的瞭解一下彙編語言,彙編語言做爲第二代計算機語言,會用一些容易理解和記憶的字母,單詞來代替一個特定的指令,做爲高級編程語言的基礎,有必要系統的瞭解一下彙編語言,那麼本篇文章但願你們跟我一塊兒來了解一下彙編語言。html
咱們在以前的文章中探討過,計算機 CPU 只能運行本地代碼(機器語言)程序,用 C 語言等高級語言編寫的代碼,須要通過編譯器編譯後,轉換爲本地代碼纔可以被 CPU 解釋執行。程序員
可是本地代碼的可讀性很是差,因此須要使用一種可以直接讀懂的語言來替換本地代碼,那就是在各本地代碼中,附帶上表示其功能的英文縮寫,好比在加法運算的本地代碼加上add(addition)
的縮寫、在比較運算符的本地代碼中加上cmp(compare)
的縮寫等,這些經過縮寫來表示具體本地代碼指令的標誌稱爲 助記符
,使用助記符的語言稱爲彙編語言
。這樣,經過閱讀彙編語言,也可以瞭解本地代碼的含義了。編程
不過,即便是使用匯編語言編寫的源代碼,最終也必需要轉換爲本地代碼纔可以運行,負責作這項工做的程序稱爲編譯器
,轉換的這個過程稱爲彙編
。在將源代碼轉換爲本地代碼這個功能方面,彙編器和編譯器是一樣的。編程語言
用匯編語言編寫的源代碼和本地代碼是一一對應的。於是,本地代碼也能夠反過來轉換成彙編語言編寫的代碼。把本地代碼轉換爲彙編代碼的這一過程稱爲反彙編
,執行反彙編的程序稱爲反彙編程序
。編輯器
哪怕是 C 語言編寫的源代碼,編譯後也會轉換成特定 CPU 用的本地代碼。而將其反彙編的話,就能夠獲得彙編語言的源代碼,並對其內容進行調查。不過,本地代碼變成 C 語言源代碼的反編譯,要比本地代碼轉換成彙編代碼的反彙編要困難,這是由於,C 語言代碼和本地代碼不是一一對應的關係。函數
咱們上面提到本地代碼能夠通過反彙編轉換成爲彙編代碼,可是隻有這一種轉換方式嗎?顯然不是,C 語言編寫的源代碼也可以經過編譯器編譯稱爲彙編代碼,下面就來嘗試一下。優化
首先須要先作一些準備,須要先下載 Borland C++ 5.5
編譯器,爲了方便,我這邊直接下載好了讀者直接從個人百度網盤提取便可 (連接:https://pan.baidu.com/s/19LqVICpn5GcV88thD2AnlA 密碼:hz1u)操作系統
下載完畢,須要進行配置,下面是配置說明 (https://wenku.baidu.com/view/22e2f418650e52ea551898ad.html),教程很完整跟着配置就能夠,下面開始咱們的編譯過程debug
首先用 Windows 記事本等文本編輯器編寫以下代碼3d
// 返回兩個參數值之和的函數 int AddNum(int a,int b){ return a + b; } // 調用 AddNum 函數的函數 void MyFunc(){ int c; c = AddNum(123,456); }
編寫完成後將其文件名保存爲 Sample4.c ,C 語言源文件的擴展名,一般用.c
來表示,上面程序是提供兩個輸入參數並返回它們之和。
在 Windows 操做系統下打開 命令提示符
,切換到保存 Sample4.c 的文件夾下,而後在命令提示符中輸入
bcc32 -c -S Sample4.c
bcc32 是啓動 Borland C++ 的命令,-c
的選項是指僅進行編譯而不進行連接,-S
選項被用來指定生成彙編語言的源代碼
做爲編譯的結果,當前目錄下會生成一個名爲Sample4.asm
的彙編語言源代碼。彙編語言源文件的擴展名,一般用.asm
來表示,下面就讓咱們用編輯器打開看一下 Sample4.asm 中的內容
.386p ifdef ??version if ??version GT 500H .mmx endif endif model flat ifndef ??version ?debug macro endm endif ?debug S "Sample4.c" ?debug T "Sample4.c" _TEXT segment dword public use32 'CODE' _TEXT ends _DATA segment dword public use32 'DATA' _DATA ends _BSS segment dword public use32 'BSS' _BSS ends DGROUP group _BSS,_DATA _TEXT segment dword public use32 'CODE' _AddNum proc near ?live1@0: ; ; int AddNum(int a,int b){ ; push ebp mov ebp,esp ; ; ; return a + b; ; @1: mov eax,dword ptr [ebp+8] add eax,dword ptr [ebp+12] ; ; } ; @3: @2: pop ebp ret _AddNum endp _MyFunc proc near ?live1@48: ; ; void MyFunc(){ ; push ebp mov ebp,esp ; ; int c; ; c = AddNum(123,456); ; @4: push 456 push 123 call _AddNum add esp,8 ; ; } ; @5: pop ebp ret _MyFunc endp _TEXT ends public _AddNum public _MyFunc ?debug D "Sample4.c" 20343 45835 end
這樣,編譯器就成功的把 C 語言轉換成爲了彙編代碼了。
第一次看到彙編代碼的讀者可能感受起來比較難,不過實際上其實比較簡單,並且可能比 C 語言還要簡單,爲了便於閱讀彙編代碼的源代碼,須要注意幾個要點
彙編語言的源代碼,是由轉換成本地代碼的指令(後面講述的操做碼)和針對彙編器的僞指令構成的。僞指令負責把程序的構造以及彙編的方法指示給彙編器(轉換程序)。不過僞指令是沒法彙編轉換成爲本地代碼的。下面是上面程序截取的僞指令
_TEXT segment dword public use32 'CODE' _TEXT ends _DATA segment dword public use32 'DATA' _DATA ends _BSS segment dword public use32 'BSS' _BSS ends DGROUP group _BSS,_DATA _AddNum proc near _AddNum endp _MyFunc proc near _MyFunc endp _TEXT ends end
由僞指令 segment
和 ends
圍起來的部分,是給構成程序的命令和數據的集合體上加一個名字而獲得的,稱爲段定義
。段定義的英文表達具備區域
的意思,在這個程序中,段定義指的是命令和數據等程序的集合體的意思,一個程序由多個段定義構成。
上面代碼的開始位置,定義了3個名稱分別爲 _TEXT、_DATA、_BSS
的段定義,_TEXT
是指定的段定義,_DATA
是被初始化(有初始值)的數據的段定義,_BSS
是還沒有初始化的數據的段定義。這種定義的名稱是由 Borland C++ 定義的,是由 Borland C++ 編譯器自動分配的,因此程序段定義的順序就成爲了 _TEXT、_DATA、_BSS
,這樣也確保了內存的連續性
_TEXT segment dword public use32 'CODE' _TEXT ends _DATA segment dword public use32 'DATA' _DATA ends _BSS segment dword public use32 'BSS' _BSS ends
段定義( segment ) 是用來區分或者劃分範圍區域的意思。彙編語言的 segment 僞指令表示段定義的起始,ends 僞指令表示段定義的結束。段定義是一段連續的內存空間
而group
這個僞指令表示的是將 _BSS和_DATA
這兩個段定義彙總名爲 DGROUP 的組
DGROUP group _BSS,_DATA
圍起 _AddNum
和 _MyFun
的 _TEXT
segment 和 _TEXT
ends ,表示_AddNum
和 _MyFun
是屬於 _TEXT
這一段定義的。
_TEXT segment dword public use32 'CODE' _TEXT ends
所以,即便在源代碼中指令和數據是混雜編寫的,通過編譯和彙編後,也會轉換成爲規整的本地代碼。
_AddNum proc
和 _AddNum endp
圍起來的部分,以及_MyFunc proc
和 _MyFunc endp
圍起來的部分,分別表示 AddNum 函數和 MyFunc 函數的範圍。
_AddNum proc near _AddNum endp _MyFunc proc near _MyFunc endp
編譯後在函數名前附帶上下劃線_
,是 Borland C++ 的規定。在 C 語言中編寫的 AddNum 函數,在內部是以 _AddNum 這個名稱處理的。僞指令 proc 和 endp 圍起來的部分,表示的是 過程(procedure)
的範圍。在彙編語言中,這種至關於 C 語言的函數的形式稱爲過程。
末尾的 end
僞指令,表示的是源代碼的結束。
## 彙編語言的語法是 操做碼 + 操做數
在彙編語言中,一行表示一對 CPU 的一個指令。彙編語言指令的語法結構是 操做碼 + 操做數,也存在只有操做碼沒有操做數的指令。
操做碼錶示的是指令動做,操做數表示的是指令對象。操做碼和操做數一塊兒使用就是一個英文指令。好比從英語語法來分析的話,操做碼是動詞,操做數是賓語。好比這個句子 Give me money
這個英文指令的話,Give 就是操做碼,me 和 money 就是操做數。彙編語言中存在多個操做數的狀況,要用逗號把它們分割,就像是 Give me,money 這樣。
可以使用何種形式的操做碼,是由 CPU 的種類決定的,下面對操做碼的功能進行了整理。
本地代碼須要加載到內存後才能運行,內存中存儲着構成本地代碼的指令和數據。程序運行時,CPU會從內存中把數據和指令讀出來,而後放在 CPU 內部的寄存器中進行處理。
若是 CPU 和內存的關係你還不是很瞭解的話,請閱讀做者的另外一篇文章 程序員須要瞭解的硬核知識之CPU 詳細瞭解。
寄存器是 CPU 中的存儲區域,寄存器除了具備臨時存儲和計算的功能以外,還具備運算功能,x86 系列的主要種類和角色以下圖所示
下面就對 CPU 中的指令進行分析
最經常使用的 mov 指令
指令中最常使用的是對寄存器和內存進行數據存儲的 mov
指令,mov 指令的兩個操做數,分別用來指定數據的存儲地和讀出源。操做數中能夠指定寄存器、常數、標籤(附加在地址前),以及用方括號([])
圍起來的這些內容。若是指定了沒有用([])
方括號圍起來的內容,就表示對該值進行處理;若是指定了用方括號圍起來的內容,方括號的值則會被解釋爲內存地址,而後就會對該內存地址對應的值進行讀寫操做。讓咱們對上面的代碼片斷進行說明
mov ebp,esp mov eax,dword ptr [ebp+8]
mov ebp,esp 中,esp 寄存器中的值被直接存儲在了 ebp 中,也就是說,若是 esp 寄存器的值是100的話那麼 ebp 寄存器的值也是 100。
而在 mov eax,dword ptr [ebp+8]
這條指令中,ebp 寄存器的值 + 8 後會被解析稱爲內存地址。若是 ebp
寄存器的值是100的話,那麼 eax 寄存器的值就是 100 + 8 的地址的值。dword ptr
也叫作 double word pointer
簡單解釋一下就是從指定的內存地址中讀出4字節的數據
對棧進行 push 和 pop
程序運行時,會在內存上申請分配一個稱爲棧的數據空間。棧(stack)的特性是後入先出,數據在存儲時是從內存的下層(大的地址編號)逐漸往上層(小的地址編號)累積,讀出時則是按照從上往下進行讀取的。
棧是存儲臨時數據的區域,它的特色是經過 push 指令和 pop 指令進行數據的存儲和讀出。向棧中存儲數據稱爲 入棧
,從棧中讀出數據稱爲 出棧
,32位 x86 系列的 CPU 中,進行1次 push 或者 pop,便可處理 32 位(4字節)的數據。
下面咱們一塊兒來分析一下函數的調用機制,咱們以上面的 C 語言編寫的代碼爲例。首先,讓咱們從MyFunc
函數調用AddNum
函數的彙編語言部分開始,來對函數的調用機制進行說明。棧在函數的調用中發揮了巨大的做用,下面是通過處理後的 MyFunc 函數的彙編處理內容
_MyFunc proc near push ebp ; 將 ebp 寄存器的值存入棧中 (1) mov ebp,esp ; 將 esp 寄存器的值存入 ebp 寄存器中 (2) push 456 ; 將 456 入棧 (3) push 123 ; 將 123 入棧 (4) call _AddNum ; 調用 AddNum 函數 (5) add esp,8 ; esp 寄存器的值 + 8 (6) pop ebp ; 讀出棧中的數值存入 esp 寄存器中 (7) ret ; 結束 MyFunc 函數,返回到調用源 (8) _MyFunc endp
代碼解釋中的(1)、(2)、(7)、(8)的處理適用於 C 語言中的全部函數,咱們會在後面展現 AddNum
函數處理內容時進行說明。這裏但願你們先關注(3) - (6) 這一部分,這對了解函數調用機制相當重要。
(3) 和 (4) 表示的是將傳遞給 AddNum 函數的參數經過 push 入棧。在 C 語言源代碼中,雖然記述爲函數 AddNum(123,456),但入棧時則會先按照 456,123 這樣的順序。也就是位於後面的數值先入棧。這是 C 語言的規定。(5) 表示的 call 指令,會把程序流程跳轉到 AddNum 函數指令的地址處。在彙編語言中,函數名
表示的就是函數所在的內存地址。AddNum 函數處理完畢後,程序流程必需要返回到編號(6) 這一行。call 指令運行後,call 指令的下一行(也就指的是 (6) 這一行)的內存地址(調用函數完畢後要返回的內存地址)會自動的 push 入棧。該值會在 AddNum 函數處理的最後經過 ret
指令 pop 出棧,而後程序會返回到 (6) 這一行。
(6) 部分會把棧中存儲的兩個參數 (456 和 123) 進行銷燬處理。雖然經過兩次的 pop 指令也能夠實現,不過採用 esp 寄存器 + 8 的方式會更有效率(處理 1 次便可)。對棧進行數值的輸入和輸出時,數值的單位是4字節。所以,經過在負責棧地址管理的 esp 寄存器中加上4的2倍8,就能夠達到和運行兩次 pop 命令一樣的效果。雖然內存中的數據實際上還殘留着,但只要把 esp 寄存器的值更新爲數據存儲地址前面的數據位置,該數據也就至關於銷燬了。
我在編譯 Sample4.c
文件時,出現了下圖的這條消息
圖中的意思是指 c 的值在 MyFunc 定義了可是一直未被使用,這實際上是一項編譯器優化的功能,因爲存儲着 AddNum 函數返回值的變量 c 在後面沒有被用到,所以編譯器就認爲 該變量沒有意義,進而也就沒有生成與之對應的彙編語言代碼。
下圖是調用 AddNum 這一函數先後棧內存的變化
上面咱們用匯編代碼分析了一下 Sample4.c 整個過程的代碼,如今咱們着重分析一下 AddNum 函數的源代碼部分,分析一下參數的接收、返回值和返回等機制
_AddNum proc near push ebp -----------(1) mov ebp,esp -----------(2) mov eax,dword ptr[ebp+8] -----------(3) add eax,dword ptr[ebp+12] -----------(4) pop ebp -----------(5) ret ----------------------------------(6) _AddNum endp
ebp 寄存器的值在(1)中入棧,在(5)中出棧,這主要是爲了把函數中用到的 ebp 寄存器的內容,恢復到函數調用前的狀態。
(2) 中把負責管理棧地址的 esp 寄存器的值賦值到了 ebp 寄存器中。這是由於,在 mov 指令中方括號內的參數,是不容許指定 esp 寄存器的。所以,這裏就採用了不直接經過 esp,而是用 ebp 寄存器來讀寫棧內容的方法。
(3) 使用[ebp + 8] 指定棧中存儲的第1個參數123,並將其讀出到 eax 寄存器中。像這樣,不使用 pop 指令,也能夠參照棧的內容。而之因此從多個寄存器中選擇了 eax 寄存器,是由於 eax 是負責運算的累加寄存器。
經過(4) 的 add 指令,把當前 eax 寄存器的值同第2個參數相加後的結果存儲在 eax 寄存器中。[ebp + 12] 是用來指定第2個參數456的。在 C 語言中,函數的返回值必須經過 eax 寄存器返回,這也是規定。也就是 函數的參數是經過棧來傳遞,返回值是經過寄存器返回的。
(6) 中 ret 指令運行後,函數返回目的地內存地址會自動出棧
,據此,程序流程就會跳轉返回到(6) (Call _AddNum)
的下一行。這時,AddNum 函數入口和出口處棧的狀態變化,就以下圖所示
這是程序員須要瞭解的硬核知識之彙編語言(一) 第一篇文章,下一篇文章咱們會着重討論局部變量和全局變量以及循環控制語句的彙編語言,防止斷更,請關注我