函數參數的傳遞方法

本文選自《專業嵌入式軟件開發——全面走向高質高效編程(DVD光盤1)》一書 編程

《專業嵌入式軟件開發——全面走向高質高效編程(DVD光盤1)》一書已由電子工業出版社正式出版數組

李雲 數據結構

函數參數的傳遞方法

ABI規範中還定義了函數參數的傳遞方式和參數的壓棧順序。在x86處理器的ABI規範中規定,全部傳遞給被調用函數的參數都是經過棧來完成的,其壓棧的順序是以函數參數從右到左的順序。在圖10.7的第54行,當main()函數調用middle()函數時所傳入3個參數在棧中的佈局能夠從圖10.9的下方找到,參數壓棧的順序是_p2_p1_p0app

x86處理器上,當向一個函數傳遞參數時,全部的參數最後造成的是一個數組。因爲採用從右到左的壓棧操做,因此數組中參數的順序(好比,從下標0到下標2)與函數從左到右的順序是一致的(_p0_p1_p2)。所以,在一個函數中若是知道了第一個參數的地址和各參數佔用字節的大小,就能夠經過訪問數組的方式去訪問每個參數。函數

整型和指針參數的傳遞

整型參數的傳遞在前面已經看到了,而指針參數的傳遞與整型是同樣的。這是由於,在32x86處理器上整型的大小與指針的大小都是同樣的,都佔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.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處理器上將結構當作函數返回類型是相對耗時的。

相關文章
相關標籤/搜索