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

痴者工良發表於2020-10-12

注:本文重要資訊使用 *** 遮蔽關鍵字。

最近國慶前,專案碰到一個很麻煩的問題,這個問題讓我們加班到凌晨三點。

大概背景:

客戶給了一些 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

kern日誌

# 系統日誌
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 轉儲檔案。

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檔案

使用命令進入 core dump 檔案。

gdb -c corefile-dotnet-376-1602236839

執行 bt 命令。

11

發現有資訊,但是可用資訊太少了,而且名稱都是 ??,這樣完全定位不到問題的位置。怎麼辦?

可以將 .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 命令檢視異常的堆疊資訊。

生成結果如下:

12

.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

33

這裡大家留意一下,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

pthread

會不會跟機器硬體有關?

為啥會這樣?

能不能找到更多的資訊?

我不熟悉 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 是完全由中國人自主開發的的國產軟體,真正做到了“安全、可靠、可控”, 具備我國黨政機關和重要企事業單位資訊化建設所需要的關鍵品質。

相關文章