Delphi内嵌汇编语言 BASM
由于Delphi是基于真正编译器的编程环境,因此可以把汇编语言代码嵌入Object Pascal的过程和函数中。这种能力主要得益于Delphi内建了汇编程序(BASM)。在学习BASM之前,首先应该清楚在Delphi程序中什么时候需要用汇编语言。虽然,能嵌入汇编语言代码是很让人兴奋的事情;但有时,使用BASM也会变成坏事。不过,如果能遵守下列的BASM的规则,就会写出更好、更简捷而且更可移植的代码。
◆只要能够利用Object Pascal 语言实现的事情就不要用汇编语言完成。例如:不要用汇编语言编写串口通信的例程,因为Win32 API提供了串口通信的函数。
◆不要过分依赖汇编语言来优化程序。汇编语言代码虽然比Object Pascal 语言代码稍快一些,但在可读性和可维护性上要差许多。另外,Delphi的优化编译器能使代码更优化,其效果并不亚于手工编写的汇编语言代码。
◆应该总是对汇编语言代码进行注释。因为,代码可能会被别的程序员阅读;而且,在没有注释的情况下,即使自己读起来也会感到困难。
◆不要使用BASM来访问机器硬件。虽然Windows95/98对此宽容些,但Windows NT对此绝对禁止。
◆.尽量把汇编语言代码封装在Object Pascal语言的过程或函数中。这样会使代码可读性更强,并且更易于
移植到其他开发平台。
一、BASM是如何工作的
在Delphi程序中使用汇编语句要比想像的简单。事实上,它只需要先键入关键字asm,然后直接输入汇编语句,最后加上end就行了。如下面代码所示:
var
i: integer;
begin
i := 0;
asm
mov eax, i
inc eax
mov i, eax
end;
{i现在已加1}
end;
以上程序片断声明了一个变量i并把初始化为0。然后,将变量i移至寄存器eax中,把这个寄存器加上1后,再把寄存器的值返还给变量i。示例不仅演示了BASM是多么容易使用,而且还演示了汇编语句中如何访问Pascal的变量。
二、简易的参数访问
不仅访问全局变量以及过程或函数中声明的局部变量是非常容易的,而且,访问传递给过程或函数的参数也同样很方便。
请看下列代码:
procedure Foo(I: Integer);
begin
{一些代码}
asm
mov eax, I
inc eax
mov I, eax
end;
{I现在已加1}
{其他代码}
end;
直接通过参数的名称来访问参数的能力是很重要的,这样就不需要再通过ebp寄存器了。在一般的汇编语言过程中,必须用[ebp+4]代表I。
注意当使用BASM去访问一个传递给过程的参数时,可以只利用它的名称访问,而不需要通过ebp寄存器的偏移来访问。这使代码更易于维护。
三、var 声明的参数
记住,如果一个参数是在一个函数或过程
参数列表中被用var声明的,它实际上是一个指针而不是值。这就是说,如果要在汇编代码中引用这个参数,一定要明确,它是一个32位的指针而不是一个值。
下面代码片断中阐述了如何利用汇编代码访问用var声明的变量:
procedure Foo(var I: Integer);
begin
{一些代码}
asm
mov eax, I
inc dword ptr [eax]
end;
{I现在已加1}
{其他代码}
end;
字符串常量所占字节个数怎么看四、Register 调用约定
在Object Pascal函数和过程中默认的调用约定是Register。利用这种方法传递参数可以使代码更加优化。因为编译器把头三个3 2位参数分别用eax、edx和ecx寄存器来传递。参看下面声明:
function BlahBlah(I1, I2, I3: Integer): Integer;
可以认为参数I1的值被存储在eax中,I2在edx中,I3在ecx中。
再看另一个声明:
procedure TSomeObject.SomeProc(S1, S2: PChar);
这里参数S1的值被存储在eax中,S2在edx中,隐含的self参数被存储在ecx中。
★注:根据我的经验,在一个对象的过程中,隐含的self参数是存储在eax中的;而S1在edx中,S2在ecx中。
五、全汇编过程
Object Pascal中允许用关键字asm取代begin作为一个函数或过程的开始符号来建立一个全汇编的过程。例如:
function IncAnInt(I: Integer): Integer;
asm
mov eax, I
inc eax
end;
注意如果你还在迷恋于16位代码的开发,应该知道Delphi 1时代的assembler指示字已经过时。它已被3
2位的Delphi编译器忽略。
上面这个函数的作用是把变量I加1,由于变量的值放在eax寄存器中,这也就是函数的返回值。下面列出了各种类型的变量是怎么样返回的。
返回类型 返回方式
Char, Byte al寄存器
SmallInt, Word ax寄存器
Integer, LongWord, AnsiString , Pointer, class eax寄存器
Real48 eax寄存器中是栈内返回值的指
nt64 edx:eax寄存器对
Single, Double, Extended, Comp 栈首寄存器ST(0)
注意一个ShortString类型是作为一个指向栈内字符串临时实例的指针来返回的。
六、记录
BASM提供了一种灵活的手段访问Object Pascal记录中的域,可以在BASM块中访问任意记录的域,其访问的语法是Rgister.Type.Field。请看下列代码:
type
TDumbRec = record
i: Integer;
c: Char;
end;
下面的函数中有一个TDumbRec类型的参数:
procedure ManipulateRec(var DR: TDumbRec);
asm
mov [eax].TDumbRec.i, 24
mov [eax].TDumbRec.c, 's'
end;
你可能已注意到了,BASM对记录域的访问非常简单。另一种方式是通过计算域的偏移量来获取或设置域的值。在BASM中使用记录的地方使用这项技术,将会使BASM更能适应数据类型的潜在变化。
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
asm
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的语法,用于访问记录、数组、对象等复杂的数据结构。
下例简单解释了如何进行数据定义和访问:
type
TRec = record
rI : Integer;
rS : String;
end;
var
I : Integer;
R : TRec;
S : String = '1234567';
A : Array [0..10] of char = 'abcdefghij'#0;
const
C = 3124;
Str = 'abcde';
asm
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=$37363534
mov ebx, OFFSET A
mov eax, [ebx+4] // 将 A[4]..S[7]四字节以DWORD值送入eax, eax=$68676665
mov eax, C // eax = 3124
mov eax, [C] // eax = PInteger(3124)^, 非法的内存地址访问
end;
在上例中,常量C总是作为数值直接被编码。因此,“mov eax, C”中,它作为立即数3124被送入EAX。而在“mov eax, [C]”却表明要访问内存地址“3124”,因为“[C]”表明是内存引用。
由于常量总是被直接编码,上例中,无法访问常量Str——Str的长度大于4 ,所以无法送入EAX。同样的原因,在BASM中,对常量使用OFFSET是没有意义的——尽管在Delphi中,字符串常量可以具有内存地址。下例中,EAX总是被送入Str的值,而非地址。
const
Str = 'abcd';
Str2 = 'ab';
asm
// eax = $61626364, OFFSET是无意义的
mov eax, OFFSET Str
// eax = $00006162, 如果字符串长不大于4, 可以送入eax.长度不够时, 在左侧补0
mov eax, Str2
end;
BASM不支持访问数组下标(可以用地址运算来替代这样的语法)。尽管类似“mov eax, TYPE Arr[2]”这样的语句可以编译通过,但它总是返回数组的整个长度(如上一节例子中的值11)。这也正好解释了“mov al, Arr[2]”这样的语句为什么不能被编译——因为要将一个类型长度为11的数据放入al寄存器,是无法做到的。
BASM中支持两种类型强制转换的语法,效果是完全一致的。
type
TCode = Record
I : Integer;
S : String;
end;
var
aRec : TCode;
aInt : Integer;
asm
mov eax, aInt.TCode.I // 使用“表达式.类型”的强制转换格式
mov eax, integer(aRec) // 使用“类型(表达式)”的强制转换格式
end;
这里的强制转换的语义与Delphi是一样的。但是,BASM的强制转换,只是把地址上的变量强制识别成目标类型,而不进行长度校验。因此可以看到,TCode的长度为8,而整型长度为4,它们之间仍然可以转换,这样的转换在Delphi中是行不通的。
BASM代码块中,也可以定义数据。但是,用BASM语句定义的数据总是在代码段里,这也是对Delphi无法在代码段里定义数据的一个弥补。
BASM支持四个用于定义数据的汇编指令DB/DW/DD/DQ。与ASM不同,不能为这些数据命名。例如:
asm
DB 0FFH // 定义一个字节
aVar DB 0FFH // 在ASM中可用,但在BASM中不支持
end;
可以通过一些技巧来解决命名问题。但是,
必须同时用操作系统的API来打开代码访问权限,才能真正的写这些数据。下面的例子展示数据定义、命名和读取的方法:
type
TCode = packed Record
CODE : WORD; // jmp @, 2 Bytes
I : Integer;
S1 : array [1..26] of char;
S2 : array [1..11] of byte;
end;
var
I : Integer;
S : String;
Code : ^TCode;
function ReadCode : Integer;
asm
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
end;
// ...
I := ReadCode; // I = 12344213
S := Code^.S1; // S = 'ABCDEFGHIJKLMJNOQRSTUVWXYZ'
这个例子以例程名作为变量的地址,但并不是一个好的例子(尽管很多代码这样做)。更方便的方法是使用标号作为变量名,与上例类同的例子是这样:
type
TCode = packed Record
I : Integer;
// ...
end;
var
I : Integer;
function ReadCode : Integer;
asm
jmp @
@CodeRec :
DD 12344213
/
/ ...
@:
mov EAX, @CodeRec.TCode.I // 使用标号作为变量
end;
// ...
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);
end;
var
GS : String = '12345678';
procedure RegisterCall;
var
LS : String;
Len : Integer;
begin
LS := 'This is a test!';
//以下汇编代码相当于Delphi语句
// Len := DelphiFunc(8, LS, GS);
asm
mov eax, 8
lea edx, LS // 传入局部变量 LS. 局部变量必须使用lea指令载入地址
mov ecx, OFFSET &GS // 传入全局变量 GS. 变量名与BASM保留字中的GS(段地址寄存器)
// 冲突, 因此加复写标识符"&". 也可以使用语句lea ecx, &GS
call DelphiFunc
mov Len, eax
end;
writeln(LS); // 'This is 12345678'
writeln(Len); // 16
end;
// ...
RegisterCall; // 调用该例程,显示局部变量LS和Len的值
下面的例子演示如何调用Windows API:
function GetFileSize(Handle: Integer; x: Integer): Integer; stdcall;
external 'kernel32.dll' name 'GetFileSize';
function stdcallDemo : Integer;
var
FH : THandle;
begin
FH := FileOpen('C:\boot.ini', fmOpenRead);
//以下汇编代码相当于Delphi语句
// Result := GetFileSize(FH, nil);
asm
push 0 // 第二个参数 nil 入栈
push FH // 第一个参数 FH 入栈
call GetFileSize // 依据stdcall约定, 例程GetFileSize()将清理栈, 所以BASM
/
/ 中不考虑nil和FH参数的出栈
mov @Result, eax // 按约定, 返回值在eax中. 将eax值送入stdcallDemo()的返回值.
// @Result由BASM定义
end;
FileClose(FH);
end;
// ...
writeln(stdcallDemo); // 输出文件'c:\boot.ini'的长度
可能的情况下,BASM总是试图调整跳转指令,尽可能地使用短程跳转(2 Bytes),否则使用近程跳转(3 Bytes)。只有在两者都不可能的情况下,才会使用远程跳转(5~6 Bytes)。此外,如果是远程条件跳转指令,例如:
JC FarJump
BASM会将指令转换成这样的形式:
JNC ShortJump
JMP FarJump
ShortJump:
// n
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论