匯編語言 - 王爽


前言

學習匯編的兩個最根本的目的:充分獲得底層編程的體驗,深刻理解機器運行程序的機理。

任何不以循序漸進的方式進行的學習,都將出現盲目探索和不成系統的情況,最終學習到的也大都是相對零散的知識,並不能建立起一個系統的知識結構。非循序漸進的學習,也達不到循序漸進學習所能達到的深度,因為后者是步步深入的,每一步都以前一步為基礎

必須遵守的原則:
①沒有通過監測點不要向下學習
②沒有完成當前的實驗不要向下學習
③每一個實驗都是后續內容的基礎,實驗的任務必須獨立完成
④本書的教學重心是:通過學習關鍵指令來深入理解機器工作的基本原理,培養底層編程意識和思想

我們必須通過一定的編程實踐,體驗一個裸機的環境,在一個沒有操作系統的環境中直接對硬件編程

第1章 基礎知識

匯編語言是直接在硬件之上工作的編程語言

機器語言/機器指令集是機器指令的集合

寄存器是CPU中可以存儲數據的器件

編譯器能夠將匯編指令轉換成機器指令的翻譯程序

匯編語言由以下3類指令組成:
1.匯編指令:機器碼的助記符,由對應的機器碼(核心) <匯編指令是機器指令便於記憶的書寫格式>
2.偽指令:沒有對應的機器碼,由編譯器執行,計算機並不執行
3.其他符號:+ - * /

要想讓一個CPU工作,就必須向它提供指令和數據。指令和數據在存儲器中存放,也就是我們平時所說的內存

CPU要從內存中讀數據,首先要指定存儲單元的地址,CPU在讀寫數據時還要指明。它要對哪個器件進行哪種操作
所以CPU要想進行數據的讀寫,必須和外部器件進行下面3類信息的交互->

  • 存儲單元的地址(地址信息)
  • 器件的選擇,讀或寫的命令(控制信息)
  • 讀或寫的數據(數據信息)

指令和數據沒有任何區別,都是二進制信息。CPU在工作的時候把有的信息看作數據,為同樣的信息賦予了不同的意義

1 Byte = 8 bit = 8個二進制位 = 8位二進制數據

在計算機中專門有連接CPU和其他芯片的導線,通常稱為總線,總線從邏輯上分為3類,地址總線、控制總線、數據總線

(1)CPU通過地址線將地址信息3發出
(2)CPU通過控制線發出內存讀命令,選中存儲器芯片並通知它將要從中讀取數據
(3)存儲器將3號單元中的數據8通過數據線送入CPU

CPU是通過地址總線來指定存儲器單元的
10根導線可以傳送10位二進制數據。而10位二進制數可以表示\(2^10\)個不同的數據
->一個CPU有N根地址線,則可以說這個CPU的地址總線的寬度為N

CPU與內存或其他器件之間的數據傳送是通過數據總線來進行的
數據總線的寬度決定了CPU和外界的數據傳送速度,1根數據總線一次傳送一個1位二進制數據
1 Byte = 8位二進制數據

8088CPU的數據總線寬度為8,8086CPU的數據總線寬度為16

8088CPU數據總線上的數據傳送情況:8088只有8根數據線,一次只能傳8位數據,所以向內存寫入數據89D8H時需要進行兩次數據傳送

8086CPU數據總線上的數據傳送情況:8088有16根數據線,一次可以傳16位數據,所以可一次傳送數據89D8H

1.10 控制總線

CPU對外部器件的控制是通過控制總線來進行的,在這里控制總線是個總稱,控制總線是一些不同控制線的集合。控制總線的寬度決定了CPU對外部器件的控制能力

1.1~1.10小結
(1)CPU可以直接使用的信息在存儲器中存放

(2)地址總線的寬度決定了CPU的尋址能力

(3)數據總線的寬度決定了CPU與其他期間進行數據傳送時的一次數據傳送量

(4)控制總線的寬度決定了CPU對系統中其他期間的控制能力

檢測點1.1
① 1個CPU的尋址能力為8KB,那么它的地址總線的寬度為---13
②8080、8088、8086、80286、80386的地址總線寬度分別為8根、8根、16根、16根、32根。則他們一次可以傳送的數據為---1B、1B、2B、2B、4B
③從內存中讀取1024字節的數據,8086至少要讀512次,80386至少要讀256次

CPU通過總線向接口卡發送命令,接口卡根據CPU的命令控制外設進行工作

1.14 各類存儲器芯片

一台PC機裝有多個存儲器芯片,從讀寫屬性上看分為兩類:隨機存儲器(RAM)和只讀存儲器(ROM)
RAM可讀可寫,但必須帶電存儲,關機后存儲的內容消失
ROM只能讀取不能寫入

存儲器從功能和連接上又可分為以下幾類:

  • RAM
  • BIOS(Basic Input Output System):基本輸入輸出系統
  • 接口卡上的RAM:顯示卡上的RAM一般稱為顯存。我們將需要顯示的內容寫入顯存,就會出現在顯示器上

CPU在操控存儲器的時候,把他們都當作內存來對待,把他們總的看做一個由若干存儲單元組成的邏輯存儲器,這個邏輯存儲器就是我們所說的內存地址空間

可以將所有的物理存儲器看作一個由若干存儲單元組成的邏輯存儲器,每個物理存儲器在這個邏輯存儲器中占有一個地址段,即一段地址空間。CPU在這段地址空間中讀寫數據,實際上就是在相對應的物理存儲器中讀寫數據

內存地址空間的大小受CPU地址總線寬度的限制

不同的計算機系統的內存地址空間的分配情況是不同的

系統中所有存儲器中的存儲單元都處於一個統一的邏輯存儲器中,它的容量受CPU尋址能力的限制。這個邏輯存儲器即是我們所說的內存地址空間

第2章 寄存器

在CPU中:

  • 運算器進行信息處理
  • 寄存器進行信息存儲
  • 控制器控制各種器件進行工作
  • 內部總線連接各種器件,在它們之間進行數據的傳送

對於一個匯編程序員來說,CPU中的主要部件是寄存器,寄存器是CPU中程序員可以用指令讀寫的部件。程序員通過改變各種寄存器中的內容來實現對CPU的控制

2.1 通用寄存器

8086CPU 的所有寄存器都是16位的

AX、BX、CX、DX這4個寄存器通常用來存放一般性的數據,被稱為通用寄存器,且都可分為兩個可獨立使用的8位寄存器來用

2.2 字在寄存器中的存儲

1 word = 2 byte

十六進制數的一位相當於二進制數的四位

**以后的課程中,我們多用十六進制來表示一個數據。為了區分不同的進制,在十六進制表示的數據的后面加 H ,在二進制表示的數據后面加 B **

2.3 幾條匯編指令

在進行數據傳送或運算時,要注意指令的兩個操作對象的位數應當是一致的

2.4 物理地址

CPU訪問內存單元時,要給出內存單元的地址。每一個內存單元在這個空間中都有唯一的地址,我們將這個唯一的地址稱為物理地址

16位結構的CPU描述了一個CPU具有下面幾方面的結構特性:

  • 運算器一次最多可以處理16位的數據
  • 寄存器的最大寬度為16位
  • 寄存器和運算器之間的通路位16位

2.6 8086CPU給出物理地址的方法

8086CPU有20位地址總線,可以傳送20位地址,達到1MB尋址能力,8086CPU又是16位結構,在內部一次性處理、傳輸、暫時存儲的地址為16位,如果將地址從內部簡單地發出,那么它只能送出16位的地址,表現出的尋址能力只有64KB

8086采用一種在內部用兩個16位地址合成的方法來形成一個20位的物理地址

物理地址 = 段地址*16 + 偏移地址

一個X進制的數據左移1位,相當於乘以X

2.7 段地址*16 + 偏移地址 = 物理地址的本質含義

  • 8086CPU的這種尋址能力時"基礎地址 + 偏移地址 = 物理地址",段地址*16可看作是基礎地址

2.8 段的概念

內存並沒有分段,段的划分來自於CPU,我們可以用分段的方式來管理內存。以后我們可以根據需要,將若干地址連續的內存單元看作一個段

段地址*16必然是16的倍數,所以一個段的起始地址也一定是16的倍數;偏移地址為16位,16位地址的尋址能力為64KB,所以一個段的長度最大為64KB

內存單元地址小結
CPU在訪問內存單元時,必須向內存地址提供內存單元的物理地址
段地址SA(Segment Address) 偏移地址OA(Offset Address) 有效地址EA(Effective Address)

檢測點2.2
①有一數據存放在內存20000H單元中,現給定段地址為SA,若想用偏移地址尋到此單元。則SA應滿足的條件是:最小為 1001H*,最大為 2000H

2.9 段寄存器

段地址再8086CPU的段寄存器中存放,8086有4個段寄存器:CS、DS、SS、ES

2.10 CS和IP

CS為代碼段寄存器,IP為指令指針寄存器

8086機中,任意時刻,設CS中的內容為M,IP中的內容為N,8086CPU將從內存M*16 + N單元開始,讀取一條指令並執行

8086機中,任意時刻,CPU將CS:IP指向的內容當作指令執行 <區別指令和數據的方法>

讀取一條指令后,IP中的值自動增加,以使CPU可以讀取下一條指令。因當前讀入的指令B82301長度為3個字節,所以IP中的值+3。此時CS:IP指向內存單元2000:0003

8086CPU的工作過程可以簡要描述如下:
(1)從CS:IP指向的內存單元讀取指令,讀取的指令進入指令緩沖器
(2)IP = IP + 所讀取指令的長度,從而指向下一條指令
(3)執行指令。轉到步驟(1),重復這個過程

CPU將CS:IP指向的內存單元中的內容看作指令。如果說內存中的一段信息曾被CPU執行過的話,那么它所在的內存單元必然被CS:IP指向過

2.11 修改CS、IP的指令

程序員可以通過改變CS、IP中的內容來控制CPU執行目標指令

若想同時修改CS、IP的內容,可用形如"jmp段地址:偏移地址"的指令完成
jmp 2AE3:3,執行后:CS = 2AE3H,IP = 0003H,CPU將從2AE33H處讀取指令

"jmp 段地址:偏移地址" 指令的功能為:用指令中給出的段地址修改CS,偏移地址修改IP

"jmp 某一合法寄存器"指令的功能為:用寄存器中的值修改IP jmp ax 在含義上相當於 mov IP,ax

若想僅修改IP中的內容,可用形如"jmp 某一合法寄存器"的指令完成,如:
jmp ax,指令執行前:ax = 1000H,CS = 2000H,IP = 0003H
指令執行后:ax = 1000H,CS = 2000H,IP = 1000H

CS、IP中的內容送入地址加法器(地址加法器完成:物理地址 = 段地址16 + 偏移地址)*

要讓CPU執行我們放在代碼段中的指令,必須將CS:IP指向所定義的代碼段中的第一條指令的首地址

檢測點2.3
下面的3條指令執行后,CPU幾次修改IP?都是在什么時候?最后IP中的值是多少?
mov ax,bx
sub ax,ax
jmp ax

一共修改4次
第一次:讀取mov ax,bx后
第二次:讀取sub ax,ax后
第三次:讀取jmp ax后
第四次:執行jmp ax修改IP
最后IP的值為0000H,因為最后ax中的值為0000H,所以IP中的值也為0000H

實驗一 查看CPU和內存,用機器指令和匯編指令編程


用 R 命令查看、改變CPU寄存器的內容
要修改AX中的值:

  • 輸入“r ax”后按 Enter 鍵,將出現“:”,在后面輸入要寫入的數據后按 Enter 鍵,即完成對AX中內容的修改

(1)用 Debug 的 D 命令查看內存中的內容

  • 要查看內存10000H處的內容,可以用“d 短地址:偏移地址”的格式來查看,輸入“d 1000:0”即可查看內存10000H處的內容

  • 使用“d段地址:偏移地址”,debug將列出從指定內存單元開始的128個內存單元的內容

  • 使用“d段地址:偏移地址”之后,接着使用D命令,可列出后續的內容;也可以指定D命令的查看范圍,此時采用的“d段地址:起始偏移地址 結尾偏移地址”的格式

(2)用 Debug 的 E 命令改寫內存中的內容

  • 要將內存1000:0~1000:9單元中的內容分別寫為0、1、2、3、4、5、6、7、8、9,可以用“e 起始地址 數據 數據 數據...”的格式來進行

  • 也可以一個一個地改寫內存中的內容
    輸入 e 1000:10 按 Enter 鍵 -> 輸入數據(如果輸入空格則不對當前內存單元進行改寫)

  • 可以用E命令向內存中寫入字符/字符串:-e 1000:0 1 'a' 2 'b' 3 'c'

用E命令向內存中寫入機器碼,用U命令查看內存中機器碼的含義,用T命令執行內存中的機器碼

  • 若要用T命令控制CPU執行我們寫到 1000:0 的指令,必須先讓CS:IP指向1000:0;接着用R命令修改CS、IP中的內容,使CS:IP 指向1000:0;再使用T命令來執行我們寫入的指令

用 Debug 的 A 命令以匯編指令的形式在內存中寫入機器指令

總結
R命令:查看、修改CPU中寄存器的內容
D命令:查看內存中的內容
E命令:修改內存中那個的內容
U命令:將內存中的內容解釋為機器指令和對飲的匯編指令
T命令:執行CS:IP指向的內存單元處的指令
A命令:以匯編指令的形式向內存中寫入指令

在匯編語言學習中,Debug是一個經常用到的工具,在學習預備知識中,應該一邊看書一邊在機器上操作

第3章 寄存器(訪問內存)

CPU中,用16位寄存器來存儲一個字。高8位存放高位字節,低8位存放低位字節。

字節是內存單元(一個單元存放一個字節),則一個字要用兩個地址連續的內存單元來存放,這個字的低位字節存放在低地址單元中,高位字節存放在高地址單元中。

字單元:存放一個字型數據(16位)的內存單元,由兩個地址連續的內存單元組成
N地址字單元:起始地址為N的字單元

問題3.1

根據圖3.1:
①0地址字單元中存放的字節型數據是多少?

  • 20H
    ②0地址字單元中存放的字型數據是多少?
  • 4E20H
    ③1地址字單元中存放的字形數據是多少?
  • 124EH

3.2 DS和[address]

ds:段寄存器 通常用來存放要訪問的段地址

要讀取10000H單元的內容,可以用如下的程序段進行:
mov bx,1000H
mov ds,bx
mov al,[0]
上面的3條指令將10000H(1000:0)中的數據讀到al中。

[...]表示一個內存單元,[...]中的0表示內存單元的偏移地址

指令執行時,8086CPU自動取ds中的數據為內存單元的段地址

8086CPU不支持將數據直接送入段寄存器的操作,要將1000H送入ds,只能用一個寄存器來進行中轉:現將1000H送入一個一般的寄存器,如bx,再將bx中的內容送入ds

問題3.2
如何將al中的數據送入內存單元10000H中?
解:從內存單元到寄存器的格式是:"mov 寄存器名,內存單元地址";從寄存器到內存單元是:"mov 內存單元地址,寄存器名"
10000H可以表示為1000:0,用ds存放段地址1000H,偏移地址是0,則mov [0],al可完成從al到10000H的數據傳送。
完整指令為:
mov bx,1000H
mov ds,bx
mov [0],al

3.3 字的傳送

8086是16位結構,有16根數據線,所以可以一次性傳送16位的數據/1個字,只要在mov指令中給出16位的寄存器就可以進行16位數據的傳送了

ds:段寄存器


mov ax,1000H
mov ds,ax //前兩條指令的目的是將ds設為1000H

mov ax,[0] //1000:0處存放的字形數據送入ax,1000:1單元存放字型數據的高8位:11H;1000:0單元存放字型數據的低8位:23H,所以1000:0處存放的字型數據為1123H

[0]對應0和1
[1]對應1和2,以此類推

寫出下面的指令執行后內存中的值

mov [0],ax //ax中的字型數據送到1000:0處

bx = 2C34H
sub bx,[2]
bx = 1B12H //bx = bx中的字型數據-1000:2處的字形數據=2C34H-1122H=1B12H

3.4 mov、add、sub指令

  • 不能將常數放在段寄存器
  • 不能兩個段寄存器
  • 不能兩個內存

mov [0000],cs 執行后,CS中的數據(0B39H)被寫入1000:0處,1000:1單元存放0BH,1000:0單元存放39H

3.5 數據段

比如將123B0H~123B9H的內存單元定義為數據段,現在要累加這個數據段中的前3個單元的數據
mov ax,123BH
mov ds,ax
mov al,0 //用al存放累加結果
add al,[0] //將數據段第一個單元(偏移地址為0)中的數值加到al中
add al,[1] //將數據段第二個單元(偏移地址為0)中的數值加到al中
add al,[2] //將數據段第三個單元(偏移地址為0)中的數值加到al中

一個字型數據占兩個單元

3.1~3.5小結*
1.字節在內存中存儲時,要用兩個地址連續的內存單元來存放,字的低位字節存放在低地址單元中,高位字節存放在高地址單元中
2.用 mov 指令訪問內存單元,可以在mov指令中只給出單元的偏移地址,此時段地址默認在DS寄存器中
3.[address]表示一個偏移地址為address的內存單元
4.在內存和寄存器之間傳送字型數據時,高地址單元和高8位寄存器、低地址單元和低8位寄存器相對應
5.mov、add、sub是具有兩個操作對象的指令。jmp是具有一個操作對象的指令

檢測點3.1

為什么AX = 2662H?
解:[0000]表示的是,而"d 0:0 1f"表示的是"d 段地址:起始偏移地址 結尾偏移地址",[0000]對應的兩個單元為62和26,然后1f就對應下面那行

3.6 棧

在這里我們對棧的研究僅限於這個角度:棧是一種具有特殊的訪問方式的存儲空間。它的特殊性在於:最后進入這個空間的數據,最先出去

棧有兩個基本的操作:入棧和出棧。入棧就是將一個新的元素放到棧頂,出棧就是從棧頂取出一個元素

3.7 CPU提供的棧機制

在基於8086CPU編程的時候,可以將一段內存當作棧來使用

8086提供入棧(PUSH)和出棧(POP)指令

push ax表示將寄存器ax中的數據送入棧中;pop ax表示從棧頂取出數據送入ax。8086CPU的入棧和出棧都是以為單位進行的

字型數據用兩個單元存放,高地址單元存放高8位,低地址單元存放

CPU執行 push 和 pop 指令時,將對這段空間按照棧的后進先出的規則進行訪問

CPU如何知道當前要執行的指令所在的位置?
解:CS、IP中存放着當前指令的段地址和偏移地址

CPU如何知道棧頂的位置?
解:任意時刻,SS:SP指向棧頂元素,8086CPU中有2個寄存器,段寄存器SS和寄存器SP

push ax的執行,由以下兩步完成
(1)SP = SP - 2,SS:SP指向當前棧頂前面的單元,以當前棧頂前面的單元為新的棧頂
(2)將ax中的內容送入SS:SP指向的內存單元處,SS:SP此時指向新棧頂


入棧時,棧頂從高地址向低地址方向增長


任意時刻SS:SP指向棧頂元素,當棧為空的時候,棧中沒有元素,就沒有棧頂元素,所以SS:SP只能指向棧的最底部單元下面的單元,該單元的偏移地址為棧最底部的子單元的偏移地址+2。棧最底部字單元的地址為1000:000E,所以棧空時,SP = 0010H

** pop ax 的執行過程和 push ax 剛好相反,由以下兩步完成**
(1)將SS:SP指向的內存單元處的數據送入ax中
(2)SP = SP + 2,SS:SP指向當前棧頂下面的單元,以當前棧頂下面的單元為新的棧頂


出棧后,SS:SP 指向新的棧頂1000EH,pop操作前的棧頂元素。1000CH處的2266H依然存在,但是它已不在棧中。當再次執行push等入棧指令后,SS:SP 移至1000CH,並在里面寫入新的數據,它將被覆蓋

3.8 棧頂超界問題

當棧滿時再使用push指令入棧,或棧空的時候再使用pop指令出棧,都將發生棧頂超界的問題。所以在編程的時候要自己操心棧頂超界的問題

3.9 push、pop指令

push 和 pop 指令的格式可以是如下形式:
push 寄存器/段寄存器; //將一個寄存器/段寄存器中的數據入棧
pop 寄存器/段寄存器; //出棧,用一個寄存器/段寄存器接收出棧的數據

也可以是如下形式:
push 內存單元; //將一個內存單元處的字入棧(注意:棧操作都是以字為單位)
pop 內存單元; //出棧,用一個內存字單元接收出棧的數據

比如:
mov ax,1000H
mov ds,ax; //內存單元的短地址要放在ds中
push [0]; //將1000:0處的字壓入棧中
pop [2]; //出棧,出棧的數據送入1000:2處

指令執行前,CPU要知道內存單元的地址,可以在push、pop指令中只給出內存單元的偏移地址,段地址在指令執行時,CPU從ds中取得

問題3.7
編程將10000H~1000FH這段空間當作棧,初始狀態棧是空的,將AX、BX、DS中的數據入棧

分析:
mov ax,1000H //
mov ss,ax //設置棧的段地址 SS = 1000H,不能直接向段寄存器SS中送入數據,所以用ax中轉
mov sp,0010H //設置棧頂的偏移地址,因棧為空,所以SP = 0010H,如果對棧為空時SP的設置還有疑問,需要復習3.7節、問題3.6
上面的3條指令設置棧頂地址,編程中要自己注意棧的大小
push ax
push bx
push ds

問題3.8
編程:
(1)將10000H~1000FH 這段空間當作棧,初始狀態棧是空的
(2)設置AX = 001AH,BX = 001BH
(3)將AX、BX中的數據入棧
(4)將AX、BX清零
(5)從棧中恢復AX、BX原來的內容

分析:
設置好棧頂位置->
mov ax,1000H
mov ss,ax
mov sp,0010H

mov ax,001AH
mov bx,001BH

push ax
push bx //ax,bx入棧

sub ax,ax //將ax清零,也可以用mov ax,0
//sub ax,ax的機器碼為2個字節
//mov ax,0的機器碼為3個字節

sub bx,bx

pop bx
pop ax //從棧中恢復ax、bx原來的數據,當前棧頂的內容是bx中原來的內容:001BH,
//ax中原來的內容001AH在棧頂的下面,所以要先pop bx,然后再pop ax


用棧暫存以后需要恢復的寄存器中的內容時,出棧的順序要和入棧的順序相反<后進先出>

問題3.9
編程:
(1)將10000H~1000FH 這段空間當作棧,初始狀態棧是空的 <棧是空的:1000F + 1 = 10010>
(2)設置AX = 001AH,BX = 001BH
(3)利用棧,交換AX和BX中的數據

分析:先初始化棧頂
mov ax,1000H
mov ss,ax
mov sp,0010H

mov ax,001AH
mov bx,001BH

push ax
push bx //ax、bx入棧

pop ax //當前棧頂的數據是bx中原來的數據(因為bx后入棧):001BH,所以先pop ax, ax = 001BH
pop bx //執行pop ax后,棧頂的數據為ax原來的數據,所以再pop bx,bx = 001AH

問題3.10
如果要在10000H處寫入字型數據2266H,可以用以下的代碼完成
mov ax,1000H
mov ds,ax
mov ax,2266H
mov [0],ax

補全下面的代碼,使它能夠完成同樣的功能:在10000H處寫入字型數據2266H。要求:不能使用"mov 內存單元,寄存器"這類指令

mov ax,1000H
mov ss,ax
mov sp,2
mov ax,2266H
push ax

解析:題目最后兩行,將ax中的2266H壓入棧中,就是說最終應該是push ax將2266H寫入10000H處,問題是:如何使push ax訪問的內存單元是10000H
push ax是入棧指令,它將在棧頂之上壓入信的數據。它的執行過程是,先將記錄棧頂偏移地址的SP寄存器中的內容-2,使得SS:SP指向新的棧頂單元,然后再將寄存器中的數據送入SS:SP指向的新的棧頂單元

所以要在執行push ax之前,將SS:SP指向10002H,這樣在執行push ax的時候,CPU先將SP = SP - 2,使得SS:SP指向10000H,再將ax中的數據送入SS:SP指向的內存單元處(即10000H處)

push、pop實質上就是一種內存傳送指令,可以在寄存器和內存之間傳送數據,與mov指令不同的是,push和pop指令訪問的內蘊單元的地址不是在指令中給出的,而是由SS:SP指出的。同時push和pop指令還要改變SP中的內容

push和pop指令同mov指令不同,CPU執行mov指令只需一步操作,就是傳送,而執行push、pop指令卻需要兩步操作。
執行push時,CPU的兩步操作是:先改變SP,后向SS:SP處傳送。
執行pop時,CPU的兩步操作是:先讀取SS:SP處的數據,后改變SP

push、pop等棧操作指令,修改的只是SP,所以棧頂的變化范圍最大是0~FFFFH

**8086CPU提供了棧操作機制,方案如下:
(1)在SS、SP中存放棧頂的段地址和偏移地址,提供入棧和出棧指令,他們根據SS:SP指示的地址,按照棧的方式訪問內存單元
(2)push指令的執行步驟:
1.SP = SP - 2
2.向SS:SP指向的字單元中送入數據
(3)pop指令的執行步驟:
1.從SS:SP指向的字單元中讀取數據
2.SP = SP + 2
(4)任意時刻,SS:SP指向棧頂元素
(5)8086只記錄棧頂,棧空間的大小我們要自己管理
(6)用棧來暫存以后需要恢復的寄存器的內容時,寄存器出棧的順序要和入棧的順序相反
(7)push、pop實質上是一種內存傳送指令,注意它們的靈活運用

棧是一種非常重要的機制,一定要深入理解,靈活掌握

3.10 棧段

我們可以將長度為N(N≤64KB)的一組地址里連續、起始地址為16的倍數的內存單元,當作棧空間來用,從而定義了一個棧段
比如我們將10010H~1001FH這段長度為16字節的內存空間當作棧來用,以棧的方式進行訪問。這段空間就可以稱為一個棧段,段地址為1001H,大小為16字節

問題3.11
如果將10000H~1FFFFH這段空間當作棧段,初始狀態棧是空的,此時SS = 1000H,SP = ?
解析:因為SS:SP指向棧頂元素,棧又是空的,所以SS:SP只能指向棧的最底部單元下面的單元,該單元的地址為棧最底部的字單元的地址+2。棧最底部字單元的地址為1000:FFFE,所以棧空時,SP = 0000H

棧頂的變化范圍是0~FFFFH,從棧空的時候SP = 0,一直壓棧,直到棧滿時 SP = 0;如果再次壓棧,棧頂將環繞,覆蓋了原來棧中的內容。

段的綜述

  • 對於數據段,將它的段地址放在DS中,用mov、add、sub等訪問內存單元的指令時,CPU就將我們定義的數據段中的內容當作數據來訪問
  • 對於代碼段,將它的段地址放在CS中,將段中第一條指令的偏移地址放在IP中,這樣CPU就將執行我們定義的代碼段中的指令
  • 對於棧段,將它的段地址放在SS中,將棧頂單元的偏移地址放在SP中,這樣CPU在需要進行棧操作的時候,比如執行push、pop指令等,就將我們定義的棧段當作棧空間來用

可見,不管我們如何安排,CPU將內存中的某段內容當作代碼,是因CS:IP指向了那里;CPU將某段內存當作棧,是因為SS:SP指向了那里。我們一定要清楚CPU的工作機理,才能控制CPU按照我們的安排運行

實驗2 用機器指令和匯編指令編程
Debug的T命令在執行修改寄存器SS的指令時,下一條指令也緊接着被執行

第四章 第一個程序

現在我們將開始編寫完整的匯編語言程序,用編譯和連接程序將他們編譯連接成為可執行文件(如*.exe文件),在操作系統中運行

4.1 一個源程序從寫出到執行的過程

1.編寫匯編源程序
2.對源程序進行編譯連接
3.執行可執行文件中的程序

4.2 源程序

匯編語言源程序中,包含兩種指令:匯編指令和偽指令
匯編指令是有對應的機器碼的指令,可以被編譯為機器指令,可以被編譯為機器指令,最終為CPU所執行
偽指令沒有對應的機器指令,最終不被CPU所執行。偽指令是由編譯器來執行的指令,編譯器根據偽指令來進行相關的編譯工作

(1)XXX segment
segment和ends是一對成對使用的偽指令,這是在寫可被編譯器編譯的匯編程序時,必須要用到的一對偽指令。
功能是定義一個段,segment說明一個段開始,ends說明一個段結束。一個段必須有一個名稱來標識
格式:
段名 segment
...
...
段名 ends

(2)end
end是一個匯編程序的結束標記。編譯器遇到end就結束對源程序的編譯

(3)assume(含義為“假設”)
在需要的情況下,編譯程序可以將段寄存器和某一個具體的段相聯系
比如用assume cs:codesg將用作代碼段的段codesg和CPU中的段寄存器cs聯系起來

編程運算\(2^3\),用源程序寫:
(1)定義一個段,名稱為abc
(2)在這個段中寫入匯編指令,來實現我們的任務
(3)指出程序在何處結束
(4)abc被當作代碼來用,所以應該將abc和cs聯系起來(也不是非做不可)

最終程序:
assume cs:abc
abc segment

mov ax,2
add ax,ax
add ax,ax

abc ends

end

程序返回
一個程序結束后,將CPU的控制權交還給使他得以運行的程序
如何返回?要在程序的末尾添加返回的程序段:
mov ax,4c00H
int 21H
這兩條指令所實現的功能就是程序返回

語法錯誤和邏輯錯誤
程序在編譯時被編譯器發現的錯誤是語法錯誤
在源程序編譯后,在運行時發生的錯誤是邏輯錯誤

4.3 編譯源程序

可以用任意的文本編輯器來編輯源程序,只要將其存儲為純文本文件即可

目標文件(.obj)是我們最終要得到的結果

4.5 連接

連接的作用有以下幾個:
1.當源程序很大時,可以將它分為多個源程序文件來編譯,每個源程序編譯成為目標文件后,再用連接程序將他們連接到一起,生成一個可執行文件
2.程序中調用了某個庫文件中的子程序,需要將這個庫文件和該程序生成的目標文件連接到一起,生成一個可執行文件
3.一個源程序編譯后,得到了存有機器碼的目標文件,目標文件中的有些內容還不能直接用來生成可執行文件,連接程序將這些內容處理為最終的可執行信息。所以只要有一個源程序文件,而又不需要調用某個庫中的子程序的情況下,也必須用連接程序對目標文件進行處理,生成可執行文件

操作系統的外殼
任何通用的操作系統,都要提供一個稱為shell(外殼)的程序,用戶使用這個程序來操作計算機系統進行工作
DOS中有一個程序command.com,這個程序在DOS中稱為命令解釋器,就是DOS系統的shell

問題4.1
此時有一個正在運行的程序將1.exe中的程序加載入內存,這個正在運行的程序是什么?它將程序加載入內存后,如何使程序得以運行?
程序運行結束后,返回到哪里?
解:
(1)在DOS中直接執行1.exe時,是正在運行的command,將1.exe中的程序加載入內存
(2)command設置CPU的CS:IP指向程序的第一條指令(即程序的入口),從而使程序得以運行
(3)程序運行結束后返回到command中,CPU繼續運行command

DOS系統中.EXE文件中的程序的加載過程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM