C/C++ 函数参数和返回值传递机制

原文:C/C++ 函数参数和返回值传递机制
作者:Breaker <breaker.zy_AT_gmail>


说明 C/C++ 函数调用中,参数和返回值传递的机制,包括低级汇编指令和高级 C++ 对象拷贝构造

关键字:参数传递,返回值传递,按值传递 (passed by value),按引用传递 (passed by reference),拷贝构造

相关参考

目录

函数调用栈示意图^

调用顺序 func_1->func_2,调用时栈操作顺序从 高地址 到 低地址

示意图如下:

***低地址***

[ ...                                    ]  2
[func_2 局部变量 2                       ]  2
[func_2 局部变量 1                       ]  2
[func_1 寄存器值: ECX, EBX, EDI, ESI 等  ]  2     保存 func_1 的运行环境
[func_2 预留栈空间                       ]  2     用 sub esp, XXh 预留栈空间
[func_1 EBP                              ]  2     保存 func_1 EBP,然后 EBP <= ESP
[func_1 中下一条 EIP                     ]  2     保存 func_2 调用完成后,func_1 的下一条指令地址
[func_2 返回值                           ]  2  1  func_1 使用 func_2 的返回值,size 小的返回值通过寄存器传递
[func_2 第 1 个参数                      ]  2  1
[func_2 第 2 个参数                      ]  2  1
[...                                     ]  2  1  IDA 中用 func_2 arg_XX 表示
[func_2 第 N 个参数                      ]  2  1  C/C++ 参数入栈方式:从后到前

***高地址***

栈示意图后面的 1、2 表示哪个函数会访问这些存储

基本过程如此,但编译器之间略有差别,如 VC 调试方式编译,用 sub esp, XXh 预留栈空间等

C++ 中参数和返回值传递都是初始化语义

prolog 和 epilog
  • prolog: 进入 func_2 时的准备工作,保存 func_1 的环境,如 EBP 和其它寄存器值,预留栈空间等
  • epilog: 离开 func_2 时的恢复工作,恢复 func_1 的环境

prolog 和 epilog 由编译器产生,但可使用 VC 的 __declspec(naked) 裸函数,手工编写 prolog 和 epilog,参考 MSDNConsiderations for Writing Prolog/Epilog Code

参数按值传递^

以按值传递一个 POD 结构 Student 为例,说明汇编指令

编译器 Linux GCC 4
编译命令 g++ -O0 -g3 -Wall

测试程序:

  1. struct Student  
  2. {  
  3.     int     id;     // 学号  
  4.     char*   name;   // 姓名  
  5.     int     age;    // 年龄  
  6.     int     sex;    // 性别  
  7. };  
  8.   
  9. // 被调函数  
  10. int print_student(Student stu);  
  11.   
  12. // 调用函数  
  13. int main()  
  14. {  
  15.     Student stu_1 = {12345, "LiMing", 21, 0};  
  16.     print_student(stu);  
  17. }  
IDA 调试 POD 参数传递^

call print_student 指令及栈操作

call print_student 指令及栈操作

print_student prolog 及栈操作

print_student prolog 及栈操作

IDA 中两个函数参数相关汇编符号:

  • var_XX: caller 访问 callee 的参数使用的符号,表示为:相对于 caller ESP+XXh 的偏移量 [ESP+XXh+var_XX]
  • arg_XX: callee 访问自己参数使用的符号,表示为:相对于 callee EBP 的偏移量 [EBP+arg_XX]

在一次函数调用中,var_XX 和 arg_XX 是同一存储的不同名称,在 caller 中用 var_XX 访问,在 callee 中用 arg_XX 访问

C++ 对象参数传递^

传递 C++ 类对象时的拷贝构造,以及按引用传递、按地址(指针)传递参数时的汇编指令

编译器 VC 2010
编译命令 cl /Od /MDd /Zi /EHsc (Debug)

测试程序:

  1. // 复数类模板  
  2. template<class Type>  
  3. class Complex  
  4. {  
  5.     // 省略无关代码  
  6.     // copy ctor  
  7.     Complex(const Complex& right) : m_real(right.m_real), m_image(right.m_image) {}  
  8. }  
  9.   
  10. // 被调函数  
  11. void some_func(Complex<double> complex_v1, Complex<double>& complex_v2, Complex<double>* complex_v3,  
  12.                double v1, double& v2, double* v3);  
  13.   
  14. // 调用函数  
  15. void caller()  
  16. {  
  17.     Complex<double> complex_v1(1.2, 2.3);  
  18.     double          v1 = 1.3;  
  19.   
  20.     some_func(complex_v1, complex_v1, &complex_v1, 1.1, v1, &v1);  
  21. }  
VC 调试 C++ 对象参数传递^

下面是 caller 中调用 some_func() 的汇编指令:

  1. 按引用传递 和 按地址(指针)传递

    两者的汇编指令相似:

    lea         eax, [v1]       eax <= v1 的地址
    push        eax             esp = esp - sizeof(eax), [esp] <= eax
    
  2. 按值传递 double 字面量

    sub         esp, 8                              预留 8 byte (64bit) 栈空间
    fld         qword ptr [__real@3ff199999999999a] 浮点寄存器 ST0 <= 浮点数 1.1 (64bit: 3ff199999999999a)
    fstp        qword ptr [esp]                     [esp] <= ST0,不移动 esp,因为已预留 8 byte
    
  3. 按值传递 Complex 对象

    调用 Complex copy ctor 拷贝构造对象:

    sub         esp, 10h                            预留 16 byte 栈空间,sizeof(Complex
       <double style="padding: 0px; margin: 0px;">
        ) = 16 byte
    mov         ecx, esp                            准备 Complex copy ctor 的 this 指针
    lea         edx, [complex_v1]                   两条指令准备 Complex copy ctor 的参数 right
    push        edx
    call        Complex
        <double style="padding: 0px; margin: 0px;">
         ::Complex
         <double style="padding: 0px; margin: 0px;">
              调用 Complex copy ctor,由 callee 平衡堆栈 (thiscall)
    
         </double>
        </double>
       </double>
  4. 调用 some_func()

    call        some_func   [esp] <= eip
    add         esp, 28h    由 caller 平衡堆栈 (cdecl),some_func() 的参数共 40 (28h) byte
    
堆栈数据
0x00000000
0x00000004
0x00000008
0x0000000C
.
...
.
0x0012FB7C  98 fd 12 00     caller edi
0x0012FB80  78 fc 12 00     caller esi
0x0012FB84  00 60 fd 7f     caller ebx
.
...                         sub esp, 0C0h 预留 192 byte 栈空间 (some_func)
.
0x0012FC48  98 fd 12 00     caller ebp

0x0012FC4C  b1 25 41 00     调用 Complex copy ctor 时是 Complex copy ctor 的参数 right
                            调用 some_func() 时是 caller eip

                            以下是 some_func() 的参数

0x0012FC50  33 33 33 33     Complex copy ctor 构造的对象,this 指针 = 起始地址 0x0012FC50
0x0012FC54  33 33 f3 3f     浮点数 1.2 (64bit: 3ff3333333333333)
0x0012FC58  66 66 66 66     浮点数 2.3 (64bit: 4002666666666666)
0x0012FC5C  66 66 02 40

0x0012FC60  84 fd 12 00     Complex&
0x0012FC64  84 fd 12 00     Complex*
0x0012FC68  9a 99 99 99     double 64bit
0x0012FC6C  99 99 f1 3f
0x0012FC70  74 fd 12 00     double&
0x0012FC74  74 fd 12 00     double*

返回值按值传递^

测试程序:

  1. class TestClass  
  2. {  
  3. public:  
  4.     // ctor  
  5.     TestClass(int d = 0) : m_data(d)  
  6.     {  
  7.         cerr << "TestClass ctor: " << this << endl;  
  8.     }  
  9.     // copy ctor  
  10.     TestClass(const TestClass& right) : m_data(right.m_data)  
  11.     {  
  12.         cerr << "TestClass copy ctor: " << this << " <= " << &right << endl;  
  13.     }  
  14.     // dtor  
  15.     ~TestClass()  
  16.     {  
  17.         cerr << "TestClass dtor: " << this << endl;  
  18.     }  
  19.     // assign  
  20.     TestClass& operator=(const TestClass& right)  
  21.     {  
  22.         cerr << "TestClass assign: " << this << "<=" << &right << endl;  
  23.         m_data = right.m_data;  
  24.         return *this;  
  25.     }  
  26.   
  27.     template<class CharT>  
  28.     std::basic_ostream<CharT>& output(std::basic_ostream<CharT>& os) const  
  29.     {  
  30.         os << m_data;  
  31.         return os;  
  32.     }  
  33.   
  34. private:  
  35.     int     m_data;  
  36. };  
  37.   
  38. // 被调函数  
  39. TestClass get_test_obj()  
  40. {  
  41.     TestClass ret_obj(200);  
  42.     return ret_obj;  
  43. }  
  44.   
  45. // 调用函数  
  46. int main()  
  47. {  
  48.     TestClass obj = get_test_obj();  
  49.     return 0;  
  50. }  
返回值传递步骤^
  1. callee 用返回对象 ret_obj 初始化 class TestClass 的 返回值临时对象

    临时对象的销毁时机

    参考 "The C++ Programming Language"

    临时对象在维持它的那条语句之后被销毁,除非临时对象被约束到其它名字,此时由这个名字控制临时对象的生存期,约束不产生初始化或赋值语义,没有拷贝

  2. callee 返回时,由 ret_obj 的存储方式,决定是否销毁

    如果 ret_obj 是局部对象或 callee 参数,则在返回时销毁

  3. caller 中根据对 callee 返回值的使用,会有不同的情况,常见如下:

    • 用返回值赋值

      1. TestClass   obj;  
      2. obj = get_test_obj();   // 返回值临时对象调用 obj.operator=() 进行拷贝,这句结束后销毁返回值临时对象  
    • 用返回值初始化

      1. TestClass   obj = get_test_obj();   // 隐式初始化对象  
      2. TestClass   obj(get_test_obj());    // 显式初始化对象  
      3.   
      4. TestClass&  obj_ref = get_test_obj();   // 初始化引用  

      上面 3 者效果相同,均将返回值临时对象约束到 obj 或 obj_ref,期间只有一个对象本体,就是返回值临时对象,没有拷贝,之后由约束名字控制其生存期

    • 即时使用返回值而不保存

      1. get_test_obj().output(cout) << endl;    // 返回值临时对象没有约束到其它名字  
      2. cout << "bla bla bla" << endl;          // 上句结束后,这句之前,销毁返回值临时对象  
返回值传递测试结果^

对上面程序的测试结果

  • 编译器 VC 2010
    编译命令 cl /Od /MDd /Zi /EHsc (Debug)

    运行结果:

    TestClass ctor: 0012FF44
    TestClass copy ctor: 0012FF64 <= 0012FF44
    TestClass dtor: 0012FF44
    TestClass dtor: 0012FF64
    
  • 编译器 MinGW GCC 4
    编译命令 g++ -O0 -g3 -Wall

    运行结果:

    TestClass ctor: 0x22ff3c
    TestClass dtor: 0x22ff3c
    
    GCC 返回值传递优化和 VC 的区别
    • GCC 不创建额外的返回值临时对象(即使 -O0 关闭优化),而直接将 callee 的局部对象作为返回值临时对象,被调函数返回后,将其栈交给调用函数控制
    • 初始化返回值到非 const 引用 TestClass& obj = get_test_obj() 时,编译报错,而 VC 不报错,应初始化到 const 引用 const TestClass& obj_ref = get_test_obj()

返回值传递效率^

  • 因为拷贝开销,一般 不建议返回对象,除非:

    • 返回值是内部类型,如 整数、浮点数、枚举、指针、数组名等
    • 返回值是小 size 类型的对象,如 Point、Rect 等 POD,或 smart pointer 等小型封装类
  • 返回引用类型实际是 caller 直接访问 callee 中返回值对象的别名,没有拷贝开销

  • 返回局部变量时,可使用 返回时构造 技巧,如:

    1. Complex<double> func_test()  
    2. {  
    3.     return Complex<double>(2.3, 1.2);  
    4. }  

    return 语句中的 Complex(2.3, 1.2) 即是返回值临时对象,不调用 copy ctor 创建第 2 个临时对象,返回时没有销毁局部对象的开销

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值