[彙編]《組合語言》第6章 包含多個段的程式
王爽《組合語言》第四版 超級筆記
目錄第6章 包含多個段的程式
在作業系統的環境中,合法地通過作業系統取得的空間都是安全的,因為作業系統不會讓一個程式所用的空間和其他程式以及系統自己的空間相沖突。
在作業系統允許的情況下,程式可以取得任意容量的空間。
程式取得所需空間的方法有兩種:一是在載入程式的時候為程式分配,再就是程式在執行的過程中向系統申請。在我們的課程中,不討論第二種方法。
載入程式的時候為程式分配空間,我們在前面己經有所體驗,比如我們的程式在載入的時候,取得了程式碼段中的程式碼的儲存空間。
我們若要一個程式在被載入的時候取得所需的空間,則必須要在源程式中做出說明。我們通過在源程式中定義段來進行記憶體空間的獲取。
上面是從記憶體空間獲取的角度上,談定義段的問題。
我們再從程式規劃的角度來談一下定義段的問題。大多數有用的程式,都要處理資料,使用棧空間,當然也都必須有指令,為了程式設計上的清晰和方便,我們一般也都定義不同的段來存放它們。
對於使用多個段的問題,我們先簡單說到這裡,下面我們將以這樣的順序來深入地討論多個段的問題:
(1)在一個段中存放資料、程式碼、棧,我們先來體會一下不使用多個段時的情況;
(2)將資料、程式碼、棧放入不同的段中。
6.1 程式碼段中使用資料
現在要累加的就是己經給定了數值的資料。我們可以將它們一個一個地加到ax暫存器中,但是,我們希望可以用迴圈的方法來進行累加,所以在累加前,要將這些資料儲存在一組地址連續的記憶體單元中。
如何將這些資料儲存在一組地址連續的記憶體單元中呢?
我們可以用指令一個一個地將它們送入地址連續的記憶體單元中,可是這樣又有一個問題,到哪裡去找這段記憶體空間呢?
從規範的角度來講,我們是不能自己隨便決定哪段空間可以使用的,應該讓系統來為我們分配。
我們可以在程式中,定義我們希望處理的資料,這些資料就會被編譯、連線程式作為程式的一部分寫到可執行檔案中。
當可執行檔案中的程式被載入入記憶體時,這些資料也同時被載入入記憶體中。與此同時,我們要處理的資料也就自然而然地獲得了儲存空間。
具體的做法看下面的程式。
程式6.1
assume cs:code code segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h mov bx,0 mov ax,0 mov cx,8 s: add ax,cs:[bx] add bx,2 loop s mov ax,4c00h int 21h code ends end
解釋一下,程式第一行中的“dw”的含義是定義字型資料。dw即“define word”。
在這裡,使用dw定義了8個字型資料(資料之間以逗號分隔),它們所佔的記憶體空間的大小為16個位元組。
程式中的指令就要對這8個數據進行累加,可這8個數據在哪裡呢?
由於它們在程式碼段中,程式在執行的時候CS中存放程式碼段的段地址,所以可以從CS中得到它們的段地址。
它們的偏移地址是多少呢?
因為用dw定義的資料處於程式碼段的最開始,所以偏移地址為0,這8個數據就在程式碼段的偏移0、2、4、6、8、A、C、E處。
程式執行時,它們的地址就是CS:0、CS:2、CS:4、CS:6、CS:8、CS:A、CS:C、CS:E。
程式中,用bx存放加2遞增的偏移地址,用迴圈來進行累加。在迴圈開始前,設定 (bx)=0,cs:bx指向第一個資料所在的字單元。
每次迴圈中(bx)=(bx)+2,cs:bx指向下一個資料所在的字單元。
將程式6.1編譯、連線為可執行檔案p61.exe,先不要執行,用Debug載入檢視一下,情況如圖6.1所示。
圖6.1中,通過DS=0B2D,可知道程式從0B3D:0000開始存放。用u命令從0B3D:0000檢視程式,卻看到了一些讓人讀不懂的指令。
為什麼沒有看到程式中的指令呢?
實際上用u命令從0B3D:0000檢視到的也是程式中的內容,只不過不是源程式中的彙編指令所對應的機器碼,而是源程式中,在彙編指令前面,用dw定義的資料。
實際上,在程式中,有一個程式碼段,在程式碼段中,前面的16個位元組是用“dw”定義的資料,從第16個位元組開始才是彙編指令所對應的機器碼。
可以用d命令更清楚地檢視一下程式中前16個位元組的內容,如圖6.2所示。
可以從0B3D:0010檢視程式中要執行的機器指令,如圖6.3所示。
從圖6.2和6.3中,我們可以看到程式載入到記憶體中後,所佔記憶體空間的前16個單元存放在源程式中用“dw”定義的資料,後面的單元存放源程式中彙編指令所對應的機器指令。
怎樣執行程式中的指令呢?
用Debug載入後,可以將IP設定為10h,從而使CS:IP指向程式中的第一條指令。然後再用t命令、p命令,或者是g命令執行。
可是這樣一來,我們就必須用Debug來執行程式。
程式6.1編譯、連線成可執行檔案後,在系統中直接執行可能會出現問題,因為程式的入口處不是我們所希望執行的指令。
如何讓這個程式在編譯、連線後可以在系統中直接執行呢?我們可以在源程式中指明程式的入口所在,具體做法如下。
程式6.2
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
start: mov bx,0
mov ax,0
mov cx,8
s : add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end start
注意在程式6.2中加入的新內容,在程式的第一條指令的前面加上了一個標號start,而這個標號在偽指令end的後面岀現。
這裡,我們要再次探討end的作用。end除了通知編譯器程式結束外,還可以通知編譯器程式的入口在什麼地方。
在程式6.2中我們用end指令指明瞭程式的入口在標號start處,也就是說,“mov bx,0”是程式的第一條指令。
在前面的課程中(參見4.8節),我們己經知道在單任務系統中,可執行檔案中的程式執行過程如下。
(1)由其他的程式(Debug、command或其他程式)將可執行檔案中的程式載入入記憶體;
(2)設定CS:IP指向程式的第一條要執行的指令(即程式的入口),從而使程式得以執行;
(3)程式執行結束後,返回到載入者。
現在的問題是,根據什麼設定CPU的CS:IP指向程式的第一條要執行的指令?也就是說,如何知道哪一條指令是程式的第一條要執行的指令?
這一點,是由可執行檔案中的描述資訊指明的。我們知道可執行檔案由描述資訊和程式組成,程式來自於源程式中的彙編指令和定義的資料;描述資訊則主要是編譯、連線程式對源程式中相關偽指令進行處理所得到的資訊。
歸根結底,我們若要CPU從何處開始執行程式,只要在源程式中用“end 標號”指明就可以了。
有了這種方法,就可以這樣來安排程式的框架:
assume cs:code
code segment
...資料...
start:
...程式碼...
code ends
end start
6.2 程式碼段中使用棧
段空間應該由系統來分配。可以在程式中通過定義資料來取得一段空間,然後將這段空間當作棧空間來用。程式如下。
程式6.3
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
;用dw定義16個字型資料,在程式載入後,將取得16個字的記憶體空間,存放這16個數據。在後面的程式中將這段空間當作棧來使用。
start: mov ax, cs
mov ss,ax
mov sp, 30h;將設定棧頂ss : sp指向cs:30
mov bx,0
mov cx,8
s : push cs:[bx]
add bx,2
loop s ;以上將程式碼段0~15單元中的8個字型資料依次入棧
mov bx,0
mov cx,8
s0: pop cs:[bx]
add bx,2
loop s0 ;以上依次出棧8個字型資料到程式碼段0~15單元中
mov ax,4c00h
int 21h
codesg ends
end start ;指明程式的入口在start處
注意程式6.3中的指令:
mov ax,cs
mov ss,ax
mov sp,30h
我們要將cs:10~cs:2F的記憶體空間當作棧來用,初始狀態下棧為空,所以ss:sp要指向棧底,則設定ss:sp指向cs:30。如果對這點還有疑惑,建議回頭認真複習一下第3章。
在程式碼段中定義了16個字型資料,它們的數值都是0。這16個字型資料的值是多少,對程式來說沒有意義。
我們用dw定義16個數據,即在程式中寫入了16個字型資料,而程式在載入後,將用32個位元組的記憶體空間來存放它們。
這段記憶體空間是我們所需要的,程式將它用作棧空間。可見,我們定義這些資料的最終目的是,通過它們取得一定容量的記憶體空間。所以我們在描述dw的作用時,可以說用它定義資料,也可以說用它開闢記憶體空間。比如對於:
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
可以說,定義了8個字型資料,也可以說,開闢了8個字的記憶體空間,這段空間中每個字單元中的資料依次是:0123h、0456h、0789h、0abch、0defh、0fedh、0cbah、0987h,因為它們最終的效果是一樣的。
6.3 資料、程式碼、棧放入不同的段
我們在程式中用到了資料和棧,將資料、棧和程式碼都放到了一個段裡面。
我們在程式設計的時候要注意何處是資料,何處是棧,何處是程式碼。這樣做顯然有兩個問題:
(1)把它們放到一個段中使程式顯得混亂;
(2)前面程式中處理的資料很少,用到的棧空間也小,加上沒有多長的程式碼,放到一個段裡面沒有問題。但如果資料、棧和程式碼需要的空間超過64KB,就不能放在一個段中(一個段的容量不能大於64KB,是我們在學習中所用的8086模式的限制,並不是所有的處理器都這樣)。
所以,應該考慮用多個段來存放資料、程式碼和棧。
怎樣做呢?我們用和定義程式碼段一樣的方法來定義多個段,然後在這些段裡面定義需要的資料,或通過定義資料來取得棧空間。
具體做法如下面的程式所示,這個程式實現了和程式6.3一樣的功能,不同之處在於它將資料、棧和程式碼放到了不同的段中。
程式6.4
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,20h ;設定棧頂ss:sp指向stack:20
mov ax,data
mov ds,ax ;ds指向data段
mov bx,0 ;ds:bx指向data段中的第一個單元
mov cx,8
s: push [bx]
add bx,2
loop s ;以上將data段中的0~15單元中的8個字型資料依次入棧
mov bx,0
mov cx,8
s0: pop [bx]
add bx,2
loop s0 ;以上依次出棧8個字型資料到data段的0~15單元中
mov ax,4c00h
int 21h
code ends
end start
下面對程式6.4做出說明。
(1)定義多個段的方法
我們從程式中可明顯地看出,定義一個段的方法和前面所講的定義程式碼段的方法沒有區別,只是對於不同的段,要有不同的段名。
(2)對段地址的引用
現在,程式中有多個段了,如何訪問段中的資料呢?
當然要通過地址,而地址是分為兩部分的,即段地址和偏移地址。如何指明要訪問的資料的段地址呢?
在程式中,段名就相當於一個標號,它代表了段地址。所以指令“mov ax,data”的含義就是將名稱為“data”的段的段地址送入ax。
一個段中的資料的段地址可由段名代表,偏移地址就要看它在段中的位置了。程式中“data”段中的資料“0abch”的地址就是:data:6。要將它送入bx中,就要用如下的程式碼:
mov ax,data
mov ds,ax
mov bx,ds:[6]
我們不能用下面的指令:
mov ds,data
mov bx,ds:[6]
其中指令“mov ds,data”是錯誤的,因為8086CPU不允許將一個數值直接送入段暫存器中。
程式中對段名的引用,如指令“mov ds,data”中的“data”,將被編譯器處理為一個表示段地址的數值。
(3)“程式碼段”、“資料段”、“棧段”完全是我們的安排
我們以一個具體的程式來再次討論一下所謂的“程式碼段”、“資料段”、“棧 段”。
在彙編源程式中,可以定義許多的段,比如在程式6.4中,定義了3個段“code”、“data”和“stack”。
我們可以分別安排它們存放程式碼、資料和棧。那麼我們如何讓CPU按照我們的這種安排來執行這個程式呢?下面來看看源程式中對這3個段所做的處理。
1、我們在源程式中為這3個段起了具有含義的名稱,用來放資料的段我們將其命名為“data”,用來放程式碼的段我們將其命名為“code”,用作棧空間的段命名為“stack”。
這樣命名了之後,CPU是否就去執行“code”段中的內容,處理“data”段中的資料,將“stack”當做棧了呢?
當然不是,我們這樣命名,僅僅是為了使程式便於閱讀。這些名稱同“start”、“s”、“s0”等標號一樣,僅在源程式中存在,CPU並不知道它們。
2、我們在源程式中用偽指令"assume cs:code,ds:data,ss:stack"將cs、ds和ss分別和code、data、stack段相連。
這樣做了之後,CPU是否就會將cs指向code,ds指向data,ss指向stack,從而按照我們的意圖來處理這些段呢?
當然也不是,要知道assume是偽指令,是由編譯器執行的,也是僅在源程式中存在的資訊,CPU並不知道它們。我們不必深究assume的作用,只要知道需要用它將你定義的具有一定用途的段和相關的暫存器聯絡起來就可以了。
3、若要CPU按照我們的安排行事,就要用機器指令控制它,源程式中的彙編指令是CPU要執行的內容。CPU如何知道去執行它們?
我們在源程式的最後用“end start”說明了程式的入口,這個入口將被寫入可執行檔案的描述資訊,可執行檔案中的程式被載入入記憶體後,CPU的CS:IP被設定指向這個入口,從而開始執行程式中的第一條指令。
標號“start”在“code”段中,這樣CPU就將code段中的內容當作指令來執行了。我們在code段中,使用指令:
mov ax,stack
mov ss,ax
mov sp,20h
設定ss指向stack,設定ss:sp指向stack:20,CPU執行這些指令後,將把stack段當做棧空間來用。
CPU若要訪問data段中的資料,則可用ds指向data段,用其他的暫存器(如bx)來存放data段中資料的偏移地址。
總之,CPU到底如何處理我們定義的段中的內容,是當作指令執行,當作資料訪問,還是當作棧空間,完全是靠程式中具體的彙編指令,和彙編指令對CS:IP、SS:SP、DS等暫存器的設定來決定的。
完全可以將程式6.4寫成下面的樣子,實現同樣的功能。
assume cs:b,ds:a,ss:c
a segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
a ends
c segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
c ends
b segment
d: mov ax,c
mov ss,ax
mov sp,20h ;希望用c段當作棧空間,設定ss:sp指向c:20
mov ax,a
mov ds,ax ;希望用ds:bx訪問a段中的資料,ds指向a段
mov bx,0 ;ds:bx指向a段中的第一個單元
mov cx,8
s : push [bx]
add bx,2
loop s ;以上將a段中的0~15單元中的8個字型資料依次入棧
mov bx,0
mov cx,8
s0: pop [bx]
add bx,2
loop s0 ;以上依次出棧8個字型資料到a段的0~15單元中
mov ax,4c00h
int 21h
b ends
end d ;d處是要執行的第一條指令,即程式的入口