64位BASM学习随笔(一)



64位BASM学习随笔(一)

    Delphi的BASM一直是我最喜爱的内嵌汇编语言,同C/C++的内联汇编相比,它更方便,更具灵活性,由于C/C++的内联汇编仅仅能是或插入式的汇编代码,函数花括号背后隐藏的函数框架,限制了汇编代码的发挥,如不管有无參数和局部变量,总是有个栈框架,更烦人的是仅仅要你在函数中使用了esi,edi,ebx寄存器。就自己主动给你保存和恢复,使得这些寄存器没法在函数之间传递信息等。而Delphi的BASM能够是插入式的汇编代码,也但是全然的汇编方法。在全然的汇编方法下,怎么发挥就是自己的事了。
    Delphi XE2后就能够编写64位应用程序了,但我一直没时间试一下64位的BASM代码有无变化,春节前后抽时间研究了一下,发现64位的BASM变化还是非常大的,不仅仅是简单的寄存器位数更长的问题,而是整个BASM方法的框架(不仅仅是BASM。而是整个64位计算机应用程序框架)都发生了根本性的变化,从编敲代码代码的角度而言,这样的变化似乎比曾经16位程序向32位程序进阶时更大。
    一、64位BASM不支持插入汇编代码。仅仅能写纯BASM方法。如以下的代码是错误的:

function Test(v: Integer): Integer;
var
  i: Integer;
begin
  i := v * v;
  asm
    mov   eax, i
  end;
end;

    仅仅能这样写:

function Test(v: Integer): Integer;
asm
  mov   eax, v
  imul  eax, eax
end;

    二、64位BASM仅仅支持一种调用方式,不论你标明stdcall,pascal,cdecl与否,调用方式都是寄存器參数传递,清栈由调用方法负责(与cdecl相似)。

这样的变化不仅仅是BASM。好像整个64位程序都是这样。仅仅有一种调用方式。

stdcall,pascal,cdecl调用说明仅仅是对32位程序代码的兼容。不会报错。
    三、64位BASM数据类型除指针类型由32位进阶64位外,其他无变化。最经常使用的Integer、LongWord长度仍然是四字节(前几天据网上传,Delphi XE8的LongWord会改为64位)。
    四、64位寄存器的变化。
    1、寄存器长度增了一倍,由32位进阶为64位。eax,ebx,ecx,edx,esi,edi,ebp,esp等变为rax,rbx,rcx,rdx,rsi,rdi,rbp,rsp,当然e字头的寄存器照样能作32位寄存器使用,相同16位,8位寄存器也可使用。

    这里有一点是要特别注意的,而64位程序中,默认的操作数长度仍然是32位。仅仅有默认地址长度才是64位。这点同16位进阶32位有些不同,操作寄存器的低16位不会不会影响其高16位,而64位下,改变寄存器的低32位。会导致寄存器的高32位清零。如以下代码:

    mov   eax, edx
    shl   rax, 32
    mov   eax, edx

其本意是在rax中形成2个并行的32位数字。结果第三句代码导致rax高32位清零。使用or  eax, edx也是一样会导致高32位清零,仅仅有or  rax, rdx才是正确的(当然要保证rdx的高32位是零)。

    另外,操作64位地址也要注意。尽管64位程序的地址默认是64位。但使用相似[esi+edx]的32位地址操作也不会报错,相同操作结果好像也是正确的,但我觉得,应尽量避免这类代码。由于眼下看来似乎结果是正确的,主要是由于眼下应用程序能操作的数据长度没超过32位,假设以后随着硬件的变化,系统也会发生变化,一旦应用能使用的数据量大于32位。你的代码就有问题了。

还有地址的增减也是这样,不管32位还是64位代码,整数长度还是32位,假设增减地址的操作数是32位的。最好转换为64位,除非你能保证其是正数,如以下过程:


function Test(v: Integer): Integer;
asm

    push  rbx
    mov   eax, v
    add   rbx, rax

    .......

    pop   rbx

end;

參数v是32整数,直接用地址rbx去加就非常easy出问题,除非你能保证v不为负数。这里能够用cdqe或者使用movsxd  rax, v进行扩展。

    压栈push和出栈pop语句的操作数仅仅能是64位,如push  eax是错误的。


    2、通用寄存器多了r8 - r15等8个寄存器,在BASM方法内,r8 - r11可直接使用,而R12 - R15在使用时同rsi,rdi,rbx一样。应注意保存和恢复。r8 - r15是64位形式。也可表示为32位,16位和8位,如r8,r8d,r8w,r8b分别为64位,32位,16位和8位,并且64位坏境下,rsi, rdi,rbp,rsp也能够用sil,dil,bpl和spl操作低8位。r8 - r15不像rax,rbx,rcx,rdx几个通用寄存器有高低8位寄存器,并且曾经通用寄存器的高8位不能和r8 - r15寄存器使用在同一语句中,如mov  ah, r8b; mov  bh, [r8]等都是错误的。


    3、XMM寄存器也多了8个,依次为xmm8 - xmm15,只是,xmm6 - xmm15在使用时应注意保存和恢复(xmm6,xmm7在32位代码中是不须要保护的)。保存SSE寄存器非常麻烦。它不能像常规寄存器使用压栈和出栈语句。但BASM中有一个savenv伪指令非常方便(我不知道这是BASM独有的,还是其他汇编共同拥有的),如.savenv  xmm7(注意savenv前有个点),Delphi编译器就会在BASM方法中加上保护和恢复xmm7寄存器的语句。


    四、64位BASM方法默认參数传递的变化。不管是32位还是64位BASM方法,默认都使用寄存器传递參数。不同的是32位BASM是前3个參数非浮点数參数使用寄存器传递。从形參左边開始,依次是ecx,edx,ecx,浮点数參数和三个以上非浮点数參数使用栈传递;而64位BASM是前4个參数使用寄存器传递。假设是非浮点数參数。从形參左边開始。依次为rcx,rdx,r8,r9。浮点数则使用xmm0 - xmm3传递。在32位方法中。前3个參数中间夹着浮点数时,寄存器參数会顺延。如方法:

    function Test(v1, v2: Integer; v3: double; v4: Integer): double;

    asm

       fld     v3

    end;

寄存器使用:eax=v1,edx=v2,[ebp+8]=v3,ecx=v4,这里ecx是顺延的。

而64位方法中不顺延。不论是否浮点数。寄存器的位置是不改变的,如以下的方法:

    procedure Test(v1, v2: Integer; v3: double; v4, v5: Int64);

    asm

    end;

寄存器使用:ecx=v1,edx=v2,xmm2=v3,r9=v4,[rsp+28h](无框架)或者[rsp+30h](有框架)=v5。假设v3是非浮点数。寄存器应该是r8,这里用xmm2表示浮点数參数。r8寄存器没有顺延。相同v3没有使用xmm0,而是严格按位置參数位置安排xmm2。至于參数v5则是使用栈传递的。至于其栈中偏移位置是28h或30h,而不是8和16的原因后面在专门谈及。

    五、64位BASM返回值的变化。64位的BASM方法的返回值也有些变换。常规的返回值还是eax或rax,最明显的是能够用rax返回64位整数类型。而不必使用edx:eax返回了。浮点数的返回值是变化最大的。如前面32位Test函数代码是使用fld  v3通过80x87寄存器来传递的,这句代码用在64位BASM函数中就是错误的。由于64位函数返回浮点数不再使用80x87寄存器。而是使用SSE寄存器xmm0。所以64位代码仅仅能是相似movaps  xmm0, v3或者直接movaps  xmm0, xmm2。

    另一种特殊的返回值。如以下的方法,返回一个TRect类型:


function GetRect(Left, Top, Right, Bottom: Integer): TRect;
asm

// 32位代码:

    push  ebx

    mov   ebx, Result // 或者mov  ebx, [ebp+8]

    mov   [ebx].TRect.Left, eax

    mov   [ebx].TRect.Top, edx

    mov   [ebx].TRect.Right, ecx

    mov   eax, Bottom // 或者mov  eax, [ebp+12]

    mov   [ebx].TRect.Bottom, eax

    pop    ebx

// 64位代码:
    mov   [rcx].TRect.Left, edx
    mov   [rcx].TRect.Top, r8d
    mov   [rcx].TRect.Right, r9d
    mov   eax, Bottom
    mov   [rcx].TRect.Bottom, eax

end;

通过对照能够看出,对于这样的结构形式的返回值。假设是小于或等于通用寄存器的偶数字节结构使用eax或rax返回,这一点32位和64位代码都是相同的。而其他结构返回值就不同了,32位代码用最后一个參数(本例是栈參数)。而64位代码则是用第一个參数。即rcx来传递结构地址的。以下是一个调用BASM过程样例:

procedure Test;
var
  r: TRect;
asm
    .params 5

    // r := GetRect(1, 2, 3, 4)
    lea   rcx, r
    mov   edx, 1
    mov   r8d, 2
    mov   r9d, 3
    mov   [rbp+20h], 4
    call  GetRect
end;

用来传递第一个參数的寄存器rec被返回值占用了,或者说,这样的结构返回值是作为第一个參数传递的。前面的GetRect函数实际上是以下的形式的变形:

procedure GetRect(var r: TRect; Left, Top, Right, Bottom: Integer);

    在调用样例过程中,有一个伪指令.params用来自己主动分配參数内存空间。而參数Bottom也是使用栈传递的。但并没有使用压栈指令,详细原因涉及64位函数架构。比較复杂,由于今天时间不早了,明天继续。。。。。


原文地址:https://www.cnblogs.com/jhcelue/p/6909475.html