本文选自《专业嵌入式软件开发——全面走向高质高效编程(含DVD光盘1张)》一书
《专业嵌入式软件开发——全面走向高质高效编程(含DVD光盘1张)》一书已由电子工业出版社正式出版
李云 著
函数参数的传递方法在ABI规范中还定义了函数参数的传递方式和参数的压栈顺序。在x86处理器的ABI规范中规定,所有传递给被调用函数的参数都是通过栈来完成的,其压栈的顺序是以函数参数从右到左的顺序。在图10.7的第54行,当main()函数调用middle()函数时所传入3个参数在栈中的布局可以从图10.9的下方找到,参数压栈的顺序是_p2、_p1和_p0。
在x86处理器上,当向一个函数传递参数时,所有的参数最后形成的是一个数组。由于采用从右到左的压栈操作,所以数组中参数的顺序(比如,从下标0到下标2)与函数从左到右的顺序是一致的(_p0、_p1和_p2)。因此,在一个函数中如果知道了第一个参数的地址和各参数占用字节的大小,就可以通过访问数组的方式去访问每一个参数。
整型和指针参数的传递整型参数的传递在前面已经看到了,而指针参数的传递与整型是一样的。这是因为,在32位x86处理器上整型的大小与指针的大小都是一样的,都占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.5中PowerPC处理器寄存器的功能分配表可以看出,R3~R10共8个寄存器用于整型或指针参数的传递,F1~F8共8个寄存器用于浮点参数的传递。当所需传递的参数个数小于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处理器完全不同。在PowerPC的ABI规范中规定,对于结构体的传递仍采用指针的方式,而不是像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 (): ®_esp = %p\n", ®_esp);
00034: printf ("foo (): ®_ebp = %p\n", ®_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 (): ®_esp = %p\n", ®_esp);
00054: printf ("main (): ®_ebp = %p\n", ®_ebp);
00055: return 0;
00056: }
图10.21
在这个示例程序中,main()和foo()函数内都定义了一个类型为func_return_t的local变量,且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 (): ®_esp = 0x22cd10
foo (): ®_ebp = 0x22cd0c
main (): EBP = 22cd58
main (): ESP = 22cd20
main (): &local = 0x22cd3c
main (): ®_esp = 0x22cd4c
main (): ®_ebp = 0x22cd48
图10.22
从图中可以看出,main()函数调用foo()函数时除了将foo()函数所需的参数压入到栈中外,还将局部变量local的地址也压入到栈中,当foo()函数在进行函数返回时会将它的local变量的值通过这一指针拷贝到main()函数的local变量中。正是因为存在这一拷贝操作,所以在x86处理器上将结构当做函数返回类型是相对耗时的。