本文共 7947 字,大约阅读时间需要 26 分钟。
今天的计划是在linux环境下,通过一个简单的程序,先了解一个进程的各个部分在内存中的分布,然后着重学习栈的概念。熟悉之后,和C相关的各种困惑就可以解决一大半。
程序如下:
/*
* file name: for_rabbit.c
*/
#include
#include
int extern_apple;
int extern_pear = 1;
int test_handler(int x, int y)
{
fprintf(stdout, "HIAHIA, You have %d apples and %d mangos now!\n", x_test, y_test);
return 0;
}
int main(int argc, char * argv[])
{
static local_banana;
static local_peach = 3;
char * string = "I am Queue rabbit!";
fprintf(stdout, "%s\n", string);
fprintf(stdout, "My addr is %p\n", string);
fprintf(stdout, "I have %d apples and %d mangos.\n", local_apple, local_mango);
/*
* First
*/
test_handler(local_apple, local_mango);
fprintf(stdout, "KEKE, WAKE UP! Apples = %d, mangos = %d.\n", local_apple, local_mango);
/*
* Second
*/
local_pear_basket = (int *)malloc(sizeof(int));
if (local_pear_basket != NULL) {
printf("The heap addr is : %p\n", local_pear_basket);
free(local_pear_basket);
local_pear_basket = NULL;
return 0;
}
编译并执行,结果如下:
$ gcc -o for_rabbit for_rabbit.c
$ ./for_rabbit
I am Queue rabbit!
My addr is 0x400867
I have 1 apples and 2 mangos.
HIAHIA, You have 256 apples and 512 mangos now!
KEKE, WAKE UP! Apples = 1, mangos = 2.
The heap addr is : 0x18f5010
先简单分析一下流程:
1: 程序一开始声明了两个外部变量(水果,嘿嘿),extern_apple, extern_pear,其中梨子的值被初始化为1。
2: 声明了函数test_handler(),它的任务是把交给它的两种水果数量都变为以前的256倍!这样兔子就有得吃了。
3: 兔子的main函数,首先声明并初始化苹果和芒果的值分别为 1 和 2,兔子不够吃~至于梨子,更可年,连放梨子的篮子都还没准备好。还有香蕉以及3个奇怪的桃子,前边加了一个static,看起来不好惹,暂时不吃它。
4: 跑龙套!
5: 那就把苹果和芒果都交给test_handler呗,让它增加256倍!它说没问题,已经完成!
6: 查查兔子现在有多少个苹果和芒果了。咳咳,醒醒……为神马还是 1 和 2!
7: 哭,先吃梨,找邻居借一个篮子去。
8: 借到!这下子我们可以往里边塞梨子,就先放3个。
9: 不过吃完之后,应该把篮子还回去,毕竟是人家的。并且记得提醒自己,俺们家穷,是没有放梨子的篮子的,下回如果还想吃梨,还得借一次。
为神马明明test_handler()把兔子的水果都增加了256倍,最后还是没变呢!!好吧。。。我们开始分析。把所有的问题一网打尽!以后就吃好喝好玩好睡好神马都不愁好嘛亲!
===============================================
BEGIN:
编译生成的可执行文件(for_rabbit),必需加载到内存里,成为一个进程才能运行。cpu会根据我们编写的流程,读取内存里的值,进行计算并得到最后的结果。那么一个可执行文件在内存里是如何分布的呢?
(注意:我们当前所说的内存,都是虚拟内存!)
先来分析一下for_rabbit.c的结构。
1: 有 extern_apple, extern_pear 两个外部变量,其中extern_pear被初始化为1。
2: 有一个函数 test_handler(), 以及它的两个参数:x 和 y。
3: 三个局部变量: local_apple(初始化为1), local_mango(初始化为2), local_pear_basket(初始化为NULL),以及指向一个字符串的指针string。
5: 两个静态局部变量,local_banana,local_peach(初始化为3)。
6: test_handler()自己内部也有两个局部变量 x_test 和 y_test 。
不同特征的数据当然应该位于不同的内存区域,我们看看在linux下,一个进程在内存中是如何构成的(截了两张图,第二张图取自《深入理解计算机系统》第9章第559页,第9章的内容专门讲的虚拟存储器,以后兔子要好好看看~):
如图所示,是一个进程在linux环境下的典型分布,linux将一个进程分成好几个不同的段。每一段的存储对象是不同的。我们来具体分析一下。
1: 从下往上,内存地址是由低到高。在32位的系统下(32位系统可以访问的内存最大值是多少?是不是4G?),最低处的地址比0要大一点点儿,最高处的地址比3G要小一点点儿。3G到4G之间,是属于linux内核的运行空间,我们的普通程序不能访问~ 所以每一个进程都被操作系统欺骗了,都以为自己独自占有了3个G的内存。其实呢,是3个G的虚拟内存,至于实际占了多少物理内存,是程序本身的大小决定的。
2: 首先是文本段。存储的是代码本身(.text),以及在程序中出现的字符串(.rodata),这里是"I am Queue rabbit!\n"。注意,.text 和 .rodata,在可执行文件的格式标准中是不同的区域,但是当加载到内存里时,被合并在一起,统称为文本段。因为它们的内容都是不可修改的(已经由程序决定)。兔子要是弄不明白,现在不必深究。
3: 然后是初始化数据段(.data),这个区域存储着外部变量中的、已经被初始化的变量(initialized data),也就是我的 extern_pear;以及局部静态变量中的已经初始化的数据,也就是 local_peach。
4: 未初始化数据段(.bss),这个区域存储着外部变量中的、未被初始化的变量(uninitialized data),也就是 extern_apple;以及局部静态变量中的未初始化变量(也就是local_banana)。未初始化数据段中的值,都会被操作系统自动赋值为0。也就是说,虽然我们没有给extern_apple 和 local_banana 赋值,但是它们的值会自动变为0。兔子可以把它们的值打印出来看看。
5: 堆(heap),由 malloc 函数申请到的内存都分布在这个区域里。很显然,local_pear_basket的值所代表的内存地址,就在堆区域里。申请到的内存不断往上增长(向地址高处)。
6: 栈(stack),所有的内部变量都存储在这个区域。也就是上边的 local_apple, local_mango, local_pear_basket, 还有test_handler()中的 x, y, x_test 和 y_test。栈不断向下增长(向地址低处)。
7: 在堆和栈之间的一大片空白区域,是各种动态库、共享内存、线程栈神马的,兔子现在不用关心。
好啦,现在兔子已经很清楚一个完整的C程序是如何分布在内存中的了。我们再了解一下栈的概念,就可以直奔今晚的主题了。
===============================================
栈是一种典型的数据结构,它的逻辑特征是(LAST IN FIRST OUT),LIFO (后进先出)。
举一个简单的例子,三只兔子(A B C)按顺序一起钻进了一个刚好容身的洞里睡觉,当它们想要出来的时候,就只能按照 C B A 的顺序退出来~ C 要是贪睡还不起来,B 和 A 也只能等着,出不来。
栈的主要作用是传递参数、存储返回信息、保存寄存器、本地存储等。
一个程序中,所有函数的非静态局部变量,也就是自动变量(这里的 local_apple, local_mango, local_pear_basket,还有test_handler()中的 x、y、x_test 和 y_test),都是按照三只兔子睡觉的方式存储在栈里的。不同的函数,把栈分成了更小的不同区域,称为栈帧(stack frame),每个栈帧里都保存着各自的变量。
我们现在根据程序流程来分析一下这几个变量的存储过程。下边的流程是一种很常见的方式,真实的实现可能会由于架构、编译器的不同而不同。但是对于理解函数调用是完全合适的。我们要清楚的是,这些都是操作系统自身的一种实现,跟c语言的标准无关。
(本图仅示意,栈帧的实际情况比这个复杂。但现在不必深究,只需理解数据的存储,以及函数的调用过程即可。)
如下图所示:
注意:每一个矩形表示4个字节。 所以相邻两个矩形之间的地址相差4(在32位系统下,int型和指针类型的长度一般都是4,刚好可以装在一个矩形里)。
先简单分析一下这张图片:
1: 栈位于上方,按照地址由高往低进行增长。并且栈被分成了两个栈帧,分别代表函数 main()和 函数test_handler()。
2: local_pear_basket 指向了地址 0x18f5010,该地址由malloc()分配,位于堆区。
3: string 指向了地址 0x400867,该地址由程序加载时自行分配,位于文本段。
我们再回过头看一下程序输出结果:
$ ./for_rabbit
I am Queue rabbit!
My addr is 0x400867
I have 1 apples and 2 mangos.
HIAHIA, You have 256 apples and 512 mangos now!
KEKE, WAKE UP! Apples = 1, mangos = 2.
The heap addr is : 0x18f5010
1: 首先是main函数里的4个自动变量,按照地址从高向低的顺序存储在栈里。咦? local_banana 和 local_peach 呢?还记得吗,它俩属于静态变量,存储在读写区域(初始化或者非初始化)。其中 string 被直接赋值为一个字符串(一定要记住,字符串本身表示一个地址。)我们把地址打印出来,可以看到,该字符串的地址位于 0x400867,我们前边说过,字符串位于位置很低的文本区,现在看到的这个位置确实挺低的吧,嘿嘿。
2: 随后调用函数test_handler(),它的功能是接受两个参数的 值 (这里是 local_apple 和 local_mango 的值),然后将这两个参数的值乘以256,再赋值给自己的两个局部变量。我们打印出来,可以看到,确实成功变大了256倍。
3: 再次打印 local_apple 和 local_mango ,发现没有变化。
4: 上边这个过程非常重要,这是我们今天一定要理解的内容。看图片,当进行函数调用的时候,test_handler()的两个形式参数,x 和 y 本身会在main()函数的栈帧里占据新的内存空间(根据一般的原则,函数右边的参数先入栈,所以 y 的 地址比 x 高)。因此,当我们直接以test_handler(local_apple, local_mango)这种方式调用函数的时候,实际上,是分别把 local_apple 的 值 复制给了 x ,local_mango 值 复制给了y,然后test_handler()的后续操作都是直接在 x 和 y 上边进行!因此我们可以看到成功将 x 和 y 的值分别增大了256倍,但是对 local_apple 和 local_mango 本身却没有任何影响,因为实质上根本就没有对这两个数进行过任何的操作。
test_handler()函数所有的操作都是针对自己的 x、y、y_test、x_test在进行。当它运行完毕之后,随着函数的退出,y_test 和 x_test 也逐次从test_handler()函数的栈帧里退出。然后回到main()函数的栈帧里。逐次将 x 和 y 也弹出栈。(现在只能根据 C B A的顺序才能出窝)。由于 y_test 和 x_test 才是真正保存了增长256倍后的水果值,所以现在也已经丢失了。兔子还是只有 1 个苹果和 2 个芒果(local_apple 和 local_mango,它们本身没有任何变化),55555,好可年。
5: 通过 malloc 分配一块整数长度的内存区域(这里是4个字节),并将这块区域的首地址交给 local_pear_basket,我们打印得知这块地址位于0x18f5010。前边分析过,堆的地址比文本段要高。此处可知确实如此。(0x18f5010 远大于 0x400867)。
6: 通过malloc()申请到的内存,如果不使用了,一定要调用函数free()将其释放。因为在堆里的内存,不会像在栈里那样,不用之后将被操作系统自动弹出。堆里的内存会一直存在于整个进程的生命周期。对于一台长期运行的服务器来说,如果我们一直向堆里申请内存而不释放,那么总有一天,我们的程序会将机器的所有内存都申请枯竭而导致系统崩溃。这种现象称为内存泄漏(memory leak),能否在程序里不出现内存泄漏是衡量一个程序员是否合格的标准之一。可得注意~ free()之后的指针再赋值为NULL,是一个很好的习惯~
===============================================
好啦~忽忽忽忽糊糊呼呼,上边的过程就是c程序的实际执行过程。我们首先看到了一个完整的c程序是如何被分成几个不同的区域,然后加载到内存里的。不同特征的数据会被分配到不同的内存区域。紧接着分析了一个函数调用的详细过程,理解这个过程的关键在于,形式参数只是原始数据的一个副本,形式参数自身也要占据内存空间,函数的相关操作都是针对形式参数在进行,并没有牵涉到原始数据。
思考题:
1: 如果我们希望通过函数的方式,将苹果和芒果的数量真正增加256倍,该如何实现呢?(如果感觉有难度,请至少思考17分钟,再进行下一道题目。)
2: 将函数重新定义为如下形式:
int test_handler(int * x, int * y)
{
fprintf(stdout, "HIAHIA, You have %d apples and %d mangos now!\n", *x, *y);
return 0;
}
运行程序,会得到什么结果?为什么?请根据上边的流程,自己完整分析一遍,并彻底理解。
3:在最初的程序中,如果我们让 test_handler() 函数返回变量 x_test 的地址,供后续代码获取 x_test 的值,会出现什么后果?这么做是否安全?
比如:
int * tmp = test_handler(local_apple, local_mango);
printf("x_test is %d\n", *tmp);
实验题:
1: 请将所有变量的地址都打印出来,分析它们的地址分布。
2: 现在我们已经了解,栈是向地址低处增长,堆是向地址高处增长,中间区域另有它用。如果栈和堆都在不断地扩大,那么中间区域就会受到影响。所以操作系统对栈的大小有一定的限制。
在linux下,执行指令: ulimit -a,可以得到操作系统对进程的各项设置参数。我的如下。
$ 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) 15669
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) 1024
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
注意 stack size 这一行,我们可以看到其值为8M,所以,在我的这台机器上,每一个进程的栈空间大小限制是8M,如果我们在栈上申请了超过8M大小的空间,就会栈溢出。导致程序出错。
#include
int main(void)
{
上边这个程序直接在栈上申请了10M的内存空间。超出了系统限制,所以程序无法运行,导致“段错误”(Segmentation fault)。
那如果我们就是需要大量的内存该肿马办?一种常见的办法就是通过malloc在堆里申请~要多少有多少,只要物理内存有这么多。
char * p = (char *)malloc(10 * 1024 * 1024); /* RIGHT */
酱紫就木有问题啦。但是千万记住,使用完毕之后,立即free,并置为NULL。
free(p);
p = NULL;
END。
转载地址:http://xswxi.baihongyu.com/