文档章节

vc如何返回函数结果及压栈参数

C
 C_Geek
发布于 2015/04/16 19:01
字数 2319
阅读 12
收藏 0

    首先说明,本文的分析对象是运行在IA32平台上的程序,试验用的编译器是Visual C++ 6.0中的cl.exe(Microsoft 32-bit C/C++ Optimizing Compiler Version 12.00.8804 for 80x86)。

    IA32程序利用程序堆栈来支持过程(procedure)调用,比如创建局部数据、传递参数、保存返回值信息、保存待今后恢复的寄存器等。为了一个过程调用而分配的堆栈空间称为一个stack frame。最顶层的stack frame由两个寄存器标识:ebp保存stack frame的基址,esp保存栈顶地址,因为在过程执行的时候栈顶寄存器的值会经常变化,所以绝大多数的信息都是通过ebp来相对寻址的。图1描绘了stack frame的基本结构。注意在IA32机器上,栈是向低地址方向增长,所以栈顶的地址值小于等于栈底的地址值。

                   stack "bottom"
                  ________________ ______
                 |                |  |
                 |       .        |  |
                 |                |  |
                 |       .        |  |
                 |                |  |--->Earlier frames
                 |       .        |  |
                 |                |  |
                 |       .        |  |       
    |            |                |  |
    |            |________________|__|___
    |            |       .        |  |
    |            |       .        |  |
    |            |       .        |  |
Decreasing       |________________|  |
 address         |   Argument n   |  |
    |       4m-->|________________|  |
    |   (m为整数) |       .        |  |-->Caller's frame
    |            |       .        |  |
    |            |________________|  |
    |            |   Argument 1   |  |
    |       +8-->|________________|  |
    V            | Return address |  | 
            +4-->|________________|__|___
                 |   Saved  ebp   |  |
 Frame Pointer-->|________________|  |
           ebp   |Saved  registers|  |
                 |local  variables|  |
                 |      and       |  |-->Current frame
                 |   temporaries  |  |
                 |________________|  |
                 |    Argument    |  |
                 |   build area   |  |
 Stack pointer-->|________________|__|___
           esp       stack "top"

                        图1

    调用C函数时,不管参数类型如何(包括浮点和struct类型)调用者(caller)负责从右至左将参数依次压栈,最后压入返回地址并跳转到被调函数入口处执行,其中每个参数都要按4字节地址对齐。按照地址来说被传递的参数和返回地址都是位于调用者stack frame中的。如果函数的返回值类型是整型(包括char,short,int,long及它们的无符号型)或指针类型的话,那么就利用EAX寄存器来传回返回值。请看下面的函数:

long foo_long(long offset)
{
   long val = 2006 + offset;
   return val;
}

用 /c /Od /FAsc 的编译选项(下文均同)编译出如下的汇编码:

PUBLIC    _foo_long
_TEXT    SEGMENT
_offset$ = 8
_val$ = -4
_foo_long PROC NEAR

; 38   : {

push    ebp ;保存调用者的stack frame基址
mov     ebp, esp
push    ecx

; 39   :    long val = 2006 + offset;

mov     eax, DWORD PTR _offset$[ebp]
add     eax, 2006
mov     DWORD PTR _val$[ebp], eax

; 40   :    return val;

; 将返回值保存进eax寄存器
mov  eax, DWORD PTR _val$[ebp]

; 41   : }

mov     esp, ebp
pop     ebp
ret     0

_foo_long ENDP
_TEXT    ENDS

    如果函数返回的是结构体数据(非结构体指针)那得通过哪个中转站传递呢?这就要分三种情况:

1、结构体大小不超过4字节,那么仍然使用EAX寄存器传递返回值。比如:

/* 4字节大小结构体 */
typedef struct small_t
{
   char m1;
   char m2;
   char m3;
   char m4;
} small_t;

small_t create_small(void)
{
   small_t small = {'s','m','a','l'};
   return small;
}

void call_small(void)
{
   small_t small_obj = create_small();
}

编译出的汇编码是:

;create_small函数
PUBLIC    _create_small
_TEXT    SEGMENT
_small$ = -4
_create_small PROC NEAR

; 16   : {

push     ebp
mov     ebp, esp
push     ecx

; 17   :    small_t small = {'s','m','a','l'};

mov BYTE PTR _small$[ebp], 115 ; 00000073H
mov BYTE PTR _small$[ebp+1], 109 ; 0000006dH
mov BYTE PTR _small$[ebp+2], 97 ; 00000061H
mov BYTE PTR _small$[ebp+3], 108 ; 0000006cH

; 18   :    return small;

;依然用EAX保存返回值
mov     eax, DWORD PTR _small$[ebp]

; 19   : }

mov     esp, ebp
pop     ebp
ret     0

_create_small ENDP
_TEXT    ENDS

;call_mall函数
PUBLIC    _call_small
_TEXT    SEGMENT
_small_obj$ = -4
_call_small PROC NEAR

; 22   : {

push     ebp
mov     ebp, esp
push     ecx

; 23   :    small_t small_obj = create_small();

call  _create_small
;从eax寄存器中取出返回值填充到small_obj变量中
mov DWORD PTR _small_obj$[ebp], eax

; 24   : }

mov     esp, ebp
pop     ebp
ret     0

_call_small ENDP
_TEXT    ENDS

2、结构体超过4字节但不等于8字节时,调用者将首先在栈上分配一块能容纳结构体的临时内存块,然后在传递完函数参数后将该临时内存块的首地址作为隐含的第一个参数最后(因为压栈顺序是从右到左)压栈,接下的动作同前所述。当被调用函数返回时,它会通过第一个隐含参数寻址到临时内存块并将返回值拷贝到其中,然后将保存有返回值内容的临时内存块的首址存进eax寄存器中,最后退出。请看下例:

typedef struct fool_t
{
   short a;
   short b;
   short c;
} fool_t;

fool_t create_fool(short num)
{
   fool_t fool = {num,num,num};
   return fool;
}

void call_fool(void)
{
   fool_t fool_obj = create_fool(2006);
}

汇编码为:
; create_fool函数
PUBLIC    _create_fool
_TEXT    SEGMENT
_num$ = 12
_fool$ = -8
; 编译器隐含传递的第一个参数(相对于stack frame基址)的偏移值,
; 该参数等于用于传递结构体返回值的临时内存块的首地址。
; $T480是编译器自动生成的标号。
$T480 = 8 
_create_fool PROC NEAR

; 9    : {

push    ebp
mov     ebp, esp
sub     esp, 8

; 10   :    fool_t fool = {num,num,num};

mov     ax, WORD PTR _num$[ebp]
mov     WORD PTR _fool$[ebp], ax
mov     cx, WORD PTR _num$[ebp]
mov     WORD PTR _fool$[ebp+2], cx
mov     dx, WORD PTR _num$[ebp]
mov     WORD PTR _fool$[ebp+4], dx

; 11   :    return fool;

; 将临时内存块的首地址存入eax
mov     eax, DWORD PTR $T480[ebp]
; 将结构体返回值的低4字节通过ecx存入临时内存块的低4字节
mov     ecx, DWORD PTR _fool$[ebp]
mov     DWORD PTR [eax], ecx
; 将结构体返回值的高2字节通过dx存入临时内存块的高2字节
mov     dx, WORD PTR _fool$[ebp+4]
mov     WORD PTR [eax+4], dx
; 将临时内存块的首地址存入eax,并准备退出
mov     eax, DWORD PTR $T480[ebp]

; 12   : }

mov     esp, ebp
pop     ebp
ret     0

_create_fool ENDP
_TEXT    ENDS

; call_fool函数
PUBLIC    _call_fool
_TEXT    SEGMENT
_fool_obj$ = -8
; 编译器为接纳结构体返回值而自动在栈上临时分配了一块内存,
; 注意fool_t结构体大小虽为6字节,但需要对齐到4字节边界,
; 所以分配了8字节大小的空间。
$T482 = -16
_call_fool PROC NEAR

; 15   : {

push    ebp
mov     ebp, esp
sub     esp, 16     ; 00000010H

; 16   :    fool_t fool_obj = create_fool(2006);

push     2006  ; 000007d6H
; 取得临时内存块的首地址并压栈
lea     eax, DWORD PTR $T482[ebp]
push    eax
; 函数调用完毕后,临时内存块将被填入结构体返回值
call     _create_fool
add     esp, 8
; 通过ecx将临时内存块的低4字节数据存进fool_obj的低4字节
mov     ecx, DWORD PTR [eax]
mov     DWORD PTR _fool_obj$[ebp], ecx
; 通过dx将临时内存块的高2字节数据存进fool_obj的高2字节
mov     dx, WORD PTR [eax+4]
mov     WORD PTR _fool_obj$[ebp+4], dx

; 17   : }

mov     esp, ebp
pop     ebp
ret     0

_call_fool ENDP
_TEXT    ENDS

3、结构体大小刚好为8个字节时编译器不再于栈上分配内存,而直接同时使用EAX和EDX两个寄存器传递返回值,其中EAX保存低4字节数据,EDX保存高4字节数据。请看下面2个函数:

/* 如果编译器的最大对齐模数是8,则该结构体大小为8字节 */
typedef struct big_t
{
   char m1;
   long m2;
} big_t;

big_t create_big(char c)
{
   big_t big = {c, 2006};
   return big;
}

void call_big(void)
{
   big_t big_obj = create_big('A');
}

编译出的汇编码是:

; create_big函数
PUBLIC    _create_big
_TEXT    SEGMENT
_c$ = 8
_big$ = -8
_create_big PROC NEAR

; 27   : {

push    ebp
mov     ebp, esp
sub     esp, 8

; 28   :    big_t big = {c, 2006};

mov     al, BYTE PTR _c$[ebp]
mov     BYTE PTR _big$[ebp], al
mov     DWORD PTR _big$[ebp+4], 2006 ; 000007d6H

; 29   :    return big;

; 通过eax和edx返回 big

mov eax, DWORD PTR _big$[ebp]
mov edx, DWORD PTR _big$[ebp+4]

; 30   : }

mov     esp, ebp
pop     ebp
ret     0

_create_big ENDP
_TEXT    ENDS

;call_big函数
PUBLIC    _call_big
_TEXT    SEGMENT
_big_obj$ = -8
_call_big PROC NEAR

; 33   : {

push  ebp
mov   ebp, esp
sub   esp, 8

; 34   :    big_t big_obj = create_big('A');

push  65       ; 00000041H
call     _create_big
add     esp, 4

; 通过eax和edx取得返回值
mov     DWORD PTR _big_obj$[ebp], eax
mov     DWORD PTR _big_obj$[ebp+4], edx

; 35   : }

mov     esp, ebp
pop     ebp
ret     0

_call_big ENDP
_TEXT    ENDS

因为结构体大小与编译时的最大对齐模数选项有关(具体关系请参见《内存对齐与结构体的内存布局》),所以当最大对齐模数改变时返回动作将可能改变。对于本例,在编译时加上/Zp2选项,则big_t结构体类型大小为6字节,create_big函数也将相应地利用临时内存块而非edx寄存器来返回数据。

    至于返回值为浮点类型则与整型和结构体型有相当的不同,因为IA32架构CPU有一套特殊的基于浮点寄存器栈操作的浮点指令集。浮点寄存器栈包括8个浮点寄存器,编号从0到7,其中0号为栈顶,7号为栈底,指令可通过编号访问相应寄存器,所有浮点运算的结果均保存在栈顶。取得浮点返回值很简单,只需直接弹出栈顶元素并拷贝到相应的变量中就可以了。我们通过下面这段小程序来验证一下。

double foo(double a)
{
   return a + 10;
}

void goo()
{
   double b;
   b = foo(10.0);
}

; foo函数
PUBLIC    _foo
PUBLIC    
__real@8 @4002a000000000000000
EXTRN    __fltused:NEAR
;    COMDAT 
__real@8@4002a000000000000000
; File convention.c
CONST    SEGMENT
__real@8@4002a000000000000000 DQ 04024000000000000r ; 10
CONST    ENDS
_TEXT    SEGMENT
_a$ = 8
_foo    PROC NEAR

; 19   : {

push     ebp
mov      ebp, esp

; 20   :    return a + 10;

; 从变量a中读入一个double型浮点数并压入浮点栈
fld     QWORD PTR _a$[ebp]

; 将浮点栈的栈顶元素与10相加,将结果压入浮点栈
fadd     QWORD PTR 
__real@8@4002a000000000000000

; 21   : }

pop     ebp
ret     0

_foo     ENDP
_TEXT    ENDS

; goo函数
PUBLIC    _goo
_TEXT    SEGMENT
_b$ = -8
_goo    PROC NEAR

; 24   : {

push    ebp
mov     ebp, esp
sub     esp, 8

; 25   :    double b;
; 26   :    b = foo(10.0);

push 1076101120 ; 40240000H
push     0
call     _foo
add   esp, 8
; 将浮点栈顶元素弹出并存入变量b,此即保存foo函数返回值的动作
fstp     QWORD PTR _b$[ebp]

; 27   : }

mov     esp, ebp
pop     ebp
ret     0

_goo     ENDP
_TEXT    ENDS


参考资料:
[1] 《Computer Systems: A Programmer's Perspective》.
    Randal E. Bryant, David R. O'Hallaron. 
    电子工业出版社


本文转载自:http://blog.csdn.net/soloist/article/details/1267147

C
粉丝 1
博文 43
码字总数 11167
作品 0
浦东
程序员
私信 提问
函数调用约定 (cdecl stdcall)

函数调用约定 (cdecl stdcall) 在 C 语言里,我们通过阅读函数声明,就知道怎么携带参数去调用函数,也能在函数体定义内使用这些参数。但是 CPU 并不直接完成函数调用的传参操作,这需要人为...

傅易
2018/08/18
61
0
__cdecl __stdcall

1.如果函数func是cdecl(默认调用方式),调用时情况如下 int main() { //参数从右到左压栈 push 4 push 3 push 2 push 1 call func add esp 0x10 //调用者恢复堆栈指针esp,4个参数的大小是0...

ryany
2011/03/21
171
0
_cdecl、_stdcall、_fastcall和_thiscall整理

cdecl、stdcall、fastcall和thiscall整理 1._cdecl是C Declaration的缩写,表示C语言默认的函数调用方法:所有参数 从右到左依次入栈,这些参数由调用者清除,称为手动清栈(由调用者把参数弹...

西昆仑
2011/11/17
211
1
lua-epoll 模块简单分析

这个模块是把Linux下的epoll操作按照Lua Cfunction 的格式封装出来,供lua使用。 Lua要求每一个扩展模块,必须提供luaopenXXX(luaState *L) 作为模块的入口函数,此函数会在require加载模块时...

kaedehao
2015/10/02
352
0
WIN32编程必知:__stdcall,__cdecl,__fastcall,thiscall,n

被这些修饰关键字修饰的函数,其参数都是从右向左通过堆栈传递的(fastcall的前面部分由ecx,edx传), 函数调用在返回前要清理堆栈,但由调用者还是被调用者清理不一定。 1、_stdcall是Pascal程...

guoliang
2014/04/03
337
0

没有更多内容

加载失败,请刷新页面

加载更多

skywalking(容器部署)

skywalking(容器部署) 标签(空格分隔): APM [toc] 1. Elasticsearch SkywalkingElasticsearch 5.X(部分功能报错、拓扑图不显示) Skywalking需要Elasticsearch 6.X docker network create......

JUKE
19分钟前
4
0
解决Unable to find a single main class from the following candidates [xxx,xxx]

一、问题描述 1.1 开发环境配置 pom.xml <plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><!--一定要对上springboot版本号,因......

TeddyIH
20分钟前
4
0
Dubbo服务限制大数据传输抛Data length too large: 13055248, max payload: 8388608解决方案

当dubbo服务提供者向消费层传输大数据容量数据时,会受到Dubbo的限制,报类似如下异常: 2019-08-23 11:04:31.711 [ DubboServerHandler-XX.XX.XX.XXX:20880-thread-87] - [ ERROR ] [com.al...

huangkejie
23分钟前
4
0
HashMap和ConcurrentHashMap的区别

为了线程安全,ConcurrentHashMap 引入了一个 “分段锁” 的概念。具体可以理解把一个大的 map 拆分成 N 个小的 Map 。最后再根据 key.hashcode( )来决定放到哪一个 hashmap 中去。 hashmap ...

Garphy
23分钟前
3
0
购买SSL证书需要注意哪些问题

为了保障网站的基本安全,为网站部署SSL证书,已经是一种常态了。各大浏览器对于安装了SSL证书的网站会更友好,并且不会发出“不安全”的提示。部署SSL证书之前首先得去给网站购买一个SSL证书...

安信证书
53分钟前
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部