1 BASM概念簡要
彙編語句由指令和零至三個表達式構成。表達式由常數(立即數)、寄存器和辨別符構成。例如:
movsb // 單指令語句
jmp @Here // 一個表達式: 辨別符
add eax,1 // 兩個表達式: 寄存器和立即數
// 三個表達式: 寄存器, 辨別符(記憶體位址), 立即數
imul edx, [ebx].RandSeed, 08088405H
一段BASM代碼以ASM關鍵字開始,END關鍵字結束。中間有任意多個彙編語句。
BASM代碼通常寫在例程中。Delphi的BASM是内嵌于語言的,無法獨立編譯出可執行程式或中間代碼(.Obj)。但是,可以使用BASM來完成一個完全彙編的程式,并使用Delphi編譯器編譯。如下例:
program TestBASM;
asm
mov eax, 100
end.
2 表達式的類别與類型
在BASM的語句中,每一個表達式都必須能夠在編譯器中計算出準确的值或者尋址位址。如果不能滿足這個條件,語句不會被編譯通過。事實上,對于指令系統來說,每一個表達式都最終對應于一個确定的操作數。是以,表達式的類别(Expression classes),按表達式的計算結果可分成三類:寄存器、立即數和記憶體引用(存儲器)。與記憶體引用相關的表達式,會涉及到存儲器尋址模式的問題,請查閱相關資料。下一小節會簡要講述在BASM中通路Delphi所定義的變量與常量,但不涉及尋址模式。
在BASM中,表達式的類型(Expression types)是一個長度值,它是指表達式值占用空間的位元組數,即值的大小。這與Delphi中SizeOf()函數含義是一樣的。但BASM中用關鍵字TYPE來傳回表達式的類型(大小)。
如下例:
type
TArr = array [0..10] of char; // SizeOf(TArr) = 11
var
Arr : TArr
mov eax, TYPE Arr
mov eax, TYPE TArr
mov eax, TYPE Arr[2]
end;
上面的三行彙編語句都會向eax送入值11。第三行看起來是要取Arr數組元素的長度,但實際上隻能取到數組的長度。
較為複雜的表達式,其類型由第一個操作數的類型來決定。是以下面這個語句送入eax的值仍然為Arr的類型值11:
mov eax, TYPE (Arr + 2)
這裡的括号不能了解成函數,而是用來改變運算優先級的。
同樣的道理,在BASM中,以下兩條語句面對的命運是不同的:
mov eax, 2 + Arr
mov eax, Arr + 2
第一代碼行會被BASM了解成Arr的位址值+2。而第二行代碼右邊表達式的長度為11,不能送入寄存器eax,因而根本不會被編譯通過。
3 資料定義和資料類型強制轉換
BASM可以使用所有通過Delphi文法定義的變量、常量。BASM擴充了ASM的文法,用于通路記錄、數組、對象等複雜的資料結構。
下例簡單解釋了如何進行資料定義和通路:
TRec = record
rI : Integer;
rS : String;
I : Integer;
R : TRec;
S : String = '1234567';
A : Array [0..10] of char = 'abcdefghij'#0;
const
C = 3124;
Str = 'abcde';
mov eax, I // I 的值送入 eax
mov eax, [I] // 同上
mov eax, OFFSET I // I 的位址送入eax, 相當于 eax = @I
mov eax, R.rI // 域rI的值送入eax
mov eax, [TRec.rI + R] // 同上
mov eax, [Offset R + TRec.rI] // 同上
mov ebx, S
dec ebx // 忽略s[0]
mov esi, 4
mov al, BYTE [ebx + esi] // 将s[4]的字元值送入al
mov al, BYTE [ebx + 4] // 同上
mov eax, [ebx+4] // 将s[4]..s[7]四位元組以DWORD值送入eax, eax=37363534movebx,OFFSETAmoveax,[ebx+4]//将A[4]..S[7]四位元組以DWORD值送入eax,eax=37363534movebx,OFFSETAmoveax,[ebx+4]//将A[4]..S[7]四位元組以DWORD值送入eax,eax=68676665
mov eax, C // eax = 3124
mov eax, [C] // eax = PInteger(3124)^, 非法的記憶體位址通路
在上例中,常量C總是作為數值直接被編碼。是以,“mov eax, C”中,它作為立即數3124被送入EAX。而在“mov eax, [C]”卻表明要通路記憶體位址“3124”,因為“[C]”表明是記憶體引用。
由于常量總是被直接編碼,上例中,無法通路常量Str——Str的長度大于4,是以無法送入EAX。同樣的原因,在BASM中,對常量使用OFFSET是沒有意義的——盡管在Delphi中,字元串常量可以具有記憶體位址。下例中,EAX總是被送入Str的值,而非位址。
Str = 'abcd';
Str2 = 'ab';
// eax = 61626364,OFFSET是無意義的moveax,OFFSETStr//eax=61626364,OFFSET是無意義的moveax,OFFSETStr//eax=00006162, 如果字元串長不大于4, 可以送入eax.長度不夠時, 在左側補0
mov eax, Str2
end;
BASM不支援通路數組下标(可以用位址運算來替代這樣的文法)。盡管類似“mov eax, TYPE Arr[2]”這樣的語句可以編譯通過,但它總是傳回數組的整個長度(如上一節例子中的值11)。這也正好解釋了“mov al, Arr[2]”這樣的語句為什麼不能被編譯——因為要将一個類型長度為11的資料放入al寄存器,是無法做到的。
BASM中支援兩種類型強制轉換的文法,效果是完全一緻的。
TCode = Record
S : String;
aRec : TCode;
aInt : Integer;
mov eax, aInt.TCode.I // 使用“表達式.類型”的強制轉換格式
mov eax, integer(aRec) // 使用“類型(表達式)”的強制轉換格式
這裡的強制轉換的語義與Delphi是一樣的。但是,BASM的強制轉換,隻是把位址上的變量強制識别成目标類型,而不進行長度校驗。是以可以看到,TCode的長度為8,而整型長度為4,它們之間仍然可以轉換,這樣的轉換在Delphi中是行不通的。
BASM代碼塊中,也可以定義資料。但是,用BASM語句定義的資料總是在代碼段裡,這也是對Delphi無法在代碼段裡定義資料的一個彌補。
BASM支援四個用于定義資料的彙編指令DB/DW/DD/DQ。與ASM不同,不能為這些資料命名。例如:
DB 0FFH // 定義一個位元組
aVar DB 0FFH // 在ASM中可用,但在BASM中不支援
可以通過一些技巧來解決命名問題。但是,必須同時用作業系統的API來打開代碼通路權限,才能真正的寫這些資料。下面的例子展示資料定義、命名和讀取的方法:
TCode = packed Record
CODE : WORD; // jmp @, 2 Bytes
I : Integer;
S1 : array [1..26] of char;
S2 : array [1..11] of byte;
Code : ^TCode;
function ReadCode : Integer;
jmp @
DD 12344213
DB 'ABCDEFGHIJKLMJNOQRSTUVWXYZ'
DB 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32
@:
mov Code, offset ReadCode
mov EAX, ReadCode.TCode.I
// ...
I := ReadCode; // I = 12344213
S := Code^.S1; // S = 'ABCDEFGHIJKLMJNOQRSTUVWXYZ'
這個例子以例程名作為變量的位址,但并不是一個好的例子(盡管很多代碼這樣做)。更友善的方法是使用标号作為變量名,與上例類同的例子是這樣:
@CodeRec :
mov EAX, @CodeRec.TCode.I // 使用标号作為變量
I := ReadCode; // I = 12344213
4 例程入口參數及調用約定
任何情況下,在寄存器的使用上,BASM遵循如下的規則: ASM語句執行過程中,必須儲存EDI、ESI、ESP、EBP、EBX的值。ASM語句可以任意使用EAX、ECX、EDX。 一個ASM代碼塊開始時,EBP指向目前堆棧,ESP指向棧頂。 SS存放堆棧段的段位址;DS存放資料段的段位址;CS存放代碼段的段位址。通常情況下,段位址寄存器滿足如下條件:SS=ES=DS。如果需要,函數總是以EAX(32位)、AX(16位)或AL(8位)作為傳回值的寄存器。
Delphi的例程入口參數有以下幾種:
procedure TestProc(I : Integer); // 值參數
procedure TestProc(var I : Integer); // 變量參數
procedure TestProc(const I : Integer); // 常數參數
procedure TestProc(out I : Integer); // 輸出參數
按照Delphi的文法規定,值參數和常數參數使用相同的傳值規則,但值參數隻是傳入值的備份;變量參數、輸出參數總是傳入值的位址。至于像“無類型參數”、“開放數組參數”等,都是在上面的基礎上聲明的,是以也符合其基本規則。
可以直接修改變量參數和輸出參數傳入的記憶體位址上的值,這種修改能被調用者識别和接收。
對于值參數,必要的情況下,編譯器會生成一段代碼,用于建立值參數的一個備份并用它的位址替換入口參數的位址。除此之外,值參數與常數參數使用相同規則:如果傳入的資料長度小于或等于4 Bytes(這存在一些例外,如Int64),則直接傳值,否則傳值的(對于值參數來說,是值的備份的)記憶體位址。
在不違背上述寄存器使用規則和例程參數傳遞規則的前提下,Delphi支援5種調用約定(如表3-1所列)。
表3-1 例程調用約定
調用約定
傳參順序
清除參數責任
寄存器傳參
實作目的
其 他
register
由左至右
例程自身
是[②]
提高效率
Delphi預設規則。
類設計中,公開的聲明強制使用該約定
pascal 由左至右 例程自身 否 與舊有過程相容 較少使用
cdecl 由右至左 調用者 否 與C/C++子產品互動 Powerbuilder等其他語言也使用該約定
stdcall 由右至左 例程自身 否 Windows API Windows API通常使用該約定
safecall 由右至左 例程自身 否 Windows API,COM 用于實作COM的雙重接口、錯誤與異常處理
5 例程和API的調用與流程控制
根據調用約定,通常以register約定來調用Delphi的函數和過程,以cdecl約定來與其他語言混合程式設計,以stdcall約定來調用Windows的API。
下面的例子示範如何調用Delphi的函數:
function DelphiFunc(I: Integer; var S1, S2:String) : Integer;
begin
if I < Length(S1) then
SetLength(S1, I);
S1 := S1 + S2;
Result := Length(S1);
GS : String = '12345678';
procedure RegisterCall;
LS : String;
Len : Integer;
LS := 'This is a test!';
//以下彙編代碼相當于Delphi語句
// Len := DelphiFunc(8, LS, GS);
mov eax, 8
lea edx, LS // 傳入局部變量 LS. 局部變量必須使用lea指令載入位址
mov ecx, OFFSET &GS // 傳入全局變量 GS. 變量名與BASM保留字中的GS(段位址寄存器)
// 沖突, 是以加複寫辨別符"&". 也可以使用語句lea ecx, &GS
call DelphiFunc
mov Len, eax
writeln(LS); // 'This is 12345678'
writeln(Len); // 16
// ...
RegisterCall; // 調用該例程,顯示局部變量LS和Len的值
下面的例子示範如何調用Windows API:
function GetFileSize(Handle: Integer; x: Integer): Integer; stdcall;
external 'kernel32.dll' name 'GetFileSize';
function stdcallDemo : Integer;
FH : THandle;
FH := FileOpen('C:\boot.ini', fmOpenRead);
// Result := GetFileSize(FH, nil);
push 0 // 第二個參數 nil 入棧
push FH // 第一個參數 FH 入棧
call GetFileSize // 依據stdcall約定, 例程GetFileSize()将清理棧, 是以BASM
// 中不考慮nil和FH參數的出棧
mov @Result, eax // 按約定, 傳回值在eax中. 将eax值送入stdcallDemo()的傳回值.
// @Result由BASM定義
FileClose(FH);
writeln(stdcallDemo); // 輸出檔案'c:\boot.ini'的長度
可能的情況下,BASM總是試圖調整跳轉指令,盡可能地使用短程跳轉(2 Bytes),否則使用近程跳轉(3 Bytes)。隻有在兩者都不可能的情況下,才會使用遠端跳轉(5~6 Bytes)。此外,如果是遠端條件跳轉指令,例如:
JC FarJump
BASM會将指令轉換成這樣的形式:
JNC ShortJump
JMP FarJump
ShortJump:
// next line ...
BASM中,可以用跳轉指令将流程指向目前單元中的任何例程。這使得一些錯誤控制更加簡單而且高效。例如System.pas中,試圖調用純虛方法時會進入例程_AbstractErro(),這時,_AbstractError()會使用一個JMP跳轉到系統的錯誤處理例程_RunError():
@@NoAbstErrProc:
MOV EAX, 210
JMP _RunError
使用JMP,而不是CALL的差別在于:JMP跳轉使得目标例程替代了目前例程的RET指令,這樣,在錯誤處理後,出錯點的後續指令将不會再被執行。如圖3.1所示。
如果要使JMP指令跳轉傳回到下一行,那麼,可以用類似下面的技巧修改EIP指針來實作:
DB E8,E8,0, 0,0,0, 0,0,8F, 04,04,24, 83,83,04, 24,24,0C
jmp proc
在BASM中的任意位置加入上述代碼,即可使得“jmp proc”執行後傳回到下一行。上面用DB定義的内嵌彙編代碼的實際代碼如下:
call @@GetEIP // $E800000000, 将标号@@GetEIP位置作為過程入口調用@@GetEIP:
pop [esp] // $8F0424, 3位元組. 從棧頂彈出EIP值到[esp],該值為@@GetEIP标 // 号的位址
add [esp], 12 // $8304240C, 4位元組. 在@@GetEIP位址上加12個位元組,作為真實的返
// 回位址在@@GetEIP和@@ReturnHere之間的三條指令長度總是為3+4+5
// =12 Bytes
jmp proc // 無條件遠端跳轉, 長度為5位元組
@@ReturnHere:
也就是說,“jmp proc”跳轉到的目标例程傳回(RET)時,使用的将是“Call @@GetEIP”時入棧的EIP值,而這個EIP值又通過“+12”被修改成@@ReturnHere的位址。是以,“jmp proc”總是傳回到@@ReturnHere位置,進而得到了與“call proc”類同的效果[③]。
6 完全彙編例程與内嵌彙編例程
BASM在例程中使用時,可以分成完全彙編例程和内嵌彙編例程兩種。完全彙編是指用asm關鍵字替換了例程的begin,進而使例程完全由彙編代碼實作。在Begin..End中間任意位置加入asm..end的Delphi例程都稱為内嵌彙編例程。
完全彙編例程中沒有例程入口時的begin,是以,Delphi不會形成值參數的複制。這意味着在完全彙編例程中,值參數與常數參數的處理是一緻的。
通常情況下,編譯器會自動處理例程的堆棧結構。但是,如果完全彙編例程不是一個子例程(例程嵌套),也沒有入口參數(或它們隻占用寄存器)和局部變量,則編譯器不會為該例程産生堆棧結構。亦即是說,這樣的例程不會在堆棧上配置設定空間。
完全彙編例程的asm關鍵字會被編譯器解釋成例程入口代碼。例如:
Unit1.pas.34: asm
0044C86C 55 push ebp
0044C86D 8BEC mov ebp,esp
隻要定義了局部變量,或入口參數使用到了棧,則會生成上面的代碼。但是,局部變量定義還會導緻類似這樣的一行代碼産生:
0044C86F 83C4D8 add esp,-28這行代碼用于在棧上為局部變量配置設定空間(本例中是28這行代碼用于在棧上為局部變量配置設定空間(本例中是28 Bytes)。但是,如果所有變量在棧上配置設定的總空間不大于4位元組,那麼編譯器會處理成:
0044C86F 51 push ecx
這樣實際上也使esp調整了4位元組。但效率會比“add esp, -4”要好得多。如果局部變量是字元串、變體或接口類型,那麼這些變量會被初始化為0。是以,這樣的情況下,編譯器通常采用“push4”要好得多。如果局部變量是字元串、變體或接口類型,那麼這些變量會被初始化為0。是以,這樣的情況下,編譯器通常采用“push00”的方式來實作空間配置設定。而在一些複雜的情況下,編譯器會直接寫棧來初始化這些變量,例如:
0044C872 33C0 xor eax,eax
0044C874 8945FC mov [ebp-$04],eax
對應于在入口代碼中加入的“push ebp”,代碼出口處,編譯器會生成“pop ebp”。
除了上述的這些情況之外,編譯器不會為完全彙編例程加入其他多餘的代碼。
如果需要在例程中加入局部變量,但又不影響堆棧,可以使用在例程中定義類型化常量的方法,來代替變量聲明。
7 彙編例程中的傳回值約定
在完全彙編例程中,函數必須按如下的規則來傳回值[④]:
F 按照資料類型的長度,序數類型和一些簡單類型(例如集合)使用AL、AX或EAX傳回。
F 實數類型通過浮點運算器的寄存器堆棧的ST(0)傳回。Currency類型須先放大10000倍。
F 指針類型、類類型以及類引用類型使用EAX傳回。
F 對于字元串、動态數組、方法指針、變體以及其他一些大小超過4位元組的資料類型(例如短字元串、變體等)的傳回值來說,傳回值是通過在函數聲明的參數之後另外傳入的變量參數傳回的。
對于最後一條規則,開發人員通常并不需要計算Delphi将如何“另外傳入一個變量參數”,而隻需要在彙編代碼中通過@Result傳回值即可——Delphi會按照上述的規則完成編譯。
對于内嵌彙編例程來說,上面的規則完全不适用——編譯器将按Delphi的規則為例程的關鍵字“Begin .. End” 生成入口與出口的處理代碼,傳回值也由例程(而非内嵌彙編代碼)處理。而且,在使用Registry調用約定的例程的内嵌彙編代碼中,EAX、EDX和ECX未必總是例程入口參數的前三個——因為例程的其他代碼可能已經重寫了這些寄存器。
本文轉自 不得閑 部落格園部落格,原文連結: http://www.cnblogs.com/DxSoft/archive/2010/10/09/1846342.html ,如需轉載請自行聯系原作者