Program terminated with signal 4, Illegal instruction


一個已進入維護狀態多年的項目最近我做了一些優化,沒想到更新出去后程序直接起不來了,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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM