天天看點

部分gcc預定義宏和函數棧幀的記憶體分布

本文簡單基于樹莓派8,linux4.4.50版本,32位arm cpu

嘗試了解函數調用棧的記憶體分布的形态。使用gcc内置的宏 __builtin_frame_address 來列印棧幀記憶體上的資訊,以及了解一下常用的gcc 内置的宏的輸出。

針對 __builtin_frame_address 在gcc官網上可以看到更多的說明:

https://gcc.gnu.org/onlinedocs/gcc/extensions-to-the-c-language-family/getting-the-return-or-frame-address-of-a-function.html

1 預定義宏的輸出

#include <stdio.h>

int call_func1(){
    printf("\n %s \n",__func__);
    printf("__func__ %s\n",__func__);
    printf("__LINE__ %d\n",__LINE__);
    printf("__FILE__ %s\n",__FILE__);
    printf("__DATE__ %s\n",__DATE__);
    printf("__time__ %s\n",__TIME__);
    printf("__func__ %s __builtin_frame_address %p\n",__func__,__builtin_frame_address(0));
    printf("__func__ %s,__builtin_return_address %p\n",__func__,__builtin_return_address(0));
}

int main(){
    call_func1();
    return 0;
}
           

上述源檔案的輸出如下:

call_func1
__func__ call_func1 //函數名
__LINE__ 51         //行号
__FILE__ main.c     //源檔案名
__DATE__ Nov 27 2022    //編譯日期
__time__ 17:58:17           //編譯時間
__func__ call_func1 __builtin_frame_address 0x7ec541a4
__func__ call_func1,__builtin_return_address 0x10794

           

__builtin_frame_address 表示函數的棧幀位址,__builtin_return_address表示函數的傳回位址。

2 棧幀的記憶體列印

以如下形式的程式調用為例

#include <stdio.h>

int call_func2(int parm, int parm2){
    int i = 10;
    int j = 0;
    void * offset = NULL;

    printf("\n %s \n",__func__);

    /*列印從函數的棧幀開始 10個cpu位址(32位)的位址和值*/
    for (j = 0 ; j < i; j++){
        offset = __builtin_frame_address(0)- j*sizeof(int*);
        printf("__builtin_frame_address %p value [%x]\n",offset,*(int*)offset);
    }

    /*列印函數的傳回位址 ,3個變量的位址,兩個入參的位址*/
    printf("__builtin_return_address %p\n",__builtin_return_address(0));
    printf("first variable i  %p\n",&i);
    printf("second variable j  %p\n",&j);
    printf("third variable j  %p\n",&offset);
    printf("first parm parm   %p\n",&parm);
    printf("second parm parm   %p\n",&parm2);
    return 0;
}

int call_func1(){
    printf("__func__ %s __builtin_frame_address %p\n",__func__,__builtin_frame_address(0));
    printf("__func__ %s,__builtin_return_address %p\n",__func__,__builtin_return_address(0));
    call_func2();
    return 0;
}

int main(){
    call_func1();
    return 0;
}
           

上述的程式的調用流程是 main->call_func1->call_func2 。其中call_func1列印了函數的棧幀位址和傳回位址,call_func2先後列印了其以幀位址為開始之後10個4位元組記憶體的位址和内容,然後列印了函數的傳回位址 ,3個變量的記憶體位址,兩個入參的記憶體位址

其輸出結果如下:

__func__ call_func1 __builtin_frame_address 0x7e8041a4
__func__ call_func1,__builtin_return_address 0x10810

 call_func2
__builtin_frame_address 0x7e985194 value [0x107cc]
__builtin_frame_address 0x7e985190 value [0x7e9851a4]
__builtin_frame_address 0x7e98518c value [0x10810]
__builtin_frame_address 0x7e985188 value [0x10a4c]
__builtin_frame_address 0x7e985184 value [0xa]
__builtin_frame_address 0x7e985180 value [0x5]
__builtin_frame_address 0x7e98517c value [0x7e98517c]
__builtin_frame_address 0x7e985178 value [0x76ffa000]
__builtin_frame_address 0x7e985174 value [0x4]
__builtin_frame_address 0x7e985170 value [0x1]
__builtin_return_address 0x107cc
first variable i  0x7e804184
second variable j  0x7e804180
third variable j  0x7e80417c
first parm parm   0x7e804174
second parm parm   0x7e804170

           

以記憶體位址的形式呈現,可以得到如下圖:

部分gcc預定義宏和函數棧幀的記憶體分布

棧幀的開始位置,其内容是是 0x107cc ,是函數的傳回位址。即__builtin_return_address(0),下一個32位的内容是0x7e8041a4 ,根據其傳回函數的列印,是傳回後函數的棧幀開始位置。可以印證 棧是向下增長的,傳回位址的棧在更高的位址。再下一個32位的内容是,0x10810,是其傳回函數的傳回位址。

再接下來一個32位的内容為空,或是任意值,該值可能為預留,使用gdb也未尋找到符号位置。

接下來三個32位均為棧上的變量的位址,i j 和offset。

再下一個值的内容為随機,其含義也不明。該32位可能也為預留值。

接下來兩個位址為函數調用的局部變量的位址,parm和parm2

如果有更多的入參調用,會發現預留給參數變量的棧幀大小大約是 4*32 ,如果超過該範圍,就會預先先進行調用參數的記憶體配置設定,再執行函數的棧上記憶體配置設定。即位址會在0x7e985194 棧幀位址之前。

在這種情況下,如果形參進行了越界的修改,就會導緻棧幀上的傳回位址被破壞,導緻出現崩潰。該點可後續繼續實驗。

繼續閱讀