文章也同時在個人博客 http://kimihe.com/更新
引言
本文亦是《讀筆 匯編語言-基于Linux環(huán)境(第7章-跟蹤指令:與機器指令親密接觸I)》。
本文將會以一個簡單的.ASM程序,step by step地幫助大家快速入門GDB,并通過GDB調(diào)試,深入底層闡述高級語言(如C語言)中循環(huán)結(jié)構(gòu)和指針的由來。
通過閱讀本文,你將知道:
- 如何快速在Linux下進(jìn)行匯編開發(fā)。
- 如何快速入門GDB。
- 高級語言循環(huán)結(jié)構(gòu)的原理。
- 指針到底是什么。
構(gòu)建匯編程序
原料
- Linux環(huán)境,筆者是Ubuntu 12.04 LTS。
- 安裝NASM: 新立得軟件包管理器。
- 安裝Kate編輯器和KWrite編輯器: 新立得軟件包管理器。
- 安裝konsole:
> sudo apt-get install konsole
第一個匯編程序
切到你喜歡的工作目錄下,執(zhí)行> kate以啟動Kate編輯器,啟動后界面類似于這樣:

左側(cè)是導(dǎo)航欄,右側(cè)是代碼編輯區(qū),下方是終端控制區(qū)(若要啟用此特性請務(wù)必先安裝konsle)。
新建一個文件,命名為sandbox.asm,在其中輸入如下內(nèi)容:
section .data
Snippet db "KANGAROO"
section .text
global _start
_start:
nop
; Put your experiments between the two nops...
mov ebx, Snippet
mov eax, 8
DoMore: add byte [ebx], 32
inc ebx
dec eax
jnz DoMore
; Put your experiments between the two nops...
nop
這段代碼是我們第一個匯編小例子,用于闡明循環(huán)結(jié)構(gòu)的原理,請確保文章例子和你的完全一致。
循環(huán)結(jié)構(gòu)的原理
如果是首次接觸匯編,你可能會一頭霧水,在這里你不必在意匯編的語法,只需要理解我對代碼的說明即可。
此處請先注意語句Snippet db "KANGAROO",其中Snippet代表一個字符串,內(nèi)容為KANGAROO。然后注意語句mov ebx, Snippet,這一步相當(dāng)于獲取字符串的首地址。緊接著的mov eax, 8用于獲知字符串的長度。這兩步很平常,高級語言的字符串處理也需要獲知字符串地址以及相應(yīng)的長度。
然后請關(guān)注如下四條語句:
DoMore: add byte [ebx], 32
inc ebx
dec eax
jnz DoMore
此處的jnz DoMore語句便是循環(huán)結(jié)構(gòu)的核心。其含義是:jnz(Jump if Not Z-Flag)進(jìn)行判斷,如果零標(biāo)志位ZF不為0,就跳轉(zhuǎn)到DoMore語句處。
于是你可以想到,只要這個ZF標(biāo)志位不是0,程序就會不停地循環(huán)跳轉(zhuǎn)(loop),循環(huán)結(jié)構(gòu)由此而來。
你可能會想問:什么時候ZF會變成0?這個問題很好,試想一下高級語言的while(n)循環(huán),我們必然需要一個操作步驟來改變n的值,使其在某一時刻變成0,從而跳出while。
此處,眼尖的讀者可能發(fā)現(xiàn)了dec指令,還記得一開始的獲取字符串長度為8嗎?我們把8存在了eax寄存器中(如果你不清楚寄存器是什么,也沒有關(guān)系,把它想象成一個可以存放數(shù)值容器即可)。通過dec eax指令,我們會不斷地對eax中的8進(jìn)行遞減,類似于int eax = 8; eax--;總有一天,eax中的值會從8減到0,此時我們的x86 Intel CPU就會執(zhí)行一項既定的操作,把ZF標(biāo)志設(shè)為1,以代表此標(biāo)志位處于激活狀態(tài)。于是,jnz在判斷的時候就發(fā)現(xiàn)ZF已經(jīng)被激活為1了,不需要再跳轉(zhuǎn),循環(huán)結(jié)果宣告結(jié)束。
此外,不知道你有沒有對于jnz跳轉(zhuǎn)指令產(chǎn)生一些聯(lián)想:它是不是很像函數(shù)指針?(jnz到一個地方,那個地方叫做DoMore,然后執(zhí)行一段過程。)當(dāng)然關(guān)于函數(shù)指針詳細(xì)的說明,本文篇幅可就不夠了,筆者會考慮以后單獨寫一篇文章詳細(xì)說明,敬請期待~
什么是指針
有讀者可能會問,還有兩行代碼沒有解釋呢。不要著急,這兩行代碼蘊含著指針的奧秘。聽起來可能有點令人驚奇,但實際情況確實如此。讓我們來看一下這兩行代碼:
DoMore: add byte [ebx], 32
inc ebx
注意add byte [ebx], 32這句話,它的專業(yè)術(shù)語叫做寄存器間接尋址。它是如此神奇,毫不夸張地說,如果沒有它,我們?nèi)粘K姷慕^大部分程序?qū)㈦y以構(gòu)建。
這句話解釋一下就是這樣:有一個內(nèi)存單元,它有一個byte大小的空間,里面存有一個數(shù)值n(具體是多少,現(xiàn)在不用關(guān)心)。把數(shù)值32 Add到這個n上,就是相當(dāng)于n+=32。然后關(guān)鍵點來了,為了加上32,我們需要知道這個內(nèi)存區(qū)域在哪兒。在哪兒呢?在ebx里存著呢!
內(nèi)存就像一個個信箱,每個信箱都有自己的編號,當(dāng)我們尋找自家的信箱時,會根據(jù)信箱的編號去尋找它。這里ebx就存著我們要的內(nèi)存區(qū)域的編號,這個編號叫做地址,根據(jù)這個地址,我們找到了那個內(nèi)存單元的具體位置,然后知道了其中存了一個數(shù)n,最后把32給加到了n上。
這里,你應(yīng)該可以看到,我們并不是直接去訪問那個數(shù)值n的,而是先去找存放它的內(nèi)存單元。這里面存在一層間接。正是有了這層間接,我們才能在高級語言中構(gòu)筑起各種華麗的調(diào)用操作。
于是指針的原理也顯而易見了,對于
char arr[4] = "abcd";
char *p = arr;
p+=3;
printf("*p: %c\\n", *p);
我們char *p = arr;操作定位的arr數(shù)組的首地址。arr信箱有四個格子,我們定位到第一個,然后p+=3;并不是直接給信箱什么的加3,這明顯不符合邏輯,而是操作信箱的編號(地址)。加3意味著往后數(shù)三個,定位到第四個格子,最后打印里面的東西,就是字符d。
內(nèi)存中的數(shù)據(jù)有兩種,分別是數(shù)據(jù)和地址,數(shù)據(jù)就是普通的變量,地址就是指針。希望你不要混淆。
使用GDB
下面進(jìn)入最后一個知識點,快速入門GDB。在此之前,我們需要把編寫的.ASM程序編譯鏈接運行起來。你可能聽說過Linux下的make工具,說白了就是個配置文件,告訴NASM,gcc等編譯器怎么有效地編譯我們的源碼,避免重復(fù)勞動。make配合makefile文件工作,如果你不知道這到底是什么,也完全沒有關(guān)系,畢竟這不是本文的重點,只是順帶提一下。
你可以在Kate編輯器中再新建一個文本,名為makefile,請確保它和我們的sandbox.asm在同一個目錄下。向其中輸入如下內(nèi)容:
sandbox: sandbox.o
ld -o sandbox sandbox.o
sandbox.o: sandbox.asm
nasm -f elf64 -g -F stabs sandbox.asm -l sandbox.lst
你可以完全不必理會這四句話到底代表了什么,只需要明白它們會讓NASM正確地生成我們的.ASM程序。
有了這個makefile,接下來可以在Kate編輯器下方的terminal中輸入> make -k,或者你自己啟動shell,切到你的工作目錄,執(zhí)行上述命令。如果正確編譯完成,那么看起來就像這樣子:

接下來,我們要使用GDB了,在Terminal中鍵入:> gdb sandbox以啟用gdb調(diào)試。
調(diào)試,我們一般都會需要設(shè)置斷點,來看看各變量的情況。這里我們已經(jīng)更加深入到底層,不在內(nèi)存中操作了,直接來到了CPU內(nèi)部的寄存器中。鍵入:> b 10即在DoMore: add byte [ebx], 32語句處加入斷點。
然后,鍵入:> r然程序開始運行。程序會停在DoMore語句那里,看起來就像這樣:

接著,鍵入:i r查看個寄存器狀態(tài),就像這樣:

你可以看到高亮的綠色部分,rax中存有字符串長度8,rbx中存有字符串地址。
啥?為什么不是eax和ebx?嗯,很有價值的問題,eax和ebx是32位CPU架構(gòu)下的寄存器,而如今64位已經(jīng)普及,我們的寄存器也隨之升級了。
然后按一下Enter鍵,或者輸入return,可以看到下一頁未顯示完全的一些寄存器:

注意到綠色高亮部分的eflags標(biāo)志位,我們發(fā)現(xiàn)其中除了IF什么都沒有,這表明我們上文提到的ZF標(biāo)志還沒有被激活。
接下來,鍵入:s,它代表單步執(zhí)行一行語句,請先執(zhí)行一次,然后再鍵入:i r看一下結(jié)果寄存器狀態(tài):

可以看到rax寄存器內(nèi)部的值從8減到7,表明執(zhí)行了一次循環(huán)中的dec指令。接下來你可以繼續(xù)單步執(zhí)行7次,即鍵入7次s。每一次都查看一下寄存器的狀態(tài),你會發(fā)現(xiàn)rax不斷遞減,直到0。7次單步之后,再次鍵入i r進(jìn)行查看:

你會發(fā)現(xiàn)rax變成0了,此時Enter到下一頁,我們發(fā)現(xiàn):

沒錯!eflags中出現(xiàn)了ZF標(biāo)志,表明其被激活,這樣jnz就不會再跳到DoMore,循環(huán)終于結(jié)束了。
最后請鍵入q,然后y退出GDB。我們的GDB快速入門到此告一段落。
留一個小問題
看到這兒,相信你已經(jīng)大概理解了循環(huán)結(jié)構(gòu)和指針的原理,對匯編工具以及GDB的使用也略知一二。那么我在這里提一個小問題:這段匯編代碼到底是做什么的?請你積極思考哦~
答案我會在留言中說明。
總結(jié)
本篇文章通過Linux下的一個最簡易的匯編開發(fā)流程,帶領(lǐng)大家熟悉了開發(fā)工具的使用,并入門了GDB這一神器。同時通過閱讀匯編代碼,從底層理解了循環(huán)結(jié)構(gòu)和指針的原理。希望對大家有所啟迪,感謝閱讀!