來看三段程式

ITPUB社群發表於2023-01-30

學習任何一門語言都不能少的了 debug ,彙編也是。

我前面這篇文章給大家寫了如何在 windows 、mac 等作業系統的機器上使用 dosbox 的 masm、link、debug 功能編譯、解釋、執行一個彙編程式,文章連結如下:

手把手教你彙編 Debug

debug 程式執行過程

下面我們就依據這幾個功能來跟蹤一下程式的執行過程。

debug 對我們來說非常重要,有很多程式碼細節和問題透過肉眼是觀察出來的,我們肉眼可能能夠判斷一些簡單的程式問題,但是對於很多隱藏較深的問題,還是要依據 debug 才能發現。

下面是一段彙編程式碼,這段彙編程式碼我之前的文章中也給大家寫過。

assume cs:codesg
codesg segment

mov ax,0123h
mov bx,0456h
add ax,bx
add ax,ax

mov ax,4c00h
int 21h

codesg ends
end

新建文字檔案,把程式碼 cv 過去,然後右鍵儲存,使用 dosbox 將其編譯為 1.obj 檔案,連結為 1.exe 檔案後,我們使用 debug 1.exe 命令來分析一下這段程式,並用 -r 命令來看一下初始的暫存器情況。

來看三段程式

程式初始狀態下,可以看到 CX 中的資料為 000F,這也表示著程式的長度是 000F,1.exe 中共有 15 個位元組,CX 中的內容為 000FH。

好,現在我們已經知道程式被成功的載入記憶體並執行起來了,但是我們現在先不妨想一下,被連結成為 EXE 的程式會被裝入記憶體的哪個地方的呢?我們怎麼知道程式被裝入在哪裡呢?

程式裝載的過程分下面幾步:

  1. 首先程式會從記憶體中找到一塊區域,記為初始地址 SA,此時的偏移地址為 0 的這樣一塊足夠容量的記憶體區域。

來看三段程式

  1. 在這段區域內的頭 256 個位元組中,會建立一塊稱為程式段字首(Program Segment Prefix ,PSP)的區域,這塊區域被 DOS 用來和被載入的程式進行通訊。

來看三段程式

  1. 從這塊程式的 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 程式中。

下面再分析一段程式,彙編原始碼

assume cs:codesg

codesg segment

mov ax,2000H
mov ss,ax
mov sp,0
add sp,10
pop ax
pop bx
push ax
push bx
pop ax
pop bx

mov ax,4c00H
int 21H

codesg ends

end

仍然是將其儲存為 test.txt,然後執行編譯和連結操作,將其生成可執行檔案 test.exe,觀察其執行過程。

我們先使用 -r 檢視一下初始暫存器的內容。

來看三段程式

主要觀察一下 CX 、DS 、CS 和 IP 的值,是否和我們上面描述的一致,CX 存放程式長度,DS 存放程式段地址,CS 存放程式初始地址,IP 存放程式偏移地址。

再使用 -u 看一下 exe 程式的原始碼,這個 exe 程式是經過編譯和連結之後的程式。

來看三段程式

我們來分析一下這段,這是一段棧段的入棧和出棧的程式,首先

mov ax,2000H
mov ss,ax
mov sp,0

是設定棧段的棧頂指令,執行完成後會設定棧頂的實體地址為 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 = 記憶體單元的值即可。

所以這段彙編程式的程式碼如下

assume cs:codesg

codesg segment

mov ax,0ffffh
mov ds,ax

mov ah,0
mov al,[6]

mov cx,3
s: add dx,ax
loop s

mov ax,4c00h
int 21h

codesg ends
end

編寫完畢,編譯連結成 exe 程式後,對其進行 debug xxx.exe 操作。

我們來看下程式的執行過程。

來看三段程式

前兩段沒毛病,設定 DS 段暫存器的值為 FFFF 。然後繼續向下執行

來看三段程式

執行到 mov al,[6] 的時候我發現,怎麼 AX 暫存器中的內容變成 0006 了?我不是想要把 06 放入 ax 中啊,我是想把 ffff:06 記憶體單元中的值放入 ax 中啊,我突然意識到編譯器是個傻子。

經過我認真仔細細心耐心用心的排查了一番問題之後,我方才大悟,原來我是個傻子!不知道各位小夥伴們看出來我程式碼的問題了嗎?

我怎麼敢在源程式中把立即數當做記憶體偏移地址來用呢?必須要用 bx 中轉啊!

這也就是說,編譯器編譯完原始碼之後,會把 06 當做立即數使用,如果想要使 06 表示記憶體地址,必須要用 bx 進行中轉,修改之後的原始碼如下:

assume cs:codesg

codesg segment

mov ax,0ffffh
mov ds,ax
mov bx,6

mov ah,0
mov al,[bx] # 必須要用 bx 進行中轉,才能表示記憶體地址
mov dx,0   # 累加暫存器清 0

mov cx,3
s: add dx,ax
loop s

mov ax,4c00h
int 21h

codesg ends
end

然後再重新連結成為 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 命令結束程式的執行。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2933262/,如需轉載,請註明出處,否則將追究法律責任。

相關文章