CPU跑飞为哪般?

持续了几年的NDB移植工程(从Windows到Linux)终于在2024早春接近尾声,一个个功能开始工作了。

但在昨天,小伙伴测试内核调试时,又报告一个很严重的问题,执行.reboot命令时,NDB会自动重启调试会话。

上GDB观察,发现有段错误,但这个段错误不一般,当前位置不可读,GDB显示两个问号,执行bt看调用栈,也是无效的地址,GDB连续给出两串问号,然后放弃了。

根据我多年的调试经验,看起来是CPU跑飞了。

Thread
18 "nanocode" received signal SIGSEGV,
Segmentation fault.
0x0000005593736b78
in ?? ()
(gdb)bt
#0  0x0000005593736b78 in ??? ()
#1  0x626d79735c3a632a in ??? ()

今天早上我先忙点其它事情,让小伙伴从执行路径上的一个已知点开始跟踪,逐步缩小范围。

一个小时后,我询问小伙伴,跟踪到哪里了?

答曰:“NdObject”

我听了大为惊诧,NdObject,这是“全宇宙”的基类,很基础很基础的东西,它怎么会出问题呢?“没搞错吧?”

小伙伴一脸诚恳,“就是在它的析构函数里飞掉了。”

听到析构函数四个字,我半信半疑了。走过去看,他继续说,跟踪这个free时飞掉的。

析构函数 + free,这都是软件世界的事故多发地带,我开始重视了。我走到小伙伴的电脑前,看有关的代码。

Thread 18 "nanocode" hit Breakpoint 1.3, NdObject::~NdObject (this=0x1c00725420,
    __in_chrg=<optimized out>) at ../ndi/NdObject.cpp:51
51      NdObject::~NdObject()
(gdb) l
46          m_hWndListenList=NULL;
47          m_pbScratchBuffer = NULL;
48          m_ulScratchBufferLength = 0;
49      }
50
51      NdObject::~NdObject()
52      {
53          if (m_pbScratchBuffer != NULL)
54              free(m_pbScratchBuffer);
55      }

上面代码46-48行是构造函数,52-55行是析构函数。

小伙伴当面演示给我看,单步free,即出现前面的CPU飞掉现场。

60265703a23ae13889ce954c18059f63.png

我坐到小伙伴的电脑前,仔细观察对象指针,看起来对象指针是完好的,可以顺利显示对象的m_dwD4DLevel和m_lpszD4DID属性。

m_dwD4DLevel的值为4,是合理的。

m_lpszD4DID的值为“NDMD”,是NanoDebugger MoDule的意思,也是合理的。

这两个字段中的D4D是Design for Debug的意思,是我喜欢用的可调式设计,在Object这样的基类中设置D4D这样的基础设施,也是我的习惯。

继续观察要释放的指针m_pbscratchBuffer,它的值就不那么正经了。

m_pbScratchBuffer = 0x7f4ddea148 <vtable for Ndobject+16> "td\330M\177"

继续看与这个字段对应的

m_ulScratchBufferLength = 4;

这就更不正经了。ScratchBuffer是我喜欢用的另一个设计模式,用作对象的“涂抹板”,为了复用内存,避免频繁释放和分配。 

为啥4这个值不正经呢?因为给“涂抹板”分内存时,会在需要的长度上多分200字节。4小于200,无效,不可能。

关于“涂抹板”的两个属性无效,我首先想到的是内存被踩了。建议小伙伴以nodejs的方式复现问题,准备上valgrind。

但是在小伙伴行动时,我想到了下一步可能遇到的一些障碍。

一边想,我一边回到自己座位,打开代码,仔细看“涂抹板”有关的各种操作。

看着看着,一道灵光闪过脑海,有新发现了。

NDB不算很大的项目,但是也有很多个模块,其中出问题的NDW是负责符号解析的,w代表DWARF。除了NDW,还有NDI,是调试器基础设施(Infrastructure)。

NDI和NDW的关系有些微妙,从解析符号的角度,NDW在底层。从使用基础设施的角度,NDI在底层。为了避免循环依赖,我们使用了函数注入的方式。

另外,对于NdObject这样很小的代码,则选择了“复制”的方式。NDW和NDI里都有一份。

虽然都有一份,但是NDW里的是简化版本,没有“涂抹板”设施。

class NdObject  
{
public:
  NdObject();
  virtual ~NdObject();
  HRESULT D4D(unsigned long ulLevel, LPCTSTR szFormat, ...); 
  HRESULT Msg(BOOL bPopup, LPCTSTR szFormat, ...); 
protected:
  DWORD m_dwD4DLevel;
  LPCTSTR m_lpszD4DID;
};

正是因为有这个差异特征,当我看到NDW里的NdObject定义后“拍案而起”,找到问题了。

再仔细看一下,下面的调用栈,#6-#1都是NDW的函数,一路执行析构函数,从派生类到基础类,这些都是正常的,但是到了NdObject这个祖先类时,调用的竟然是 ../ndi/NdObject.cpp:51的代码了。

(gdb) bt
#0  NdObject::~NdObject (this=0x1c00725420, __in_chrg=<optimized out>) at ../ndi/NdObject.cpp:51
#1  0x0000007f4d468a14 in NdwELF::~NdwELF (this=0x1c00725420, __in_chrg=<optimized out>) at NdwElf.cpp:286
#2  0x0000007f4d4988b0 in NdwModule::~NdwModule (this=0x1c00725400, __in_chrg=<optimized out>) at NdwModule.cpp:80
#3  0x0000007f4d48faa4 in NdwLKM::~NdwLKM (this=0x1c00725400, __in_chrg=<optimized out>) at NdwLKM.cpp:23
#4  0x0000007f4d48fac4 in NdwLKM::~NdwLKM (this=0x1c00725400, __in_chrg=<optimized out>) at NdwLKM.cpp:23
#5  0x0000007f4d4a5064 in NdwProcess::SymUnloadModule64 (this=0x1c01290600, BaseOfDll=-4785149766008832) at NdwProcess.cpp:529
#6  0x0000007f4d496cf0 in NdwMgr::SymUnloadModule64 (this=0x7f4d509c48 <g_NdwMgr>, hProcess=0xf0f0f0f0, BaseOfDll=-4785149766008832) at NdwMgr.cpp:712
#7  0x0000007f4d47e81c in SymUnloadModule64 (hProcess=0xf0f0f0f0, BaseOfDll=18441958923943542784) at ndw.cpp:274
#8  0x0000007f4d696c18 in tu::DeleteResources (this=this@entry=0x1c00320c00, FullDelete=FullDelete@entry=1) at ../elk/image.cpp:184
#9  0x0000007f4d696ce8 in tu::~tu (this=this@entry=0x1c00320c00, __in_chrg=<optimized out>) at ../elk/image.cpp:165
#10 0x0000007f4d67db78 in hu::tu_rm_all (this=this@entry=0x1c01061180) at ../elk/hu.cpp:559
#11 0x0000007f4d6a3148 in meta::Reload

涉及到动态库的链接规则时,Windows与Linux的做法有非常多的差异。所以当我看到这里时,我觉得找到问题的根源了。

问题的关键是NDI和NDW两个so里都有NdObject类。特别的都有NdObject的析构函数。使用readelf可以进行确认:

geduer@ulan:/gewu/NanoCode/nd3/bin$ readelf  -s -C libndw.so  --wide | grep ~NdObject
   527: 00000000000bdda8    40 FUNC    GLOBAL DEFAULT   11 NdObject::~NdObject()
  2213: 00000000000bddd0    36 FUNC    GLOBAL DEFAULT   11 NdObject::~NdObject()
  2463: 00000000000bdda8    40 FUNC    GLOBAL DEFAULT   11 NdObject::~NdObject()
  2753: 00000000000bdda8    40 FUNC    GLOBAL DEFAULT   11 NdObject::~NdObject()
  3418: 00000000000bddd0    36 FUNC    GLOBAL DEFAULT   11 NdObject::~NdObject()
  6000: 00000000000bdda8    40 FUNC    GLOBAL DEFAULT   11 NdObject::~NdObject()

在Windows下,链接器会优先选择自己模块里的析构函数,所以没有问题。

但是在Linux下,链接器优先选择了其它模块(NDI)里的析构函数,于是出问题了。

复盘一下野指针的来源,在要释放的NdwLKM对象里,根本没有m_pbScratchBuffer 成员,但是一旦进入了错误的析构函数后,它就硬生生地使用它认为的对象布局来套数据了。

根据C++的对象模型,这样套到的多半是类树上的第二代派生类的成员,是个vector。

class NdwELF: public NdObject
{
public:
    typedef std::vector<NdwCU*> m_VecCompilationUnit;
    typedef std::vector<CDebugARange*> m_VecDebugARange;

野指针就是这么套出来的。

概而言之,在Linux下,共享库里的代码重名可能导致各种严重问题。应该避免。想到这里,我立刻下手替换,将NDW里的NdObject全部替换为NdwObject。

替换完成后,让小伙伴重新构建测试,问题解决。

软件世界无限精彩,对于热爱技术的格友,今年春季有两期线下研习班,分别在大美庐山和魔都上海举行,每人一套挥码枪(NDB)和GDK8,三天时间,上调试器,看活代码,深度游历软件世界,感兴趣的朋友欢迎联系我们。

87691e5248bfce74c6e5adcf0fa0acec.png

LINUX平台高级调试和优化

2024.4.19-21日 庐山 山南

2023.5.17-19日 上海 徐家汇

Lisa: 13801874134

(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)

*************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

d1bfd0effe04b7c84bcb37008d1e41f7.png

也欢迎关注格友公众号

5950769482230325dff2dfb61833d06c.jpeg

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值