1. 程式人生 > >GDB 除錯 .NET 程式實錄 - .NET 呼叫 .so 出現問題怎麼解決

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 容器的啟動指令碼,進入容器後,手動執行命令啟動程式。 最後發現: ```shell 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/ 由於這個問題是核心級別的,所以可以從系統日誌中找到詳細的日誌資訊。 ### 檢視 核心日誌 容器和物理機都可以檢視日誌,但是容器裡面的資訊太少,主要從物理機找到資訊的日誌。 在物理機: ```shell # 核心日誌 cat /var/log/kern.log ``` ![kern日誌](https://img2020.cnblogs.com/blog/1315495/202010/1315495-20201012181913785-1569076048.png) ```powershell # 系統日誌 cat /var/log/syslog ``` 剛開始時,大佬提示可能是記憶體已被回收,函式等沒有使用靜態來避免 gc 回收,可能在 C 回撥之前,C# 中的那部分記憶體就以及回收了。 但是我修改程式碼,都改成靜態,並且列印地址,還禁止 GC 回收,結果還是一樣的。 檢視引用型別在記憶體中的地址 : ```csharp public string getMemory(object o) { GCHandle h = GCHandle.Alloc(o, GCHandleType.WeakTrackResurrection); IntPtr addr = GCHandle.ToIntPtr(h); return "0x" + addr.ToString("X"); } ``` 禁止 GC 回收: ```csharp 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安裝: ```csharp apt install binutils ``` binutils 包含了 objcopy 和 strip。 ### 除錯、轉儲 core 檔案 在使用 GDB 除錯之前,我們瞭解一下 core dump 轉儲檔案。 core dump 是包含程序的地址空間(儲存)時的過程意外終止的檔案。詳細瞭解請點選:https://wiki.archlinux.org/index.php/Core_dump 相當於 .NET Core 的 dotnet-dump 工具生成的 快照檔案。 為了生成轉儲檔案,需要作業系統開啟功能。 **在物理機上執行**: ```shell ulimit -c unlimied ``` **在 docker 裡面執行**: ```shell ulimit -c unlimied ``` 自定義將轉儲檔案存放到目錄 ```shell 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檔案](https://img2020.cnblogs.com/blog/1315495/202010/1315495-20201012181938287-1526406328.png) 使用命令進入 `core dump` 檔案。 ```shell gdb -c corefile-dotnet-376-1602236839 ``` 執行 `bt` 命令。 ![11](https://img2020.cnblogs.com/blog/1315495/202010/1315495-20201012182004066-838005490.png) 發現有資訊,但是可用資訊太少了,而且名稱都是 `??`,這樣完全定位不到問題的位置。怎麼辦? 可以將 .so 檔案一起包進來檢查。 ```shell gdb -c corefile-dotnet-376-1602236839 /***/lib***.so ``` 也可以使用多個 .so 一起加入 ```shell gdb -c corefile-dotnet-376-1602236839 /***/libAAA.so /***/libBBB.so ``` ### strace 的使用 Linux中的 strace 命令可以跟蹤系統呼叫和訊號。 如果系統沒有這個命令,可以使用 `apt install strace` 或者 `yum install strace` 直接安裝。 然後使用 strace 命令啟動 .NET 程式。 ```shell strace dotnet /***/***.dll ``` 啟動後就可以看到程式的堆疊資訊,還可以看到函式呼叫時的函式定義。 ### GDB 除錯啟動 .NET 程式 執行以下命令即可啟動 .NET Core runtime: ```shell gdb dotnet ``` 在 gdb 中 執行 `start` 啟動程式。但是因為僅啟動 ` .NET Core runtime` 是沒用的,還要啟動 .NET 程式。 所以,要啟動的 .NET 程式,要將其路徑作為引數傳遞給 dotnet。 ```shell 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. ``` 這樣有點麻煩,我們可以在啟動時就定義好引數: ```shell gdb --args dotnet /***/***.dll ``` 另外,run 是立即執行,start 會出現詢問資訊,還可以進行斷點除錯。 待程式執行崩潰之後。 然後使用 `bt` 命令檢視異常的堆疊資訊。 生成結果如下: ![12](https://img2020.cnblogs.com/blog/1315495/202010/1315495-20201012182031100-1809600454.png) ### .so 檔案剝除錯資訊 在 linux中, strip 命令具體就是從特定檔案中剝掉一些符號資訊和除錯資訊,可以使用以下步驟的命令,將除錯資訊從 .so 檔案中剝出來。 ```shell objcopy --only-keep-debug lib***.so lib***.so.debug ``` ```shell strip lib***.so -o lib***.so.release ``` ```shell objcopy --add-gnu-debuglink=lib***.so.debug lib***.so.release ``` ```shell cp lib***.so.release lib***.so ``` ### 檢查 .so 是否有符號資訊 要除錯 .NET Core 程式,需要 .pdb 符號檔案;要除錯 .so 檔案,當然也要攜帶一下符號資訊才能除錯。 可以通過以下方式判斷一個 .so 檔案是否能夠除錯。 ```shell 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 ``` 或者 ```shell gdb dotnet ... # 進入GDB 後 set args /***/***.dll ``` ### 檢視呼叫棧資訊 以下兩個 **gdb** 命令都可以檢視當前呼叫堆疊資訊,如果程式在呼叫某個函式時崩潰退出,則執行這些命令,會看到程式終止時的函式呼叫堆疊。 ```shell bt bt full backtrace backtrace full ``` bt 是 backtrace 的縮寫,兩者完全一致。 檢視當前程式碼執行位置,如果程式已經終止,則輸出程式終止前最後執行的函式堆疊。 ```shell where ``` 使用 `bt` 可以看到函式的呼叫關係,哪個函式呼叫哪個函式,在哪個函式裡面出現了異常。 ```shell #0 0x00007fb2cd5f66dc in ?? () from /lib/lib***.so #1 0x00007fb2ccf29d46 in ***_receiveThread () from /lib/lib***BBB.so.1 #2 0x00007fb456ef1fa3 in start_thread (arg=) at pthread_create.c:486 #3 0x00007fb456afc4cf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95 ``` `bt full` 可以看到更加詳細的資訊。 ```shell [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=) at pthread_create.c:486 ret = pd = now = 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 = #3 0x00007fb456afc4cf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95 No locals. ``` 可以看到,實際問題發生在另一個 .so 庫上,所以我們還需要對這個 .so 製作除錯資訊。 ```shell 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 ``` ![33](https://img2020.cnblogs.com/blog/1315495/202010/1315495-20201012182054330-1567157873.png) 這裡大家留意一下,`pthread` ,出現問題終止程式之前,都出現了 `pthread` 這個關鍵字。 然後查詢了一下資料:https://man7.org/linux/man-pages/man7/pthreads.7.html 查詢資料得知,linux 的 pthread 都是 kernel thread(一般情況下):https://www.zhihu.com/question/35128513 先停一下,我們來猜想一下,會不會是多執行緒導致的問題?我們把相關的記錄拿出來看一下: ```csharp #1 0x00007fb2ccf29d46 in MQTTAsync_receiveThread () from /lib/lib***BBB.so.1 #2 0x00007fb456ef1fa3 in start_thread (arg=) at pthread_create.c:486 ``` ```csharp 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 ![pthread](https://img2020.cnblogs.com/blog/1315495/202010/1315495-20201012182358198-1629501078.png) 會不會跟機器硬體有關? 為啥會這樣? 能不能找到更多的資訊? 我不熟悉 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 然後使用結構體轉換函式: ```csharp private static IntPtr StructToPtr(object obj) { var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(obj)); Marshal.StructureToPtr(obj, ptr, false); return ptr; } ``` 改成不安全程式碼呼叫 C 的函式: ```csharp 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 是完全由中國人自主開發的的國產軟體,真正做到了“安全、可靠、可控”, 具備我國黨政機關和重要企事業單位資訊化建設所需要的關鍵品質。