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 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 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 命令都可以查看当前调用堆栈信息,如果程序在调用某个函数时崩溃退出,则执行这些命令,会看到程序终止时的函数调用堆栈。

 btbt  fullbacktracebacktrace 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: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不但非常快,而且拥有IIS和其它Web服务器所不具备的高度的安全性。同时,Jexus Web Server 是完全由中国人自主开发的的国产软件,真正做到了“安全、可靠、可控”, 具备我国党政机关和重要企事业单位信息化建设所需要的关键品质。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307380.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode236. 二叉树的最近公共祖先(java递归)

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode(int x) { val x; }* }*/ class Solution {/**思路:1.后序遍历 从下往上遍历 找到目标结点后然后做逻辑判断处理1…

WPF MVVM 弹框之等待框

WPF MVVM 弹框之等待框目录一、效果二、弹框主体改造三、等待动画用户控件四、弹窗 ViewModel 和帮助类的改造五、使用方法和代码地址独立观察员 2020年10月13日之前写过一篇《WPF MVVM 模式下的弹窗》&#xff0c;里面实现了确认框和消息框&#xff0c;经过一段时间的演化&…

设计一个具有等待队列的连接池

说到连接池相关很多人都使用过&#xff0c;常见的有数据连接池&#xff0c;HttpClient连接池等。连接池的作用是保持一定量的连接让交互过程复用这些连接&#xff0c;从而大大节省连接创建过程或过多的损耗。在连接池策略中往往当池没有连接的情况都会抛出异常告诉使用者资源无…

leetcode701. 二叉搜索树中的插入操作

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

你不该错过的2020中国开源年报,填开源开发者问卷,成为国内开源的见证者

点击上方“开源社”关注我们| 作者&#xff1a;王伟| 编辑&#xff1a;黄欣宜| 设计&#xff1a;冯艺怡| 责编&#xff1a;王玥敏卷首语一年一度的中国开源年报再度启动&#xff5e;中国开源年报由开源社发起。旨在从多种维度&#xff0c;多种方式&#xff0c;多种协作来呈现国…

leetcode450. 删除二叉搜索树中的节点(java详解版)

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

.NET Core使用FluentEmail发送邮件

前言在实际的项目开发中&#xff0c;我们会遇到许多需要通过程序发送邮件的场景&#xff0c;比如异常报警、消息、进度通知等等。一般情况下我们使用原生的SmtpClient类库居多&#xff0c;它能满足我们绝大多数场景。但是使用起来不够简洁&#xff0c;许多场景需要我们自行封装…

进击吧! Blazor !第四期 组件开发

Blazor 是一个 Web UI 框架&#xff0c;可通过 WebAssembly 在任意浏览器中运行 .Net 。Blazor 旨在简化快速的单页面 .Net 浏览器应用的构建过程&#xff0c;它虽然使用了诸如 CSS 和 HTML 之类的 Web 技术&#xff0c;但它使用 C&#xff03;语言和 Razor 语法代替 JavaScrip…

HttpReports 2.0 发布了 !!!

https://www.cnblogs.com/myshowtime/p/13806631.html来源???? 前言介绍HttpReports 是基于.Net Core 开发的APM监控系统&#xff0c;使用MIT开源协议&#xff0c;主要功能包括&#xff0c;统计, 分析, 可视化&#xff0c; 监控&#xff0c;追踪等&#xff0c;适合在微服务…

.NET Standard 来日苦短去日长

作者&#xff1a;Richard翻译&#xff1a;精致码农-王亮原文&#xff1a;http://dwz.win/Q4h自从 .NET 5 宣贯以来&#xff0c;很多人都在问这对 .NET Standard 意味着什么&#xff0c;它是否仍然重要。在这篇文章中&#xff0c;我将解释 .NET 5 是如何改进代码共用并取代 .NET…

leetcode538. 把二叉搜索树转换为累加树

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

【BCVP升级】泛型主键的使用

&#xff08;图片来源于SqlSugar官网&#xff0c;5年5.0&#xff09;大家假期已经结束了吧&#xff0c;还有80天左右就要到2021年了&#xff0c;你准备好了么&#xff1f;BCVP&#xff08;Blog.Core&Vue Project&#xff09;项目已经开源2年多&#xff0c;从来没有停更过&a…

leetcode404. 左叶子之和(迭代和递归)

一:题目 二:上码 迭代 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right…

Dotnet Core使用特定的SDKRuntime版本

Dotnet Core的SDK版本总在升级&#xff0c;怎么使用一个特定的版本呢&#xff1f;假期过完了&#xff0c;心情还在。今天写个短的。一、前言写这个是因为昨天刷微软官方文档&#xff0c;发现global.json在 SDK 3.0 后&#xff0c;更新了一些内容。文档提到了这个更新&#xff0…

Spring中IOC的理解(通俗易懂版)

文章目录1.IOC提出背景2:IOC的核心概念3:IOC的实现方式4:IOC的入门案例(1):思路分析(2):代码解析5:DI入门案例(1):思路分析(2):代码解析6:DI依赖注入的方式(1):前言(2):Set方式注入(3):构造器注入(4):依赖的自动装配7:注解开发模式的依赖注入(1):前言介绍(2):注解模式的依赖注入…

首个使用Blazor 技术实现的社区软件 BlazorCommunity 发布

BlazorCommunity 是首个使用Blazor 实现的开源社区软件&#xff0c; 其组件基于Element-Blazor &#xff0c; Element-Blazor 是一个 API 模仿 Element&#xff0c;CSS 直接使用 Element 样式&#xff0c;HTML 结构直接使用 Element HTML 结构 的 Web开发库。由于基于了…

leetcode112. 路径总和

一:题目 二:上码 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

全球顶级开源大神们现身 COSCon'20

点击上方“开源社”关注我们| 作者&#xff1a;Will Wang| 编辑&#xff1a;沈于蓝| 责编&#xff1a;王皓月业界最具影响力的开源年度盛会2020中国开源年会 (COSCon20) 将于 10月24-25日由开源社举办。COSCon 以其独特定位及日益增加的影响力&#xff0c;吸引越来越多的顶级企…

做.NET开发多年,公司要我转Java...

10月13日&#xff0c;.NET5发布了(Release Candidate)RC2版本&#xff0c;包含语言新版本C#9和F#5等&#xff0c;这是正式版前的最后更新&#xff01;终于&#xff0c;万众期待的.NET5真的要来了&#xff01;公司让我转Java&#xff0c;我成功说服老板&#xff01;机会永远留给…

ASP.NET Core Blazor WebAssembly 之 .NET JavaScript互调

Blazor WebAssembly可以在浏览器上跑C#代码&#xff0c;但是很多时候显然还是需要跟JavaScript打交道。比如操作dom&#xff0c;当然跟angular、vue一样不提倡直接操作dom&#xff1b;比如浏览器的后退导航。反之JavaScript也有可能需要调用C#代码来实现一些功能&#xff0c;毕…