本文简单基于树莓派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
#include
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表示函数的返回地址。
以如下形式的程序调用为例
#include
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
以内存地址的形式呈现,可以得到如下图:
栈帧的开始位置,其内容是是 0x107cc ,是函数的返回地址。即__builtin_return_address(0),下一个32位的内容是0x7e8041a4 ,根据其返回函数的打印,是返回后函数的栈帧开始位置。可以印证 栈是向下增长的,返回地址的栈在更高的地址。再下一个32位的内容是,0x10810,是其返回函数的返回地址。
再接下来一个32位的内容为空,或是任意值,该值可能为预留,使用gdb也未寻找到符号位置。
接下来三个32位均为栈上的变量的地址,i j 和offset。
再下一个值的内容为随机,其含义也不明。该32位可能也为预留值。
接下来两个地址为函数调用的局部变量的地址,parm和parm2
如果有更多的入参调用,会发现预留给参数变量的栈帧大小大约是 4*32 ,如果超过该范围,就会预先先进行调用参数的内存分配,再执行函数的栈上内存分配。即地址会在0x7e985194 栈帧地址之前。
在这种情况下,如果形参进行了越界的修改,就会导致栈帧上的返回地址被破坏,导致出现崩溃。该点可后续继续实验。