文章目錄
基礎不會,啥都白費;基礎不牢,地動山搖。
彙編在基礎中的地位舉足輕重,學習彙編,可以幫助我們從CPU角度出發,理解程序,寫出更好的高級語言 程序。可以幫助我們理解程序的運行機制,知道原理,解決一些隱蔽的BUG。
學習步驟: 看視頻,看書,做筆記,理解爲主。習題獨立完成,全對,才能進入下一章的學習。
視頻: 小甲魚
教材: 《彙編語言(王爽)》(三版)
日期: 2020-07-01
進度:31/77
四、第一個程序:22/77
- 編寫源程序文件
- 編譯鏈接可執行文件
- 執行可執行文件中的程序
可執行文件:
- 程序(彙編指令翻譯成的機器碼) + 數據(源程序中定義的數據)
- 描述信息(如,程序有多大、要佔用多少內存空間)
1. 源程序結構
源程序: 源程序文件中的所有程序,皆稱爲源程序
程序: 源程序中最終 由計算機執行,處理的指令或數據。
彙編源程序由僞指令 和彙編指令 構成
彙編指令: 用來翻譯成機器碼
僞指令: 給編譯器執行的,讓編譯器執行相關編譯工作。
;
在彙編中表示註釋
段名 segment
和段名 ends
是一對,定義一個段,這個段用來存放代碼。(ends
後面的s
表示的是segment而不是複數的意思)
標號:
XXX segment
裏的XXX就是標號。它是一個段的名稱 ,最終會被編譯,連接成爲一個段的段地址(就類似C中的指針)
end
用來標識程序的結束assume
假設寄存器和程序中的某一個XXX segment ... XXX ends
段相關聯,很像給寄存器取別名
注意: 至少要有一個段(代碼段)
下圖展示了彙編指令 到可執行文件 的過程
2. 程序返回
DOS是一個單任務系統:
- 一個程序p2想要運行,必須得有一個正在運行的p1,把它從可執行文件加載入內存,並將CPU的控制權交給p2,p2才能運行。
- p2運行時,p1暫停運行。p2運行完畢,p1繼續運行。
比如仿DOS程序CMD,執行p2.exe時,CMD.exe停止執行。等到p2.exe執行完畢後,CMD.exe再次執行。
一個程序結束,然後返回調用的程序的行爲,叫做程序的返回 ret
要讓程序返回,需要以下兩行代碼
int 21H
是中斷的意思,中斷機制是DOS的發明。在Win中,變成了消息機制,從而單任務系統變爲多任務系統。
注意: 上面兩行代碼是固定的,原理後面再講。
3. 程序錯誤
程序錯誤分爲:語法錯誤 和邏輯錯誤
- 語法錯誤: 編譯器報錯
- 邏輯錯誤: 運行報錯
很類似
JAVA
中的運行期異常 和編譯期異常 。但是 錯誤 和 異常 一定要區分開來。錯誤是不可避免的,異常是可以避免的。所以,語法錯誤叫成 語法異常 更準確。邏輯錯誤,可能是由於代碼不規範,那就應該列爲 邏輯異常 ;如果是由於溢出之類導致內存出錯之類的,應該列爲 邏輯錯誤
源文件得不到目標文件的兩類錯誤:
severe errors
有億些錯誤- 找不到給出的源文件程序
4. 第一個程序
- 編寫源程序,源程序以
.asm
爲擴展名
- 利用masm.exe編譯
1.asm
生成目標文件1.obj
。直接輸入masm,然後如圖操作
- 利用link.exe編譯
1.obj
生成可執行文件1.exe
。直接輸入link,然後如圖操作
鏈接時報了個錯誤:
no stack segment
沒有棧段
原因是: 在連接過程中,並未因爲有“stacksg segment”,和assume了“ss:stacksg”就認爲設置了堆棧段。
解決辦法: 在代碼段開頭添加stack
關鍵字。但是! 這樣處理後,文件不能停止了,所以這個錯誤直接忽略吧
- 直接
1.exe
運行。(輸入1
運行也可以哦)
有簡化方式:
masm 文件名.asm;
可以直接默認編譯,不用輸入參數。link
也同樣有這個功能。關鍵在於;
。ml 文件名.asm
,編譯同時鏈接
5. 程序執行過程
Shell殼:
- 操作系統是一個龐大的、複雜的軟件系統
- 通用的操作系統,需要提供一個
Shell
程序,讓用戶能操作計算機系統 DOS
中的Shell
,叫做command.com
命令解釋器。DOS
啓動先初始化環境,然後運行command.com
,等待用戶輸入(命令也是一個可執行文件)- 用戶輸入可執行文件路徑,
command.com
會將這個程序加載入內存,設置CS/IP
指向程序入口。然後command.com
暫停,把CPU
控制權交出。程序結束後,將控制權交給command.com
,等待用戶輸入
下面這個過程,要爛熟於心:
6. Debug單步執行
之前的程序是沒有入口的,我們編寫一個有入口的(不一定用start,只要和end後面的標號相同即可,不要忘記冒號)
用Debug單步執行: 用t單步執行,到了程序返回的代碼int 21
用p
補充: 用
Debug
程序運行時,cx
默認記錄的是程序的長度(2.EXE爲c,表示12字節大小)
程序返回是返回上一級,2.asm
結束返回到debug.exe
,debug.exe
使用q
命令返回到command.com
程序被加載後,CS
應該和DS
指向同一個地址。看上圖,會發現,中間那一段是什麼?
- 在內存中找一塊地兒(特徵是,偏移地址爲0)。段地址爲SA,同時也是DS的值。
- 弄一個程序前綴數據區
PSP
,來進行程序和DOS的通信(和操作系統通信的一個接口)。這個區域佔256字節,也就是10H。 - 所以:,
查看PSP部分內存:
查看CS指向部分內存:
實驗3
- 編寫(答案在註釋裏)
- 編譯鏈接
- 單步執行
PSP
的頭兩個字節確實是CD
,通過查看彙編,我們可以知道,PSP
的第一條語句是int 20
,又是中斷機制,以後再學
五、[BX]和loop指令:28/77
1. debug運行程序和可執行文件寫代碼的不同
問題: Debug中有R命令,可以直接修改段寄存器的值。而mov只能通過通用寄存器間接修改。這兩者修改有什麼區別?
答: debug是程序,而mov是彙編語句,兩者沒關係!彙編中,只能通過jmp來跳轉程序。
- 先來一段程序:
- 查看內存:
- 怎麼回事? 會發現中括號沒被識別
- 查看源代碼: 果然沒識別
2. 內存單元&描述性符號
要完整描述一個內存單元,需要兩種信息:
- 內存單元地址
DS:[偏移地址]
- 內存單元長度(類型)
- 補充:
mov ax, [0]
,傳遞的是字。mov al, [0]
,傳遞的是字節。- 重要: 如
五.1
所示,[0]
這個不會被識別爲內存單元,需要使用:
mov bx, 0
mov ax, [BX]
也就是拿bx中轉
描述性符號1: ()
用來描述一個地址存放的內容。段寄存器:偏移地址
就相當於一個指針,它指向的內存地址的值也可以用()
描述。
注意: ax之類通用寄存器,本身保存的內容可以說是一個值,也可以說是一個地址。用
()
描述會得到這個寄存器本身保存的內容。寄存器更像是一個普通變量。
段寄存器雖然名義上是保存地址的,實際上保存的還是一個值。DS:0000
可以描述一個內存單元,(DS):0000
也可以描述一個內存單元。
上面的地址描述不規範,應當爲(DS)*16 + 0000
- 可以簡單粗暴的理解爲
()
就是取值的意思
描述性符號2: idata
表示常量
3. Loop指令
格式: loop 標號
執行loop會進行兩步操作:
- (cx) -= 1
- 如果(cx)==0,跳到標號處執行;否則向下執行
3.1 用循環計算10次方
通常,我們用loop
進行循環,cx
中存放循環次數
循環結構:
mov cx, 循環次數
s: 循環程序段
loop s
注意: 寫程序前,要小心數據溢出,要事先估算數據範圍
3.2 值得注意的點&調試
5.3節代碼:
注意: 彙編程序中,程序不能以字母開頭
一個問題: 一次t
只能走一步,如果循環的次數多,那是不是要一直搞t
?有沒有簡便方法?有的,g
命令(會將指定偏移地址前的代碼執行完畢)
用p
命令,可以後臺靜默執行循環:
4. Debug和彙編編譯器masm對指令的不同處理
- Debug默認數據16進制;masm默認數據10進制
- Debug用[0]來取內存單元的值;masm用[bx]來取內存單元的值,或DS:[0]來取
段前綴: 利用DS:[0]來取內存單元的值,這個DS叫做段前綴。也可以使用
es
/ss
/es
等其他方式指定段前綴。不寫的話,默認段前綴爲DS。
5. 安全的內存空間
內存空間不止我們寫的程序在用,還有包括操作系統在內的程序在使用。所以,我們在操作一個空間時,需要先知道它是安全的
- 我們在面臨一個選擇:在操作系統中安全、規矩的編程。還是利用匯編,直接去探查硬件真相?(我選後者,直接乾硬件!)
補充: 在純DOS方式(實模式)下,可以不理會DOS,直接用匯編去操作硬件
一般情況下,0:200~0:2ff
這個空間不被其他程序使用,是安全的。
安全的空間不夠了咋辦: 只要是合法的請求,系統會給的。具體怎麼給,日後再說。
6. 複製內存內容
實驗4答案: 0020H, 40H
看寄存器是多少字節的。
六、包含多個段的程序:31/77
爲什麼要使用多個段: 爲了日後的封裝
1. 程序的入口
我們先來看一個程序:
-
dw
即define word
,定義字型數據(db
定義字節數據) -
段地址: 這八個數據存在哪兒呢?因爲是放在cs代碼段中,所以數據存在cs代碼段中
-
數據的偏移地址: 從0開始,0、2、4…
-
代碼的偏移地址: 從16開始
? 程序如何知道從16開始執行呢?可以注意到我們把程序的入口定義在了第一條程序語句前,就是這個入口。
最關鍵的不是讀錯行,而是讀錯指令。可以試下,當沒有指定程序入口時,大概率也不會讀錯。8086很聰明,它知道從第16條開始執行。但是!因爲實際讀取的是機器碼,從而導致語句錯誤(直觀就是,用u查看我們所寫代碼對應的內存,會發現這些指令和我們寫的不一樣。原因就是被CPU把機器碼錯誤組合導致的。當然,有概率不會出錯。因爲本人寫的沒出錯,所以就沒截圖了。)總結:不一定報錯,但是結果一定錯
程序怎麼知道程序的入口: 直接去找end
,end
後面的標號是一個地址,程序直接去找這個地址執行
2. 程序中使用棧
需求: 把數據存入棧,然後逆序排列
- 代碼: 注意閱讀註釋
補充: 可以看出,彙編語言並沒有區域註釋。這一點上和C如出一轍。
- 查看內存:
- 程序執行完後,查看內存:
補充: 我們可以說,定義了八個字形數據。也可以說,開闢了八個字的內存空間,然後在裏面存放了數據。兩種角度,表達不同,理解不同,效果一樣
檢測點6.1
- mov CS:[bx], ax。題目的意思是,內存的數據在0:0 15,程序的數據在CS:0 15中,需要用內存的數據把程序的數據換了。
- CS
0020H(當然,也可以寫32)
pop CS:[bx](有的地方會說這裏的答案是SS:[bx]。注意,雖然SS的值等於CS,但是從邏輯上不符合題意。題意是用棧作中介,改變程序的數據。當然了,合理利用棧溢出是一種靈活性,所以我才說是注意,沒說是錯誤)
3. 在程序中使用不同的段
前面我們在程序中放入了數據、棧、代碼,我們需要時刻注意內存空間是數據、棧、還是段
- 這樣的程序未免過於混亂
- 以8086爲例,如果 數據段+棧段+代碼段>64K ,那麼一個段就放不下了
- 程序耦合性高。如果需要把8個字數據,變成9個字數據呢?是整體右移?還是在內存中再開闢一塊,然後利用代碼進行邏輯上的聯結?顯然,兩種方法都可行,但是牽一髮而動全身
代碼: 注意閱讀註釋
補充1: 可以看出,每個部分佔用一個段。(默認是連續的三個段)
原因: 如果一個數據 佔用N個字節,程序加載後,該段實際佔有的空間是16*(N/16 + 1)
補充2: 一個段的時候,DS比CS小10H,大的部分用來放
PSP
了。多個段的時候,DS只比CS小1H,那麼PSP在哪呢?經過本人試驗,發現PSP
在DS - 10H
處。
注意: CPU默認把機器碼當指令而不是數據(數據就直接讀寫,指令需要CPU運算)
補充3: CPU是從上往下讀的。先定義數據段,DS就小;先定義代碼段,CS就小。
補充4: 如果不指定程序入口,CPU會從上到下,把碰到的機器碼都當成指令
實驗5
- 沒什麼好說的
- 如果一個數據佔用N個字節,程序加載後,該段實際佔有的空間是16*(N/16 + 1)
- CPU是從上往下讀的。先定義數據段,DS就小;先定義代碼段,CS就小。
- 如果不指定程序入口,CPU會從上到下,把碰到的機器碼都當成指令
- 可以利用多個段寄存器,如圖
- 沒什麼好說的