本文介绍在x86汇编中没有加优化选项时的函数体struct传参和返回的方法。
结构体返回值
测试代码
我们先讲讲较为简单的函数返回结构体的方法——
Type return_struct(int n) { Type local_struct; local_struct.aaa = 1*n; local_struct.bbb = 2*n; local_struct.ccc = 3*n; local_struct.ddd = 4*n; local_struct.eee = 5*n; i = local_struct.eee + local_struct.aaa*2; return local_struct; } int function1() { Type main_struct = return_struct(i); i = main_struct.ddd; return 0; }
上面的代码实现了一个非常简单的返回结构体的例子,对应的汇编代码如下——
00000000004004e0 <return_struct>: 4004e0: 55 push %rbp 4004e1: 48 89 e5 mov %rsp,%rbp 4004e4: 48 89 7d d8 mov %rdi,-0x28(%rbp) 4004e8: 89 75 d4 mov %esi,-0x2c(%rbp) 4004eb: 8b 45 d4 mov -0x2c(%rbp),%eax 4004ee: 89 45 e0 mov %eax,-0x20(%rbp) 4004f1: 8b 45 d4 mov -0x2c(%rbp),%eax 4004f4: 01 c0 add %eax,%eax 4004f6: 89 45 e4 mov %eax,-0x1c(%rbp) 4004f9: 8b 55 d4 mov -0x2c(%rbp),%edx 4004fc: 89 d0 mov %edx,%eax 4004fe: 01 c0 add %eax,%eax 400500: 01 d0 add %edx,%eax 400502: 89 45 e8 mov %eax,-0x18(%rbp) 400505: 8b 45 d4 mov -0x2c(%rbp),%eax 400508: c1 e0 02 shl0x2,%eax 400516: 01 d0 add %edx,%eax 400518: 89 45 f0 mov %eax,-0x10(%rbp) 40051b: 8b 45 f0 mov -0x10(%rbp),%eax 40051e: 8b 55 e0 mov -0x20(%rbp),%edx 400521: 01 d2 add %edx,%edx 400523: 01 d0 add %edx,%eax 400525: 89 05 05 0b 20 00 mov %eax,0x200b05(%rip) # 601030 <i> 40052b: 48 8b 45 d8 mov -0x28(%rbp),%rax 40052f: 48 8b 55 e0 mov -0x20(%rbp),%rdx 400533: 48 89 10 mov %rdx,(%rax) 400536: 48 8b 55 e8 mov -0x18(%rbp),%rdx 40053a: 48 89 50 08 mov %rdx,0x8(%rax) 40053e: 8b 55 f0 mov -0x10(%rbp),%edx 400541: 89 50 10 mov %edx,0x10(%rax) 400544: 48 8b 45 d8 mov -0x28(%rbp),%rax 400548: 5d pop %rbp 400549: c3 retq 000000000040054a <function1>: 40054a: 55 push %rbp 40054b: 48 89 e5 mov %rsp,%rbp 40054e: 48 83 ec 20 sub
0x0,%eax 400574: c9 leaveq 400575: c3 retq
分析
在上面的程序中,内层 return_struct 函数在被调用时,有两个有意义的寄存器,分别是%rdi 记录了在function1中定义的那个结构体的位置,%rsi 记录传入的n。
在函数进入时,两个寄存器都被保存到了内存里面,接下来按部就班实现C++中的逻辑。重点部分在第30-35行,使用了三个位移操作赋值了五个值。最初理解代码,尝试寻找拷贝内存的语句时,将这三句话直接排除在外,实际上,由于结构体里面是5个整形,而使用64位寄存器,一次可以拷贝两个整数,因此三次拷贝恰好可以用最快的时间完成拷贝。
结论
因此,结构体返回的方式是将返回的结构体的起始位置指针放入%rdi 寄存器,并在子程序中通过该寄存器定位修改结构体的内容。
结构体传参
测试代码
首先设计样例代码如下——
typedef struct{ int aaa; int bbb; int ccc; int ddd; int eee; }Type; int i= 2; int input_struct(Type in_struct) { int ret = in_struct.eee + in_struct.aaa*2; return ret; } int function2() { Type main_struct; main_struct.aaa = i; main_struct.bbb = 2*i; main_struct.ccc = i; main_struct.ddd = i; main_struct.eee = i; return input_struct(main_struct); }
反汇编出的信息如下——
00000000004004d6 <input_struct>: 4004d6: 55 push %rbp 4004d7: 48 89 e5 mov %rsp,%rbp 4004da: 8b 45 20 mov 0x20(%rbp),%eax 4004dd: 8b 55 10 mov 0x10(%rbp),%edx 4004e0: 01 d2 add %edx,%edx 4004e2: 01 d0 add %edx,%eax 4004e4: 89 45 fc mov %eax,-0x4(%rbp) 4004e7: 8b 45 fc mov -0x4(%rbp),%eax 4004ea: 5d pop %rbp 4004eb: c3 retq 00000000004004ec <function2>: 4004ec: 55 push %rbp 4004ed: 48 89 e5 mov %rsp,%rbp 4004f0: 48 83 ec 20 sub0x18,%rsp 400527: 48 89 e0 mov %rsp,%rax 40052a: 48 8b 55 e0 mov -0x20(%rbp),%rdx 40052e: 48 89 10 mov %rdx,(%rax) 400531: 48 8b 55 e8 mov -0x18(%rbp),%rdx 400535: 48 89 50 08 mov %rdx,0x8(%rax) 400539: 8b 55 f0 mov -0x10(%rbp),%edx 40053c: 89 50 10 mov %edx,0x10(%rax) 40053f: e8 92 ff ff ff callq 4004d6 <input_struct> 400544: 48 83 c4 18 add $0x18,%rsp 400548: c9 leaveq 400549: c3 retq
分析
有了上面的铺垫,我们可以迅速找到在30-35行有一段结构体的拷贝——将位于局部变量中的结构体拷贝到了在28行分配的一段连续空间中,而拷贝操作都是由%rax 定位的。有意思的是,当我们认为%rax 正是一个向子程序定位结构体的指针是,发现在子程序中%rax 的寄存器中的值并没有被使用,反而是使用了%rsp 进行定位。从另一个方面思考其实也并不奇怪,结构体参数本身是不能通过寄存器传参的,实际上,传参使用了最简单的方式——“通过压栈方式传参”,参数按照“从右到左”的顺序压栈,倘若遇到结构体,则压栈的内容变得多一些而已。
结论
结构体作为参数传入使用压栈方式传参,参数按照“从右到左”的顺序压栈,特别的,当参数为结构体,将结构体整体压栈。