关于函数栈帧的考虑与分析

关于函数栈帧的思考与分析

1. ebp和esp是什么?

ebp是指向当前stack frame底部的指针;esp是指向当前stack frame顶部的指针。

 

2. stack是什么?

stack是存储器中一种很昂贵的资源,默认情况下linux给每个线程分配的空间为8MB,

可以使用ulmit -s或者ulmit -a进行查看:

sh-# ulimit -s
8192
sh-# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 2303
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 2303
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

因为stack是昂贵的资源,所以我们时时刻刻都要省着点用,通常不建议:

(1)在函数内部开辟大数组,比如int a_info[10000] = {0};

会瞬间让你的stack爆掉从而使系统crash掉,通常这种大数组建议使用malloc函数进行dynamically allocation。

(2)太深的函数递归调用,也有可能会造成线程的stack很快耗尽甚至爆掉。

由于32位linux系统下process的user space是从0~3GB,这部分是process独占的。

所以理论上一个process最多可以创建8MB*N<3*1024MB,即N<384个thread。

 

3. linux内存布局之初次肤浅分析?

对32位linux系统,从高地址到底地址:

(1)3GB~4GB(>=0xc0000000),这部分是内核态使用,用户态代码不可见,所有进程共享内核空间;

(2)地址空间小于3GB(<0xc0000000)的部分都是用户空间。首先是stack空间,stack从高地址向地址值增长;

(3)文件映射区(>0x40000000),这部分没有研究过,暂不讨论;

(4)堆空间,堆是从底地址向高地址增长;

(5)代码段、初始化数据区以及未初始化数据区;

(6)保留区域(<0x08048000?)。

关于存储器布局部分,未来有机会会再做一次比较详细的分析。

 

4. 函数调用的真实过程?

假设有如下的代码:

void function_b(void)
{
    int m = 0;
    int n = 0;

    do_something();
}

void function_a(void)
{
    function_b(a, b);
    do_something();
}

此时function_a调用function_b的栈变化过程是:

(1)将do_something();这一条语句的地址push到stack中,目的是当从function_b中return时执行下一条语句;

(2)将ebp push到stack中,因为此时ebp仍然是function_a的栈帧底部的地址,存ebp的目的也是为了在

function_b返回时恢复function_a的栈帧用;

注意(1)(2)两步仍然是在function_a的栈帧中完成的;

(3)move ebp, esp

注意哦,到了这一步,才是建立function_b的栈帧的开始,可以看到起始时ebp与esp都指向栈底;

(4)为function_b的变量m在栈上面分配空间,同时ebp = ebp -4移动栈顶指针;

(5)为function_b的变量n在栈上面分配空间,同时ebp = ebp -4移动栈顶指针;

所以我们经常看到的因为数组越界、字符串copy越界等造成的系统crash的现象也就不会奇怪了,

因为函数的返回地址可能会被轻易的被改掉。

 

5. 函数返回的过程?

当function_b执行完return时,

(1)move esp, ebp

这一步的目的就是用来恢复function_a的栈帧的栈顶;

(2)pop ebp

将之前压栈的function_a的栈帧的栈底地址恢复出来,有了ebp和esp,那function_a的栈帧就恢复了。

(3)pop next_statement's address

将下一条待执行命令的address从栈中pop出来,function_a可以继续做事了。

 

6. 为什么栈上分配的内存可以自动释放?

如问题5中分析,当函数function_b返回时会执行move esp, ebp,

这一步即是释放了function_b栈帧中分配的局部变量了。

那为什么从堆上动态分配的内存必须要用户自己手动释放呢?

要想清晰了解这部分,可能需要看一下malloc函数的实现了。

目前个人粗浅的猜测可能是如果用malloc分配的话,操作系统会标记已经分配的内存;

如果用户不主动释放,那这部分已分配的就会一直标记为已用的状态,

所以内存就会越来越少了,这就是内存泄漏。

关于堆这一块内容,后面会在适当的时候再来分析。

 

7. 如何确认stack是从高地址到底地址增长呢?

uint8 ui1_test_1 = 0x11;
uint8 ui1_test_2 = 0x22;
printf("\npui1_test_1=0x%x,pui1_test_2=0x%x\n", &ui1_test_1, &ui1_test_2);
编译运行程序,会看到如下的log:
pui1_test_1=0x7007BA87,pui1_test_2=0x7007BA86

可以看到先申请的变量ui1_test_1的地址比后申请的ui1_test_2的地址的值要高,

而且正好相差的是一个byte。

实验结果很好的验证了stack是从高地址到底地址增长的说法。

1楼boyxulin1986昨天 12:45
自己必须顶自己