注:本文重要資訊使用 ***
遮蔽關鍵字。
最近國慶前,專案碰到一個很麻煩的問題,這個問題讓我們加班到凌晨三點。
大概背景:
客戶給了一些 C語言 寫的 SDK 庫,這些庫打包成 .so 檔案,然後我們使用 C# 呼叫這些庫,其中有一個函式是回撥函式,引數是結構體,結構體的成員是函式,將 C# 的函式賦值給委託,然後儲存到這個委託中。
C# 呼叫 C 語言的函式,然後 C 語言執行到一些步驟後, C 語言函式呼叫 C# 的函式。這個在 ARM64 的機器下,是正常的,例如樹莓派,華為的鯤鵬伺服器等。由於突然改成使用 X64 的機器部署專案,沒有測試就直接打包了(Docker)。
沒有測試的原因有兩個:一是,眾所周知 .NET Core 是跨平臺的,既然在 ARM64 下已經測試過,那麼應該沒問題;
二是,專案是華為 edge IoT 專案,必須走華為雲註冊邊緣裝置,然後通過雲服務下發應用(Docker)到機器才能成功執行(有許多系統自動建立的環境變數和裝置連線華為 IoT 的憑證)。在機器上直接啟動,是無法正常完成整個流程的。
三是,事情來得太突然,沒有時間測試。
事實上,就是這麼幸福,出事的時候就是加班福報~~~
大家記得,要部署上線、演示專案之前,一定要測試,測試再測試。
出現問題
應用在雲上下發到裝置後,啟動一會兒就會掛了,然後修改 Docker 容器的啟動指令碼,進入容器後,手動執行命令啟動程式。
最後發現:
dotnet xxx.dll
...
...
Segmentation fault (core dumped)
出現這個 Segmentation fault (core dumped)
問題可能是指標地址越界、訪問不存在的記憶體、記憶體受保護等,參考: http://ilinuxkernel.com/?p=1388
https://www.geeksforgeeks.org/core-dump-segmentation-fault-c-cpp/
由於這個問題是核心級別的,所以可以從系統日誌中找到詳細的日誌資訊。
檢視 核心日誌
容器和物理機都可以檢視日誌,但是容器裡面的資訊太少,主要從物理機找到資訊的日誌。
在物理機:
# 核心日誌
cat /var/log/kern.log
# 系統日誌
cat /var/log/syslog
剛開始時,大佬提示可能是記憶體已被回收,函式等沒有使用靜態來避免 gc 回收,可能在 C 回撥之前,C# 中的那部分記憶體就以及回收了。
但是我修改程式碼,都改成靜態,並且列印地址,還禁止 GC 回收,結果還是一樣的。
檢視引用型別在記憶體中的地址 :
public string getMemory(object o)
{
GCHandle h = GCHandle.Alloc(o, GCHandleType.WeakTrackResurrection);
IntPtr addr = GCHandle.ToIntPtr(h);
return "0x" + addr.ToString("X");
}
禁止 GC 回收:
GC.TryStartNoGCRegion(1);
...
...
GC.EndNoGCRegion();
工具除錯
經過提示,知道可以使用 GDB 除錯 .so,於是馬上 Google 查詢資料,經過一段時間後,學會了使用這些工具查詢異常堆疊資訊。
GDB
GNU Debugger,也稱為 gdb,是用於 UNIX系統除錯 C 和 C ++ 程式的最流行的偵錯程式。
If a core dump happened, then what statement or expression did the program crash on?
If an error occurs while executing a function, what line of the program contains the call to that function, and what are the parameters?
What are the values of program variables at a particular point during execution of the program?
What is the result of a particular expression in a program?
你可以使用線上的 C/C++ 編譯器和 GDB ,線上體驗一下:https://www.onlinegdb.com/
回到正題,要在 物理機或者 Docker 裡面除錯 .NET 的程式,要安裝 GDB,其過程如下。
使用 apt install gdb
或者 yum install
就直接可以安裝 gdb。
strace
另外 strace 這個工具也是很有用的,能夠看到堆疊資訊,使用 apt install strace
即可安裝上。
binutils
objcopy、strip 這兩個工具可以將 .so 庫的符號資訊整理處理。
objcopy 、strip安裝:
apt install binutils
binutils 包含了 objcopy 和 strip。
除錯、轉儲 core 檔案
在使用 GDB 除錯之前,我們瞭解一下 core dump 轉儲檔案。
相當於 .NET Core 的 dotnet-dump 工具生成的 快照檔案。
為了生成轉儲檔案,需要作業系統開啟功能。
在物理機上執行:
ulimit -c unlimied
在 docker 裡面執行:
ulimit -c unlimied
自定義將轉儲檔案存放到目錄
echo "/tmp/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
然後進入容器,直接使用 dotnet 命令啟動 .NET 程式,等待程式崩潰出現:
dotnet xxx.dll
...
...
Segmentation fault (core dumped)
檢視 tmp 目錄,發現生成了 corefile-dotnet-{程式id}-{時間}
格式的檔案。
使用命令進入 core dump
檔案。
gdb -c corefile-dotnet-376-1602236839
執行 bt
命令。
發現有資訊,但是可用資訊太少了,而且名稱都是 ??
,這樣完全定位不到問題的位置。怎麼辦?
可以將 .so 檔案一起包進來檢查。
gdb -c corefile-dotnet-376-1602236839 /***/lib***.so
也可以使用多個 .so 一起加入
gdb -c corefile-dotnet-376-1602236839 /***/libAAA.so /***/libBBB.so
strace 的使用
Linux中的 strace 命令可以跟蹤系統呼叫和訊號。
如果系統沒有這個命令,可以使用 apt install strace
或者 yum install strace
直接安裝。
然後使用 strace 命令啟動 .NET 程式。
strace dotnet /***/***.dll
啟動後就可以看到程式的堆疊資訊,還可以看到函式呼叫時的函式定義。
GDB 除錯啟動 .NET 程式
執行以下命令即可啟動 .NET Core runtime:
gdb dotnet
在 gdb 中 執行 start
啟動程式。但是因為僅啟動 .NET Core runtime
是沒用的,還要啟動 .NET 程式。
所以,要啟動的 .NET 程式,要將其路徑作為引數傳遞給 dotnet。
start /***/***.dll
終端顯示:
(gdb) start /***/***.dll
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Temporary breakpoint 1 (main) pending.
這樣有點麻煩,我們可以在啟動時就定義好引數:
gdb --args dotnet /***/***.dll
另外,run 是立即執行,start 會出現詢問資訊,還可以進行斷點除錯。
待程式執行崩潰之後。
然後使用 bt
命令檢視異常的堆疊資訊。
生成結果如下:
.so 檔案剝除錯資訊
在 linux中, strip 命令具體就是從特定檔案中剝掉一些符號資訊和除錯資訊,可以使用以下步驟的命令,將除錯資訊從 .so 檔案中剝出來。
objcopy --only-keep-debug lib***.so lib***.so.debug
strip lib***.so -o lib***.so.release
objcopy --add-gnu-debuglink=lib***.so.debug lib***.so.release
cp lib***.so.release lib***.so
檢查 .so 是否有符號資訊
要除錯 .NET Core 程式,需要 .pdb 符號檔案;要除錯 .so 檔案,當然也要攜帶一下符號資訊才能除錯。
可以通過以下方式判斷一個 .so 檔案是否能夠除錯。
gdb xxx.so
如果不能讀取到除錯資訊,則是:
Reading symbols from xxx.so...(no debugging symbols found)...done.
如果能夠讀取到除錯資訊,則是:
Reading symbols from xxx.so...done.
同時還可以使用 file xxx.so 命令,
xxx.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=8007fbdc7941545fe4e0c61fa8472df1475887c3c1, stripped
如果最後是 stripped,則說明該檔案的符號表資訊和除錯資訊已被去除或不攜帶,不能使用 gdb 除錯。
啟動除錯,目的是啟動 .NET Core runtime 啟動 .NET 程式,Linux 和 GDB 是無法直接啟動 .NET 程式的。
這時就需要使用到 CLI 命令,使用 dotnet
命令啟動一個 .NET 程式。
gdb --args dotnet /***/***.dll
或者
gdb dotnet
...
# 進入GDB 後
set args /***/***.dll
檢視呼叫棧資訊
以下兩個 gdb 命令都可以檢視當前呼叫堆疊資訊,如果程式在呼叫某個函式時崩潰退出,則執行這些命令,會看到程式終止時的函式呼叫堆疊。
bt
bt full
backtrace
backtrace full
bt 是 backtrace 的縮寫,兩者完全一致。
檢視當前程式碼執行位置,如果程式已經終止,則輸出程式終止前最後執行的函式堆疊。
where
使用 bt
可以看到函式的呼叫關係,哪個函式呼叫哪個函式,在哪個函式裡面出現了異常。
#0 0x00007fb2cd5f66dc in ?? () from /lib/lib***.so
#1 0x00007fb2ccf29d46 in ***_receiveThread () from /lib/lib***BBB.so.1
#2 0x00007fb456ef1fa3 in start_thread (arg=<optimized out>) at pthread_create.c:486
#3 0x00007fb456afc4cf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
bt full
可以看到更加詳細的資訊。
[Thread 0x7fb2b53b7700 (LWP 131) exited]
Thread 31 "dotnet" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fb2affff700 (LWP 133)]
0x00007fb2cd5f66dc in ?? () from /lib/lib***.so
(gdb) bt full
#0 0x00007fb2cd5f66dc in ?? () from /lib/lib***.so
No symbol table info available.
#1 0x00007fb2ccf29d46 in ***_receiveThread () from /lib/lib***BBB.so.1
No symbol table info available.
#2 0x00007fb456ef1fa3 in start_thread (arg=<optimized out>) at pthread_create.c:486
ret = <optimized out>
pd = <optimized out>
now = <optimized out>
unwind_buf = {cancel_jmp_buf = {{jmp_buf = {140405433693952, 264024675094789190, 140405521476830, 140405521476831, 140405433693952, 140407320872320,
-229860650334651322, -233434198962832314}, mask_was_saved = 0}}, priv = {pad = {0x0, 0x0, 0x0, 0x0}, data = {prev = 0x0, cleanup = 0x0,
canceltype = 0}}}
not_first_call = <optimized out>
#3 0x00007fb456afc4cf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
No locals.
可以看到,實際問題發生在另一個 .so 庫上,所以我們還需要對這個 .so 製作除錯資訊。
lib***BBB.so.1
之前定位到,問題也許跟 in ?? () from /lib/lib***.so
有關,但是這裡的資訊為 ??
,能不能找到更多的資訊呢?
我們先刪除 /tmp 目錄中的檔案內容。
然後使用 strace dotnet /xxx/dll
或者 dotnet xxx.dll
重新執行一次,等待 /tmp 目錄生成 core dump 轉儲檔案。
發現還是結果還是一樣~~~沒辦法了,算了~
檢視所有執行緒的呼叫堆疊資訊
gdb 的下 thread 命令可以檢視所有執行緒呼叫堆疊的資訊。
thread apply all bt
這裡大家留意一下,pthread
,出現問題終止程式之前,都出現了 pthread
這個關鍵字。
然後查詢了一下資料:https://man7.org/linux/man-pages/man7/pthreads.7.html
查詢資料得知,linux 的 pthread 都是 kernel thread(一般情況下):https://www.zhihu.com/question/35128513
先停一下,我們來猜想一下,會不會是多執行緒導致的問題?我們把相關的記錄拿出來看一下:
#1 0x00007fb2ccf29d46 in MQTTAsync_receiveThread () from /lib/lib***BBB.so.1
#2 0x00007fb456ef1fa3 in start_thread (arg=<optimized out>) at pthread_create.c:486
Thread 1 (Thread 0x7fa6a0228740 (LWP 991)):
#0 futex_wait_cancelable (private=0, expected=0, futex_word=0x171dae0) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
#1 __pthread_cond_wait_common (abstime=0x0, mutex=0x171da90, cond=0x171dab8) at pthread_cond_wait.c:502
#2 __pthread_cond_wait (cond=0x171dab8, mutex=0x171da90) at pthread_cond_wait.c:655
#3 0x00007fa69fa619d5 in CorUnix::CPalSynchronizationManager::ThreadNativeWait(CorUnix::_ThreadNativeWaitData*, unsigned int, CorUnix::ThreadWakeupReason*, unsigned int*) () from /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.1/libcoreclr.so
#4 0x00007fa69fa615e4 in CorUnix::CPalSynchronizationManager::BlockThread(CorUnix::CPalThread*, unsigned int, bool, bool, CorUnix::ThreadWakeupReason*, unsigned int*) () from /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.1/libcoreclr.so
#5 0x00007fa69fa65bff in CorUnix::InternalWaitForMultipleObjectsEx(CorUnix::CPalThread*, unsigned int, void* const*, int, unsigned int, int, int) ()
會不會是由於 CoreCLR 和 .so 庫相關的 pthread 導致的?不過我不是 C 語言的專家,對 Linux 的 C 不瞭解,這時候需要大量惡補知識才行。
大膽猜一下,會不會是類似 https://stackoverflow.com/questions/19711861/segmentation-fault-when-using-threads-c 這樣的錯誤?
還有這樣的:https://stackoverflow.com/questions/8018272/pthread-segmentation-fault
會不會跟機器硬體有關?
為啥會這樣?
能不能找到更多的資訊?
我不熟悉 C 語言呀?怎麼辦?
解決了問題
難道使用 GDB 操作比較騷,就可以解決問題了?No。
眼看解決問題無果,進群問了 Jexus 的作者-宇內流雲大佬,我將詳細的報錯資訊給大佬看了,大佬給建議試試使用 InPtr。
於是我使用不安全程式碼,將函式引數
ST_MODULE_CBS* module_cbs, ST_DEVICE_CBS* device_cbs
改成
IntPtr module_cbs, IntPtr device_cbs
剩下就是將結構體轉為 IntPtr 的問題了,IntPtr 文件親參考 https://docs.microsoft.com/zh-cn/dotnet/api/system.intptr?view=netcore-3.1
然後使用結構體轉換函式:
private static IntPtr StructToPtr(object obj)
{
var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(obj));
Marshal.StructureToPtr(obj, ptr, false);
return ptr;
}
改成不安全程式碼呼叫 C 的函式:
unsafe
{
IntPtr a = StructToPtr(cbs);
IntPtr b = StructToPtr(device_cbs);
EdgeSDK.edge_set_callbacks(a, b);
}
重新放上去測試,終於,正常了!!!
實踐證明,要使用 C# 呼叫 C 語言的程式碼,或者回撥,要多掌握 C# 中的不安全程式碼和 ref 等寫法~~~
事實證明,當出現無法解決的問題時,不如緊緊抱住大佬的大腿比較好~~~
推一波 Jexus:
Jexus 是強勁、堅固、免費、易用的國產 WEB 伺服器系統,可替代 Nginx 。Jexus 支援 Arm32/64、X86/X64、MIPS、龍芯等型別的 CPU,是一款Linux平臺上的高效能WEB伺服器和負載均衡閘道器伺服器,以支援ASP.NET、ASP.NET CORE、PHP為特色,同時具備反向代理、入侵檢測等重要功能。
可以這樣說,Jexus是.NET、.NET CORE跨平臺的最優秀的宿主伺服器,如果我們認為它是Linux平臺的IIS,這並不為過,因為,Jexus不但非常快,而且擁有IIS和其它Web伺服器所不具備的高度的安全性。同時,Jexus Web Server 是完全由中國人自主開發的的國產軟體,真正做到了“安全、可靠、可控”, 具備我國黨政機關和重要企事業單位資訊化建設所需要的關鍵品質。