一個已進入維護狀態多年的項目最近我做了一些優化,沒想到更新出去后程序直接起不來了,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。網上查了一堆資料后,排除了一些情況:
- 這程序是x86,跑在雲服務器上。和別人arm、交叉編譯這些不相關
- 硬件環境和程序很久沒變了,沒有使用什么特殊的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鏡像安裝系統,那個鏡像有點舊。
事后總結一下:
- 這個項目比較老,代碼也不規范,編譯有成千上萬的警告沒處理,所以我發版本時多了個"non-POD type"的警告也沒人發現
- gcc報錯的行數不准確,是printf的上一行而不是printf這一行,這誤導我查問題(我猜測gcc是在這一行有問題的代碼只產生了一個ud2a指令,所以行數對不上)
- 我處理問題的經驗不足,出現
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)