有本事就出来,没本事就当鳖!
假设让我回答关于进程栈,线程栈的问题,仅仅要问题不笼统,仅仅要问题明白。我会一五一十地回答,正确率上九成,然而,可悲的是,问题往往他妈的都不是非常明白,因此,游戏到此结束。!
艹。可是假设给我一个问的机会。我会问以下一个问题,记住。使出你拉屎的劲来回答(该问题足够糙。不必太当回事,重要的东西在以下-):
UNIX/Linux的stack在大多数平台是向下扩展的(注意,我已经告诉事实了。我并没有问...是怎样扩展的,这是能够背诵下来并朗读出来的),在一个运行流调用了一个函数A,而该函数A在stack上分配了一个大数组导致了stack扩展(注意,这又是一段陈述。我还没有给出问题),然后A返回了,UNIX/Linux理应回收调A里面大数组分配stack空间-由于它再也没实用了。可是它并没有这么做(这里可能是一个陷阱,真的是UNIX/Linux理应这么做却没有做,还是说我仅仅是在逗你玩...不确定,但陈述就是如此)。(注意,我的问题来了),请问,UNIX/Linux为什么这么做???!!!时间限制:5分钟。
作答方式:全口述。不能绘图,不能打手势...语言含糊不清的。表达能力不好的,算错误。
答题建议:假设你对OS虚拟内存管理以及Linux的VMA实现细节没有相当深入的理解,请不要推測答案。请直接回答“不知道”,然后看完此文。
5分钟过去。我要发布一点我的想法了。
首先,这个问题在本身看来。有问题。由于尽管Linux理应这么做,但它:
第一,它不一定能做到;
第二,它根本没有必要做。
那么论据是什么?凭什么这样说?
积极论点
不是必需这样做。运行流还会调用别的函数或者再次调用A。频繁回收栈损耗性能。消极论点
非常难或者不能做到。stack操作是处理器控制的,和OS内核地址空间管理机制之间没有同步机制。一个函数调用结束后。CPU自己主动处理stack寄存器的收缩,弹出栈帧。然而它无法通知OS内存管理系统去更新进程地址空间的映射关系。
怎样处理stack所在地址空间区域的争议
stack会一直扩展到碰到异常的地址B,B可能是一个readonly的地址或者是一个保护空洞。在向下扩展stack情况下。假设地址B偏上,会导致stack空间变小,假设偏下,一旦函数局部变量差点儿占满了stack底到B的空间,mmap尽管也能unmap掉这段区域然后remap,然而这会使数据混乱,造成严重问题。拍脑袋的结论
mmap或者brk期间,比較stack顶部与esp寄存器,若小于则回收(等于是正常的。大于是不可能的)。Linux真实的做法
Linux没有推断什么esp寄存器,Linux的原则非常easy,仅仅要一个地址处在一个vma范围内或者处在stack可扩展的范围内,且拥有权限的。它就是能够訪问,内核是无论这个VMA是属于stack还是heap或者别的什么。详细由应用程序自己控制。也就是说,你全然能够写一段代码。把地址空间中所有能够写的区域所有清零,这全然有可能。缓冲区溢出可能是一种蓄意的破坏,然而程序猿偶然的错误也会造成破坏,尽管他们大多数都不知道错误是怎样发生的。我不想用文字长篇大论Linux是怎样管理VMA的,你知道这个应该是一个前提,你必须知道这个。我用一段代码以及两个图示来展示Linux系统内核是怎样管理stack附近的地址空间映射的,而且在第二张图中给出,假设你非要蓄意破坏。会造成什么问题。
也就是说。一旦发生莫名奇异的错误,你必须能从细节上理解这个错误是怎样发生的。
演示代码
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <sys/mman.h> #define LARGE 70000000 #define PAGESIZE 4096 // 该函数什么都不做。仅仅为了把stack向下扩展 // 请注意用ulimit将stack限制大小去掉。这样会更easy说明问题 void call() { int i; char a[LARGE]; // 请相信,一定是在中间的赋值中触发segfault,由于两边的元素要么处在stack/fixmap vma, // 要么处在游离的,及其孤独的,被fixmap给截断了的vma中。因此以下的赋值不会引发段错误: // a[0] = 1; // a[LARGE-1] = 1; for (i = 0; i < LARGE; i++) { a[i] = 1; } } int main(int argc, char **argv) { int i; char *p_map, *p_base, *p_base2; printf("%d init state ", getpid()); // 获取stack的大致地址,而且PAGESIZE对齐。p_base = (char *)&i; p_base2 = (char *)(((unsigned long)p_base) & ~4095); // 获取pagesize对齐的用来fixmap的地址,该地址起点在当前stack的以下。 p_base2 = (char *)((unsigned long)p_base2 - (unsigned long)36*PAGESIZE); getchar(); // 调用fixmap,显然。假设你细致在getchar期间分析了/proc/xx/maps文件而且 // 得到了上述的那些magic number,以下的mmap无论怎样是会成功的! p_map = (char *)mmap((void*)p_base2, PAGESIZE*3, PROT_READ | PROT_WRITE, MAP_FIXED |MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (p_map == MAP_FAILED) { printf("failed 1 "); } else { printf("before unmap fixmap around stack "); getchar(); // 成功了就释放掉它,此时的地址空间恢复成mmap之前的状况 munmap(p_map, PAGESIZE*3); } printf("after unmap fixmap around stack "); getchar(); call(); printf("after extend stack[first] "); getchar(); // 依旧调用之前的那个一模一样的mmap进行fixmap,由于调用了call,stack // 空间已经扩展到了这个fixmap的fixaddress,非常遗憾,成功了。然而它将stack vma // 一刀切成了两段。无论怎样。訪问还是能够进行的。 p_map = (char *)mmap((void*)p_base2, PAGESIZE*3, PROT_READ | PROT_WRITE, MAP_FIXED |MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (p_map == MAP_FAILED) { printf("failed 2 "); } printf("after second fixmap around stack at the same address "); getchar(); // 这里更狠。部分unmap了上面那个fixmap vma,留下一个空洞。 munmap(p_map, PAGESIZE); printf("after unmap fixmap around stack incompletely "); getchar(); // 当空洞被touch的时候,不会引发stack extend!而是直接segfault!
爆炸!
call(); // 永远不会到达这里! printf("after extend stack[second] "); getchar(); return 0; }
针对上述代码的图解
以下一幅图展示一直到出事之前,该进程的stack附近的地址空间映射区域是怎么演化的:測试方式
假设你认为图是我自己画出来的,那么肯定有一个疑问。我是基于什么画出来的,其实,我并非通过看代码画出来的。我是通过不断查看procfs的maps文件实时了解该进程地址空间的细节,将其转换成了上面的图示,为了让我有机会到还有一个终端去查maps文件,我在代码中添加了getchar调用。每次查看完maps文件。我会拍一下键盘的回车键。我的測试例如以下:代码编译后的进程输出
root@abcd:~# ./a.out7846
init state
before unmap fixmap around stack
after unmap fixmap around stack
after extend stack[first]
after second fixmap around stack at the same address
after unmap fixmap around stack incompletely
段错误 (core dumped)
以下是查看每一步进程maps文件的输出:
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 42b2c01483000-2b2c01487000 r--p 00157000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01487000-2b2c01488000 rw-p 0015b000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff6236a000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01487000-2b2c01488000 rw-p 0015b000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff62359000-7fff6235c000 rw-p 00000000 00:00 0
7fff6236a000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01483000-2b2c01487000 r--p 00157000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01487000-2b2c01488000 rw-p 0015b000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff6236a000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01483000-2b2c01487000 r--p 00157000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01487000-2b2c01488000 rw-p 0015b000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff5e0bb000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff5e0bb000-7fff62359000 rw-p 00000000 00:00 0
7fff62359000-7fff6235c000 rw-p 00000000 00:00 0
7fff6235d000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff5e0bb000-7fff62359000 rw-p 00000000 00:00 0
7fff6235a000-7fff6235c000 rw-p 00000000 00:00 0
7fff6235d000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
我怕上面的文字信息太乱,格式在不同浏览器会有问题。我还特意截了一张图:
有什么用
你能够用这样的方式彻底限制一个进程的stack的大小。越界了不是报错,而是segfault。然后你能够signal捕获这个segfault,在里面把那个未全然unmap的fixmap vma以及那个可怜且孤独的残缺的stack vma给彻底unmap掉。只是这确实没什么好玩的。有什么用呢?它的作用就是让你更加深入理解Linux对虚拟地址空间的管理方式。
小Tips
本文不涉及线程栈,可是倒也不难,线程栈一般在heap区或者中间的大块mmap区动态分配,mmap的时候给它一个MAP_GROWSDOWN标志就能够了。关于它的管理方式。没啥区别。核心问题在于。缺页异常处理程序是怎么识别到一个缺页是一个vma内部的缺页(结果就是调页),还是vma外部的缺页。在后一种情况下。缺页处理逻辑还要进一步识别是stack的缺页(结果就是extend stack然后调页),还是非stack缺页(结果就是segfault...)。Linux的stack除非遇到本文所述的这样的方式的挤兑收缩,它是永远扩展的。假设你想阅读Linux的内核代码。那么也须要理解以下的事实:
1.find_vma函数能找到vma仅仅有一个限制,即输入地址仅仅要小于查找vma的end就可以。并非非常多人想象的那样输入地址必须处在查找vma的start和end之间;
2.find_vma函数之所以实现得如此incompletely。是由于为了简化缺页中断的处理,同一时候也是为了提供一种更加统一的方式同一时候处理upgrows和downgrows的vma。
后话
尽管这个问题问得有点乱,可是假设能找上述回答连续扯5分钟的,应该是真行!只是我不知道怎样的语言表达能力能够不用图解和代码把上面的每个细节说清楚...总之,我认为我的这个题目是一个好题目。能够建议给看到此文的人,把它做面试题吧。凡是发现不了题目问题的以及说不出所以然的。一律不要。这真是一道好測试题啊。它是如此之好,以至于我还想再出几道比它更好的。