【软件开发底层知识修炼】二十五 ABI之函数调用约定二之函数返回值为结构体时的约定

1 函数返回值为结构体类型

前一篇文章我们学习了当函数的返回值是整形时,函数返回时,如何将返回值传递给调用者。是通过eax寄存器来传递的。

但是当返回值为结构体时,eax寄存器显然存不下结构体。那么该如何将返回值传递给调用者呢?

  • 函数调用时,用来接收函数返回值的结构体变量的地址需要入栈。
  • 然后被调用函数直接通过该结构体变量的地址,将返回值拷贝过去

但是有一点要注意,就是函数返回值用于初始化以及用于赋值时,这两个过程,内部的调用约定是不一样的。参考下面。

1.1 函数返回值用于初始化变量

【软件开发底层知识修炼】二十五 ABI之函数调用约定二之函数返回值为结构体时的约定

上述图示的过程还是很简单的,当函数返回值作为其他变量的初始值的时候:

  • 首先将变量的地址入栈
  • 当函数返回时,将返回值拷贝到变量st的地址处即可

1.2 函数返回值给变量赋值

【软件开发底层知识修炼】二十五 ABI之函数调用约定二之函数返回值为结构体时的约定

上述图示与11节内容不太一样。当函数返回值是给一个变量赋值而不是初始化的时候:

  • 首先生成一个临时的变量temp,将temp地址入栈
  • 然后当函数返回时,将返回值拷贝到这个临时变量的地址处
  • 最后再将临时变量的值赋值给st

可以看到,当函数返回值作为其他变量的初始值时只需要一次的数据拷贝,但是当函数返回值给其他变量赋值时,却是两次的数据拷贝。所以在平时的代码中,尽量都是直接将函数返回值作为初始值,而尽量不要将返回值以赋值的形式给其他变量以免造成不必要的开销。

2 代码案例分析

本来是想将实验过程写清楚的,但是想想,这个代码的调试过程还是留给读者吧。毕竟我前面写的二十几篇都是将完整的步骤写出来了,如果学会了前面gdb调试的内容那么自己调试应该不在话下。我只给出调试的思路和代码。

  • 代码:

return.c

#include <stdio.h>

struct ST
{
    int x;
    int y;
    int z;
};

struct ST f(int x, int y, int z)
{
    struct ST st = {0};
    
    printf("f() : &st = %p\n", &st);
    
    st.x = x;
    st.y = y;
    st.z = z;
    
    return st;
}

void g()
{
    struct ST st = {0};
    
    printf("g() : &st = %p\n", &st);
    
    st = f(1, 2, 3);
    
    printf("g() : st.x = %d\n", st.x);
    printf("g() : st.y = %d\n", st.y);
    printf("g() : st.z = %d\n", st.z);
}

void h()
{
    struct ST st = f(4, 5, 6);
    
    printf("h() : &st = %p\n", &st);
    printf("h() : st.x = %d\n", st.x);
    printf("h() : st.y = %d\n", st.y);
    printf("h() : st.z = %d\n", st.z);
}

int main()
{
    h();
    g();
    
    return 0;
}

调试思路:使用gdb进行调试。在不同的函数栈帧中查看当前函数栈帧中,结构体变量的地址是否入栈或者是否有一个临时变量的地址入栈。然后通过使用gdb打断点的形式,证明最终函数返回时是将返回值拷贝到相应的地址。当然,最后最干脆的方法还是查看该程序的反汇编代码,通过阅读反汇编代码来更加清晰的认识整个函数的运行机制。

好了,这次就不写调试步骤了,有心的人可以自己调试哦~

3 总结

学会了

  • 函数返回值为结构体的时候,如何将返回值传递给调用者
  • 函数返回值作为初始化与赋值时的不同。注意效率问题