一:背景
1. 講故事
當 .NET程序
在Linux上崩潰時,我們可以配置一些參考拿到對應程序的core文件,拿到core文件后用windbg打開,往往會看到這樣的一句信息 Signal SIGABRT code SI_USER (Sent by kill, sigsend, raise)
,參考如下:
(1.1d): Signal SIGABRT code SI_USER (Sent by kill, sigsend, raise)
libc_so!wait4+0x57:
00007fbd`09313c17 483d00f0ffff cmp rax,0FFFFFFFFFFFFF000h
0:023> ? 1d
Evaluate expression: 29 = 00000000`0000001d
0:023> ~29s
*** WARNING: Unable to verify timestamp for libSystem.Native.so
libc_so!read+0x4c:
00007fbd`0933829c 483d00f0ffff cmp rax,0FFFFFFFFFFFFF000h
從字面上看是 kill,sigsend,raise
發出了攜帶 SI_USER 代碼的 SIGABRT 信號,看起來和Linux信號機制有關,那具體是什么意思呢?這就是本篇和大家詳聊的。
二:Linux 信號機制
1. 信號機制簡介
簡單的說Linux信號
是一種進程間通信機制,大概可以做三件事情。
- 通知進程發生了某種事件,比如:段錯誤。
- 允許進程間發送簡單的消息。
- 控制進程行為,比如:終止、暫停、繼續等。
在 linux 上有60多個信號,默認能產生core文件的有11個,這也是我們最關心的,整理成表格如下:
信號名稱 | 信號編號 | 說明 |
---|---|---|
SIGQUIT | 3 | 通常由 Ctrl+\ 觸發 |
SIGILL | 4 | 非法指令 |
SIGABRT | 6 | 由 abort() 函數產生 |
SIGFPE | 8 | 浮點異常 |
SIGSEGV | 11 | 段錯誤(非法內存訪問) |
SIGBUS | 7 | 總線錯誤(內存訪問對齊問題等) |
SIGSYS | 31 | 錯誤的系統調用 |
SIGTRAP | 5 | 跟蹤/斷點陷阱 |
SIGXCPU | 24 | 超出 CPU 時間限制 |
SIGXFSZ | 25 | 超出文件大小限制 |
SIGEMT | 7 | EMT 指令(某些架構) |
有了這些基礎之后就可以解讀 Signal SIGABRT code SI_USER (Sent by kill, sigsend, raise)
這句話了。
1) SIGABRT
全稱 signal abort ,是一種能產生 core 的信號。
2) SI_USER
在 linux 源碼中有這樣一句代碼(type == PIDTYPE_PID) ? SI_TKILL : SI_USER
,參考如下:
static void prepare_kill_siginfo(int sig, struct kernel_siginfo *info,enum pid_type type)
{clear_siginfo(info);info->si_signo = sig;info->si_errno = 0;info->si_code = (type == PIDTYPE_PID) ? SI_TKILL : SI_USER;info->si_pid = task_tgid_vnr(current);info->si_uid = from_kuid_munged(current_user_ns(), current_uid());
}
代碼中的 kernel_siginfo.si_code
字段用來表示信號的來源,比如說 SI_USER
表示信號來源于用戶進程,而后者的 SI_TKILL
表示信號來源于 tgkill,tkill
系統調用。
3) kill,sigsend,raise
熟悉 linux 的朋友應該對 kill
和 raise
方法非常熟悉,畢竟他們遵守 POSIX
標準,至于他們有什么區別,看簽名就知道了。。。
/* Raise signal SIG, i.e., send SIG to yourself. */
extern int raise (int __sig) __THROW;/* Send signal SIG to process number PID. If PID is zero,send SIG to all processes in the current process's process group.If PID is < -1, send SIG to all processes in process group - PID. */
#ifdef __USE_POSIX
extern int kill (__pid_t __pid, int __sig) __THROW;
#endif /* Use POSIX. */
相比前面的函數,這個 sigsend
就不是 POSIX
標準了,只在部分Unix上可用,比如 Solaris,SunOS,不過功能還是很強大的,不僅可以指定 pid,還可以指定 pidgroup 以及 user 來批量的 kill 進程,這里做個了解即可,簽名如下:
int sigsend(idtype_t idtype, id_t id, int sig);
這些信息匯總之后更準確的意思就是:你的程序可能調用了 kill(SIGABRT) ,raise(SIGABRT),abort 引發的程序崩潰
,那是不是這樣的呢?可以用 windbg 的 ~* k
觀察每個線程的調用棧,最終還真給找到了。
0:023> k# Child-SP RetAddr Call Site
00 00007fbd`03c62a70 00007fbd`090bf635 libc_so!wait4+0x57
01 00007fbd`03c62aa0 00007fbd`090c0580 libcoreclr!PROCCreateCrashDump+0x275 [/__w/1/s/src/coreclr/pal/src/thread/process.cpp @ 2307]
02 00007fbd`03c62b00 00007fbd`090be22f libcoreclr!PROCCreateCrashDumpIfEnabled+0x770 [/__w/1/s/src/coreclr/pal/src/thread/process.cpp @ 2524]
03 00007fbd`03c62b90 00007fbd`090be159 (T) libcoreclr!PROCAbort+0x2f [/__w/1/s/src/coreclr/pal/src/thread/process.cpp @ 2555]
04 (Inline Function) --------`-------- (T) libcoreclr!PROCEndProcess+0x7c [/__w/1/s/src/coreclr/pal/src/thread/process.cpp @ 1352]
05 00007fbd`03c62bb0 00007fbd`08db667f (T) libcoreclr!TerminateProcess+0x84 [/__w/1/s/src/coreclr/pal/inc/pal_mstypes.h @ 1249]
...
09 00007fbd`03c63950 00007fbd`08d4524e libcoreclr!UMEntryThunk::Terminate+0x38 [/__w/1/s/src/coreclr/inc/clrtypes.h @ 260]
0a (Inline Function) --------`-------- libcoreclr!InteropSyncBlockInfo::FreeUMEntryThunk+0x24 [/__w/1/s/src/coreclr/vm/syncblk.cpp @ 119]
19 00007fbd`03c63e30 00007fbd`092c91f5 libcoreclr!CorUnix::CPalThread::ThreadEntry+0x1fe [/__w/1/s/src/coreclr/pal/inc/pal.h @ 1763]
1a 00007fbd`03c63ee0 00007fbd`09348b00 libc_so!pthread_condattr_setpshared+0x515
1b 00007fbd`03c63f80 ffffffff`ffffffff libc_so!_clone+0x40
1c 00007fbd`03c63f88 00000000`00000000 0xffffffff`ffffffff
在上面的代碼中我們看到了 libcoreclr!PROCAbort
函數,在 coreclr 中方法定義如下:
/*++
Function:PROCAbort()Aborts the process after calling the shutdown cleanup handler. This functionshould be called instead of calling abort() directly.Parameters:signal - POSIX signal numberDoes not return
--*/
PAL_NORETURN
VOID
PROCAbort(int signal)
{// Do any shutdown cleanup before aborting or creating a core dumpPROCNotifyProcessShutdown();PROCCreateCrashDumpIfEnabled(signal);// Restore the SIGABORT handler to prevent recursionSEHCleanupAbort();// Abort the process after waiting for the core dump to completeabort();
}VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, bool serialize)
{// If enabled, launch the create minidump utility and wait until it completesif (!g_argvCreateDump.empty()){std::vector<const char*> argv(g_argvCreateDump);...}
}
卦中的代碼邏輯非常清楚,在 abort 退出之前,先通過 PROCCreateCrashDumpIfEnabled(signal)
方法踩了一個dump,也就是說 dump 中看到的信息就是用他來填充的,可以觀察 libcoreclr!g_argvCreateDump
全局變量,參考如下:
0:023> x libcoreclr!*g_argvCreateDump*
00007fbd`09192360 libcoreclr!g_argvCreateDump = {size=8}
0:023> dx -r1 (*((libcoreclr!std::vector<const char *, std::allocator<const char *> > *)0x7fbd09192360))
(*((libcoreclr!std::vector<const char *, std::allocator<const char *> > *)0x7fbd09192360)) : {size=8} [Type: std::vector<const char *, std::allocator<const char *> >][<Raw View>] [Type: std::vector<const char *, std::allocator<const char *> >][size] : 8[capacity] : 8[0] : 0x5555b5d71140 : "/usr/share/dotnet/shared/Microsoft.NETCore.App/8.0.15/createdump" [Type: char *][1] : 0x7fbd08b61d8f : "--name" [Type: char *][2] : 0x7ffd1b7e1cec : "/db/xxxx/crash.dmp" [Type: char *][3] : 0x7fbd08b6ce5f : "--full" [Type: char *][4] : 0x7fbd08b4c7ee : "--diag" [Type: char *][5] : 0x7fbd08b58630 : "--crashreport" [Type: char *][6] : 0x5555b5dd7230 : "1" [Type: char *][7] : 0x0 [Type: char *]
2. C代碼眼見為實
為了能夠讓大家有一個更加貼切的眼見為實,我們用 C 代碼親自演示一下,為產生 core 文件,配置如下:
root@ubuntu2404:/data2# ulimit -c unlimited
root@ubuntu2404:/data2# echo /data2/core-%e-%p-%t | sudo tee /proc/sys/kernel/core_pattern
/data2/core-%e-%p-%t
配置好之后,大家可以使用 abort,kill,raise
這三個方法的任何一個,這里我就用 kill
來演示吧。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>void sig_handler(int signo, siginfo_t *info, void *context)
{fprintf(stderr, "Received signal: %d (sent by PID: %d, UID: %d)\n",signo, info->si_pid, info->si_uid);
}int main()
{struct sigaction sa;sa.sa_sigaction = sig_handler;sa.sa_flags = SIGABRT;sigemptyset(&sa.sa_mask);if (sigaction(SIGSEGV, &sa, NULL) == -1){perror("sigaction");return 1;}printf("My PID: %d\n", getpid());printf("Press Enter to send SIGABRT to myself...\n");getchar();kill(getpid(), SIGABRT); // 第一種方式// raise(SIGABRT); // 第二種方式// abort(); //第三方方式printf("This line may not be reached.\n");return 0;
}
ternimal 如下:
root@ubuntu2404:/data2# ./app
My PID: 7403
Press Enter to send SIGABRT to myself...Aborted (core dumped)
root@ubuntu2404:/data2#
root@ubuntu2404:/data2# ls -lh
total 160K
-rwxr-xr-x 1 root root 21K May 27 10:25 app
-rw-r--r-- 1 root root 813 May 27 10:25 app.c
-rw------- 1 root root 432K May 27 10:25 core-app-7403-1748312729
用 windbg 打開 core-app-7403-1748312729 文件,熟悉的畫面又回來了,哈哈。截圖如下:
三:總結
要分析linux 上的.NET程序崩潰,理解Linux信號機制
是一個必須要過的基礎,調試之路艱難哈。。。