实战总结|抽丝剥茧,记一次神奇的崩溃
阿里妹导读
不讲“武德”的崩溃
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) disas
Dump 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, x1
0x0000000078432d48 <+24>: mov w21, w2
0x0000000078432d4c <+28>: adrp x20, 0x786b0000
0x0000000078432d50 <+32>: b 0x78432d60 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+48>
0x0000000078432d54 <+36>: nop
0x0000000078432d58 <+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, x1
0x0000000078432d74 <+68>: b.eq 0x78432d58 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+40>
0x0000000078432d78 <+72>: mov w2, w21
0x0000000078432d7c <+76>: mov x1, x22
0x0000000078432d80 <+80>: blr x3
0x0000000078432d84 <+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],#48
0x0000000078432d98 <+104>: ret
End of assembler dump.
使用gdb的disas指令查看当前栈顶函数的反汇编,确实将notifyIdle的7行C++代码变成了27行汇编指令,让我们得以看到更多细节。
2.2 发现直接原因
=> 0x0000000078432d68 <+56>: ldr x2, [x0]
这个0x0000000078432d68就是当前pc寄存器的值,崩溃就发生在这一条ldr指令。该指令的含义是将x0寄存器中存的值作为内存地址,将内存中该地址存储的值load到x2寄存器中:
i register x0
x0 0x2e002e 3014702
x 0x2e002e
0x2e002e: 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 x19
x19 0x17bb988e0 6370724064
x 0x17bb988e0
0x17bb988e0: 0x7998e758
x 0x7998e758
0x7998e758: 0x79989f40
可以看到x19中存的是0x17bb988e0,对这个地址取内容得到0x7998e758,正常这个值应该存入x0,但实际上x0中存储的却是非法地址0x2e002e,而0x7998e758是一个合法地址,可以正常取到它内容0x79989f40。
3.2 疑似原因一:踩内存
(gdb) i register x19
x19 0x17bb988e0 6370724064
(gdb) x 0x17bb988e0
0x17bb988e0: 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 0x79989f40
0x79989f40: 0x7990c9e0 0x7990c9f0
0x79989f50: 0x78411698 <asl::IMessageLooper::Observer::onLooperStart(asl::IMessageLooper*, int, int)> 0x79909598
0x79989f60: 0x799097d8 0x799099d0
0x79989f70: 0x784116b8 <asl::IMessageLooper::Observer::onLooperBusy(asl::IMessageLooper*)> 0x79909bd8
0x79989f80: 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)> 0x7990c988
0x79989fa0: 0x7990c990 0x7990c998
0x79989fb0: 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;
else
node->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;
else
node->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) {
}
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;
};
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;
else
node->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;
else
node->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 0x79989f40
0x79989f40: 0x7990c9e0 0x7990c9f0
0x79989f50: 0x78411698 <asl::IMessageLooper::Observer::onLooperStart(asl::IMessageLooper*, int, int)> 0x79909598
0x79989f60: 0x799097d8 0x799099d0
0x79989f70: 0x784116b8 <asl::IMessageLooper::Observer::onLooperBusy(asl::IMessageLooper*)> 0x79909bd8
0x79989f80: 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)> 0x7990c988
0x79989fa0: 0x7990c990 0x7990c998
0x79989fb0: 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, x1
0x0000000078432d48 <+24>: mov w21, w2
0x0000000078432d4c <+28>: adrp x20, 0x786b0000
0x0000000078432d50 <+32>: b 0x78432d60 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+48>
0x0000000078432d54 <+36>: nop
0x0000000078432d58 <+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, x1
0x0000000078432d74 <+68>: b.eq 0x78432d58 <asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)+40>
0x0000000078432d78 <+72>: mov w2, w21
0x0000000078432d7c <+76>: mov x1, x22
0x0000000078432d80 <+80>: blr x3
0x0000000078432d84 <+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],#48
0x0000000078432d98 <+104>: ret
0x0000000078432d40 <+16>这一行的cbz x19, 0x78432d8c是x19为空则跳转到0x78432d8c的意思,x19就是node的地址,即while(node)判断node为空则跳转到0x0000000078432d8c <+92>行,这一行其实就是弹出函数栈中备份的寄存器,然后返回,即while结束,函数return。
(gdb) i register x1
x1 0x784116c0 2017531584
(gdb) x 0x784116c0
0x784116c0 <asl::IMessageLooper::Observer::onLooperIdle(asl::IMessageLooper*, int)>: 0xd503201fd65f03c0
(gdb) info symbol 0x784116c0
asl::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,
此处实际上取到了Observer的this指针,即子类对象的this指针,它指向的就是子类的虚表:
i register x19
x19 0x17bb988e0 6370724064
x 0x17bb988e0
0x17bb988e0: 0x7998e758
x 0x7998e758
0x7998e758: 0x79989f40 // 虚表地址
p *node->observer
{_vptr.Observer = 0x79989f40} =
这之后0x0000000078432d6c <+60>:ldrx3, [x2,#56]即this指针偏移56字节后取内容存入x3,虚表地址偏移56字节就是0x79989f78:
(gdb) x /16a 0x79989f40
0x79989f40: 0x7990c9e0 0x7990c9f0
0x79989f50: 0x78411698 <asl::IMessageLooper::Observer::onLooperStart(asl::IMessageLooper*, int, int)> 0x79909598
0x79989f60: 0x799097d8 0x799099d0
0x79989f70: 0x784116b8 <asl::IMessageLooper::Observer::onLooperBusy(asl::IMessageLooper*)> 0x79909bd8
0x79989f80: 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)> 0x7990c988
0x79989fa0: 0x7990c990 0x7990c998
0x79989fb0: 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 0x400d30
0x400d30 <_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 感悟
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
微信扫码关注该文公众号作者