一种处理栈越界的方法

做者:吉林小伙
连接:http://zhuanlan.zhihu.com/p/20642841
来源:知乎
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

在linux下,栈越界写坏返回地址会致使调用栈没法回溯,这就致使咱们直接使用bt没有办法查看崩溃时调用栈,今天我讲一下我最近研究出来的一种方法(虽然是原创,但可能互联网上早有人发布过此种方法,只不过我没有查到而已).linux

废话少说,步入正题,首先我写了个简单的程序来构造一个栈溢出的状况,为了使效果更加明显,我使用了一些递归来增长调用栈的深度,代码以下:c++

不要吐槽命名方式,我也知道很丑,栈都能越界的程序,必定漂亮不到哪去,哈哈!函数

简单描述下这个代码的功能,func2里递归调用了func2,这样能有效增长调用栈深度,而后调用func3的时候,因为func3里写buf越界了,致使栈被破坏了,而后段错误崩溃.崩溃后生成了core文件,咱们用gdb打开,输入bt,效果以下:学习

因为栈破坏有不少种方式,bt也有可能显示出一排??,总之栈破坏颇有可能致使bt没法回溯就是了.那咱们如何应对呢?咱们首先来看一下调用栈的一些知识:测试

通常状况下,在调用函数以前,(部分)参数会放入栈内,而后执行汇编指令call, 执行call后会自动将返回地址压入栈中,而后执行被调用函数,被调用函数开头的两条汇编指令极可能是:
push rbp
mov  rbp,rsp
这两条指令的做用就是把rbp压入栈中,把栈顶指针rsp赋值给rbp,这样在栈内就会造成一个 链表,以便咱们回溯调用栈.
注意:
在开启优化的时候,默认是 -fomit-frame-pointer,这样可能致使不少函数开头不会出现那两条汇编指令,-fno-omit-frame-pointer选项开启后就会生成以上两条指令.

好了,详细的栈资料请你们自行查阅资料学习,我就再也不赘述了.你只要知道调用栈在栈内是以链表的方式保存便可.那栈越界写坏的地方咱们能够认为是链表的头部,因为链表的头部被写坏了,致使gdb的bt指令没法回溯调用栈了.优化

既然如此,那咱们能够再找一个节点做为链表的头部.只不过回溯的调用栈可能比"完美"的调用栈少那么一两条,不过半个面包总比没有好,说干就干:指针

上图的代码是gdb的扩展,我扩展了一个bts(backtrace stack)指令,其做用是打印给定addr后的count条内容.gdb中可使用source指令来加载扩展,也能够在home路径下新建.gdbinit文件,将脚本内容写入,这样在gdb启动时就会自动生效,咱们使用source .gdbinit来加载一个这个bts扩展指令.而后在gdb里输入i r rsp指令:调试

,rsp的值通常状况下是栈顶,不过不排除这个值是不对的,只是不对的几率比较小而已,而后键入指令:code

(gdb) set pagination off
(gdb) set loggin on ./log
Copying output to ./log.
(gdb)bts 0x7fff81f7c250 1000

第一条指令的做用是关闭"超过一屏内容等待键入回车"功能,第二条是打开log,这样gdb里的输出就会写入log文件内,第三条就是咱们写的扩展指令了,执行一下,等待结果写入到文件中吧.因为咱们的测试程序很短,栈也没用多大,因此1000应该就能够了,实际程序中这个1000可能要变得很大,可能要跑几分钟,不过我很享受这几分钟,由于我就喜欢敲入一条命令而后屏幕刷刷滚的感受,逼格高,哈哈!!递归

跑题了,好了,输出结束咱们去看看./log文件,执行head ./log命令,结果以下:

这正是咱们想要的内容,第一列是栈地址,第二列是该地址的内容,既然调用栈在栈内是链表,那咱们就能够写个代码把栈内全部的链表都暴力搜出来,而后看下哪一个最多是调用栈.

这是我写的暴力搜索栈中链表的程序,其实你们彻底能够用脚本写,比c++方便多了.好了,g++ stack.cpp -o stack编译一下,而后执行:

./stack log 100 > symbol

log就是咱们的log文件了,100呢是调用栈的深度,当你指定100的时候,会把全部调用栈深度为100的链表打印出来,因为咱们递归超过100次了,因此这里我就指定100了,若是你在实际应用中,100没有结果,那能够尝试逐渐减少这个数值,而后咱们看看symbol里的内容:

大概是这样的,还有好多条,我只截取了部分,之因此有info symbol,是由于我要在gdb中加载这个symbol文件,加载后会自动执行info symbol address,做用就是打印出这条地址附近的符号:

(gdb)source ./symbol

此处应该有掌声!!!

从下往上看,依次是__libc_start_main()->main()->func1()->func2()...,这的确是咱们程序中的调用栈,只不过丢失了func3而已.

最后总结一下,此片专栏只是提供一种解决方法而已,具体可否成功,要看运气了.我一直以为调试找bug是要看运气的,尤为是那些偶然出现的crash,在你不知道缘由没法重现时只能从core dump里寻找蛛丝马迹了.

经验:通常栈越界颇有多是字符串越界,此时能够查看rsp附近的内存,说不定有很明显的字符串,一下就定位问题了呢.

因为水平有限,文章中有错误在所不免,请你们包含并指教,谢谢!

-----------------------------------------------

更新一下暴力搜索调用栈的那个代码,其中有一处bug可能致使在遍历调用栈的时候死循环.更新后如上图所示,这样就不怕栈里有回路了,还有一处修改,这个地方用sizeof(void*),就兼容32bit 64bit的程序了,之前写的8只支持64bit的程序

相关文章
相关标签/搜索