zoukankan      html  css  js  c++  java
  • Delphi内嵌汇编语言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=37363534movebx,OFFSETAmoveax,[ebx+4]//A[4]..S[7]DWORDeax,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,OFFSETmoveax,OFFSETStr//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:oot.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:oot.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,0, 0,0, 0,8F, 04,24, 83,04, 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 Bytes)。但是,如果所有变量在栈上分配的总空间不大于4字节,那么编译器会处理成: 

    0044C86F 51 push ecx 

    这样实际上也使esp调整了4字节。但效率会比“add esp, -40push00”的方式来实现空间分配。而在一些复杂的情况下,编译器会直接写栈来初始化这些变量,例如: 

    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未必总是例程入口参数的前三个——因为例程的其他代码可能已经重写了这些寄存器。

  • 相关阅读:
    [LeetCode] Power of Three 判断3的次方数
    [LeetCode] 322. Coin Change 硬币找零
    [LeetCode] 321. Create Maximum Number 创建最大数
    ITK 3.20.1 VS2010 Configuration 配置
    VTK 5.10.1 VS2010 Configuration 配置
    FLTK 1.3.3 MinGW 4.9.1 Configuration 配置
    FLTK 1.1.10 VS2010 Configuration 配置
    Inheritance, Association, Aggregation, and Composition 类的继承,关联,聚合和组合的区别
    [LeetCode] Bulb Switcher 灯泡开关
    [LeetCode] Maximum Product of Word Lengths 单词长度的最大积
  • 原文地址:https://www.cnblogs.com/qi123/p/5109725.html
Copyright © 2011-2022 走看看