函數入參分析

背景

一般來說,我們調用一個函數的時候,需要傳入對應數據類型和個數的參數,例如調用int sum(int x,int y);我們需要傳入兩個int類型的數,且只能傳入兩個int類型的數。如果類型不匹配或者個數不匹配,編譯器都會報warning或者error。我們將int x ,int y稱作形參,下面就來分析以下參數是怎麼傳遞給被調用函數的。
備註:
測試的平臺x86_64-redhat-linux

固定參數

固定參數指的是,一個函數的形參個數是確定的,如上面舉例的int sum(int x,int y); 它的參數個數是2,是一個固定值。還有一種參數個數是可變的,例如printf函數,對於可變參數入參在下一節中分析。
大部分的課本或者圖書上說,函數入參的順序是從右往左依次壓入棧中,由於棧的增長是由高地址向低地址增長,故可以得知越是靠近右邊的參數,所處的地址應該是越大的。例如my_printf(char* fmt,int a,int b,int c),入棧的順序:c->b->a->fmt(指針),地址的大小也是c->b->a->fmt(指針)。
我寫了一個測試程序來驗證這個說法,這個測試程序把函數的入參地址打印出來了:

void my_printf(char* fmt,int a,int b,int c)
{
	printf("addr: fmt:%p a:%p b:%p c:%p\n",&fmt,&a,&b,&c);
}

int main()
{
	my_printf("%d",2,3,4);
	return 0;
}

運行的結果是:

addr: fmt:0x7ffcd7e3d978 a:0x7ffcd7e3d974 b:0x7ffcd7e3d970 c:0x7ffcd7e3d96c

從運行的結果看,c的地址最小(0x7ffcd7e3d96c),fmt的地址最大(0x7ffcd7e3d978 ),和大多數的資料上描述不相符。原因實際是:
函數的入參順序和編譯器有關,編譯器決定函數怎麼入參,我測試的平臺是x86_64-redhat-linux,大多數資料講的平臺是32位平臺,所以會有差異。

下面通過objdump生成彙編代碼來分析參數入棧的過程:
可以看出,main函數僅僅做了寫入寄存器的操作
main函數
my_printf將寄存器中的參數寫入到棧中:
my_printf彙編代碼

可變參數

按照我的理解,可變參函數入參應該和正常的函數一樣,都會依次將參數壓入到棧中,也會存儲到寄存器中。爲了驗證這個想法,所以做了一個實驗:
1)編寫測試代碼

int myprintf(int num,...)
{
    va_list p_args ;
    va_start(p_args,num);
    printf("start addr:%p value:%d\n",p_args,*(int*)p_args);
    printf("num addr:%p\n",&num);
    printhex(&num-3,200);
}
int main()
{
	myprintf(3,1,2,3);
	return 0;
}

2)gdb加載測試代碼
3)查看棧幀信息,可以看到args中只有num,並沒有看到後面的入參1,2,3

(gdb) info frame
Stack level 0, frame at 0x7fffffffe3d0:
 rip = 0x400698 in myprintf (main.c:20); saved rip 0x40100f
 called by frame at 0x7fffffffe3f0
 source language c.
 Arglist at 0x7fffffffe3c0, args: num=3
 Locals at 0x7fffffffe3c0, Previous frame's sp is 0x7fffffffe3d0
 Saved registers:
  rbp at 0x7fffffffe3c0, rip at 0x7fffffffe3c8

4)查看寄存器,在寄存器中可以看到入參1,2,3

(gdb) info registers
rax            0x0      0
rbx            0x0      0
rcx            0x3      3
rdx            0x2      2
rsi            0x1      1
rdi            0x3      3

可變參數在gdb上表現不出來,實際也是壓入到棧中了
在這裏插入圖片描述

把棧內存打印出來,棧中有參數,但是參數2,3,4和參數1的位置並不是緊鄰的,從彙編代碼中也可以看出來,以下是棧的內容:
在這裏插入圖片描述
網上的資料和課本上所說的va_list va_arg等,都是在x86平臺上,對於x64平臺上來說,實現完全不一樣。X86平臺的va_list是一個char*類型,但是在我的平臺上,va_list是一個結構體,從gdb可以看出來,p_args是va_list類型的變量,它有gp_offset,fp_offset,overflow_arg_area,reg_save_area成員。所以x86和x64的參數入參是不一樣的,直接套用x86的參數入參會得不到想要的結果:

(gdb) info locals
p_args = {{gp_offset = 8, fp_offset = 48, overflow_arg_area = 0x7fffffffe3d0,
    reg_save_area = 0x7fffffffe310}}
args_val = -134224728
args_index = 32767

總結

平臺不同,編譯器不同會導致入參的順序不同,課本和資料上所說的,可能和實際情況不相符,所以只能作爲參考,具體情況還需要具體分析。通過分析彙編代碼,會讓你對入參有一個深入的瞭解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章