Ch 02 DEBUG的使用


上次我們寫了一個簡單的程式,並解釋原始檔中每一行的意義,但並不是這樣就結束了,最重要的是要來看看 EXAM01.COM 載入到記憶體中是怎樣的情形,這也是將來寫複雜程式時除錯時必要的步驟。最簡單的除錯工具就是 DOS/Windows 9x 所附的 DEBUG.EXE ( DOS 打從 1.0 版就附送 DEBUG;而 Windows 9x 的 DEBUG.EXE 在 C:\WINDOWS\COMMAND 子目錄內;XP 也附有 DEBUG,在 C:\WINDOWS\SYSTEM32 子目錄 )。使用 DEBUG 載入要除錯的程式時用法如下:( 黃色的字是你必須輸入的,記得每次輸入完後要按 Enter 鍵 )

E:\HomePage\SOURCE>..\masm50\debug exam01.com [Enter]
就是 DEBUG 後面接上“要除錯的程式名”即可,假如“要除錯的程式名”後面還有參數,就直接接在“要除錯的程式名”後面即可。

DEBUG 的每個命令都用一個英文字母表示,進入 DEBUG 後你會看到螢幕最左邊有一個「-」號,表示 DEBUG 已經準備好,等你輸入命令,「-」可說是 DEBUG 的提示符號。好,現在開始試試第一個命令,D,它也是很常用的命令。


Dump 指令

第一個是查看記憶體內容 D ( dump ) 指令,試試看執行結果如何:

-d [Enter]
10F7:0100  EB 17 90 48 69 2C 20 49-20 6C 65 61 72 6E 20 61  k..Hi, I learn a
10F7:0110  73 73 65 6D 62 6C 79 2E-24 BA 03 01 B4 09 CD 21  ssembly.$:..4.M!
10F7:0120  B8 00 4C CD 21 46 EA 89-56 EC 80 7C 1E 01 74 12  8.LM!Fj.Vl.|..t.
10F7:0130  83 7E F0 02 74 0C 83 7E-F0 07 74 06 83 7E F0 0A  .~p.t..~p.t..~p.
10F7:0140  75 04 80 4C 10 20 F6 44-10 20 75 03 E9 6D 01 80  u..L. vD. u.im..
10F7:0150  7E F8 07 74 03 E9 64 01-83 7E F0 03 75 03 E9 5B  ~x.t.id..~p.u.i[
10F7:0160  01 F6 44 1D 0B 75 76 8B-04 0B F5 07 02 00 24 10  .vD..uv...u...$.
10F7:0170  3F 00 A0 FF 3B E1 06 62-78 00 F1 10 E5 07 00 00  ?. .;a.bx.q.e...
輸入 d 後,你會看到電腦列出一大堆文字,事實上這些文字都是十六進位數字,DEBUG 所顯出的數都以十六進位表示,如果你不懂十六進位,請看附錄一數字系統

螢幕上可分為三部份,最左邊是表示位址,中間部份表示記憶體內容,而最右邊表示該記憶體內容的 ASCII 碼。

位址

什麼是「位址」呢?我們知道電腦要執行程式時,需要把程式與所需資料載入記憶體中,方能執行。當電腦要讀取某一筆資料時,必須要知道那筆資料位於記憶體何處,畢竟記憶體是以千、百萬甚至十億 ( 即 K、M 甚至 G ) 來計數的。我們把每個記憶體以一個位元組為單位,由零開始編號,一直向上延伸到你的電腦裝有的記憶體容量。假想你的電腦裝有 1MB 的記憶體,那麼可以把它想成每個記憶體只能裝下一個位元組,並且由零開始為每個記憶體編號,一直到 1048575,共有 1048576 個記憶體編號,也代表你的電腦可容納 1048576 個位元組 ( 在記憶體中 1K=1024,1M=1048576 )。這些編號就好像住宅的地址,假想有一條街有許多住宅,從零號開始編門牌號碼,但是每個住宅只能住一人,因此郵差要遞送信件給某人,只要知道門牌號碼就可把信件送到某人手中。這應該是最理想、最簡單的位址表示方式了,像這種表示方式稱「絕對位址」或「實體位址」。

但不幸的是,當初 8086 設計時,有 20 根位址線,最多可以表示 220 個編號 ( 一根位址線可有高電壓與低電壓兩種選擇,因此可表示兩個數,0、1;兩根位址線就有四種選擇,可表示 4 個數……,20 根位址線,就可表示 220=1048576 ),換句話說,8086 可以表示 1048576 個記憶體編號,術語說 8086 可定址 1M 個位元組。但是,8086 是 16 位元的 CPU,其內部暫存器能表示的最大數為 216 ( 216=65536 ),亦即 8086 最多能表示的記憶體編號只有 65536。那麼要如何用 16 位元去表示 20 位元的數呢?

當初設計者就採用變通的方法:雖然一個 16 位元的暫存器無法表示 20 位元的數,但是兩個 16 位元暫存器就可以了。設計者把位址以「區段」及「偏移位址」來表示位址,就是在 DEBUG 中輸入 D 命令後,左邊所看到的 XXXX:YYYY 的樣子。XXXX 表示區段,YYYY 表示偏移位址,他在記憶體中真正的位址是 XXXX*16+YYYY,舉例來說,10F7:0100 這個位址應該是 10F7(此為16進位)*16(十進位)再加上十六進位的 100,等於 11070H。以「區段:偏移位址」這種方式表示位址雖然可以得到較大的位址,但是同位址,有許多不同方式表達,例如 10F7:0100 可以是 1107:0000,也可以是 10F0:0170,這些都是指同一實體位址。請參考下圖,左邊的數字是實體位址,中間的是用區段:偏移表示的位址,右邊則是該位址記憶體的內容。

使用這種「區段:偏移」的方法表示位址,還有一個特點,那就是重疊。每個區段都是從偏移位址 0000 開始,但是因為同一記憶體位址可以有許多種不同的「區段:偏移」表示法,因此區段與區段有時會互相重疊。例如上圖堙A用 DEBUG 載入 EXAM01.COM 時,EXAM01.COM 由 10F7:0000 開始,換算成實體位址是 10F70,因此由 10F70∼20F6F 都是這個區段的範圍;若有一個區段從 1000:0000 開始,那麼其實體位址範圍是 10000∼1FFFF,所以這兩個區段就會發生重疊。

讓我們再來看看 DEBUG 堙A執行「D」指令的結果:

10F7:0100  EB 17 90 48 69 2C 20 49-20 6C 65 61 72 6E 20 61  k..Hi, I learn a
10F7:0110  73 73 65 6D 62 6C 79 2E-24 BA 03 01 B4 09 CD 21  ssembly.$:..4.M!
10F7:0120  B8 00 4C CD 21 46 EA 89-56 EC 80 7C 1E 01 74 12  8.LM!Fj.Vl.|..t.

左側是「區段:偏移」位址,中間所表示的是記憶體中的內容,對照上表 10F7:0100 的內容是 EB,10F7:0101 的內容是 17 等等,這些都是十六進位數。最右邊是記憶體內容的 ASCII 碼。你可以查表對照 ASCII 碼 48h 相當於英文字的 H、69h 相當英文字的 i,若超過 7Fh 的數字,DEBUG 自動將最高位元變成 0,例如 0EBh,就變成 6Bh,就是 ASCII 的 k。(有關 ASCII 碼的說明請參考第三章,有關 ASCII 的列表請參考附錄四

D 指令完整的用法是 d [起始位址] [L長度]。例如如果你只要觀察 100h 到 110h 的記憶體內容,就可以輸入

d 100 L 10
此處為了方便區別 1 與 L,故用大寫的 L,實際使用時大小寫都是一樣的。

D 指令也可以直接指定要顯示由那一位址到那一位址,用法如下:

d [起始位址] [結束位址]
假如像第一個指令沒有指定長度,也沒有結束位址,僅僅輸入一個 d 而已,DEBUG 會自動顯示 80H 個位元組的資料就停止了。

Un-assembly 指令

現在請你輸入 u 119 試試看:

-u 119 [Enter]
10F7:0119 BA0301         MOV    DX,0103
10F7:011C B409           MOV    AH,09
10F7:011E CD21           INT    21
10F7:0120 B8004C         MOV    AX,4C00
10F7:0123 CD21           INT    21
10F7:0125 1F             POP    DS
10F7:0126 BB0002         MOV    BX,0200
10F7:0129 81FB0002       CMP    BX,0200

你會發現最右邊,好像跟我們所寫的原始程式類似,只是有些地方變成了數字。沒錯,u 命令就是把記憶體的內容變成組合語言,這些記憶體內容可能是資料,也可能是可執行碼。像這樣把記憶體內容 ( 就是類似 BA、03、01 等數值 ) 變成組合語言助記碼的過程,稱為「反組譯」,它是破解所必需的過程。

而最左邊,很明顯的就是每一個指令的位址,而中間的就是記憶體內容,其實這就是機械碼,或也有人稱為機械語言。電腦只認得 0 與 1 組成的數字,在電腦內部都是這樣的數字,不管是資料或程式碼,經 CPU 執行後才能確定那些是程式碼那些是資料,這樣的語言很難讓人一目瞭然,於是就發明了組合語言來幫助記憶(助記碼),這些助記碼就是最右邊看到的類似我們所寫的組合語言原始程式。

你可以這樣想,組合語言的每一條指令,都有一個或數個十六進位數與之配合,例如 BAh 就相當於 「mov dx,」、B4h 相當於「mov ah,」但每一個指令所用的位元組長度不同。

現在請注意到位址 0119H、011AH、011BH 的內容分別是 BA、03、01,「翻譯」成組合語言是 MOV DX,0103,注意到沒有,在我們的習慣上 0103H,數字大的 1 寫在左邊;但是在 DEBUG 內卻照位址高低排列,01 數字大於 03,所以 01 在高位址,故排在右邊。這點要請讀者注意,在 DEBUG 中,數字位數大的,在高位址,排在右邊。其他像位址 0120H、0126H、0129H 也都類似,像這種排列方式稱之為 Little-Endian。事實上在 IBM PC 及其相容電腦的 CPU 都是以 Little-Endian 方式儲存,也就是較不重要的位元組 ( LSB ) 存放於低位址,最重要的位元組 ( MSB ) 存放於高位址 ( MSB 是 Most Significant Bit/Byte 的縮寫,通常譯為最重要的位元或者最重要的位元組;LSB 是 Least Significant Bit/Byte 的縮寫,通常譯為最不重要的位元或最不重要的位元組 )。

在「10F7:0119」位址的指令是「MOV DX,0103」,如果您對照第一章的 exam01.asm 原始程式,就會發現本來是寫「mov dx,offset mes」,但被組譯之後,變成「MOV DX,0103」。這就是 OFFSET 假指令的功能,它計算出 mes 位址,而把此位址當成數值,填入 DX 堙C您可再對照上面「D」指令的結果,mes 字串就是由位址「10F7:0103」開始。

現在請你試試 u 100 120 看看,應該如下所示:

-u 100 120 [Enter]
10F7:0100 EB17           JMP    0119
10F7:0102 90             NOP
10F7:0103 48             DEC    AX
10F7:0104 692C2049       IMUL   BP,[SI],4920
10F7:0108 206C65         AND    [SI+65],CH
10F7:010B 61             POPA
10F7:010C 726E           JB     017C
10F7:010E 206173         AND    [BX+DI+73],AH
10F7:0111 7365           JNB    0178
10F7:0113 6D             INSW
10F7:0114 626C79         BOUND  BP,[SI+79]
10F7:0117 2E24BA         AND    AL,BA
10F7:011A 0301           ADD    AX,CS:[BX+DI]
10F7:011C B409           MOV    AH,09
10F7:011E CD21           INT    21
10F7:0120 B8004C         MOV    AX,4C00

你會發現從 0102∼011B、0125 以後都不是我們所輸入的程式,而是要印出的文字「Hi, I learn assembly.$」以及亂碼,但是一樣可以翻譯成程式碼,所以如果 CPU 執行的位址 (CS:IP) 不對,很可能就會當機。(執行位址稍後說明)

再看看位址 0100H 是一個跳躍指令,其實就是 EXAM01.COM 程式的第一個指令,而位址 011C 其實就是代表 begin 標記。換句話說,COM 程式檔被載入記憶體時,是從位址 100H 開始放入的,這也就是撰寫 COM 程式時為什麼要加上 org 100h。


Registor 指令

此指令有兩個功用,一是顯示所有暫存器(詳細說明請按這裡)及旗標內容,二是修改暫存器內容。請試試看只輸入「r」來顯示暫存器的內容:
-r [Enter]
AX=0000  BX=0000  CX=0025  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=10F7  ES=10F7  SS=10F7  CS=10F7  IP=0100   NV UP EI PL NZ NA PO NC
10F7:0100 EB19           JMP    0119

DEBUG 共顯示三行,最底下一行就是 DEBUG 現在將執行但尚未執行的 CPU 指令,他的位址就是 CS:IP 所指的位址,CPU 會把正要執行命令的位址存入 CS:IP 中,而一開始載入 COM 檔時,IP 必定為 100H,而 CS 則不一定,視你開機時載入的驅動程式多寡而定。

請不要為這麼多暫存器擔心,我們一次只介紹一兩個,現在你只要知道 CS、IP、AX、BX、CX、DX 就可以了。後面四個,你可以把她們想成在 CPU 中暫時存放 16 位元資料的地方,而每一個又可以分成高位元的和低位元的兩個暫存器。例如 AX 可以分成較高的 8 位元 AH 和較低的 8 位元 AL 兩個暫存器。如下圖

如果你想修改暫存器內容,只要在 r 後面輸入暫存器名稱就可以了,r 和暫存器名之間有沒有空白都無關緊要。( 在 DEBUG 堙A要修改暫存器之數值,只能修改 AX、BX、CX…等 16 位元暫存器的數值,無法修改 AL、AH 等 8 位元的數值,我的意思是,您無法輸入

rah

修改 AH 暫存器的數值,但是您可以用

rax

修改 AX 暫存器的數值。)


Trace 指令

這是追蹤指令,每輸入一個 t 後,DEBUG 就執行一個指令,然後停下來顯示所有暫存器之內容。現在請先輸入「r」指令,觀察暫存器後,再輸入一個「t」指令試試看:

-r [Enter]
AX=0000  BX=0000  CX=0025  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=10F7  ES=10F7  SS=10F7  CS=10F7  IP=0100   NV UP EI PL NZ NA PO NC
10F7:0100 EB17           JMP    0119
-t [Enter]
AX=0000  BX=0000  CX=0025  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=10F7  ES=10F7  SS=10F7  CS=10F7  IP=0119   NV UP EI PL NZ NA PO NC
10F7:0119 BA0301         MOV    DX,0103

是不是跳到 0119 準備執行下一指令?CS:IP 也指向 10F7:0119 了。原來 80X86 CPU 是以 CS:IP ( 還記得前面說以 XXXX:YYYY 表示位址吧? ) 指向正要執行的指令,CS 暫存器是程式碼的區段暫存器 ( code segment register ),而 IP 暫存器稱為指令指標暫存器 ( instruction pointer )。原來當 CPU 要執行程式時,必須先到記憶體去提取要執行的指令,但是要到那一個記憶體位址去提取指令呢?這時 CPU 就會察看 CS:IP 指到那一個位址,然後到該位址提取指令,當該指令執行完畢後,CS:IP 又會址向下一個指令的位址,於是 CPU 就再度重複上述過程,如此忙碌不停地工作。現在,再連續輸入兩個 t 看看:

-t [Enter]
AX=0000  BX=0000  CX=0025  DX=0103  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=10F7  ES=10F7  SS=10F7  CS=10F7  IP=011C   NV UP EI PL NZ NA PO NC
10F7:011C B409           MOV    AH,09
-t [Enter]
AX=0900  BX=0000  CX=0025  DX=0103  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=10F7  ES=10F7  SS=10F7  CS=10F7  IP=011E   NV UP EI PL NZ NA PO NC
10F7:011E CD21           INT    21

您應當可以看見每次執行完一條指令後,CS:IP 便會改變,同時也會把執行結果顯示出來,以此例來說,AH、DX 是不是都變成我們預計的數值?


Go 指令

G 是執行程式到結束,當你確定程式在某位址前都沒有錯誤,那你可以直接輸入
-g 結束位址
現在執行到 10F7:011E 了,接下來是一個 DOS 中斷服務程式,我想這裡面應該不會有錯誤,因為這是微軟寫的,經過許多次檢驗,因此沒必要追蹤,直接輸入:(當然以後也可以用來研究中斷程式怎麼寫的)
-g 120 [Enter]
Hi, I learn assembly.
AX=0924  BX=0000  CX=0025  DX=0103  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=10F7  ES=10F7  SS=10F7  CS=10F7  IP=0120   NV UP EI PL NZ NA PO NC
10F7:0120 B8004C        MOV     AX,4C00

電腦是不是依 AH/DX 的內容印出“Hi, I learn assembly.”字串來了,同時使 CS:IP 指向下一個指令。現在再輸入僅僅一個 g 試試看:

-g [Enter]
Program terminated normally
如果只輸入一個 g 的話,就會一直執行到結束,並印出正常結束程式的字樣。

你也可以輸入由那一個位址開始執行,這時輸入

-g=起始位址


Quit 指令

這是結束 DEBUG 的指令,輸入 q,DEBUG 就會將控制權交回給 DOS。
-q [Enter]
E:\HomePage\SOURCE>

Edit 指令

這是修改記憶體內容的指令,先進入 DOS 模式,再試試看以下操作:

C:\WINDOWS\COMMAND>debug [Enter]
-d 100 L20 [Enter]
146F:0100  65 20 68 69 67 68 20 6D-41 6D 6F 72 79 20 61 72   e high mAmory ar
146F:0110  65 61 2E 0D 0A 22 4D 6F-64 75 6C 65 34 00 5E 14   ea..."Module4.^.
-e 107 [Enter]
146F:0107  6D.41 [Enter]
注意!輸入 41 後,有兩種選擇,如果不要修改下一位址的內容,要按 Enter 鍵結束 e 指令;如果要修改的話,就按空白鍵,DEBUG 會自動印出原來內容,等待你數新值。我們按空白鍵,再輸入 42 試試,
C:\WINDOWS\COMMAND>debug [Enter]
-d 100 L20 [Enter]
146F:0100  65 20 68 69 67 68 20 6D-41 6D 6F 72 79 20 61 72   e high mAmory ar
146F:0110  65 61 2E 0D 0A 22 4D 6F-64 75 6C 65 34 00 5E 14   ea..."Module4.^.
-e 107 [Enter]
146F:0107  6D.41 [Space]
146F:0108  41.42 [Space]   6D.43 [Enter]
輸入 41 後按空白鍵就會出現輸入下一位址的提示,再輸入42 ,然後再按空白鍵並輸入 43。輸入 43 後,再按 Enter 鍵結束 e 指令。顯示記憶體內容看看:
-d 100 L10 [Enter]
146F:0100  65 20 68 69 67 68 20 41-42 43 6F 72 79 20 61 72   e high ABCory ar
-q [Enter]

C:\WINDOWS\COMMAND>
紅色的部分就是我們修改的地方。

Assemble 指令

這個指令是可以讓 DEBUG 寫簡單的組合語言程式,但是所有數字都得用十六進位表示,且所有標記也都得用位址表示。用法如下:
a [位址]

這樣您就可以在輸入的位址直接輸入 X86 指令,但是無法使用標記或變數名,都要用位址表示。底下是用「a」指令寫一個在螢幕上印出「ABCD」的小程式:

E:\HomePage\SOURCE>debug [Enter]
-a 100 [Enter]
128A:0100 mov   ah,9 [Enter]
128A:0102 mov   dx,200 [Enter]
128A:0105 int   21 [Enter]
128A:0107 mov   ah,4c [Enter]
128A:0109 int   21 [Enter]
128A:010B [Enter]
-e 200 [Enter]
128A:0200  00.41   00.42   00.43   00.44   24.24 [Enter]
→每輸完一個 ASCII 碼後,按 [SPACE] 鍵輸入下一個
AX=0900  BX=0000  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=128A  ES=128A  SS=128A  CS=128A  IP=0102   NV UP EI PL NZ NA PO NC
128A:0102 BA0002        MOV     DX,0200
-g [Enter]
ABCD

Name 指令

這個指令是指定檔名,但是僅止於指定檔名而已並不會載入或存入檔案。其語法是

N [路徑名]檔名

如果檔名錯誤的話,這個指令也不會提出錯誤訊息,要到實際載入時才會通知您。

Load 指令

這個指令是使 DEBUG 載入磁區 (sector) 的資料或載入檔案。如果要載入磁區,語法是

L  載入位址  磁碟機名  起始磁區  載入磁區數

載入位址是指從磁碟上讀取的資料會被 DEBUG 放在這個位址,用 XXXX:YYYY 表示,如果區段位址省略,則自動用 DS 代替。磁碟機名的用法是 A: 磁碟機用 0,B: 磁碟機用 1,C: 磁碟機用 2,依此類推。這裡的磁區是指邏輯磁區,參考第十八章。例如要載入硬碟 C: 的啟動磁區到 DS:0100 位址,可以用

-L 100 2 0 1 [Enter]

如果要載入一個檔案的話,必須先用 N 指令先指定檔名,再用 L 指令載入,語法為

L  [載入位址]

即可,如果載入位址省略,會自動將檔案存放在 DS:0100 處,同時 CX 之數值表示檔案長度。但是如果您載入的是可執行檔 EXE 的話,DEBUG 會將 EXE 的內容自動分配好,也就是載入後 CS 指向 EXE 的程式碼區段,DS 指向資料段……(見第十一章)。

其他指令

還有一些 DEBUG 指令,請參考 DOS 使用手冊。

結論

這一章裡介紹幾個常用的 DEBUG 指令,但事實上還有許多指令我沒有介紹,一來是較少使用,二來不想一下子就講了太多用不著的而使人搞不清楚。因此最好能有一本書(去圖書館借 DOS 使用手冊之類的書吧。)在旁才好。您也可以在 DEBUG 的「-」提示符號下輸入「?」得到簡要的說明。

此外微軟還出了一個較為強悍的除錯器,SYMDEB.EXE,稱為符號除錯器,請按這裡看看。


回到首頁到第一章到第三章