彙編系列文章已經更新了三篇,每一篇都是筆者用心總結,希望對你有幫助
之前的文章我們主要聊了一些基本的彙編指令,並且通過一個名為 Debug 的除錯軟體,讓我們看到了記憶體中是如何儲存指令和資料的,在學習了這些之後,我們就可以瞭解彙編程式了。
程式的執行過程
首先通過一個示意圖給大家介紹一下程式的執行過程,我們以 C 語言一個簡單的 hello.c 程式為例。
這就是一個完整的 hello world 程式執行過程,會涉及幾個核心元件:前處理器、編譯器、彙編器、聯結器,下面我們逐個擊破。
- 預處理階段(Preprocessing phase),前處理器會根據開始的
#
字元,修改源 C 程式。#include <stdio.h> 命令就會告訴前處理器去讀系統標頭檔案stdio.h
中的內容,並把它插入到程式作為文字。 - 然後是
編譯階段(Compilation phase)
,編譯器會把文字檔案hello.i
翻譯成文字hello.s
,它包括一段組合語言程式(assembly-language program)。
組合語言是非常有用的,因為它能夠針對不同高階語言來提供自己的一套標準輸出語言。
- 編譯完成之後是
彙編階段(Assembly phase)
,這一步彙編器會把 hello.s 翻譯成機器指令,把這些指令打包成可重定位的二進位制程式(relocatable object program) 放在 hello.c 檔案中。 - 最後一個是
連結階段(Linking phase)
,這個階段就是用連結器把翻譯過後的程式合併在一起,生成在作業系統上直接執行的可執行檔案的過程。
所以,一般來說,可執行檔案包括兩個方面
- 程式和資料,這些是構成可執行檔案的基本資訊。
- 相關的描述資訊,比如空間多大,程式有多大等,這些是構成可執行檔案的必要因素。
認識彙編程式
同樣的,先上一則彙編程式碼,然後下面再慢慢概述。
assume cs:code
code segment
mov ax,1234H
add ax,ax
mov bx,1111H
add bx,bx
code ends
end
這段彙編程式碼有幾個地方你可能不太瞭解,不過 mov、add 指令你應該知道是什麼意思(如果你看完筆者之前文章並進行了仔細研究的話)。
構成彙編程式的指令分為兩種:一種是彙編指令
,一種是偽指令
,彙編指令就是我們上面提到的 mov 、add 指令,這些指令有實際的意義,比如 mov 就是移動暫存器或者資料,add 就是對暫存器或者資料進行加法操作。而且 mov 和 add 這類彙編指令在記憶體中有對應的機器碼存在,最終會有 CPU 執行。而偽指令沒有實際的意義,它們指令簡單的定義一個程式段,這些偽指令會由編譯器
來直接解釋,它們在記憶體中沒有對應的機器碼,所以不會由 CPU 來執行。
上面提到的偽指令有三種,即
code segment
......
code ends
segment 和 ends 是一組成對出現的指令,而且這一對指令必須成對出現,缺了誰都不行。這一對指令定義了一個段,segment 標識著段的開始,ends 標識著段的結束。code 表示段的名稱,段名稱可以隨意替換。
彙編程式由多個段組成(至少包含一個段),這些段被用來存放程式碼、資料或者當做棧空間來使用。上面例子程式碼中的段由程式碼組成,所以叫程式碼段。
除了段之外,彙編程式還需要有 assume
,這同樣是一條偽指令,它的意思是假設,它假設某一段暫存器和某個段相關聯,通過 assume 來說明這種關聯關係。assume 不用深入理解,我們只要知道程式設計時將特定用途的段和相關暫存器關聯起來即可。
end
是一段彙編程式結束的標誌,它也是一條偽指令,編譯器在編譯彙編程式的過程中,遇到 end 就會停止編譯,所以,如果我們彙編程式寫完了,就需要在程式的末尾加上 end ,表示程式的結束。
在彙編程式中,除了彙編指令和偽指令,還有一種標號
,比如上面程式碼中的 code,標號位於 segment 的前面,作為段的名稱,這個段的名稱最終將被編譯、連線處理為一個段的段地址。
再次提醒下,注意這裡不要搞混了 end 和 ends ,ends 是和 segment 一起使用的表示彙編段,而 end 是彙編結束的標識。
所以總結下,用匯編語言編寫的源程式,包括偽指令和彙編指令,偽指令是由編譯器來執行,彙編指令可以翻譯成機器程式碼並最終由 CPU 執行。
以後,我們可以將源程式檔案中的內容稱為源程式,將源程式中最終由計算機執行、處理的指令或資料,稱為程式。程式最先以彙編指令的形式存在於原程式中,然後經過編譯、連線後轉變為機器碼,儲存在可執行檔案中,如下圖所示
所以,總結一點來說,編寫一個彙編程式主要分為下面這幾步
- 首先定義一個段 ,比如 code、abc 等
- 在段中寫入彙編指令
- 指出程式在何時處結束
- 標號要和暫存器關聯起來。
- 程式返回(後面要說)
程式返回
一個完整的程式是要有返回條件的,程式只有在執行完相關程式碼後,執行返回條件,讓出 CPU 執行權,作業系統才會分配時間片給其他程式,程式不能一直霸佔著 CPU 不放,這是一種資源的浪費,而且一直佔用著 CPU,也會導致程式崩潰。
組合語言中,實現程式返回的指令只有兩行
mov ax,4c00H
int 21H
解釋下這兩句指令的意思:
mov ax,4c00H 就是把 4c00 移動到 ax,中,INT 21H 是呼叫系統中斷指令,這兩句程式碼起作用的就是 AH = 4CH,意思就是呼叫 INT 21H 的 4CH 號中斷,該中斷就是安全退出程式。
到目前為止,我們已經瞭解到了幾種和結束的相關內容,比如段結束,彙編程式結束、還有我們剛剛說的程式返回,下表列出了這三個指令的區別。
程式錯誤
一般來說,組合語言的程式錯誤分為兩種:即語法錯誤和邏輯錯誤。
語法錯誤很簡單,說白了就是你組合語言指令寫錯了,這個程式編譯時期就能夠發現。
邏輯錯誤是在執行時發生的,一般不容易被發現,排查起來比較困難,比如下面這段程式碼不寫程式返回就是屬於邏輯錯誤。
assume cs:code
code segment
mov ax,1234H
add ax,ax
mov bx,1111H
add bx,bx
code ends
end
為什麼?因為你這段程式碼沒有加程式返回邏輯。類似的這種邏輯錯誤還有很多,這些錯誤需要在具體的場景中才能發現。
編寫彙編
下面我們開始用編輯器來編寫彙編源程式,只要將彙編儲存為文字檔案,再經過編譯器編輯,CPU 執行即可。
我們可以使用多種文字格式來編寫彙編程式,比如我們可以使用最簡單的文字檔案來編寫(基於 win7 作業系統環境)
assume cs:codeseg
codeseg segment
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
codeseg ends
end
編寫完成後,儲存為 .asm
字尾檔案,這是一種彙編格式。
編譯
一個完整的彙編程式執行流程分為編寫、編譯、連結和執行,所以接下來我們需要對編寫完成的彙編程式進行編譯。在編譯之前我們需要找到一個相應的編譯器,這裡我們採用的是 masm 5.0 彙編編譯器,執行程式是 masm.exe
。
(為了防止大家再從網站上亂找資源,我下載下來放在了網盤中,大家在程式設計師cxuan 後臺回覆 masm
即可領取)
說到使用 masm 5.0 的這個過程我踩了很多坑,這裡給大家提示下,及時閉坑!!!
- masm 5.0 是穩定版本,網路上流傳的 6.x 不知道怎麼樣,我是沒執行成功。
- masm 5.0 要在 win7 環境下執行,我使用 win11 測試,程式不相容,不知道其他版本如何。win7 版本可以正常執行。
安裝完成後,我們開啟 cmd ,進入下載並解壓好的 masm 5.0 資料夾下。
然後直接鍵入 masm
執行 masm 後,首先會顯示一些版本資訊,然後輸入需要被編譯的原程式檔名稱,這裡需要注意一下,[.ASM]
提示我們,預設的副檔名是 asm,比如我們要編譯的源程式檔名是 test.asm
,這裡直接輸入 asm 即可。如果源程式檔案不是以 .asm 為字尾,需要輸入它的全名,也就是 test.txt。
這裡我們輸入的是 test,因為我們編寫的檔案是 .asm 字尾。
輸入源程式檔名後,按 enter 鍵,程式會提示我們輸入要編譯出的目標檔名稱,目標檔名稱是我們對源程式進行編譯後的最終結果。Object filename 的字尾名是 .obj
,因為 .asm 檔案會自動編譯為 .obj 檔案,所以我們不必再指定檔名,直接按 enter 鍵,會直接生成 .obj 檔案。
確定了目標檔名稱後,會出現 Source listing ,這是提示我們要輸入列表檔案的名稱,這個檔案是編譯器將源程式編譯為目標檔案的過程中產生的中間結果,可以讓編譯器不生成這個檔案,直接鍵入 enter 即可。如果編譯器要生成這個檔案,它的字尾名是 .lst
。
然後繼續提示出 Cross-reference ,這是提示我們要輸入交叉引用檔名稱,這個檔案和 Source listing 一樣,是編譯器產生的中間結果,可以不讓編譯器生成這個檔案,我們直接按 enter 即可。如果編譯器要生成這個檔案,它的字尾名是 .crf
。
最後編譯器會進行一個結果輸出,這個輸出結果會顯示警告錯誤和必須要改正的錯誤,可以從上圖中看出來,我們程式沒有警告和編譯錯誤。
在輸入源程式檔名的時候要指出所在路徑,如果遇到 unable to open input file 這個問題,最好把彙編程式直接放在 C 盤,我放在桌面上,也就是 C:\Users\Administrator\Desktop 下,也會出現此錯誤。
連線
在對源程式編譯後得到目標檔案後,我們需要對目標檔案進行連線,從而得到可執行檔案。上一步我們得到了 .obj檔案,現在我們需要將 .obj 檔案連線成為 .exe 也就是可執行檔案。
為了實現我們的需求,我們需要藉助微軟的 Overlay Linker 3.60 聯結器,檔名為 link.exe,這個應用程式不用再次下載(在我公眾號回覆拿到的軟體會包括編譯器和聯結器,解壓後,它們都會在 masm 資料夾下)。
現在我們進入 DOS,cd 到 masm 檔案中,鍵入 link
。
執行 link 後,會出現一些版本資訊,然後提示需要被連線的目標檔名稱,這裡仍需要注意,預設檔案是 .obj 結尾,所以如果你需要連線的檔案是 obj 檔案,就不用輸入字尾名,如果不是 obj 檔案,則需要輸入全名。
我們剛剛編譯了一個 test.obj 檔案,所以我們直接對這個 obj 檔案進行連線。
輸入要連線的檔名(這裡仍需要輸入 obj 所在的路徑),按 enter 。
輸入 enter 後,會繼續來一個三連提示。
第一個提示表明程式繼續提示我們輸入要生成可執行檔案的名稱,可執行檔案是我們對一個程式進行連線要得到的最終結果,預設的 .exe 檔案是 TEST.EXE ,所以我們不再需要指定檔名。這裡也可以指定生成可執行檔案所在的目錄,我們也不需要,繼續向下走。
第二個提示是連線程式提示輸入映像檔案的名稱,這個檔案是連線程式將目標檔案連線為可執行檔案過程中的中間結果,也可以讓連線程式不生成這個檔案,繼續向下走。
第三個提示是連線程式提示輸入庫檔案的名稱,庫檔案包含了一些可以呼叫的子程式,如果程式呼叫了庫中的子程式,就需要指定,否則不需要。
最後會出現一個 waring: no stack segment,我曾一直以為出現這個提示就不會再生成最終執行檔案,但是當我仔細檢查之後我才發現這只是一個 waning ,最終的執行檔案在 masm 資料夾下,我截個圖給你看。
這個提示只是告訴我們沒有棧段,我們可以完全忽略這個提示,當然如果你的程式有問題,是無法生成連線之後的檔案的。
連線這個過程很有用,歸結來說,主要有三個作用
- 當源程式很大時,可以將它分為多個源程式檔案來進行編譯,每個單獨編譯之後的目標檔案,可以再通過連線將它們連線到一起生成可執行檔案。
- 程式中呼叫了某個庫檔案中的子程式,需要將這個庫檔案和目標檔案連線到一起生成一個可執行檔案。
- 在編譯過後生成的機器碼檔案,其中有些內容還不能直接執行,連線程式需要將這些內容轉換為可執行資訊,才能夠把編譯過後的機器碼檔案,連線成為可執行檔案。
執行應用程式
現在我左手一個 asm 檔案,右手一個 obj 檔案,嘴裡叼著一個 exe 檔案,所以我就是嘴遁王者。廢了半天勁,終於將 asm 搞成 exe 檔案了,累屁了,不過先別急著休息,還差最後一步,執行它!
於是我們執行以下 TEST.EXE 檔案
我有點蒙,這怎麼啥都沒有啊,輸出結果呢?。。。。。。
細想了一下,哦,我們沒有用任何庫來向控制檯輸出資訊,我們只是做了一些資料和暫存器的移動、相加操作。
當然我們可以向控制檯輸出資訊,不過這個我們後面在演示。
簡單聊聊程式的裝載過程
我們大家知道,一個程式如果要執行,就需要裝載進入記憶體,然後 CPU 從記憶體中取指執行命令。
那麼,當我們使用 DOS 的時候,誰負責將可執行程式裝載進入記憶體的呢?
在 DOS 中,有一個叫做命令直譯器 command.com
這個玩意兒,它也是 DOS 系統的 shell。
DOS 啟動後,會先進行初始化,然後執行 command.com ,command.com 執行後,執行完其他相關任務後,會在螢幕上顯示提示符,等待使用者輸入。
如果使用者輸入要執行的命令,比如 cd ,taskkill 等,這些命令由 command 執行,執行完成後再次等待使用者輸入。
如果使用者輸入要執行的程式,command 會通過檔名找到可執行檔案,然後將它載入記憶體,設定 CS:IP 執行入口,然後 command 暫停執行,CPU 執行程式,程式執行完成後,返回 command ,command 再次等待使用者輸入。
所以,一個完整的彙編程式的執行過程如下。
如果這篇文章寫的不錯並且對你有一些幫助,那我就求個贊呀!