一个已进入维护状态多年的项目最近我做了一些优化,没想到更新出去后程序直接起不来了,core dump的文件显示程序因为Program terminated with signal 4, Illegal instruction.直接挂掉。第一次看到这个错误的我有点懵,从字面上理解“Illegal instruction”就是遇到了不合法的汇编指令。可是这个项目是x86的,也没有使用汇编代码,也没有使用和CPU架构相关的优化。而且跑了这么多年都没问题。我的第一直觉是这个信息不准,应该是堆栈被破坏掉了导致后续的信息错误。多次尝试重启程序,发现是100%必现,使用gdb调试,发现在报错之前,所有函数执行正常,没发现空指针之类的异常。而且gdb报错的这一行是一个函数调用,单步跟进去直到函数执行完也没有问题。但是在跳出函数时直接报错,我以为是析构函数出了问题,但检查后又没发现问题,这导致我没法定位问题。

void test()
{
    obj->init(); // gdb显示这是最后一行,但单步进去直到函数执行完也没问题

    print("object init finish, handle = %d", obj->handle());
}

于是开始怀疑是最近做的优化出了问题,只能回滚最近的修改,重新编译了一个版本更新出去,没想到还是在同样的位置宕机。让人十分费解的是,这个项目已经跑很久了,而且我在自己的开发环境上都是能正常跑的,为啥线上会出现100%的bug。网上查了一堆资料后,排除了一些情况:

  1. 这程序是x86,跑在云服务器上。和别人arm、交叉编译这些不相关
  2. 硬件环境和程序很久没变了,没有使用什么特殊的CPU指令

但是stackoverflow上有一种情况引起了我的注意,那就是无效的代码产生了ud2a汇编指令。在他的例子里,是出现了"warning: cannot pass objects of non-POD type 'struct sqlrw_request_cb' through '...'; call will abort at runtime"这个警告。我随手在编译的时候grep一下POD,没想到是真的出现了这种情况。改掉之后,程序可以顺利跑起来。

把出问题的代码简化一下:

#include <cstdio>

class Handle
{
public:
    Handle(int i)
    {
        _i = i;
    }
    operator int() const
    {
        return _i;
    }
private:
    int _i;
};

int main()
{
    Handle h(9);
    printf("object init handle = %d", h);
    return 0;
}

出问题的代码在printf("object init handle = %d", h);这一行,把一个Handle类型的变量传给了printf函数,而printf是可变参数(即...的写法),这就是为什么gcc警告“cannot pass objects of non-POD type 'struct sqlrw_request_cb' through '...'; call will abort at runtime”的原因,这里会写入一个ud2a汇编指令,这个指令就会触发Illegal instruction错误。

那为什么这项目之前没问题,在我的开发环境上没问题,而只是在线上有问题?这就是各种巧合罢了。首先,这个上古的项目用的是CentOS 6.5,我接手这个项目后,部署开发环境的时候用的是CentOS 6.10,我觉得大版本不变,应该是兼容的,用新一点的版本。而且发布版本的时候,有一台专用的CentOS 6.5机器,所以和我开发用什么版本也没关系,只要能开发就行。不过就在上个月,那台专用的机器硬盘挂了要重装系统,负责机器的同事问我要什么版本,我说CentOS 6.5,装好后我确认了系统版本,重新部署发布版本的环境就没管了。今天第一次用这台机器发布版本,结果就出问题了。

经过仔细的对比,用gcc -v查看版本(不是gcc --version)我发现线上环境是CentOS 6.5,gcc版本是4.4.7 20120313 (Red Hat 4.4.7-23),我的CentOS 6.10也是这个版本,而发布那台机器,则是4.4.7 20120313 (Red Hat 4.4.7-4),可以看到gcc的版本低了几个小版本。上面的程序,在4.4.7-23上编译是没有问题的,但在4.4.7-4是会出现警告并且程序跑不起来,应该是gcc版本太低没有识别那个operator int()函数,或者是bug。至于线上的gcc版本为什么高?应该是通过网络update了,而专门用来发布版本的那台机器是离线的,用iso镜像安装系统,那个镜像有点旧。

事后总结一下:

  1. 这个项目比较老,代码也不规范,编译有成千上万的警告没处理,所以我发版本时多了个"non-POD type"的警告也没人发现
  2. gcc报错的行数不准确,是printf的上一行而不是printf这一行,这误导我查问题(我猜测gcc是在这一行有问题的代码只产生了一个ud2a指令,所以行数对不上)
  3. 我处理问题的经验不足,出现Illegal instruction,第一反应该是查最后执行的汇编
    用gdb打开core dump的文件,输入x/i $pc即可
(gdb) x/i $pc
=> 0x5289a1 <HandleMgr::init(int)+287>:   ud2a

分析这个汇编能否被执行,当前CPU是否支持这个汇编指令,就比较容易定位问题。如果还有疑问,还可以查看当前函数的汇编,对分析问题也有帮助

(gdb) disassemble
Dump of assembler code for function HandleMgr::init(int):
   0x0000000000528882 <+0>:     movslq %edx,%edx
   0x0000000000528884 <+2>:     add    $0x60,%rdx
   0x0000000000528888 <+6>:     mov    (%rax,%rdx,4),%eax
   0x000000000052888b <+9>:     mov    %eax,-0x14(%rbp)
   0x000000000052888e <+12>:    cmpl   $0x0,-0x14(%rbp)
   0x0000000000528892 <+16>:    jns    0x52889e <HandleMgr::init(int)+28>
   0x0000000000528894 <+18>:    mov    $0x0,%eax
   0x0000000000528899 <+23>:    jmpq   0x5289a3 <HandleMgr::init(int)+289>
   ...
---Type <return> to continue, or q <return> to quit---
=> 0x00000000005289a1 <+287>:   ud2a
   0x00000000005289a3 <+289>:   leaveq
   0x00000000005289a4 <+290>:   retq
   0x00000000005289a5 <+291>:   nop
   0x00000000005289a6 <+292>:   push   %rbp
   0x00000000005289a7 <+293>:   mov    %rsp,%rbp
   0x00000000005289aa <+296>:   push   %rbx
   0x00000000005289ab <+297>:   sub    $0x28,%rsp
   0x00000000005289af <+301>:   mov    %rdi,-0x28(%rbp)
   0x00000000005289b3 <+305>:   mov    %rsi,-0x30(%rbp)
posted on 2022-04-16 16:29  coding my life  阅读(1488)  评论(0编辑  收藏  举报