來看三段程序,你學(xué)會了什么?
學(xué)習(xí)任何一門語言都不能少的了 debug ,匯編也是。
debug 程序執(zhí)行過程
下面我們就依據(jù)這幾個功能來跟蹤一下程序的執(zhí)行過程。
debug 對我們來說非常重要,有很多代碼細(xì)節(jié)和問題通過肉眼是觀察出來的,我們?nèi)庋劭赡苣軌蚺袛嘁恍┖唵蔚某绦騿栴},但是對于很多隱藏較深的問題,還是要依據(jù) debug 才能發(fā)現(xiàn)。
下面是一段匯編代碼,這段匯編代碼我之前的文章中也給大家寫過。
新建文本文件,把代碼 cv 過去,然后右鍵保存,使用 dosbox 將其編譯為 1.obj 文件,鏈接為 1.exe 文件后,我們使用 ??debug 1.exe?
? 命令來分析一下這段程序,并用 -r 命令來看一下初始的寄存器情況。
程序初始狀態(tài)下,可以看到 CX 中的數(shù)據(jù)為 000F,這也表示著程序的長度是 000F,1.exe 中共有 15 個字節(jié),CX 中的內(nèi)容為 000FH。
好,現(xiàn)在我們已經(jīng)知道程序被成功的載入內(nèi)存并運(yùn)行起來了,但是我們現(xiàn)在先不妨想一下,被鏈接成為 EXE 的程序會被裝入內(nèi)存的哪個地方的呢?我們怎么知道程序被裝入在哪里呢?
程序裝載的過程分下面幾步:
- 首先程序會從內(nèi)存中找到一塊區(qū)域,記為初始地址 SA,此時的偏移地址為 0 的這樣一塊足夠容量的內(nèi)存區(qū)域。
- 在這段區(qū)域內(nèi)的頭 256 個字節(jié)中,會創(chuàng)建一塊稱為程序段前綴(Program Segment Prefix ,PSP)的區(qū)域,這塊區(qū)域被 DOS 用來和被加載的程序進(jìn)行通信。
- 從這塊程序的 256 個字節(jié)開始處,也就是在 PSP 程序段前綴的后面,程序會被加載到這里,此時程序的初始地址是 SA + 10H,偏移地址為 0 。也就是 SA + 10H : 0,所以程序的初始地址就是 CS = 076AH ,IP = 0000H。
程序被裝入內(nèi)存后,由 DS 段寄存器存放著內(nèi)存區(qū)的段地址,此時內(nèi)存區(qū)域的偏移量為 0 ,所以此時的物理地址為 SA * 16:0,我們并不用知道真實的 DS 是多少,反正都是由操作系統(tǒng)和 DOS 分配的。
然后這個內(nèi)存區(qū)域的前 256 個字節(jié)被用于存放 PSP ,所以程序的物理地址為 SA * 16 + 256 : 0 。
SA * 16 + 256 = SA * 16 + 16 * 16 = (SA + 16) * 16 ,轉(zhuǎn)換為 16 進(jìn)制就是 SA + 10H,所以物理地址就是 SA + 10H : 0。
我們上面 debug 1.exe 之后可以看到,DS 段寄存器的值為 076AH ,而 CS 段寄存器的值為 076BH ,正好符合 076A * 16 + 10 = 076BH (注意這里的 * 16 就是左移 4 位的意思,之前文章中也解釋過原因。)
我們使用 -u 指令可以看到完整的匯編源代碼。
上圖中用紅框圈出來的就是我們這段匯編程序的源代碼,可以看到這是一個程序段,程序段的段地址始終為 076A,偏移地址在不斷變化。
我們使用 -t 命令來單步執(zhí)行以下這段程序,如下圖所示。
(為了連續(xù)的觀察一下程序的執(zhí)行結(jié)果,我索性直接把主要的程序步驟執(zhí)行完了。)
這段程序就是 mov 和 add 的基本使用,將 0123 送入 AX 寄存器,將 0456 送入 BX 寄存器,對 AX 寄存器執(zhí)行 AX = AX + BX ,再對 AX 執(zhí)行 AX = AX + AX。
程序繼續(xù)向下執(zhí)行,當(dāng)執(zhí)行到 int 21H 處,程序執(zhí)行完畢,此時要使用 -p 命令結(jié)束程序的執(zhí)行,如下圖所示。
當(dāng)顯示 Program terminated normally 時,表示程序正常結(jié)束,這里大家先不用考慮為什么執(zhí)行到 int 21 處才執(zhí)行 -p 命令,也不用關(guān)心 mov ax,4c00 和 int 21 是什么意思,大家先記住就行。
由于程序裝載的過程是 command 將程序裝載進(jìn)入內(nèi)存,然后 debug 程序?qū)?exe 程序其進(jìn)行跟蹤,所以程序退出后也是先從 exe 程序退出到 debug 程序中,由 debug 程序再退回到 command 程序中。
下面再分析一段程序,匯編原代碼
仍然是將其保存為 test.txt,然后執(zhí)行編譯和鏈接操作,將其生成可執(zhí)行文件 test.exe,觀察其執(zhí)行過程。
我們先使用 -r 查看一下初始寄存器的內(nèi)容。
主要觀察一下 CX 、DS 、CS 和 IP 的值,是否和我們上面描述的一致,CX 存放程序長度,DS 存放程序段地址,CS 存放程序初始地址,IP 存放程序偏移地址。
再使用 -u 看一下 exe 程序的源代碼,這個 exe 程序是經(jīng)過編譯和鏈接之后的程序。
我們來分析一下這段,這是一段棧段的入棧和出棧的程序,首先
是設(shè)置棧段的棧頂指令,執(zhí)行完成后會設(shè)置棧頂?shù)奈锢淼刂窞?20000 H ,即 SS:SP = 2000:0000。
我們執(zhí)行這個程序的過程中,發(fā)現(xiàn) mov sp,0 這個指令為什么沒有出現(xiàn)呢?難道是我們漏寫了?查看了一下,源代碼確實是有這條指令的,難道是沒有執(zhí)行?
為了驗證這個假設(shè),我們重新 debug 一下這段程序,然后先把 SP 的值進(jìn)行修改,如下圖所示。
剛開始,我們使用 -r 把 sp 的值改成 0002,然后單步執(zhí)行,在執(zhí)行到 mov ss,ax 之后,發(fā)現(xiàn) SP 的值變?yōu)?0000,這也就是說 mov sp,0 這條指令其實是執(zhí)行了的,只是 debug 模式下沒有顯示而已。
程序繼續(xù)向下執(zhí)行,下面是兩個 pop 出棧操作。
pop ax 和 pop bx 做了兩件事:把寄存器清空;棧頂位置 + 2 ,所以 ax 和 bx 寄存器的內(nèi)容為 0 ,并且 SP = SP + 2 ,執(zhí)行后 SP = 000E。
之后是兩個 push 操作,把出棧的兩個寄存器再進(jìn)行入棧,如下圖所示。
push 操作也做了兩件事情,將寄存器入棧,SP = SP - 2,由于 ax 和 bx 已經(jīng) pop 出棧了,所以寄存器內(nèi)容為 0 ,最后再進(jìn)行 pop 操作,然后再結(jié)束程序的執(zhí)行過程。
我們再來看一下 PSP 的情況,由于程序被裝入的時候前 256 個字節(jié)是 PSP 所占用的,此時 DS(SA)處就是 PSP 的起始地址,而 CS = SA + 10H ,也就是 CS = 076AH。
debug 循環(huán)程序
下面我們來 debug 一下循環(huán)程序,看看有哪些有意思的細(xì)節(jié)。
現(xiàn)在有這樣一道問題,計算 ffff:0006 單元中的數(shù)乘 3 ,讓結(jié)果存儲在 dx 中。
針對這個問題,有幾個點需要思考:
- 我們知道 ,8086 匯編語言中單個存儲單元所能存儲的最大值是 8 位,一個字節(jié)長度,范圍是 0 - 255 之間,而一個寄存器 dx 中可容納的最大值是 16 位,兩個字節(jié)長度,范圍是 0 - 65535,即使 255 * 3 也小于 65535,很顯然乘以 3 之后,dx 中能夠存放的下。
- 數(shù)乘 3 相當(dāng)于是循環(huán)做 add 自身操作 3 次,所以需要用加法來實現(xiàn)乘法,可以直接使用 dx 進(jìn)行累加,不過需要一個 ax 來進(jìn)行中轉(zhuǎn)。
- ffff:6 內(nèi)存單元是一個字節(jié)單元,而 ax 寄存器能容納的是一個字單元,無法直接賦值,該如何做呢?因為 ax 可以看做 al 和 ah ,而 al 和 ah 又是兩個單獨(dú)的寄存器,它們之間不會發(fā)生值溢出,所以讓 ah = 0 ,al = 內(nèi)存單元的值即可。
所以這段匯編程序的代碼如下
編寫完畢,編譯鏈接成 exe 程序后,對其進(jìn)行 debug xxx.exe 操作。
我們來看下程序的執(zhí)行過程。
前兩段沒毛病,設(shè)置 DS 段寄存器的值為 FFFF 。然后繼續(xù)向下執(zhí)行
執(zhí)行到 mov al,[6] 的時候我發(fā)現(xiàn),怎么 AX 寄存器中的內(nèi)容變成 0006 了?我不是想要把 06 放入 ax 中啊,我是想把 ffff:06 內(nèi)存單元中的值放入 ax 中啊,我突然意識到編譯器是個傻子。
經(jīng)過我認(rèn)真仔細(xì)細(xì)心耐心用心的排查了一番問題之后,我方才大悟,原來我是個傻子!不知道各位小伙伴們看出來我代碼的問題了嗎?
我怎么敢在源程序中把立即數(shù)當(dāng)做內(nèi)存偏移地址來用呢?必須要用 bx 中轉(zhuǎn)啊!
這也就是說,編譯器編譯完源代碼之后,會把 06 當(dāng)做立即數(shù)使用,如果想要使 06 表示內(nèi)存地址,必須要用 bx 進(jìn)行中轉(zhuǎn),修改之后的源代碼如下:
然后再重新鏈接成為 exe 程序之后,我們一步一步 debug 看一下。
執(zhí)行到 mov al,[bx] 的時候,我們發(fā)現(xiàn),此時右側(cè)有個 ds:0006 = 31,這段代碼表示的是 ds:0006 處內(nèi)存單元的值是 31,這才表明我們的程序是正確的。
繼續(xù)向下執(zhí)行程序。
前兩條指令執(zhí)行完成后,(dx) = 0 ,(cx) = 3,完成對累加寄存器的清空和循環(huán)計數(shù)器的賦值操作。最后一條指令是第一次循環(huán)操作指令,此時 CS:IP 指向 076A:0012 ,繼續(xù)向下執(zhí)行。
可以看到,第一次 add dx,ax 執(zhí)行完成后 IP = 0014H ,此時指向的指令是 LOOP 0012,這條指令的意思是讓程序再執(zhí)行一次 (IP) = 0012H 處的指令,也就是再執(zhí)行一次 add dx,ax,可以看到 cx 的值變成了 0002,因為循環(huán)指令執(zhí)行后 (cx) = (cx) - 2 ,然后再向下執(zhí)行,發(fā)現(xiàn)后面的循環(huán)指令還是 LOOP 0012 ,再執(zhí)行一次 add dx,ax,一直到 (cx) = 0 后結(jié)束程序執(zhí)行,如下圖所示
可以發(fā)現(xiàn),整個程序一共循環(huán)三次,最終 dx 中的值是 93 ,程序執(zhí)行到 int 21H 處,使用 -p 命令結(jié)束程序的執(zhí)行。