新来的小伙伴Denny在使用JS脚本测试NDB时遇到拦路虎,node进程打印出下面这个信息后就崩溃闪退了。
geduer@ulan:/gewu/nanocode/nd3/ndi$ node ndunix.js
register 'jtag'
register 'swim'
register 'dapdirect_jtag'
register 'dapdirect_swd'
register 'swd'
0
NdAgent {}
1
11111111111111111
Trace/breakpoint trap (core dumped)
崩溃现场的最后一条信息最重要:Trace/breakpoint trap (core dumped)
直接翻译便是:追踪或断点陷阱 (内存已转储)
我看了这个信息后,也觉得有些奇怪。哪里来的“追踪或断点陷阱”呢?也没有挂调试器啊!
上调试器
为了搞清楚原因,我决定上调试器,在本来的命令行前面加上gdb --args,让整个过程在调试器下再来一遍。
geduer@ulan:/gewu/nanocode/nd3/ndi$ gdb --args node ndunix.js
执行r命令不久,果然又中断下来。
Thread 1 "node" received signal SIGTRAP, Trace/breakpoint trap.
0x0000007fe31c1808 in NdBoss::UiUpdateEngine (this=0x55557884b0) at ../ndi/NdBoss.cpp:1639
1639 return this->m_pUiClient->exit_dispatch((IDebugClient*)this->m_pDbgClient);
执行bt观察执行经过,崩溃发生在主线程,是从脚本调用过来的。
(gdb) bt
#0 0x0000007fe31c1808 in NdBoss::UiUpdateEngine (this=0x55557884b0) at ../ndi/NdBoss.cpp:1639
#1 0x0000007fe31d1dd8 in NdFront::StartEngineForCommand (this=0x55558779f0) at ../ndi/NdFront.cpp:185
#2 0x0000007fe31d2008 in NdFront::Execute (this=0x55558779f0, lpszCmd=0x7fffffdf90 "r", ulFlags=0) at ../ndi/NdFront.cpp:279
#3 0x0000007fe324b1bc in NdAgent::Execute(Napi::CallbackInfo const&) () from /usr/share/nanocode/ndjs.node
#4 0x0000007fe3253280 in Napi::ObjectWrap<NdAgent>::InstanceMethodCallbackWrapper(napi_env__*, napi_callback_info__*) ()
from /usr/share/nanocode/ndjs.node
#5 0x0000007ff5eeb694 in ?? () from /lib/aarch64-linux-gnu/libnode.so.108
#6 0x0000007ff62f512c in v8::internal::FunctionCallbackArguments::Call(v8::internal::CallHandlerInfo) ()
from /lib/aarch64-linux-gnu/libnode.so.108
#7 0x0000007ff62f5a70 in ?? () from /lib/aarch64-linux-gnu/libnode.so.108
#8 0x0000007ff62f5e98 in v8::internal::Builtin_HandleApiCall(int, unsigned long*, v8::internal::Isolate*) ()
from /lib/aarch64-linux-gnu/libnode.so.108
#9 0x0000007ff61aa0ac in ?? () from /lib/aarch64-linux-gnu/libnode.so.108
#10 0x0000007fe3360411 in ?? ()
执行c尝试恢复执行,结果会再次中断下来。
(gdb) c
Continuing.
Thread 1 "node" received signal SIGTRAP, Trace/breakpoint trap.
0x0000007fe31c1808 in NdBoss::UiUpdateEngine (this=0x55557884b0) at ../ndi/NdBoss.cpp:1639
1639 return this->m_pUiClient->exit_dispatch((IDebugClient*)this->m_pDbgClient);
(gdb) c
Continuing.
Thread 1 "node" received signal SIGTRAP, Trace/breakpoint trap.
0x0000007fe31c1808 in NdBoss::UiUpdateEngine (this=0x55557884b0) at ../ndi/NdBoss.cpp:1639
1639 return this->m_pUiClient->exit_dispatch((IDebugClient*)this->m_pDbgClient);
(gdb) c
Continuing.
Thread 1 "node" received signal SIGTRAP, Trace/breakpoint trap.
0x0000007fe31c1808 in NdBoss::UiUpdateEngine (this=0x55557884b0) at ../ndi/NdBoss.cpp:1639
1639 return this->m_pUiClient->exit_dispatch((IDebugClient*)this->m_pDbgClient);
执行disassemble观察汇编指令。
(gdb) disassemble
Dump of assembler code for function _ZN6NdBoss14UiUpdateEngineEv:
0x0000007fe31c17b8 <+0>: stp x29, x30, [sp, #-32]!
0x0000007fe31c17bc <+4>: mov x29, sp
0x0000007fe31c17c0 <+8>: str x0, [sp, #24]
0x0000007fe31c17c4 <+12>: ldr x0, [sp, #24]
0x0000007fe31c17c8 <+16>: ldr x0, [x0, #256]
0x0000007fe31c17cc <+20>: cmp x0, #0x0
0x0000007fe31c17d0 <+24>: b.eq 0x7fe31c1808 <_ZN6NdBoss14UiUpdateEngineEv+80> // b.none
0x0000007fe31c17d4 <+28>: ldr x0, [sp, #24]
0x0000007fe31c17d8 <+32>: ldr x0, [x0, #256]
0x0000007fe31c17dc <+36>: ldr x0, [x0]
0x0000007fe31c17e0 <+40>: add x0, x0, #0xe8
0x0000007fe31c17e4 <+44>: ldr x2, [x0]
0x0000007fe31c17e8 <+48>: ldr x0, [sp, #24]
0x0000007fe31c17ec <+52>: ldr x3, [x0, #256]
0x0000007fe31c17f0 <+56>: ldr x0, [sp, #24]
0x0000007fe31c17f4 <+60>: ldr x0, [x0, #264]
0x0000007fe31c17f8 <+64>: mov x1, x0
0x0000007fe31c17fc <+68>: mov x0, x3
0x0000007fe31c1800 <+72>: blr x2
0x0000007fe31c1804 <+76>: b 0x7fe31c180c <_ZN6NdBoss14UiUpdateEngineEv+84>
=> 0x0000007fe31c1808 <+80>: brk #0x3e8
0x0000007fe31c180c <+84>: ldp x29, x30, [sp], #32
0x0000007fe31c1810 <+88>: ret
End of assembler dump.
程序指针卡在下面这一句:
=> 0x0000007fe31c1808 <+80>: brk #0x3e8
原来是很熟悉的断点指令。特别扎眼的是指令跟着的立即数:0x3e8,对于我来说,一眼就看出来它是1000。
brk 1000
这是多么有个性的一条指令啊!1000这样的值绝对不是偶然,一定是有人故意如此设计啊!
如果是故意,那么是谁放了这么一条具有炸弹性质的指令呢?
考虑到没有调试器时也会以相同的信号崩溃,那么这条指令显然不是调试器插入的。那么是谁干的呢?
如果没有人动态篡改指令,那么这条指令就是编译器产生的。
使用l命令列出对应的源代码:
HRESULT NdBoss::UiUpdateEngine()
{
if (m_pUiClient != NULL)
return this->m_pUiClient->exit_dispatch((IDebugClient*)this->m_pDbgClient);
}
眼睛看着这几行代码,我的大脑在努力思考这几行代码的特别之处,是哪个特征触发编译器插入了一条炸弹指令呢?
找到原因
几秒钟之后,我的大脑灵机一动,想出了原因。增加两行代码,再次编译,问题果然不见了。
HRESULT NdBoss::UiUpdateEngine()
{
if (m_pUiClient != NULL)
return this->m_pUiClient->exit_dispatch((IDebugClient*)this->m_pDbgClient);
else
return E_FAIL;
}
看来问题的根源是本来代码不够严谨,函数声明了返回值,但是只有if分支有明确的返回值,缺少else分支,不全了就好了。
举一反三
为了进一步研究这个问题,我写了一小段代码,取名为gebrk.c。
geduer@ulan:~/gelabs/gebrk$ cat gebrk.c
#include <stdio.h>
int fn(int p)
{
if(p<0)
return p*p;
}
int main(int argc, const char*argv[])
{
int a = fn(argc);
printf("result is %d\n", a);
return 0;
}
使用gcc编译上面的代码,没有任何警告,运行时也没有崩溃,打印出的结果是1。
geduer@ulan:~/gelabs/gebrk$ gcc gebrk.c
geduer@ulan:~/gelabs/gebrk$ ./a.out
result is 1
使用g++编译同样代码,有如下警告:
geduer@ulan:~/gelabs/gebrk$ g++ gebrk.c
gebrk.c: In function 'int fn(int)':
gebrk.c:7:1: warning: control reaches end of non-void function [-Wreturn-type]
7 | }
| ^
G++的警告信息让人困惑。
如果使用微软的vc编译器编译,给出的警告信息更好理解。
<source>(7) : warning C4715: 'fn': not all control paths return a value
Compiler returned: 0
运行g++编译出的可执行文件,可以重现Denny最初遇到的问题。
geduer@ulan:~/gelabs/gebrk$ ./a.out
Trace/breakpoint trap (core dumped)
使用gdb调试,也可以复现前面介绍的症状。
(gdb) disassemble fn
Dump of assembler code for function _Z2fni:
0x0000005555550754 <+0>: sub sp, sp, #0x10
0x0000005555550758 <+4>: str w0, [sp, #12]
0x000000555555075c <+8>: ldr w0, [sp, #12]
0x0000005555550760 <+12>: cmp w0, #0x0
0x0000005555550764 <+16>: b.ge 0x5555550774 <_Z2fni+32> // b.tcont
0x0000005555550768 <+20>: ldr w0, [sp, #12]
0x000000555555076c <+24>: mul w0, w0, w0
0x0000005555550770 <+28>: b 0x5555550778 <_Z2fni+36>
0x0000005555550774 <+32>: brk #0x3e8
0x0000005555550778 <+36>: add sp, sp, #0x10
0x000000555555077c <+40>: ret
End of assembler dump.
特别值得一提的是,如果打开优化选项,那么不会再有崩溃。
geduer@ulan:~/gelabs/gebrk$ g++ -g -O1 gebrk.c
gebrk.c: In function 'int fn(int)':
gebrk.c:7:1: warning: control reaches end of non-void function [-Wreturn-type]
7 | }
| ^
geduer@ulan:~/gelabs/gebrk$ ./a.out
result is 1
归纳一下,导致崩溃的BRK炸弹是G++编译器故意加入的。加入的目的是帮助程序员发现代码中的设计缺欠,是一种辅助纠错的措施。因为此,打开优化选项后,这个“炸弹机制”会消失。
软件世界,精彩无限。LINUX研习班上海站正在招生中,三天时间,面对面交流,每人一套GDK8套件+挥码枪硬件调试器,上调试器,看活代码,既学操作系统原理,又学调试器和20多种重要工具,一举多得。
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物
也欢迎关注格友公众号