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去加就很容易出问题,除非你能保证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位函数架构,比较复杂,由于今天时间不早了,明天继续。。。。。。