记一次gtest EXPECT_DEATH遭遇sigsegv的debug过程

the crash

实习生小哥上周突然发现单元测试在dev机器上跑不起来了,–verbose一看发现是gtest报错Failure death test:
gtest_expect_death_failure.png

现象其实是所有写了EXPECT_DEATH的单元测试都挂掉了,感觉问题并不简单~ 问了一下边上的几位最近代码有什么变动,一说是用了个新的计算库。

gdb

gdb一波,除看代码发现这个测试的原理是起一个子进程,然后把stderr写到一个临时文件里,子进程退出后,从临时文件里读取log信息,和预期值比对。发现第一现象是stderr没抓到,结果在找stderr为什么没抓到的邪路上走了很久,发现根本没产生stderr:/tmp下面写的文件,大小是0字节。

然后继续gdb,由于是要跟踪子进程,要把选项打开:

set detach-on-fork off
set follow-fork-mode child

run了一下,看到了一点端倪:其实在fork之后进程就sigsegv了,而且居然是在动态加载glibc里的chdir函数过程中挂的。查了一下,机器上的glibc已经至少一年没有动过了,因此基本排除是glibc自己有问题。

然后开始各种翻来覆去的看,先看了几个local变量,发现一切都显得那么完美,根本没有任何内存值被搞烂的迹象。接着只能看挂掉的现场了,info registers看到寄存器的值如下:
gdb_info_registers.png

突然发现红框处的rsp地址有点太“整”了,google了一下,确认这个地址应该是个用户态栈地址的下界。然后查看一下当前的指令,发现是一条push %ebx,也就是说还准备压栈————挂掉的清晰一些了,其实是栈地址空间不够了,也就是传说中的stack overflow。这也是为什么gtest能够看到程序挂掉,但是拿不到预期的错误信息的原因。

the stack overflow

那么,为什么栈空间不够了呢?翻了一下frame 0里的$rsp值,发现在0x7fc0附近,和刚才看到顶上的$rsp=0x7000相差了4032,也就是估计这个程序栈总的大小在4kb。查了一下ulimit -s有32767,诶? 于是接着看代码,发现gtest-death-test.cc里的做法, ExecDeathTestSpawnChild()里写道

  • mmap一个stack,它的大小是getpagesize() // ==4KB
  • clone一个新的线程来跑测试例,用的这个新创建出来的stack

真相大白了。解决方法是mmap的时候分配多一点的栈空间。至于为什么到了上周才触发,大概是因为.so又多了几个吧~

后记

当然,其实这个bug早就有fix了,只是我是后来通过搜”ExecDeathTestChildMain stack overflow”这个关键词才找到的。之前搜了半天EXPECT_DEATH没什么结果~
相关的PR有:

所以说thirdparty code还是要偶尔更新一下的呀~

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.