函数参数的传递方法

本文选自《专业嵌入式软件开发——全面走向高质高效编程(DVD光盘1)》一书

《专业嵌入式软件开发——全面走向高质高效编程(DVD光盘1)》一书已由电子工业出版社正式出版

李云

函数参数的传递方法

ABI规范中还定义了函数参数的传递方式和参数的压栈顺序。在x86处理器的ABI规范中规定,所有传递给被调用函数的参数都是通过栈来完成的,其压栈的顺序是以函数参数从右到左的顺序。在图10.7的第54行,当main()函数调用middle()函数时所传入3个参数在栈中的布局可以从图10.9的下方找到,参数压栈的顺序是_p2_p1_p0

x86处理器上,当向一个函数传递参数时,所有的参数最后形成的是一个数组。由于采用从右到左的压栈操作,所以数组中参数的顺序(比如,从下标0到下标2)与函数从左到右的顺序是一致的(_p0_p1_p2)。因此,在一个函数中如果知道了第一个参数的地址和各参数占用字节的大小,就可以通过访问数组的方式去访问每一个参数。

整型和指针参数的传递

整型参数的传递在前面已经看到了,而指针参数的传递与整型是一样的。这是因为,在32x86处理器上整型的大小与指针的大小都是一样的,都占4个字节。来自x86处理器ABI规范中的图10.17总结了这两种类型的参数在栈帧中的位置关系。注意,该表是基于tail()函数的栈帧而言的。

Call

Argument

Stack Address

tail (1, 2, 3, (void *)0);

1

8(%ebp)

2

12(%ebp)

3

16(%ebp)

(void *)0

20(%ebp)

10.17

浮点参数的传递

浮点参数的传递与整型其实是相似的,唯一的区别就是参数的大小。在x86处理器中,浮点类型占8个字节,因此在栈中也需要占用8个字节。来自x86处理器ABI规范的图10.18示例说明了浮点参数在栈帧中的位置关系。图中,调用tail()函数的第一个和第三个参数都是浮点类型,因此各需要占用8个字节,三个参数共需要占用20个字节。图中的word类型的大小是4个字节。

Call

Argument

Stack Address

tail (1.414, 1, 2.998e10);

word 0, 1.414

8(%ebp)

word 1, 1.414

12(%ebp)

1

16(%ebp)

word 0, 2.998e10

20(%ebp)

word 1, 2.998e10

24(%ebp)

10.18

结构体和联合体参数的传递

结构体(struct)和联合体(union)参数的传递与前面提到的整型、浮点参数相似,只是其占用字节的大小需视数据结构的定义不同而异。但是无论如何,结构体在栈上所占用的字节数一定是4的倍数。这是因为在32位的x86处理器上栈宽是4字节的,因此编译器也会“很聪明地”对结构体进行适当的填充以使得结构体的大小满足4字节对齐的要求。

上面讲解的内容都是以x86处理器为例的。对于一些RISC处理器,比如PowerPC,其参数传递并不是全部通过栈来实现的。从图10.5PowerPC处理器寄存器的功能分配表可以看出,R3R108个寄存器用于整型或指针参数的传递,F1F88个寄存器用于浮点参数的传递。当所需传递的参数个数小于8时,根本不需要用到栈。

10.19是一个在PowerPC处理器上多参数传递的例子,图10.20则是处理器寄存器的分配和栈帧在参数传递时的布局。

example.c

typedef struct {

    int int1_, int2_;

    double double_;

} parameter_t;

void middle ()

{

    parameter_t p1, p2;

    int int1, int2, int3, int4, int5, int6;

    long double long_double;

    double double1, double2, double3, double4, double5, double6, double7, double8, double9;

    tail (int1, double1, int2, double2, int3, double3, int4, double4, int5, double5,

        int6, long_double, double6, double7, p1, double8, p2, double9);

}

10.19

General Purpose Registers

Floating Point Registers

Stack Frame Offset

R3: int1

F1: double1

8(%ebp): pointer to p2

R4: int2

F2: double2

12(%ebp): (padding)

R5: int3

F3: double3

16(%ebp): word 0 of double9

R6: int4

F4: double4

20(%ebp): word 1 of double9

R7: int5

F5: double5


R8: int6

F6: double6


R9: pointer to long_double

F7: double7


R10: pointer to p1

F8: double8


10.20

可以看出,结构体和long double参数的传递是通过指针来完成的,这一点与x86处理器完全不同。在PowerPCABI规范中规定,对于结构体的传递仍采用指针的方式,而不是像x86处理器那样将结构从一个函数的栈帧中拷贝到另一个函数的栈帧中,显然x86处理器的方式更低效。由此看来,“在实现函数时,其参数应当尽量用指针而不是用结构体以便提高效率”这一原则对于PowerPC处理器上的程序并不成立。但无论如何,养成函数参数传指针而非结构体这一编程习惯还是有益的。

函数返回值的返回方法

x86处理器上,当被调用函数需要返回结果给调用函数时存在两种情形。一种是返回的数据是标量(比如整型、指针等),在这种情形下,返回值将会放入EAX寄存器中。如果返回的是浮点数,则返回值是放在协处理器的寄存器栈上的。

另一种情形是函数需要返回结构体或联合体(非标量)。这种情形需要通过栈来完成。为了了解在这种情形下栈帧的作用,我们可以借助图10.21所示的示例程序。

embedded/code/application/return/main.c

00001: #include <stdio.h>

00002:

00003: //lint -e530 -e123 -e428

00004:

00005: typedef struct {

00006:     int i0_;

00007:     int i1_;

00008:     int i2_;

00009: } func_return_t;

00010:

00011: func_return_t foo (int _param)

00012: {

00013:     func_return_t local;

00014:     int reg_esp, reg_ebp;

00015:

00016: asm volatile(

00017:     // get EBP

00018:     "movl %%ebp, %0 \n"

00019:     // get ESP

00020:     "movl %%esp, %1 \n"

00021:     : "=r" (reg_ebp), "=r" (reg_esp)

00022: );

00023:     printf ("foo (): EBP = %x\n", reg_ebp);

00024:     printf ("foo (): ESP = %x\n", reg_esp);

00025:     printf ("foo (): (EBP) = %x\n", *(int *)reg_ebp);

00026:     printf ("foo (): return address = %x\n", *(((int *)reg_ebp + 1)));

00027:     local.i0_ = 1;

00028:     local.i1_ = 2;

00029:     local.i2_ = 3;

00030:     printf ("foo (): &_param = %p\n", &_param);

00031:     printf ("foo (): return value = %x\n", *(((int *)&_param) - 1));

00032:     printf ("foo (): &local = %p\n", &local);

00033:     printf ("foo (): &reg_esp = %p\n", &reg_esp);

00034:     printf ("foo (): &reg_ebp = %p\n", &reg_ebp);

00035:     return local;

00036: }

00037:

00038: int main ()

00039: {

00040:     int reg_esp, reg_ebp;

00041:     func_return_t local = foo (100);

00042:

00043: asm volatile(

00044:     // get EBP

00045:     "movl %%ebp, %0 \n"

00046:     // get ESP

00047:     "movl %%esp, %1 \n"

00048:     : "=r" (reg_ebp), "=r" (reg_esp)

00049: );

00050:     printf ("main (): EBP = %x\n", reg_ebp);

00051:     printf ("main (): ESP = %x\n", reg_esp);

00052:     printf ("main (): &local = %p\n", &local);

00053:     printf ("main (): &reg_esp = %p\n", &reg_esp);

00054:     printf ("main (): &reg_ebp = %p\n", &reg_ebp);

00055:     return 0;

00056: }

10.21

在这个示例程序中,main()foo()函数内都定义了一个类型为func_return_tlocal变量,且foo()的返回值类型也是func_return_t。毫无疑问,两个local变量的内存都将分配在各自函数的栈帧中,那foo()函数的local变量的值是如何通过函数返回值传递到main()函数的local变量中的呢?编译这个程序并运行以观察其结果,如图10.22所示。图10.23示例说明了在foo()函数内所看到的栈布局。

yunli.blog.51CTO.com /embedded/build

$ make

yunli.blog.51CTO.com /embedded/build

$ ./release/return.exe

foo (): EBP = 22cd18

foo (): ESP = 22cce0

foo (): (EBP) = 22cd58

foo (): return address = 40125a

foo (): &_param = 0x22cd24

foo (): return value = 22cd3c

foo (): &local = 0x22cd00

foo (): &reg_esp = 0x22cd10

foo (): &reg_ebp = 0x22cd0c

main (): EBP = 22cd58

main (): ESP = 22cd20

main (): &local = 0x22cd3c

main (): &reg_esp = 0x22cd4c

main (): &reg_ebp = 0x22cd48

10.22

嵌入7.jpg

从图中可以看出,main()函数调用foo()函数时除了将foo()函数所需的参数压入到栈中外,还将局部变量local的地址也压入到栈中,当foo()函数在进行函数返回时会将它的local变量的值通过这一指针拷贝到main()函数的local变量中。正是因为存在这一拷贝操作,所以在x86处理器上将结构当做函数返回类型是相对耗时的。

你可能感兴趣的:(函数参数的传递方法)