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