Windows、Linux等現代操做系統都運行於CPU的保護模式下。學習保護模式的彙編語言編程,要選用合適的編譯、調試工具,編譯工具決定了彙編程序的語法、結構,而調試工具則可以幫助咱們迅速查找程序中的錯誤,提升調試效率。編程
本實驗指導書採用Microsoft公司的MASM 6.14做爲編譯工具,Microsoft Visual C/C++做爲開發調試環境。windows
和其餘語言同樣,彙編語言的源程序也要符合必定的格式,才能被編譯程序所識別和處理。學習和掌握這些格式,是進行彙編編程的第一步。緩存
下面是一個簡單的彙編程序。sass
;程序清單:test.asm(在控制檯上顯示一個字符串)app
.386函數
.model flat, stdcall工具
option casemap:none學習
; 說明程序中用到的庫、函數原型和常量ui
includelib msvcrt.libspa
printf PROTO C :ptr sbyte, :vararg
; 數據區
.data
szMsg byte 「Hello World!」, 0ah, 0
; 代碼區
.code
start:
mov eax, OFFSET szMsg
invoke printf, eax
ret
end start
在源程序test.asm中,以分號(;)開始的行是註釋行。註釋行對程序的編譯和執行沒有影響。
程序的第一部分是有關模式定義的3條語句:
.386
.model flat, stdcall
option casemap:none
這些語句定義了程序使用的指令集、工做模式。
(1)指令集
.386語句是彙編語言的僞指令,說明本程序使用的指令集是哪種CPU的。還可使用:.808六、.18六、.28六、.38六、.386p、.48六、.486p、.58六、.586p等。
後面帶p的僞指令則表示程序中可使用特權指令。
(2)工做模式
.model語句用來定義程序工做的模式,它的格式是:
.model 內存模式[, 調用規則][, 其餘模式]
內存模式的定義影響最後生成的可執行文件,可執行文件的規模能夠有不少種類型,在Windows環境下,內存模式爲flat,可執行文件最大能夠用4 GB內存。
在test.asm中,.model語句指明使用stdcall調用規則。調用規則就是子程序的調用方式,即調用子程序時參數傳遞的次序和堆棧平衡的方法。
(3). option語句
option語句有許多選項,這裏介紹一種:
option casemap:none
這條語句說明程序中的變量和子程序名是否對大小寫敏感。對大小寫敏感表示區分大寫、小寫形式,例如變量XYZ和xyz是兩個不一樣的變量。對大小寫不敏感則不區分大寫、小寫形式,變量XYZ和xyz是同一個變量。
因爲Windows API函數中的函數名稱是區分大小寫的,因此應該指定這個選項「casemap:none」,不然在調用函數的時候會出現問題。
和C程序同樣,在彙編程序中也須要調用一些外部模塊(子程序/函數)來完成部分功能。例如,在hello.asm中,就須要調用printf函數將字符串顯示在屏幕上。
printf函數屬於C語言的庫函數。它的執行代碼放在一個動態連接庫DLL(dynamic-load library)中,這個動態庫的名字叫msvcrt.dll。
在彙編源程序中,須要用includelib語句指出庫文件的名稱,連接時LINK就從庫文件中找出了函數的位置,避免出現上面的錯誤提示。這種庫文件也叫導入庫(import library)。例如:
includelib msvcrt.lib
一個DLL文件對應一個導入庫,如msvcrt.dll的導入庫是msvcrt.lib;kernel32.dll的導入庫是kernel32.lib;user32.dll的導入庫是user32.lib等。導入庫文件在Visual C/C++的庫文件目錄中,在連接生成可執行文件時使用。
可執行文件執行時,只須要DLL文件,不須要導入庫。
對於全部要用到的庫函數(或Windows API函數),在程序的開始部分必須預先聲明。包括函數的名稱、參數的類型等,如:
在彙編語言程序中,函數聲明爲:
函數名稱 PROTO [調用規則] :[第一個參數類型] [,:後續參數類型]
其中,PROTO後的調用規則是可選項。若是不寫,則使用model語句中指定的調用規則。
若是函數使用C調用規則,則PROTO後跟一個C。接下來是參數的說明。若是參數個數、類型不定,則用VARARG說明(varible argument)。
先看在C語言頭文件stdio.h中printf的函數聲明:
_CRTIMP int __cdecl printf(const char *, ...);
可知printf函數的調用規則爲C調用規則(__cdecl, 即c declare),第一個參數是字符串指針,後面的參數數量及類型不定。
這裏,用ptr sbyte表明const char *。
printf PROTO C :ptr sbyte,:vararg
程序中的數據部分和代碼部分是分開定義的,數據部分從這一行開始:
.data
代碼部分從這一行開始:
.code
遇到end語句時,代碼部分結束。
end語句通常是整個程序的最後一條語句。end語句後面跟的是起始標號。它指出了程序執行的第一條指令的位置。在例子中,使用start做爲起始標號,程序從start處開始執行。注意,程序並不必定要從代碼部分的第一行開始執行。例如,start前面能夠寫一些子程序等。
end 起始標號
若是要定義堆棧部分,可使用堆棧定義語句:
.stack [堆棧大小]
Microsoft Visual C/C++(簡稱VC)是一個典型的集成開發環境(IDE,integrated development environment),在國內外十分流行。集成開發環境大大地提升了程序開發過程的效率,並且它還可以動態地調試程序。除了能夠編寫調試C/C++程序外,VC還能夠用來編輯、修改、編譯、調試彙編程序。本書使用的版本是Microsoft Visual C/C++ 6.0。
首先,按照如下步驟創建一個能編譯、調試彙編程序的工程:
(1) 啓動VC後,從菜單中選擇「File」→「New」。
(2) 如圖1-1所示,在打開的「New」對話框頂部,單擊「Projects」,再選中「Win32 Console Application」。在Location編輯框中輸入「c:"asm」,再在「Project name」中輸入「test」。輸入「test」時,它自動地添加到Location編輯框中「c:"asm」的後面。
圖1-1 創建彙編程序工程之一
(3) 單擊「OK」鍵後,出現一個新的對話框,單擊「Finish」。
(4) 接下來,VC的窗口的左邊顯示出「test classes」,下面有「ClassView」和「FileView」兩種視圖,如圖1-2所示。
(5) 這時,可將hellow.asm(或其餘的一個.asm源程序文件)複製到c:"asm"test中,並更名爲test.asm;也能夠將其餘的彙編程序源文件複製到c:"asm"test"test.asm。
圖1-2 創建彙編程序工程之二
(6) 接下來,再從菜單中選擇「Project」→「Add to Projects」→「Files」,在該對話框中的文件名處輸入「c:"asm"test"test.asm」,如圖1-3所示。
圖1-3 創建彙編程序工程之三
(7) 在VC窗口左邊的視圖中,展開「FileView」中的「Source Files」,顯示出「test.asm」。在「test.asm」上,單擊鼠標右鍵,出現如圖1-4所示的菜單。
圖1-4 創建彙編程序工程之四
(8) 在菜單中選擇「Setting」。彈出另外一個對話框,如圖1-5所示。在「Commands」編輯框中輸入「ml /c /coff /Zi test.asm」,在「Outputs」編輯框中輸入「test.obj」。再單擊「OK」。
圖1-5 創建彙編程序工程之五
(9) 最後,再將「ML.EXE」和「ML.ERR」兩個文件複製到「c:"windows」。若是Windows安裝到其餘目錄,則須要把這兩個文件複製到相應的目錄。可用「set windir」命令顯示出Windows的安裝目錄。
(10)最後,驗證是否能在VC中編譯test.asm。在VC中按F7鍵,應該自動編譯生成test.exe。若是源程序中有錯誤,編譯後將錯誤信息顯示在「Output」的「Build」視圖中。點擊該錯誤信息,光標自動定位到出現錯誤的程序行(也能夠按F4鍵定位到錯誤的程序行)。
爲了使VC適合於彙編語言的調試,可對它進行以下設置,如圖1-6所示。
(11)從「Tools」菜單中選擇「Options…」,再選擇「Debug」頁,選中「Disassembly window」中的「Code bytes」(前面打上對勾)。
(12)在「Memory window」中,選中「Fixed width」,在後面填入數字16。
(13)在「General」中,選中「Hexdecimal display」。
(14)不選「View floating point registers」。
圖1-6 VC的調試設置選項
程序編譯成功後,按Ctrl+F5能夠運行已編譯好的程序。
若是程序運行的結果不正確,能夠在VC中調試。單擊「FileView」視圖中的test.asm,這個源程序就會自動地進入VC的編輯窗口。
將光標移動到程序入口所在的程序行上,按F9鍵。就在該行設置了一個斷點。斷點的程序行前有一個紅色的小圓點,如圖1-7所示。
按F5鍵在Debug狀態下執行程序,或者從菜單中選擇「Build」→「Start Debug」→「Go」。
這時,當前窗口顯示出程序中的指令序列,有一個黃色箭頭,它就是程序要執行的下一條指令,如圖1-8所示。
從菜單中選擇「View」→「Debug Windows」→「Memory」,打開內存窗口,在地址「Address:」後面的編輯框中能夠輸入內存變量的名稱,這裏輸入szTitle。內存窗口中就顯示出該變量所在內存單元的值。前面的部分是以十六進制的形式顯示出來的,後面是以ASCII字符的形式顯示出來的。
在內存窗口上單擊鼠標右鍵,能夠選擇:按字節、字、雙字顯示內存單元的值。
從菜單中選擇「View」→「Debug Windows」→「Registers」,打開寄存器窗口。在寄存器窗口中,顯示了各個32位寄存器和段寄存器的值。
在調試程序時,若是某一個寄存器或內存單元的值被改變,則它的值用紅色顯示出來。
在寄存器窗口中的最後一行,顯示的內存單元就是當前指令要讀或寫的操做數。
EFLAGS狀態寄存器的值是按位顯示的。可是,VC並無使用咱們所熟悉的OF、DF、IF、SF、ZF、AF、PF、CF名稱,而是用它本身的一套名稱,如表1-1所示。
表1-1 VC中的EFLAGS標誌位
VC格式 |
OV |
UP |
EI |
PL |
ZR |
AC |
PE |
CY |
FLAGS位 |
OF |
DF |
IF |
SF |
ZF |
AF |
PF |
CF |
含義 |
溢出 |
方向 |
中斷容許 |
符號 |
爲零 |
輔助進位 |
奇偶 |
進位 |
例如UP=0表示DF=0。
從菜單中選擇「View」→「Debug Windows」→「Watch」,打開監視窗口。在「Name」一欄下面,能夠輸入想要監視的變量或寄存器名稱。監視窗口會隨時將這些變量的值顯示出來。
要在調試過程當中改動寄存器或內存變量的值,能夠在Watch窗口的該寄存器或變量的內容(在Value列)用鼠標左鍵單擊,修改其值後,按回車鍵便可。
也能夠在內存窗口中修改變量的值。在要修改的內存單元上點擊,直接輸入新的內容便可。
另一種方法是按Shift+F9。彈出對話框後,在「Expression」處輸入寄存器或內存變量的名稱,再在下面的Value一列處修改其內容。最後,按「OK」。
圖1-7 編輯、編譯彙編源程序並設置斷點
圖1-8中爲打開內存窗口、監視窗口和寄存器窗口後的屏幕顯示。
調試過程當中,編輯窗口中顯示出彙編源程序。若是要查看程序的實際執行代碼,從菜單中選擇「View」→「Debug Windows」→「Disassmebly」。在運行過程當中,實際上運行的是機器代碼,而不是彙編源程序。機器代碼及其反彙編的指令和源程序混合顯示在編輯窗口中。反彙編中的程序地址和指令中的數據都是用十六進制顯示的。在調試過程當中,使用十六進制來表示地址和(變量或寄存器的)數值更方便。
按F10鍵可一步一步地執行程序。執行過程當中,能夠在內存窗口中觀察變量的變化;在寄存器窗口中能夠看到寄存器的變化;更加方便的是,能夠把鼠標移動到編輯窗口中的寄存器或變量上,停留幾秒鐘後,VC會自動地顯示它們的值。
按Shift+F5鍵,可結束調試。
圖1-8 VC調試環境:編輯窗口、內存窗口、監視窗口和寄存器窗口
經常使用的調試命令如表1-2所示。
表1-2 VC的經常使用調試命令
功能鍵 |
做 用 |
描 述 |
F11 |
單步執行 |
Step Into |
F10 |
執行 |
Step Over |
Ctrl+F10 |
執行到當前光標的位置的指令 |
Run to Cursor |
F9 |
在當前光標的位置的指令上設置/清除斷點 |
Set/Clear Breakpoint at Cursor |
F5 |
執行程序 |
Go |
Shift+F5 |
終止程序,退出程序 |
Stop Debugging |
設置當前指令 |
將光標處的指令設爲當前指令 |
Set Next Statement |
Shift+F11 |
當前子程序執行結束 |
Step Out |
l F11:單步執行當前指令。當前指令在反彙編窗口中用一個黃色箭頭指示,CS:EIP指向當前指令。按F11鍵後,當前指令執行,黃色箭頭和EIP隨之變化,指向新的當前指令。
l F10:執行當前進程指令。F10和F11在執行通常指令時沒有區別。在當前指令是一條CALL、INT指令的狀況下有所區別。若是當前指令是CALL指令,按F11後,進入到子程序的第一條指令,子程序執行前就進入調試狀態,可調試子程序的執行過程;按F10後,子程序執行完畢後纔回到調試狀態,不須要調試子程序的執行過程。
l Ctrl+F10:先把光標移動到一條指令上,能夠用鍵盤上的上、下箭頭移動光標,或者在某一行上點擊。再按Ctrl+F10,程序就從當前指令處開始執行,一直到光標處的指令再停下來。
l F9:先把光標移動到一條指令上,按F9,就在該指令上設置了一個斷點。再按F9,這個斷點就清除了。設置斷點後,指令的前面標有一個紅色的圓點。程序運行到斷點時,會停下來,這時就能夠檢查各個變量、寄存器的內容以及程序的執行流程是否正確,以查找程序中的錯誤。
l F5:從當前指令開始執行程序,直到遇到斷點或程序結束時爲止。
l Shift+F5:終止程序,再也不執行後面的程序。終止後,能夠再按F11鍵(或Ctrl+Shift+F5)從新開始調試過程。
l 設置當前指令:在調試時,可能但願跳過一部分程序不執行,也可能想將已執行過的一段程序再執行一遍。這能夠經過改變當前指令來實現。在新的當前指令上按下鼠標右鍵,彈出一個菜單,在其中選擇「Set Next Statement」。這時,黃色箭頭就指到新設置的當前指令上。
l Shift+F11:先按住Shift鍵,再按下F11。當前指令在子程序中時,若是想使整個子程序執行完畢,返回到主程序,則使用Shift+F11。
某些功能也能夠從「Debug」菜單中選擇。如圖1-9所示。
圖1-9 VC的部分Debug菜單項
在C語言中,經常使用printf、scanf、sprintf等函數來實現字符串的的輸入輸出,在彙編語言中,能夠調用這些函數。
在前面的程序例子中已經用到過printf。在程序中,要指明printf的調用規則,以及它的參數類型。
printf PROTO C :dword,:vararg
printf使用C調用規則(參數自右至左入棧,由主程序平衡堆棧)。第1個參數是一個雙字(:dword),即字符串的地址,後面的其餘參數個數可變,能夠1個沒有,也能夠跟多個參數。
如下C語句輸出3個整數A、B、R和一個字符Op:
printf ("%d %c %d = %d"n" , A, Op, B, R);
在彙編語言中,在數據區中要定義szOutputFmtStr:
szOutputFmtStr byte '%d %c %d = %d', 0ah, 0
使用invoke語句調用printf。Printf後面跟的第1個參數是格式字符串的地址;第二、三、4個參數分別是要輸出的整數。
invoke printf, offset szOutputFmtStr, A, Op, B, R
在程序中,還須要包括如下語句,指示連接程序在msvcrt.lib庫文件尋找連接信息。
includelib msvcrt.lib
sprintf與printf類似,它將輸出保存在第1個字符串szStr中。
invoke sprintf, offset szStr, offset szOutputFmtStr, A, Op, B, R
scanf是從控制檯將用戶的輸入讀入到程序的變量中,變量的類型能夠是整數、字符、字符串等。
scanf的調用規則和參數類型說明爲:
scanf PROTO C :dword,:vararg
scanf的連接信息也包括在msvcrt.lib庫文件中。
程序中須要輸入A、Op、B時,A、B是整數,OP是字符。它的第1個參數是格式字符串的地址;第二、三、4個參數分別是A、Op、B的地址。
szInputFmtStr byte '%d %c %d', 0
invoke scanf,offset szInputFmtStr,offset A,offset Op,offset B
其效果等價於:
scanf("%d %c %d", &A, &Op, &B);
下面的程序首先調用printf顯示字符串,提示用戶輸入要計算的表達式,再調用scanf接收用戶的輸入。根據輸入的運算符Op,經過條件跳轉指令實現對加、減、乘、除的判斷和處理。最後,調用printf輸出計算結果。其執行結果爲:
input (a Op b, Op=+-/*, ex: 28-2): 4*5
4 * 5 = 20
;程序清單:equation.asm(四則運算)
.386
.model flat,stdcall
Option casemap:none
includelib msvcrt.lib
scanf PROTO C :dword,:vararg
printf PROTO C :dword,:vararg
.data
szInPmt byte 'input (a Op b, Op=+-/*, ex: 28-2): ', 0 ;
szInputFmtStr byte '%d %c %d', 0
szOutputFmtStr byte '%d %c %d = %d', 0ah, 0
szErrMsg byte 'invalid Operation. ', 0ah, 0
A sdword ?
B sdword ?
Op dword ?
R sdword ?
.code
start:
invoke printf, offset szInPmt
invoke scanf, offset szInputFmtStr,
offset A,
offset Op,
offset B
mov eax, A
mov ebx, B
cmp Op, '+'
jz lab_add
cmp Op, '-'
jz lab_sub
cmp Op, '*'
jz lab_mul
cmp Op, '/'
jz lab_div
lab_err:
invoke printf, offset szErrMsg
jmp lab_exit
lab_add:
add eax, ebx
mov R, eax
jmp lab_output
lab_sub:
sub eax, ebx
mov R, eax
jmp lab_output
lab_mul:
imul eax, ebx
mov R, eax
jmp lab_output
lab_div:
cdq
idiv ebx
mov R, eax
jmp lab_output
lab_output:
invoke printf, offset szOutputFmtStr, A, Op, B, R
lab_exit:
ret
end start
equation.asm在C:"asm"sample"chap-01目錄中,編譯鏈接的步驟爲:
(1) 進入C:"asm"sample"chap-01目錄
cd C:"asm"sample"chap-01
(2) 設置編譯環境
c:"asm"bin"asmvars.bat
(3) 編譯鏈接
ml /coff equation.asm /link /subsystem:console
API是application programming interface的縮寫,表明應用程序編程接口。API通常使用stdcall調用規則。
printf和scanf適用於控制檯程序(連接選項爲/subsystem:console),而帶窗口的Windows程序(連接選項爲/subsystem:windows)不能使用printf和scanf。這裏介紹一個輸出信息用的API-MessageBox。
它的調用規則和參數類型說明爲:
MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:DWORD
MessageBoxA的連接信息包括在user32.lib庫文件中。
MessageBoxA的C語言原型在VC附帶的"winuser.h"中提供。
在編程中,應儘量地利用已有的C的庫函數和Windows API函數,以減小編程的工做量。
經過查閱在MSDN(或VC等工具)資料和幫助文件、閱讀示例程序等方法,弄清函數的功能以及入口、出口參數,以及每個參數的用法。根據函數的名稱、參數的個數、類型、調用規則等,寫出這樣的聲明語句:
printf PROTO C :dword,:vararg
再肯定它屬於哪個庫文件。經常使用的庫文件有:msvcrt.lib、kernel32.lib、user32.lib等。
函數可能返回的是一個整數、指針或其餘類型。不管如何,返回值都在EAX中。要注意有些函數是經過傳遞地址指針的方式來改變參數的值,如scanf。
CPUID指令是得到CPU信息的彙編指令,它能夠返回CPU類型、型號、製造商信息、商標信息、序列號、緩存等CPU相關信息。
CPUID指令使用eax做爲輸入參數,eax、ebx、ecx、edx做爲輸出參數。例如:
mov eax, 0
cpuid
執行結果爲:EAX = 00000002 EBX = 756E6547 ECX = 6C65746E EDX = 49656E69。結果以十六進制顯示。EBX、ECX、EDX中各存儲4個字符,所有12個字符爲:GenuineIntel。
使用eax=3做爲輸入參數時,在ECX、EDX中返回CPU序列號的第0~31位、第32~63位。
mov eax, 0
cpuid
;程序清單:cpuid.asm(讀取CPU標識)
.586
.model flat,stdcall
Option casemap:none
includelib msvcrt.lib
printf PROTO C :dword,:vararg
.data
szVendorID byte 13 dup (0)
szFormatStr byte 'VendorID = %s; Processor SN = %08X%08X', 0ah
.code
start:
mov eax, 0
cpuid
mov dword ptr szVendorID, ebx
mov dword ptr szVendorID+4, edx
mov dword ptr szVendorID+8, ecx
mov eax, 3
cpuid
invoke printf, offset szFormatStr,
offset szVendorID, ecx, edx
ret
end start
程序運行結果以下所示:
VendorID = GenuineIntel; Processor SN = 00000000007B7040
WinDbg是微軟公司發佈的免費源碼級調試工具。Windbg能夠用於Kernel模式調試和用戶模式調試,還能夠調試Dump文件。
使用WinDbg調試彙編程序的主要步驟爲:
1.首先,使用下面的命令編譯、連接,產生.EXE文件。/coff選項要求MASM生成連接器所須要的COFF格式的.obj文件,/Zi則指定編譯生成的目標文件中含有調試信息,/link表示要生成.EXE文件,而/subsystem:console則表示生成控制檯程序。
ml /coff /Zi cpuid.asm /link /subsystem:console
2.啓動WinDbg,從菜單File→Open Executable裝入.EXE文件。
3.在WinDbg命令窗口的最下方,可輸入命令,按回車鍵執行。如:
x cpuid!
在窗口內顯示出cpuid.exe內的全部數據變量:
0:000> x cpuid!
*** WARNING: Unable to verify checksum for cpuid.exe
00404000 cpuid!szVendorID = 0x00 ''
0040400d cpuid!szFormatStr = 0x56 'V'
4.輸入u命令,能夠查看彙編指令:
0:000> u start l e
cpuid!start [cpuid.asm @ 11]:
00401010 b800000000 mov eax,0
00401015 0fa2 cpuid
00401017 891d00404000 mov dword ptr [cpuid!szVendorID (00404000)],ebx
0040101d 891504404000 mov dword ptr [cpuid!szVendorID+0x4 (00404004)],edx
00401023 890d08404000 mov dword ptr [cpuid!szVendorID+0x8 (00404008)],ecx
00401029 b803000000 mov eax,3
0040102e 0fa2 cpuid
00401030 52 push edx
00401031 51 push ecx
00401032 6800404000 push offset cpuid!szVendorID (00404000)
00401037 680d404000 push offset cpuid!szFormatStr (0040400d)
0040103c e811000000 call cpuid!printf (00401052)
00401041 83c410 add esp,10h
00401044 c3 ret
5.輸入bp命令,能夠設置斷點,例如:
bp start
6.從菜單中選擇Debug→Go,或者按F5鍵,能夠執行到斷點處。
7.從菜單中選擇View→Disassembly,能夠看到斷點處的指令以高亮方式顯示。
8.從菜單中選擇View→Register,打開一個窗口,顯示寄存器的當前值。
9.在WinDbg命令窗口輸入r命令,也能夠顯示寄存器的當前值。還能夠修改寄存器的值,如「r eax=5」。
10.在WinDbg命令窗口輸入輸入d命令,能夠查看內存內容,如:「d szVendorID L c」。將d換爲db、dw、dd,則指定用字節、字、雙字的格式查看。L後面跟的數字則表示查看的單元個數。
11.按F十一、F十、Shift+F十一、Ctrl+F10分別表示單步執行當前指令、執行完畢當前指令、執行完當前函數、執行到光標處,其用法與表1-2相同。
cpuid.asm採用的是控制檯模式(連接選項爲/subsystem:console),調用printf輸出信息。
要求:
1. 使用VC 6.0創建工程文件;
2. 採用帶窗口的Windows程序(連接選項爲/subsystem:windows);
3. 使用sprintf 、MessageBoxA函數;
4. 輸出製造商信息、序列號;
5. 輸出CPU商標信息,執行CPUID的輸入參數爲80000002H、80000003H、80000004H,具體格式可查閱CPU指令手冊。
運行結果如圖1-10所示: