事情是这样的,在第一次因协程栈溢出导致的进程崩溃后,过了不知道多久由于代码量的增加,协程的栈又溢出了程序又崩溃了,gdb core文件基本上找不到什么有助于问题定位的信息,有时候连个函数调用栈都没有,你都不知道运行到那个函数那行代码时出现了问题,,,
可能我还是太菜了吧!!!
因此协程库需要一种机制来检测栈溢出或者说栈破坏,其实要确定是不是由于栈导致的进程崩溃很简单,只要是进程崩溃就不管3721先增大协程栈再说,增加栈空间后不崩了,或者gdb core文件bt得不到完整的函数调用栈,那八成就是栈溢出导致的此次崩溃,在增加了跨线程负载均衡调度后,我还遇到过无论栈给多大,进程总会崩溃的问题,最后发现是同一个协程栈被多个线程并发使用了,,,
但是这种事并不是时常发生,当发生的时候总是会忘了是栈的问题,到最后实在是找不到原因,没办法了才会又想到栈溢出,就比如第一次栈溢出,我排查了一个星期,可能我还是太菜了吧!!!
为了以后不再重蹈覆辙,为了折腾而折腾,因为爱所以爱,还是搞吧,,,
gcc是怎样实现栈溢出检测的?也适用于协程?
man gcc,人家不叫stackOverFlow-detect,人家叫stack-protect,不是栈溢出检测?怎么成了栈保护,保护什么?保护栈不被破坏?显然从描述来看,它是无法保护的,它只能在栈帧被破坏后,在函数调用返回时检测到栈帧被破坏了然后给出警告退出,就是这一退阻止了缓冲区溢出攻击保护了栈
我想应该这样来理解,在最近的一次函数调用返回时检测到栈帧被破坏,然后终止进程,防止返回到一个未知的地方,保护了函数调用栈,这时从产生的core文件中还可以得到完整的函数调用栈,而栈帧破坏一般是由缓冲区溢出写导致,因此当触发了栈保护时就是检测到缓冲区溢出了
如果没有栈保护,当保存在栈帧上的rbp/rip被缓冲区覆盖后,函数就会继续返回到一个未知的地方使用一个未知的栈,这时如果进程崩溃,从core文件中将得不到函数调用栈,你无法知道运行到哪行代码
来看一个不加-fstack-protector-all编译选项与加-fstack-protector-all编译选项的可执行文件的的反汇编代码
不加-fstack-protector-all编译选项
加-fstack-protector-all编译选项
从反汇编代码可以看到在加了-fstack-protector-all编译选项后,编译器在函数调用的栈帧上-0x8(%rbp)处放入了8个字节的标识,并在返回时插入了检测代码,就是在返回时对比这8个字节跟放入时是不是一致的,如不一致则发生栈溢出,栈布局如下图
偷个懒只画出加了fstack-protector-all后函数的调用栈
我觉得这个图这样画也合理,返回到上一个函数调用栈帧的信息不该属于当前的栈帧?
在编译时加上-fstack-protector-all选项会在所有的函数内插入标识与检测代码,如果我们把代码改成这样
当向a中放置超过4个字节的内容时,多出的内容就会覆盖标识,在函数返回时,就会检测到栈被破坏了,如果rip被成功覆盖利用那就是缓冲区溢出攻击了
我们称其为栈的下溢,而协程库需要的是检测的是栈的上溢,在协程使用了超过自身栈大小时能及时检测出来,由于协程的栈是在堆上分配的,一旦发生上溢,整个进程使用的堆就会被破坏,在下一次malloc,free时进程就有可能崩溃
如下图比如有两个协程的栈在堆上是相邻的(协程栈是使用malloc分配的)
当协程1使用了超过自身栈大小的空间,越过了图中标红处就会上溢到协程2的栈上,此时协程2的栈就会被破坏,准确的说应该是栈帧被破坏了(进程使用的堆也会被破坏),当协程2的函数调用链开始返回,调用栈回退时协程2就会检测到栈被破了,这样看来gcc的栈保护对协程而言也是有效的,但是有限
- 如果协程1只上溢覆盖了协程2的rip,rbp,并未覆盖到标识,协程2的栈虽然已经被被破坏了但还是检测不到,仍然会继续返回到一个未知的地方使用一个未知的栈
- 如果协程2在一个函数调用中不返回那么也无法检测到自己的栈已经被破坏了
- 因为协程对象与协程栈都是在堆中分配的,如果由于协程栈的上溢而导致协程对象被覆盖破坏,也是无法检测到的
gcc提供的栈保护仅仅是为了应对在栈上分配的缓冲区溢出导致的栈下溢,那么问题来了为什么gcc不检测栈的上溢?
gcc栈保护是以函数调用的栈帧为检测单位的而不是线程栈,检测的栈上溢要以栈为检测单位,如果我们在所有的协程对象及栈的起始与结束置8字节的标识,时常检测它,那么就有可能及时主动发现协程栈或对象被破坏了,极大方便了测试时程序崩溃问题的定位
要怎么检测?
栈及对象是协程库分配创建的,协程库知道它们的大小及从什么地方开始结束,在每个函数返回时插入检测点
检测什么?
- 取出当前栈指针也就是CPU的SP寄存器的值与协程栈的结束地址做比较,如果SP小于协程栈的结束地址,那么此次函数调用已经产生了栈的上溢,当前协程已经使用了超过了给其分配的栈空间,其它协程及栈有可能被破坏
- 在协程栈与协程对象的的开始与结束存放若干字节的标识,如8字节” 0x0123456789abcde0”,检测这8字节是否被改变,如被改变则说明自已的栈或对象已经被破坏了
第一种方法必需要在每个函数调用返回前插入检测代码,第2种就不必,实现如下图
使用如下图,在函数返回前进行检测,如果是第三方库函数怎么搞?没法搞,这就是它的鸡肋之处
慢慢的我发现,擦,既然协程栈与协程对象都是在堆上分配的,协程的栈溢出不就是堆内存的越界访问?
终于可以去除那丑陋的检测代码啦,如此说来我又是在瞎搞喽,,,,
那么valgrind能否完美支持ToyCoroutine?