GDB 调试 .NET 程序实录 - .NET 调用 .so 出现问题怎么解决
注:本文重要信息使用 *** 屏蔽關鍵字。
最近國慶前,項目碰到一個很麻煩的問題,這個問題讓我們加班到凌晨三點。
大概背景:
客戶給了一些 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 binutilsbinutils 包含了 objcopy 和 strip。
調試、轉儲 core 文件
在使用 GDB 調試之前,我們了解一下 core dump 轉儲文件。
core dump 是包含進程的地址空間(存儲)時的過程意外終止的文件。詳細了解請點擊:https://wiki.archlinux.org/index.php/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.sostrace 的使用
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 命令都可以查看當前調用堆棧信息,如果程序在調用某個函數時崩潰退出,則執行這些命令,會看到程序終止時的函數調用堆棧。
btbt fullbacktracebacktrace fullbt 是 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:95bt 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:486ret = <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不但非???#xff0c;而且擁有IIS和其它Web服務器所不具備的高度的安全性。同時,Jexus Web Server 是完全由中國人自主開發的的國產軟件,真正做到了“安全、可靠、可控”, 具備我國黨政機關和重要企事業單位信息化建設所需要的關鍵品質。
總結
以上是生活随笔為你收集整理的GDB 调试 .NET 程序实录 - .NET 调用 .so 出现问题怎么解决的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 中 System.Range 结构
- 下一篇: asp.net ajax控件工具集 Au