汇编中函数返回结构体的方法
来源:互联网 发布:stata软件下载 编辑:程序博客网 时间:2024/04/27 14:30
汇编中函数返回结构体的方法
代码生成,函数的返回值是个问题,如果返回值是简单类型,如int, char等,一个字节可以容纳,那编译器的做法是将值直接存在eax寄存器中.
代码为证
c代码:
#include <stdio.h>
int
add(
int
a,
int
b){
return
a + b;
}
int
main(){
int
a = add(2,3);
return
0;
}
gcc -S add.c
add.s汇编代码:
.globl add .type add, @functionadd: pushl %ebp movl %esp, %ebp movl 12(%ebp), %eax movl 8(%ebp), %edx leal (%edx,%eax), %eax popl %ebp ret .size add, .-add.globl main .type main, @functionmain: pushl %ebp movl %esp, %ebp subl $24, %esp movl $3, 4(%esp) movl $2, (%esp) call add movl %eax, -4(%ebp) movl $0, %eax leave ret
那么如果返回一个结构体,寄存器无法存下怎么办呢?
方法是, 调用者在调用的时候,先把需要的参数反向入栈, 最后再把变量的地址入栈,如 struct A t = get_struct(int a);
则先int a 入栈,然后将t的地址放入eax, pushl %eax.
在被调用的函数中,我们就完成了返回值的赋值工作, 因为有了返回值赋值的变量的地址,所以依次将struct的各成员变量赋值给返回变量的对应内存即可.
好吧,上代码
c代码:
#include <stdio.h>struct A{ int a; char c;};struct A add(int a, int b){ struct A t; t.a = a*b; return t;}int main(){ struct A t = add(3, 4); printf("%c\n", t.c); return 0;}
gcc -S temp.c
temp.s:
.globl add .type add, @functionadd: pushl %ebp movl %esp, %ebp subl $16, %esp movl 8(%ebp), %ecx # 8(%ebp)最后一个参数,就是存储了返回变量在调用者中的地址,即调用前main中的%eax的值 movl 12(%ebp), %eax #取出第二个int 参数 imull 16(%ebp), %eax #取出第一个int参数,记住,参数反向入栈,后面的参数在低地址 movl %eax, -8(%ebp) #-8(%ebp) ~ (%ebp)存储的是局部结构体变量t movl -8(%ebp), %eax #依次将局部变量的各个成员变量移动到寄存器 movl -4(%ebp), %edx movl %eax, (%ecx) #赋值给传入地址的调用者的变量 movl %edx, 4(%ecx) movl %ecx, %eax leave ret $4.LC0: .string "%c\n".globl main .type main, @functionmain: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $36, %esp #为什么是36呢? main函数的int argc, char *argv[]8个字节, #struct A t 局部变量8个字节,然后add()调用12个字节--其中有一个是隐式的入栈,%eax记录 #局部变量的地址, 然后printf8个字节 leal -16(%ebp), %eax movl $4, 8(%esp) #参数入栈,这儿入栈没有用pushl,而是先subl esp,预留好空间,然后movl,是等效的 movl $3, 4(%esp) movl %eax, (%esp) #需要赋值的局部变量的地址入栈,有了这个地址,赋值工作就在add()zh中完成了 call add subl $4, %esp movzbl -12(%ebp), %eax movsbl %al,%edx movl $.LC0, %eax movl %edx, 4(%esp) movl %eax, (%esp) call printf movl $0, %eax movl -4(%ebp), %ecx leave leal -4(%ecx), %esp ret
好了,基本上解决问题了,知道碰到struct A function();的赋值问题怎么解决了, 但还有个问题, 如果没有赋值,我只是调用add(3, 4);
那调用者的%eax,应该把什么地址入栈呢?
经过实验法现,不管你有没i有var = add(3,4); 编译器都回在调用者的栈上预留一个局部变量的空间,然后把这个变量地址 -> eax -> pushl stack
恩,这些基本上算是没啥问题了~~:)
==========================================================================
【前言】写作本文,源于最近回复的 《汇编中函数返回结构体的方法》 一文。在网络上也已经有一些相关文章和相关问题,有的文章已经给出了一部分结果,但总体而言还缺少比较重要的结论。本文以分析 VC6 编译器,32 位架构为主来重复性分析这个话题。
(一)不超过 8 bytes 的小结构体可以通过 EDX:EAX 返回。
本文的范例代码取材于 《汇编中函数返回结构体的方法》一文,并在此基础上进行修改和试验。要研究的第一份代码如下,定义一个不超过 8 bytes 的小结构体,不超过 8 bytes 是因为这个结构体能够用 EDX:EAX 容纳,我们之后将看到在 release 编译时,编译器能够向返回普通基础类型那样进行返回。
#include <stdio.h>//不超过 8 bytes 的“小结构体”struct A{ int a; int b;};//返回结构体的函数struct A add(int x, int y){ struct A t; t.a = x * y; return t;}int main(){ struct A t = add(3, 4); printf("t.a = %ld\n", t.a); return 0;}
首先,我们需要解决一个常见困惑,就是要明确这段代码和下面的典型错误代码的区别:
char* get_buffer()
{
char buf[8];
return buf;
}
上面的 get_buffer 返回的是栈上的临时变量空间,在函数返回后,其所在的空间也就被“回收/释放”了,也就是说函数返回的地址位于栈的增长方向上,是不稳定和不被保证的。
那么返回结构体的函数则不同,你可以发现返回结构体的函数是工作正常有效的。在 add 函数中有一个临时性结构体 t,毫无疑问,t 将在 add 函数返回时被释放,但由于 t 被当做“值”进行返回,因此编译器将保证 add 的返回值对于 add 的调用者(caller)来说是有效的。
另外需要明确的一点是,我个人觉得,现实里这种返回结构体的方式比较少见,后面将会看到这样做会产生临时对象和多余拷贝过程,效率不高。常见方法是传递结构体指针。但作为语言上允许的方式,有必要弄清楚编译器如何实现这种方式,而要弄清楚这个问题,需要查看汇编代码。使用 VC6 输入上述代码,下面分别给出其汇编代码。
(1)debug 版本,汇编代码如下。
下面是实现方式的栈示意图:
总结:
(1.1)用 edx:eax 传递返回值。调用方不需要在栈上向 add 函数传递接受返回值的地址。
(2.2)debug 版本在调用方生成临时对象返回值,然后再把临时对象拷贝到 main 临时变量所在地址。效率低。
(2)release 版本,汇编代码如下:
总结:
(2.1)同(1.1),用 edx:eax 传递返回值,不需要传递接收返回值的地址。
(2.2)release 版本调用方没有临时对象,效率基本等同于传结构体指针。
(2.3)release 版本优化的太厉害,甚至都没有把返回值完整的拷贝到临时变量 t (只拷贝了结构体中的成员t.b,t.a 的拷贝被认为没有存在价值而被优化掉了,因为 t.a 的值存于 eax),和高级语言有较大差别。
(二)超过 8 bytes 的结构体,调用方需要提供用于接收返回值的地址。
如果是超过 8 bytes 的结构体,EDX:EAX 将容纳不下,这时就需要调用方提供接受返回值的地址,即调用方在栈上分配临时对象,并把其地址通过栈传递给函数(先 push 参数,最后 push 用于设置返回值的结构体地址)。
把上述代码中的结构体定义增加一个 int 成员即可令结构体超过 8 bytes,即调整上述代码的 struct 定义:
struct A
{
int a;
int b;
int c;
};
使用 VC6 编译后产生的汇编代码如下:
debug 版本:
release 版本:
上述两种编译结果,实现的模型基本相同。因此在这里以debug版本代码为主,一并分析,其栈示意图如下,下图左侧为 debug 版本,右侧是 release 版本:
总结:
(1)当结构体超过 8 bytes,不能用 EDX:EAX 传递,这时调用方在栈上保留有一个用于填充返回值的结构体,其地址在入栈参数后 push 到栈上。函数将会根据这个地址,把返回值设置到这个地址。
(2)在 main 函数中,debug 版本比 release 版本还多了一个临时对象,效率低。而 release 版本中只有返回值和临时变量 t,效率略高于 debug。但两者模型基本一致,总体效率低于传结构体指针。
(3)release 版本同样优化比较厉害,main 函数中对 t 的赋值是不完整的,因为编译器认为没有必要,只要满足代码等效即可。
最后我们总结针对较大结构体(超过 8 bytes)时,返回结构体的函数的实现方式的基本模型:
(1)调用方在栈上分配用于接收返回值的临时结构体,并把地址通过栈传递给函数。
(2)函数根据返回值的地址,设置返回值。
(3)调用方根据需要,把返回值再赋值给需要的临时变量。
(4)返回时,eax 存储的是返回值的那个地址。
因此,从上面的过程可以看到,由于存在临时对象和拷贝操作,其效率比传递结构体指针的函数低。
由于不管 debug 还是 release,对于“大结构体”都会在栈上传递返回值的地址,所以我们可以通过下面的代码,来测试出这样的结论:函数 add 的返回值(临时结构体)的地址和 main 中的变量 t 的地址是不同的。原理是,第一个形参的栈顶方向的相邻元素就是返回值的地址,因此用一个指针指向第一个形参,然后向栈顶移动一格,取出其值,就是返回值的地址。
#include <stdio.h>struct A{ int a; int b; int c;};struct A add(int x, int y){ struct A t; int* p = &x; p--; printf("address of return struct: %08X\n", *p); t.a = x * y; return t;}int main(int argc, char* argv[]){ struct A t = add(3, 4); struct A *p1 = &t; printf("address of t in main: %p\n", &t); return 0;}
上面的代码中,有一点需要注意,返回值的地址和 t 的地址的关系是依赖编译器的,也就是说,没有任何保证,两者之间是否相邻以及它们之间的大小关系。但你可以通过尝试移动上面的指针 p1,试图将 p1 指向返回值,但这并不是一个简单容易的事情(因为编译器的行为效果是尽量避免让这个返回值被其他指针指到)。
- 汇编中函数返回结构体的方法
- C函数返回结构体在汇编下的实现
- C 语言函数返回结构体汇编分析
- C语言函数入参和返回值是结构体时的汇编分析
- 内核中返回结构体首地址的函数container_of
- C语言返回值为结构体的汇编分析
- 使用ctypes调用C共享库中函数返回值为链表式结构时的方法
- 汇编中结构体的使用
- 关于返回结构体的函数
- 函数返回结构体的内幕
- 函数可以返回结构体的原因
- 从函数中返回多个值的方法
- 函数中返回字符串的方法
- 函数中返回字符串的方法
- 函数中返回字符串的方法
- JNI 返回结构体参数的方法
- 有关结构体指针与函数传参返回值类型的六种方法
- C#函数的参数中返回结构数组
- 个人的专业知识库
- Radar Installation
- 几种任务调度的 Java 实现方法与比较
- JavaScript 自定义对象 for in
- C语言消除逗号,PS:malloc基本用法
- 汇编中函数返回结构体的方法
- 在本地配置tomcat
- ZOJ3492 Kagome Kagome
- Singleton
- 读 《think in java》 有感 -- 第(七)章
- nyist14年3月月赛题解
- 301重定向循环研究
- PullToRefreshListView实现根据拖拉的方向进行在顶部或者底部加载数据。
- DAO模型的增删改查