持续了几年的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飞掉现场。

我坐到小伙伴的电脑前,仔细观察对象指针,看起来对象指针是完好的,可以顺利显示对象的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,三天时间,上调试器,看活代码,深度游历软件世界,感兴趣的朋友欢迎联系我们。
LINUX平台高级调试和优化
2024.4.19-21日 庐山 山南
2023.5.17-19日 上海 徐家汇
Lisa: 13801874134
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

也欢迎关注格友公众号

1896

被折叠的 条评论
为什么被折叠?



