汇编解析C语言函数结构体struct参数的方式

本文介绍在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             	shl    0x2,%eax   40050b:	89 45 ec             	mov    %eax,-0x14(%rbp)   40050e:	8b 55 d4             	mov    -0x2c(%rbp),%edx   400511:	89 d0                	mov    %edx,%eax   400513:	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    0x20,%rsp   400552:	8b 15 d8 0a 20 00    	mov    0x200ad8(%rip),%edx        # 601030 <i>   400558:	48 8d 45 e0          	lea    -0x20(%rbp),%rax   40055c:	89 d6                	mov    %edx,%esi   40055e:	48 89 c7             	mov    %rax,%rdi   400561:	e8 7a ff ff ff       	callq  4004e0 <return_struct>   400566:	8b 45 ec             	mov    -0x14(%rbp),%eax   400569:	89 05 c1 0a 20 00    	mov    %eax,0x200ac1(%rip)        # 601030 <i>   40056f:	b8 00 00 00 00       	mov0x0,%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          	sub    0x20,%rsp   4004f4:	8b 05 36 0b 20 00    	mov    0x200b36(%rip),%eax        # 601030 <i>   4004fa:	89 45 e0             	mov    %eax,-0x20(%rbp)   4004fd:	8b 05 2d 0b 20 00    	mov    0x200b2d(%rip),%eax        # 601030 <i>   400503:	01 c0                	add    %eax,%eax   400505:	89 45 e4             	mov    %eax,-0x1c(%rbp)   400508:	8b 05 22 0b 20 00    	mov    0x200b22(%rip),%eax        # 601030 <i>   40050e:	89 45 e8             	mov    %eax,-0x18(%rbp)   400511:	8b 05 19 0b 20 00    	mov    0x200b19(%rip),%eax        # 601030 <i>   400517:	89 45 ec             	mov    %eax,-0x14(%rbp)   40051a:	8b 05 10 0b 20 00    	mov    0x200b10(%rip),%eax        # 601030 <i>   400520:	89 45 f0             	mov    %eax,-0x10(%rbp)   400523:	48 83 ec 18          	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 进行定位。从另一个方面思考其实也并不奇怪,结构体参数本身是不能通过寄存器传参的,实际上,传参使用了最简单的方式——“通过压栈方式传参”,参数按照“从右到左”的顺序压栈,倘若遇到结构体,则压栈的内容变得多一些而已。

结论

结构体作为参数传入使用压栈方式传参,参数按照“从右到左”的顺序压栈,特别的,当参数为结构体,将结构体整体压栈。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据