本文選自《專業嵌入式軟件開發——全面走向高質高效編程(含DVD光盤1張)》一書 編程
《專業嵌入式軟件開發——全面走向高質高效編程(含DVD光盤1張)》一書已由電子工業出版社正式出版數組
李雲 著數據結構
函數參數的傳遞方法在ABI規範中還定義了函數參數的傳遞方式和參數的壓棧順序。在x86處理器的ABI規範中規定,全部傳遞給被調用函數的參數都是經過棧來完成的,其壓棧的順序是以函數參數從右到左的順序。在圖10.7的第54行,當main()函數調用middle()函數時所傳入3個參數在棧中的佈局能夠從圖10.9的下方找到,參數壓棧的順序是_p2、_p1和_p0。app
在x86處理器上,當向一個函數傳遞參數時,全部的參數最後造成的是一個數組。因爲採用從右到左的壓棧操做,因此數組中參數的順序(好比,從下標0到下標2)與函數從左到右的順序是一致的(_p0、_p1和_p2)。所以,在一個函數中若是知道了第一個參數的地址和各參數佔用字節的大小,就能夠經過訪問數組的方式去訪問每個參數。函數
整型和指針參數的傳遞整型參數的傳遞在前面已經看到了,而指針參數的傳遞與整型是同樣的。這是由於,在32位x86處理器上整型的大小與指針的大小都是同樣的,都佔4個字節。來自x86處理器ABI規範中的圖10.17總結了這兩種類型的參數在棧幀中的位置關係。注意,該表是基於tail()函數的棧幀而言的。佈局
Callui |
Argumentspa |
Stack Address指針 |
tail (1, 2, 3, (void *)0);code |
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處理器上將結構當作函數返回類型是相對耗時的。