request-free-img

函数返回结构体时,返回值内存分配详解:Microsoft x64 ABI下的小结构体优化

粉丝问:函数要返回结构体类型,那返回值在哪里分配内存呢?

这也是一个非常有深度的问题,涉及底层原理,因此挑出来说一说。

这涉及到编译器优化,内存管理、函数调用约定以及操作系统原理的知识,不管你是新人程序员还是老程序员,都建议看一看。

这将让你的编程技能更上一个台阶。

直接结论

在 Microsoft x64 ABI下,当函数返回类型是结构体时,小结构体优化(Small Struct Optimization,简称 SSO)是一种 用于返回小结构体 的优化策略。它的基本原则是:

  • 如果结构体大小 ≤ 8 字节 就使用寄存器 RAX 返回
  • 如果结构体大小 > 8 字节 就通过调用者提供的内存来存储返回值

8字节结构体示例与汇编分析

按照惯例,我们还是看代码。

假设我们有这样一个占8个字节大小的结构体,然后我们有一个函数createMyStruct,在函数中创建了一个结构对象局部变量。

函数中会对结构体赋值,最后返回这个结构体局部变量。

#include <iostream>

struct MyStruct {
int a;
float b;
};

MyStruct createMyStruct() {
MyStruct s;
s.a = 10;
s.b = 3.14f;
return s; // 返回副本
}

int main()
{
MyStruct s = createMyStruct();
return 1;
}

现在我们在这里设置断点并运行程序,然后进入汇编。

可以看到,它是直接调用createMyStruct并没有传入什么参数:

MyStruct s = createMyStruct();
00007FF7472118BC call createMyStruct (07FF747211334h)
00007FF7472118C1 mov qword ptr [rbp+0E4h],rax
00007FF7472118C8 mov rax,qword ptr [rbp+0E4h]
00007FF7472118CF mov qword ptr [s],rax

我们跟到函数里边去,前边是栈帧的操作以及内存初始化操作,还有调试器相关的操作。

这些不是我们这个视频的重点,我们跳过。

然后下边这里是对结构体成员变量赋值。

这里就是返回这个结构体对象的局部变量了,因为此时结构的大小刚好是8个字节,符合小结构体优化。

所以它将s变量的内存数据直接当成一个qword 整体读取到rax寄存器中返回。

明白了吧,它是把整个结构体内容当成一个qword的值来返回的。

MyStruct createMyStruct() {
00007FF747211800 push rbp
00007FF747211802 push rdi
00007FF747211803 sub rsp,108h
00007FF74721180A lea rbp,[rsp+20h]
00007FF74721180F lea rdi,[rsp+20h]
00007FF747211814 mov ecx,0Ah
00007FF747211819 mov eax,0CCCCCCCCh
00007FF74721181E rep stos dword ptr [rdi]
00007FF747211820 lea rcx,[__BBCA1470_ConsoleApplication11@cpp (07FF747221029h)]
00007FF747211827 call __CheckForDebuggerJustMyCode (07FF747211357h)
MyStruct s;
s.a = 10;
00007FF74721182C mov dword ptr [s],0Ah
s.b = 3.14f;
00007FF747211833 movss xmm0,dword ptr [__real@4048f5c3 (07FF747219CA4h)]
00007FF74721183B movss dword ptr [rbp+0Ch],xmm0
return s; // 返回副本
00007FF747211840 mov rax,qword ptr [s]
}
00007FF747211844 mov rdi,rax
00007FF747211847 lea rcx,[rbp-20h]
00007FF74721184B lea rdx,[__xt_z+160h (07FF747219C00h)]
00007FF747211852 call _RTC_CheckStackVars (07FF7472112EEh)
00007FF747211857 mov rax,rdi
00007FF74721185A lea rsp,[rbp+0E8h]
00007FF747211861 pop rdi
00007FF747211862 pop rbp
00007FF747211863 ret

大于8字节结构体的情况

现在我们在这个结构体中再添加2个字段,这样它的大小就会大于8个字节了。

我们在createMyStruct中对新添加的2个字段也赋值,其它的不动。

#include <iostream>

struct MyStruct {
int a;
float b;
int c;
int d;
};

MyStruct createMyStruct() {
MyStruct s;
s.a = 10;
s.b = 3.14f;
s.c = 20;
s.d = 30;
return s; // 返回副本
}

int main()
{
MyStruct s = createMyStruct();
return 1;
}

仍在在这里设置断点,再运行这个程序,并进入汇编模式。

MyStruct s = createMyStruct();
00007FF7C9D9181D lea rcx,[rbp+128h]
00007FF7C9D91824 call createMyStruct (07FF7C9D91334h)
00007FF7C9D91829 lea rcx,[rbp+0F8h]
00007FF7C9D91830 mov rdi,rcx
00007FF7C9D91833 mov rsi,rax
00007FF7C9D91836 mov ecx,10h
00007FF7C9D9183B rep movs byte ptr [rdi],byte ptr [rsi]
00007FF7C9D9183D lea rax,[s]
00007FF7C9D91841 lea rcx,[rbp+0F8h]
00007FF7C9D91848 mov rdi,rax
00007FF7C9D9184B mov rsi,rcx
00007FF7C9D9184E mov ecx,10h
00007FF7C9D91853 rep movs byte ptr [rdi],byte ptr [rsi]

核心机制解析

注意了,这时候在调用createMyStruct前是不是将本函数的栈内存传给了rcx。

如果你看过我之前发的视频应该知道64位程序下函数的第一个参数是通过RCX传递的。

也就是它隐式的传入了一个栈内存地址给createMyStruct。

传个内存地址给函数做什么用呢? 就是用来保存返回结构体的。

那其实看到这里想必你一切都明白了,后边的汇编代码也不用再看了。

当结构体大于8个字节时,函数要返回结构体类型,它是通过调用者提供的内存来存储返回值的。

现在,你明白了吗?


更多问题探讨,请关注公众号:程序员角