x64汇编基础知识
x64汇编语言在win32asm上做了较大改进,如果只凭借之前win32asm的只是来试水x64asm,则会有很多意想不到的bug,总的来说x64asm更加自由,更加有趣。
1.对32位寄存器的写操作和运算操作,则会对相应的64位寄存器的高32位清零。
如在x64dbg上实验,mov eax, 1和add eax, 1会使rax的高32位清零;
xor eax, eax是对eax的清零运算操作,所以xor rax, rax会被编译器优化为指令更短的xor eax, eax因为二者在x64汇编中的效果是一样的;
但是mov ax,1和mov al, 1不会对rax的高32位进行清零的操作。
2.立即数的使用,优先使用32位扩展,64位的立即数使用较少。
push指令和对内存的写操作只支持4字节的立即数数据,比如push 0x12345678和mov qword ptr [rax], 0x12345678是合法的,但是如果要对长度长于4字节的
立即数使用(比如0x2134567890),就需要分两步进行,借用寄存器进行操作,如需要将0x1234567890压栈,应当:mov rax, 0x2134567890; push rax.
3.x64汇编的一些其他的基础知识
比较常用的通用寄存器:
rax eax ax al
rcx ecx cx cl
rdx edx dx dl
rbx ebx bx bl
rsp esp sp spl
rbp ebp bp bpl
rsi esi si sil
rdi edi di dil
r8 r8d r8w r8b
r9 r9d r9w r9b
r10 r10d r10w r10b
r11 r11d r11w r11b
r12 r12d r12w r12b
r13 r13d r13w r13b
r14 r14d r14w r14b
r15 r15d r15w r15b
此外还有rip, xmm0~xmm15的多媒体用寄存器,rflags。
虚拟地址空间:
00000000`00000000 ~ 00007fff`ffffffff是用户层代码(Ring3)空间;
00007fff`ffffffff ~ ffff8000`00000000是不可用地址空间(not valid address);
ffff8000`00000000 ~ ffffffff`ffffffff是内核地址空间;
4.内存寻址优先使用相对便宜寻址,直接寻址指令较少。
5.各种jmp指令的比较(以下指令需要在x64dbg上做实验增加理解)
几种常见的jmp指令的opcode:
E8 jmp 2字节长度跳转
EB FE jmp,常用的死循环跳转
E9 jmp 4字节跳转(±2GB地址空间)
FF25 jmp qword ptr[相对地址]
FF2425 jmp qword ptr[绝对地址],貌似用处不是特别广泛,FF2425后面会接4个字节。
HOOK时候一般用的寄存器跳转:
mov rax, 0x1234567890 jmp rax
此等同于
mov rax, 0x1234567890 push rax ret
6.x64汇编语言调用约定
x64的调用约定一般没有特定的指明,__cdecl,__stdcall,__fastcall一般都会被编译器修饰为__fastcall。
调用方分配和清理参数所需要的栈空间(外平栈),前四个参数使用rcx, rdx, r8, r9传递,即使用寄存器传参,也需要分配栈空间。
比如x64asm程序:
1 option casemap:none 2 3 func Proto 4 5 .code 6 7 asm_fun Proc 8 9 sub rsp, 20h 10 mov rcx, 1 11 mov rdx, 2 12 mov r8, 3 13 mov r9, 4 14 15 call func 16 17 add rsp, 20h 18 ret 19 asm_fun Endp 20 21 END
64位C语言程序:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <Windows.h> 4 5 extern void _fastcall asm_fun(); 6 7 ULONG64 x; 8 9 void func(ULONG64 a1, ULONG64 a2, ULONG64 a3, ULONG64 a4) 10 { 11 printf("0x%p\n", a1); 12 printf("0x%p\n", a2); 13 printf("0x%p\n", a3); 14 printf("0x%p\n", a4); 15 } 16 17 18 19 int main(int argc, char *argv[]) 20 { 21 asm_fun(); 22 23 printf("hello world\n"); 24 return 0; 25 }
运行结果:
易变寄存器(易挥发寄存器):rax,rcx,rdx,r8,r9,r10,r11
push和pop指令仅用来保存非异变寄存器,其他栈指针显式写rsp实现
进入call之前rsp满足0x10字节对齐,通常不使用rbp寻址栈内存,所以rsp在函数栈中尽量保持稳定(一次性分配局部变量和参数空间)。
x64中每次调用要手动来平衡栈,要16字节对齐,且call指令还要用8个字节的栈空间来存放它返回的地址;则比如当有4个参数时候,参数需要的栈空间是4*8 = 32,call返回的地址需要8个字节的栈空间,则一共需要32 + 8 = 40个字节的栈空间由于40无法被16整除而需要至少加上8个字节变成48字节,此时可以被16整除,所以此时需要48(0x30)字节,而我们需要手动分配的是40(0x28)字节空间。
小弟实验了以下几个例子(C语言内嵌汇编),将在C语言中写了func函数代替一下常见例子中的messagebox函数,同时为了简单分析问题,参数全部选了8字节长度的ULONG64变量,传递参数为4个参数时候可以参考帖子中的例子。
当传入5个参数时候可以使用push来压栈的方法和sub rsp, xxx + mov qword ptr[rsp+xxx],yyy的两种方法。
首先是push的方法,需要手动在纸上先进行演算rsp所指向的位置和变化:
当使用push方法传入6个参数时候就不好使了,因为push方法只能将最后一个参数(第六个)传进去,所以第5个参数要想访问就比较麻烦了(也可能是我错了)。
Visual Studio2013中调试64位应用程序可以看到反汇编代码很少使用push方法传递参数的,基本都是使用sub rsp, xxx + mov qword ptr[rsp+xxx],yyy来进行参数的传递,这种方法也更加好算,更加稳定,我以后就用这种办法了。
在使用前同样我需要在纸上先算一下栈空间的分配;