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

简单描述下这个代码的功能,func2里递归调用了func2,这样能有效增加调用栈深度,然后调用func3的时候,由于func3里写buf越界了,导致栈被破坏了,然后段错误崩溃.崩溃后生成了core文件,我们用gdb打开,输入bt,效果如下:

一般情况下,在调用函数之前,(部分)参数会放入栈内,然后执行汇编指令call, 执行call后会自动将返回地址压入栈中,然后执行被调用函数,被调用函数开头的两条汇编指令很可能是:
这两条指令的作用就是把rbp压入栈中,把栈顶指针rsp赋值给rbp,这样在栈内就会形成一个 链表,以便我们回溯调用栈.push rbp mov rbp,rsp
注意:
在开启优化的时候,默认是 -fomit-frame-pointer,这样可能导致很多函数开头不会出现那两条汇编指令,-fno-omit-frame-pointer选项开启后就会生成以上两条指令.
好了,详细的栈资料请大家自行查阅资料学习,我就不再赘述了.你只要知道调用栈在栈内是以链表的方式保存即可.那栈越界写坏的地方我们可以认为是链表的头部,由于链表的头部被写坏了,导致gdb的bt指令无法回溯调用栈了.
既然如此,那我们可以再找一个节点作为链表的头部.只不过回溯的调用栈可能比"完美"的调用栈少那么一两条,不过半个面包总比没有好,说干就干:


(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命令,结果如下:

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

./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可能导致在遍历调用栈的时候死循环.

