唐凌 发表于 2018-6-26 13:31:56

详解Windows x64上的fastcall调用约定

在32位的系统上,调用约定繁多,一旦定义错了基本上就GG。不过在64位上,调用约定基本上就分为fastcall和usercall两类了。不论你把函数定义为fastcall stdcall cdecl,最终出来的都是fastcall。
所谓usercall,就是瞎JB定义。爱怎么传参数怎么传参数,爱怎么返回值怎么返回值。

而在x64的fastcall上,假定参数均为64位整数,微软有下列规定:

第一,前四个参数分别放入rcx, rdx, r8, r9四个寄存器中。
这个就很爽了,寄存器操作是相当快的。

第二,不论有几个参数,必须在栈上分配至少32个字节的“阴影空间”,用于放置第五到第八个参数。
理解起来可能有点困难,简单说明一下:
如果没有第五个参数,则调用前必须要有类似sub rsp,20h这样的代码。
如果没有第六个参数,则调用前必须要有类似sub rsp,18h这样的代码。
至于第七第八,不必多说。
由于这个阴影空间是调用者分配的,调用完成后必须释放:
如果没有第五个参数,则返回后必须要有类似add rsp,20h这样的代码。
第六第七第八则依次类推。

当有第八个参数的时候,阴影空间被占满,不需要额外分配阴影空间。
当参数超过八个的时候,阴影空间分配量需要增加,同样释放量也要增加。

上述是理论内容,实际应用不会这么干,在禁用优化的情况下,微软的编译器做法如下:
不在调用前才去分配阴影空间,而是直接在函数头分配出一大段栈空间作为阴影空间去放参数变量。阴影空间的分配会被直接忽略。
至于开了优化。。。不管它了,编译器生成的代码列表不是凡人看的懂的,需要拖进IDA才行。

追加内容:
MASM64上的调用约定使用老版的stdcall风格,即每个参数都要从右至左放进栈里。此外汇编器不再会为ret指令自动计算栈弹出长度,需要手动指定。因此如果想在MASM64里定义参数并被C代码正常调用,需要另写一层thunk函数。
寄存器的易失性由常用调用约定决定,其中rax, rcx, rdx, r8, r9, r10, r11被视为易失性寄存器,而rbx, rsp, rbp, rsi, rdi, r12, r13, r14, r15被视为非易失性寄存器。

唐凌 发表于 2020-2-2 06:06:24

watermelon 发表于 2020-2-1 20:45
小弟我也根据本贴和0xAA55大佬的另一个帖子:https://www.0xaa55.com/forum.php?mod=viewthread&tid=1434#l ...

1. 不知道你为什么用%p打印64位值,怎么兼容编译到32位程序呢,用%llx %lld啥的不好么。
2. 声明个外部函数不用extern,此外没有返回值不能算完整练习了调用约定。
3. 我觉得你可以试试在汇编里玩玩printf和scanf来练习一下。
4. 用64位值也不需要包含windows.h,直接(un)signed __int64,可以定义:
typedef unsigned __int64 u64;
typedef signed __int64 i64;
5. 虽然不建议玩内联汇编,不过你可以用Intel Parallel Studio XE的编译器。安装后在Visual Studio里选用Intel的编译器,于是你可以在64位程序中使用内联汇编,语法和MSVC一样,"__asm"即可。虽然Intel编译器是要钱的,不过如果注册帐号时使用edu邮箱,就可以免费注册使用正版。
6. 我习惯上在分配栈空间时把sub rsp写在函数的第一句,直到ret前再add rsp。加减多少的最小值取决于过程中所调用参数最多的那个函数。

Ayala 发表于 2018-6-26 20:51:47

一般来说 调用者 再调用api之前会对所有api的参数进行预计,然后对栈进行一次性的分配
例如有如何几个函数foo1 foo4 foo foo7 foo12 foo23
函数demo在调用这几个函数时进行参数预计 比如foo23有23个参数
demo proc
prolog..
sub rsp,8*23
...
call foo1
...
call foo4
...
call foo
...
call foo7
...
call foo32
...
call foo12
...
call foo4
...
add rsp,8*23
epilog
demo endp

如果不调用foo23就会重新查找参数最多的函数进行栈分配

0xAA55 发表于 2018-6-30 18:56:19

这里可以帮你补充一个具体的例子。void very_simple_process(int foo, int bar, int baz)
{
        char buf;
        printf("Process start.\n");
        fprintf(stdout, "This program should not be compiled by using link-time optimization.\n");
        sprintf(buf, "Because it may be inlined due to it will be called only few times.\n\n");
        fputs(buf, stdout);

        printf("The result of foo + bar + baz could be %d + %d + %d,\n", foo, bar, baz);
        printf("Which should be %d.\n", foo + bar + baz);
        printf("Process end.\n");
}

// 如果我用中文写
void 非常简单的一个程序(int 甲, int 乙, int 丙)
{
        char 缓冲区;
        printf("程序开始。\n");
        fprintf(stdout, "你不能用链接时间优化来优化这个程序。\n");
        sprintf(缓冲区, "因为这个程序大概会因为被调用的次数太少而被内联。\n\n");
        fputs(缓冲区, stdout);

        printf("甲+乙+丙的结果,应该是%d + %d + %d,\n", foo, bar, baz);
        printf("它大概应该等于%d\n", foo + bar + baz);
        printf("程序结束。\n");
}如果我用汇编写,我大概会这样写:segment .rdata
str1 db "Process start.", 0xa
str2 db "This program should not be compiled by using link-time optimization.", 0xa
str3 db "Because it may be inlined due to it will be called only few times.", 0xa, 0xa
str4 db "The result of foo + bar + baz could be %d + %d + %d,", 0xa
str5 db "Which should be %d.", 0xa
str6 db "Process end.", 0xa

segment .text
_very_simple_process:

push rbx
push rsi
push rdi

sub rsp, 256 ;buf

mov rbx, rcx
mov rsi, rdx
mov rdi, r8

mov rcx, str1
call _printf

mov rcx,
mov rdx, str2
call _fprintf

lea rcx, ;指向buf
mov rdx, str3
call _sprintf

lea rcx, ;指向buf
mov rdx,
call _fputs

mov rcx, str4
mov edx, ebx
mov r8d, esi
mov r9d, edi
call _printf

mov rcx, str5
lea edx,
add edx, edi
call _printf

mov rcx, str6
call _printf

add rsp, 256

pop rdi
pop rsi
pop rbx

ret在这个示例里面,栈上的内存布局大概如下图所示:


然后rsp的位置在最左边。

watermelon 发表于 2020-2-1 20:45:28

小弟我也根据本贴和0xAA55大佬的另一个帖子:https://www.0xaa55.com/forum.php?mod=viewthread&tid=1434#lastpost学习了一下
小弟我是这么认为的,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函数代替一下0xAA55大佬帖子中的被调用的messagebox函数,同时为了简单分析问题,参数全部选了8字节长度的ULONG64变量,传递参数为4个参数时候可以参考帖子中的例子。
当传入5个参数时候可以使用push来压栈的方法和sub rsp, xxx + mov qword ptr,yyy的两种方法。
首先是push的方法,我认为需要手动在纸上先进行演算rsp所指向的位置和变化:

当使用push方法传入6个参数时候就不好使了,因为push方法只能将最后一个参数(第六个)传进去,所以第5个参数要想访问就比较麻烦了(也可能是我错了)。

Visual Studio2013中调试64位应用程序可以看到反汇编代码很少使用push方法传递参数的,基本都是使用sub rsp, xxx + mov qword ptr,yyy来进行参数的传递,这种方法也更加好算,更加稳定,我以后就用这种办法了。
在使用前同样我需要在纸上先算一下栈空间的分配;



学习了!orz

watermelon 发表于 2020-2-2 10:12:53

本帖最后由 watermelon 于 2020-2-2 10:15 编辑

tangptr@126.com 发表于 2020-2-2 06:06
1. 不知道你为什么用%p打印64位值,怎么兼容编译到32位程序呢,用%llx %lld啥的不好么。
2. 声明个外部函 ...

哦哦是的,一般的代码生成的反汇编也是在开头就sub rsp分配了被调用函数需要的最多的那个栈空间,然后在末尾再add rsp,很有道理,我再练习一下,orz

完玩 发表于 2022-4-28 15:42:23

进入call之前,rsp必须0x10对齐
x64反汇编下最麻烦的就是call外面看不出参数个数了,必须进入call里面看参数的使用情况才知道:'(
页: [1]
查看完整版本: 详解Windows x64上的fastcall调用约定