实战总结|抽丝剥茧,记一次神奇的崩溃
阿里妹导读
不讲“武德”的崩溃
1.1 查看崩溃调用栈
(gdb) bt#0 0x0000000078432d68 in asl::LooperObserverMan::notifyIdle (this=<optimized out>, looper=0x160eebd40, delay_queue_size=0)at ../../../../src/asl_message_framework/src/BaseMessageLooper.cpp:371#1 0x00000000784928e4 in asl::MessageQueue::fetchNext (this=this@entry=0x160eedfc0, timing=@0xf4e9f60: 0)at ../../../../src/asl_message_framework/src/MessageQueue.cpp:83#2 0x0000000078492b24 in asl::MessageQueue::next (this=0x160eedfc0, timing=@0xf4e9f60: 0) at ../../../../src/asl_message_framework/src/MessageQueue.cpp:60#3 0x000000007832036c in asl::Looper::loop (this=0x160eebd40) at ../../../../src/asl_message_framework/src/Looper.cpp:107#4 0x0000000078495ee0 in asl::MessageThread::run (this=0x7998e678) at ../../../../src/asl_message_framework/src/MessageThread.cpp:56#5 0x000000007851cc70 in asl::Thread::runCallback (param=0x7998e678) at ../../../../src/asl_message_framework/src/Thread.cpp:183#6 0x00000000010314e0 in ?? ()
1.2 段错误位置不符合预期
(gdb) p node$8 = (asl::LooperObserverMan::ObserverNode *) 0x17bb988e0(gdb) p node->observer$10 = (asl::IMessageLooper::Observer *) 0x7998e758
结果大大出乎意料,这两个指针居然可以正常访问。
汇编之下,纤毫毕现
2.1 用汇编“放大”源码
(gdb) disasDump of assembler code for function asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int):0x0000000078432d30 <+0>: stp x19, x20, [sp,#-48]!0x0000000078432d34 <+4>: stp x21, x22, [sp,#16]0x0000000078432d38 <+8>: str x30, [sp,#32]0x0000000078432d3c <+12>: ldr x19, [x0]0x0000000078432d40 <+16>: cbz x19, 0x78432d8c <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+92>0x0000000078432d44 <+20>: mov x22, x10x0000000078432d48 <+24>: mov w21, w20x0000000078432d4c <+28>: adrp x20, 0x786b00000x0000000078432d50 <+32>: b 0x78432d60 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+48>0x0000000078432d54 <+36>: nop0x0000000078432d58 <+40>: ldr x19, [x19,#8]0x0000000078432d5c <+44>: cbz x19, 0x78432d8c <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+92>0x0000000078432d60 <+48>: ldr x0, [x19]0x0000000078432d64 <+52>: ldr x1, [x20,#1160]=> 0x0000000078432d68 <+56>: ldr x2, [x0]0x0000000078432d6c <+60>: ldr x3, [x2,#56]0x0000000078432d70 <+64>: cmp x3, x10x0000000078432d74 <+68>: b.eq 0x78432d58 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+40>0x0000000078432d78 <+72>: mov w2, w210x0000000078432d7c <+76>: mov x1, x220x0000000078432d80 <+80>: blr x30x0000000078432d84 <+84>: ldr x19, [x19,#8]0x0000000078432d88 <+88>: cbnz x19, 0x78432d60 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+48>0x0000000078432d8c <+92>: ldp x21, x22, [sp,#16]0x0000000078432d90 <+96>: ldr x30, [sp,#32]0x0000000078432d94 <+100>: ldp x19, x20, [sp],#480x0000000078432d98 <+104>: retEnd of assembler dump.
使用gdb的disas指令查看当前栈顶函数的反汇编,确实将notifyIdle的7行C++代码变成了27行汇编指令,让我们得以看到更多细节。
2.2 发现直接原因
=> 0x0000000078432d68 <+56>: ldr x2, [x0]这个0x0000000078432d68就是当前pc寄存器的值,崩溃就发生在这一条ldr指令。该指令的含义是将x0寄存器中存的值作为内存地址,将内存中该地址存储的值load到x2寄存器中:
(gdb) i register x0x0 0x2e002e 3014702(gdb) x 0x2e002e0x2e002e: Cannot access memory at address 0x2e002e
至此,崩溃的直接原因找到了,机器终于“沉冤得雪”。说明它确实是遇到了无法访问的内存,因此才触发的段错误异常中断。
抽丝剥茧,详细分析
3.1 分析汇编,发现端倪
0x0000000078432d60 <+48>: ldr x0, [x19]0x0000000078432d64 <+52>: ldr x1, [x20,#1160]=> 0x0000000078432d68 <+56>: ldr x2, [x0]
(gdb) i register x19x19 0x17bb988e0 6370724064(gdb) x 0x17bb988e00x17bb988e0: 0x7998e758(gdb) x 0x7998e7580x7998e758: 0x79989f40
可以看到x19中存的是0x17bb988e0,对这个地址取内容得到0x7998e758,正常这个值应该存入x0,但实际上x0中存储的却是非法地址0x2e002e,而0x7998e758是一个合法地址,可以正常取到它内容0x79989f40。
3.2 疑似原因一:踩内存
(gdb) i register x19x19 0x17bb988e0 6370724064(gdb) x 0x17bb988e00x17bb988e0: 0x7998e758(gdb) p node$2 = (asl::LooperObserverMan::ObserverNode *) 0x17bb988e0(gdb) p node->observer$3 = (asl::IMessageLooper::Observer *) 0x7998e758
发现x19中存的是node的地址,对它取内容正是node->observer的地址,符合预期,observer正是node的第一个成员:
struct ObserverNode {IMessageLooper::Observer * observer;ObserverNode * next;};
进一步查看observer内容:
(gdb) p *(node->observer)$4 = {_vptr.Observer = 0x79989f40}
(gdb) x /16a 0x79989f400x79989f40: 0x7990c9e0 0x7990c9f00x79989f50: 0x78411698 <asl::IMessageLooper::Observer::onLooperStart(asl::IMessageLooper*, int, int)> 0x799095980x79989f60: 0x799097d8 0x799099d00x79989f70: 0x784116b8 <asl::IMessageLooper::Observer::onLooperBusy(asl::IMessageLooper*)> 0x79909bd80x79989f80: 0x784116c8 <asl::IMessageLooper::Observer::onLooperQuit(asl::IMessageLooper*)> 0x784116d0 <asl::IMessageLooper::Observer::onLooperDestroy(asl::IMessageLooper*)>0x79989f90: 0x784116d8 <asl::IMessageLooper::Observer::onLooperCancelMsg(asl::IMessageLooper*, asl::Message*, unsigned long, unsigned long)> 0x7990c9880x79989fa0: 0x7990c990 0x7990c9980x79989fb0: 0x7990c9a0 0x7990c9a8
可以看到虚表中各个函数指针,发现node和node->observer指向的内存符合预期。
回头来看崩溃前的三行指令:
0x0000000078432d60 <+48>: ldr x0, [x19]0x0000000078432d64 <+52>: ldr x1, [x20,#1160]=> 0x0000000078432d68 <+56>: ldr x2, [x0]
刚才已经确认最终崩溃时x19指向的内存正常,但是x0内容不正常,如果是踩内存,则需要在ldr x0 [x19]时将x19指向的内存踩坏,在崩溃时将其恢复正常,因此第一种假设不太可能。
3.3 疑似原因二:未初始化变量访问
查看给node->observer赋值的源码:
bool LooperObserverMan::addObserver(IMessageLooper::Observer * observer) {if(observer == NULL)return false;...ObserverNode * new_node = new ObserverNode();new_node->next = NULL;new_node->observer = observer;if(node == NULL)_observers = new_node;elsenode->next = new_node;return true;}
可以看到node得到赋值之前已经提前对它的observer分量进行了赋值(new_node->observer = observer;)。
LooperObserverMan::LooperObserverMan() : _observers(NULL) {}
而3.3.1的源码显示_observers赋值成new_node之前,new_node->observer已经完成赋值。
3.4 初步分析结论
3.5 问题复现,闪电再次劈中?
重新审视之前的分析。发现3.3中我们排除疑点二的一个重要依据为变量_observers初值为NULL,后续赋值顺序为:
new_node->observer = xxxx;_observers = new_node;
拆分出来就是两个依据:
1)指针_observers赋值是原子的,读线程要么读到NULL,要么读到好的_observers;
new_node->observer = xxxx;_observers = new_node;
3.6 show me the code,demo验证
bool LooperObserverMan::addObserver(Observer * observer) {if(observer == NULL)return false;ObserverNode * node = _observers;while(node) {if(node->observer == observer)return false;if(node->next == NULL)break;node = node->next;}ObserverNode * new_node = new ObserverNode();new_node->next = NULL;new_node->observer = observer;if(node == NULL)_observers = new_node;elsenode->next = new_node;return true;}
bool LooperObserverMan::notifyIdle(Observer * observer) {ObserverNode * node = _observers;while(node) {if (observer != node->observer) {std::cout << "error: observer not match!!!" << std::endl;std::cout << "observer: " << observer << ", node->observer: " << node->observer << std::endl;}node->observer->onLooperIdle();node = node->next;return true;}return false;}
LooperObserverMan::LooperObserverMan() : _observers(NULL) {}
#include <iostream>class Observer {public:virtual ~Observer() {}virtual void onLooperIdle() {std::cout << "onLooperIdle()" << std::endl;};};class LooperObserverMan {public:struct ObserverNode {Observer * observer;ObserverNode * next;};LooperObserverMan();~LooperObserverMan();bool addObserver(Observer * observer);bool notifyIdle(Observer * observer);private:ObserverNode * _observers;};
#include <thread>#include "LooperObserverMan.h"int main(){Observer ob;LooperObserverMan* looper = new LooperObserverMan();std::thread t = std::thread([&]() {looper->addObserver(&ob);});while (1) {if (looper->notifyIdle(&ob)) {break;}}t.join();delete looper;return 0;}
num=0;while true; do sleep 1; date; ./test_reorder; num=`expr $num + 1`; echo $num; done
3.6.2 压测结果
pOVar1 = (ObserverNode *)operator.new(0x10); // new_node = new ObserverNode();this->_observers = pOVar1; // _observers = new_node;pOVar1->observer = observer; // new_node->observer = observer;pOVar1->next = (ObserverNode *)0x0; // new_node->next = NULL;
bool LooperObserverMan::addObserver(Observer * observer) {...ObserverNode * new_node = new ObserverNode();new_node->next = NULL;new_node->observer = observer;if(node == NULL)_observers = new_node;elsenode->next = new_node;...}
ppOVar3 = (Observer **)operator_new(8); // new_node = new ObserverNode();*ppOVar3 = param_1; // new_node->observer = observer;ppOVar3[1] = (Observer *)0x0; // new_node->next = NULL;if (bVar1) {*(Observer ***)this = ppOVar3; // _observers = new_node;}
mac平台同样没有优化,反编译得到的变量pauVar3就是源码中的new_node变量。
2)struct ObserverNode定义如下:
struct ObserverNode {Observer * observer;ObserverNode * next;};
bool LooperObserverMan::addObserver(Observer * observer) {...ObserverNode * new_node = new ObserverNode();new_node->next = NULL;new_node->observer = observer;__asm__ __volatile__("":::"memory"); // 插入内存屏障if(node == NULL)_observers = new_node;elsenode->next = new_node;return true;}
pOVar1 = (ObserverNode *)operator.new(0x10);pOVar1->observer = observer;pOVar1->next = (ObserverNode *)0x0;if (pOVar2 == (ObserverNode *)0x0) {this->_observers = pOVar1;}
可以看到增加内存屏障后编译器已经不再进行相关优化了,new分配的内存赋值给pOVar1,pOVar1->observer完成赋值后才会将this->_observers赋值成pOVar1。赋值顺序得到了保障。
水落石出,最终结论
方案2:业务封装的TimerCtrl,将addObserver操作绑定到消息队列回调函数(notifyIdle)的线程上,避免读写异步。
知识点回顾
5.1 C++多态实现&类内存布局
Base *p = new Driver();new Driver()构造的是子类对象,因此生成的vptr指向的是子类的虚表,这样当使用指针p调用子类override的函数时就能从虚表中找到override后的函数指针了。
Base b;Driver d;b = static_cast<Base>(d);
原因是这种强转赋值时vptr指针并不会做拷贝动作,因此赋值后对象b中的vptr还是指向的Base类的虚表,因此无法调用子类方法,即无法达到多态的效果的。
(gdb) x /16a 0x79989f400x79989f40: 0x7990c9e0 0x7990c9f00x79989f50: 0x78411698 <asl::IMessageLooper::Observer::onLooperStart(asl::IMessageLooper*, int, int)> 0x799095980x79989f60: 0x799097d8 0x799099d00x79989f70: 0x784116b8 <asl::IMessageLooper::Observer::onLooperBusy(asl::IMessageLooper*)> 0x79909bd80x79989f80: 0x784116c8 <asl::IMessageLooper::Observer::onLooperQuit(asl::IMessageLooper*)> 0x784116d0 <asl::IMessageLooper::Observer::onLooperDestroy(asl::IMessageLooper*)>0x79989f90: 0x784116d8 <asl::IMessageLooper::Observer::onLooperCancelMsg(asl::IMessageLooper*, asl::Message*, unsigned long, unsigned long)> 0x7990c9880x79989fa0: 0x7990c990 0x7990c9980x79989fb0: 0x7990c9a0 0x7990c9a8
class Observer {public:virtual ~Observer() {}virtual void onLooperStart(IMessageLooper * looper, int queue_size, int delay_queue_size) {};virtual void onLooperPostMsg(IMessageLooper * looper, Message * msg, uint32_t delay) {};virtual void onLooperStartMsg(IMessageLooper * looper, Message * msg, uint64_t timing, uint64_t now) {};virtual void onLooperEndMsg(IMessageLooper * looper, Message * msg, uint64_t timing, uint64_t now, uint32_t duration) {};virtual void onLooperBusy(IMessageLooper * looper) {};virtual void onLooperIdle(IMessageLooper * looper, int delay_queue_size) {};virtual void onLooperQuit(IMessageLooper * looper) {};virtual void onLooperDestroy(IMessageLooper * looper) {};virtual void onLooperCancelMsg(IMessageLooper * looper, Message * msg, uint64_t timing, uint64_t now) {}};
0x0000000078432d40 <+16>: cbz x19, 0x78432d8c <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+92>0x0000000078432d44 <+20>: mov x22, x10x0000000078432d48 <+24>: mov w21, w20x0000000078432d4c <+28>: adrp x20, 0x786b00000x0000000078432d50 <+32>: b 0x78432d60 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+48>0x0000000078432d54 <+36>: nop0x0000000078432d58 <+40>: ldr x19, [x19,#8]0x0000000078432d5c <+44>: cbz x19, 0x78432d8c <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+92>0x0000000078432d60 <+48>: ldr x0, [x19]0x0000000078432d64 <+52>: ldr x1, [x20,#1160]=> 0x0000000078432d68 <+56>: ldr x2, [x0]0x0000000078432d6c <+60>: ldr x3, [x2,#56]0x0000000078432d70 <+64>: cmp x3, x10x0000000078432d74 <+68>: b.eq 0x78432d58 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+40>0x0000000078432d78 <+72>: mov w2, w210x0000000078432d7c <+76>: mov x1, x220x0000000078432d80 <+80>: blr x30x0000000078432d84 <+84>: ldr x19, [x19,#8]0x0000000078432d88 <+88>: cbnz x19, 0x78432d60 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+48>0x0000000078432d8c <+92>: ldp x21, x22, [sp,#16]0x0000000078432d90 <+96>: ldr x30, [sp,#32]0x0000000078432d94 <+100>: ldp x19, x20, [sp],#480x0000000078432d98 <+104>: ret
0x0000000078432d40 <+16>这一行的cbz x19, 0x78432d8c是x19为空则跳转到0x78432d8c的意思,x19就是node的地址,即while(node)判断node为空则跳转到0x0000000078432d8c <+92>行,这一行其实就是弹出函数栈中备份的寄存器,然后返回,即while结束,函数return。
(gdb) i register x1x1 0x784116c0 2017531584(gdb) x 0x784116c00x784116c0 <asl::IMessageLooper::Observer::onLooperIdle(asl::IMessageLooper*, int)>: 0xd503201fd65f03c0(gdb) info symbol 0x784116c0asl::IMessageLooper::Observer::onLooperIdle(asl::IMessageLooper*, int) in section .text of libbase_utils.so
即x1是通过x20找到的Observer::onLooperIdle函数指针,但是这个函数是libbase_utils.so的符号,即父类的虚函数指针(子类classTimerMessageObserver定义在libGAdasUtils.so中)。
0x0000000078432d68 <+56>: ldr x2, [x0]此处实际上取到了Observer的this指针,即子类对象的this指针,它指向的就是子类的虚表:
(gdb) i register x19x19 0x17bb988e0 6370724064(gdb) x 0x17bb988e00x17bb988e0: 0x7998e758(gdb) x 0x7998e7580x7998e758: 0x79989f40 // 虚表地址(gdb) p *node->observer$6 = {_vptr.Observer = 0x79989f40}
这之后0x0000000078432d6c <+60>:ldrx3, [x2,#56]即this指针偏移56字节后取内容存入x3,虚表地址偏移56字节就是0x79989f78:
(gdb) x /16a 0x79989f400x79989f40: 0x7990c9e0 0x7990c9f00x79989f50: 0x78411698 <asl::IMessageLooper::Observer::onLooperStart(asl::IMessageLooper*, int, int)> 0x799095980x79989f60: 0x799097d8 0x799099d00x79989f70: 0x784116b8 <asl::IMessageLooper::Observer::onLooperBusy(asl::IMessageLooper*)> 0x79909bd80x79989f80: 0x784116c8 <asl::IMessageLooper::Observer::onLooperQuit(asl::IMessageLooper*)> 0x784116d0 <asl::IMessageLooper::Observer::onLooperDestroy(asl::IMessageLooper*)>0x79989f90: 0x784116d8 <asl::IMessageLooper::Observer::onLooperCancelMsg(asl::IMessageLooper*, asl::Message*, unsigned long, unsigned long)> 0x7990c9880x79989fa0: 0x7990c990 0x7990c9980x79989fb0: 0x7990c9a0 0x7990c9a8
虽然虚表没有打印出来这个地址对应的函数指针,但是可以确认是函数onLooperBusy后面声明的那个虚函数,即onLooperIdle()与notifyIdle的源码得以对应。这之后汇编码中做了比较cmp x3 x1,当x3和x1相等则跳转b.eq0x78432d58 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+40>,而<+40>行中直接开始load node偏移8字节的内存了ldrx19, [x19,#8],相当于直接取node->next却不执行任何函数,显然我们这里observer指向的是子类对象,因此这个cmp指令结果是false的,不会跳转,会继续执行到0x0000000078432d80 <+80>:blrx3,跳转到x3指向的函数指针执行完该函数后才执行的0x0000000078432d84 <+84>:ldrx19, [x19,#8],即node = node->next继续循环。
(gdb) p *pa$1 = {_vptr.A = 0x400d30 <vtable for A+16>}(gdb) x /16a 0x400d300x400d30 <_ZTV1A+16>: 0x400ab6 <A::~A()> 0x400ae4 <A::~A()>0x400d40 <_ZTV1A+32>: 0x400b0a <A::func1()> 0x400b34 <A::func2()>0x400d50 <_ZTV1A+48>: 0x400b5e <A::func3(int, int)> 0x4231
可以看到这个虚表中有两个析构函数A::~A(),这是因为gcc实现了两个虚析构函数(msvc只有一个)。许多编译器为一个类生成两个不同的析构函数:一个用于销毁动态分配的对象,另一个用于销毁非动态对象(静态对象、局部对象、基子对象或成员子对象,称为complete object destructor)。前者从内部调用operator delete,后者则不调用。有些编译器通过向一个析构函数添加隐藏参数来实现这一点(较老版本的GCC是这样做的,msvc++是这样做的),有些编译器只是生成两个独立的析构函数(较新版本的GCC是这样做的)。
5.2 理解pc指针与芯片异常处理
对于异步异常,它是中断发生时的下一条指令,或没有执行的第一条指令;
对于不是system call的同步异常,它是触发同步异常的那一条指令;
对于system call, 它是svc指令的下一条指令。
尝试访问异常等级不恰当的寄存器;
尝试执行被关闭或没有定义(UNDEFINED)的指令;
使用没有对齐的SP;
尝试执行PC没有对齐的指令;
软件产生的异常,比如执行系统调用(SVC)、HVC或SMC指令;
因地址翻译或权限等导致的数据异常;
因地址翻译或权限等导致的指令异常;
调试导致的异常,比如断点异常、观察点异常、软件单步异常等;
5.3 内存乱序与内存屏障
小结
本文详细回放了一个崩溃案例的分析过程。
回顾了C++多态和类内存布局相关知识,了解原理后查看内存让我们看到了更多代码内部的细节。
回顾了pc指针的含义并了解了更多arm异常处理机制,解释了一些日常认为理所当然的结论背后的原理。
回顾了内存屏障相关知识,并构造了demo对理论分析进行了实践验证。
6.1 启发
该案例非常经典,对我们后续分析问题和编码设计都有一定的启发。
汇编码是高级语言源码的放大版,当在高级语言层面看不出问题时,不妨试一下查看汇编,因为它更接近机器执行的“源码”,具有更高的“分辨率”。
无锁设计的代码,尤其是我们“精心”设计依赖赋值顺序的代码,不要忘记内存序优化的存在。
编码设计除了coding部分,还要与编译器和谐相处,明确编译器行为,确保最终的编译产物符合设计预期,避免编译器“自由发挥”。
6.2 感悟
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。