当前位置: 首页 > news >正文

网站建设推广文章wordpress适配手机

网站建设推广文章,wordpress适配手机,网络广告推广方法,全国网站建设公司作者#xff1a;靳倡荣 本文详细回放了一个崩溃案例的分析过程。回顾了C多态和类内存布局、pc指针与芯片异常处理、内存屏障的相关知识。 一、不讲“武德”的崩溃 1.1 查看崩溃调用栈 客户反馈了一个崩溃问题#xff0c;并提供了core dump文件#xff0c;查看崩溃调用栈如下…作者靳倡荣 本文详细回放了一个崩溃案例的分析过程。回顾了C多态和类内存布局、pc指针与芯片异常处理、内存屏障的相关知识。 一、不讲“武德”的崩溃 1.1 查看崩溃调用栈 客户反馈了一个崩溃问题并提供了core dump文件查看崩溃调用栈如下 (gdb) bt #0 0x0000000078432d68 in asl::LooperObserverMan::notifyIdle (thisoptimized out, looper0x160eebd40, delay_queue_size0)at ../../../../src/asl_message_framework/src/BaseMessageLooper.cpp:371 #1 0x00000000784928e4 in asl::MessageQueue::fetchNext (thisthisentry0x160eedfc0, timing0xf4e9f60: 0)at ../../../../src/asl_message_framework/src/MessageQueue.cpp:83 #2 0x0000000078492b24 in asl::MessageQueue::next (this0x160eedfc0, timing0xf4e9f60: 0) at ../../../../src/asl_message_framework/src/MessageQueue.cpp:60 #3 0x000000007832036c in asl::Looper::loop (this0x160eebd40) at ../../../../src/asl_message_framework/src/Looper.cpp:107 #4 0x0000000078495ee0 in asl::MessageThread::run (this0x7998e678) at ../../../../src/asl_message_framework/src/MessageThread.cpp:56 #5 0x000000007851cc70 in asl::Thread::runCallback (param0x7998e678) at ../../../../src/asl_message_framework/src/Thread.cpp:183 #6 0x00000000010314e0 in ?? () 显然崩溃发生在了asl::LooperObserverMan::notifyIdle()函数中BaseMessageLooper.cpp文件的第371行源码如下 1.2 段错误位置不符合预期 崩溃时提示segment fault通常就是非法地址访问结合源码我们有理由怀疑node-observer指针异常(空指针或者野指针)导致这行发生了崩溃或者node虽然非空但是可能是个野指针导致崩溃。查看node和node-observer (gdb) p node $8 (asl::LooperObserverMan::ObserverNode *) 0x17bb988e0 (gdb) p node-observer $10 (asl::IMessageLooper::Observer *) 0x7998e758 结果大大出乎意料这两个指针居然可以正常访问。 至此问题的分析陷入了僵局这个崩溃看起来毫无道理简直不讲武德一个这么合法正常的内存访问居然导致了段错误。 二、汇编之下纤毫毕现 都说“源码面前了无秘密”现在源码就摆在眼前nodifyIdle函数一共7行但是计算机却在我们面前变了“魔术”。其实计算机也很委屈因为人眼看的“源码”并非机器看到的“源码”机器看到的是二进制呀这时候人也委屈了机器看的0101二进制我人脑也很难处理呀那么大家各退一步在高级语言和机器二进制码之间的不就是汇编么 2.1 用汇编“放大”源码 一行C代码可以转换成多条汇编指令汇编码就是高级语言源码的放大版。那么我们就来看看崩溃时的汇编吧。 (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)920x0000000078432d44 20: mov x22, x10x0000000078432d48 24: mov w21, w20x0000000078432d4c 28: adrp x20, 0x786b00000x0000000078432d50 32: b 0x78432d60 asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)480x0000000078432d54 36: nop0x0000000078432d58 40: ldr x19, [x19,#8]0x0000000078432d5c 44: cbz x19, 0x78432d8c asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)920x0000000078432d60 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)400x0000000078432d78 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)480x0000000078432d8c 92: ldp x21, x22, [sp,#16]0x0000000078432d90 96: ldr x30, [sp,#32]0x0000000078432d94 100: ldp x19, x20, [sp],#480x0000000078432d98 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寄存器中 (gdb) i register x0 x0 0x2e002e 3014702 (gdb) x 0x2e002e 0x2e002e: Cannot access memory at address 0x2e002e 查看x0寄存器中存的是0x2e002e后面的3014702是0x2e002e的十进制我们尝试取该地址的内存数据时果然发生了错误。 至此崩溃的直接原因找到了机器终于“沉冤得雪”。说明它确实是遇到了无法访问的内存因此才触发的段错误异常中断。 三、抽丝剥茧详细分析 3.1 分析汇编发现端倪 查看崩溃前的三条汇编指令 0x0000000078432d60 48: ldr x0, [x19]0x0000000078432d64 52: ldr x1, [x20,#1160]0x0000000078432d68 56: ldr x2, [x0] 这三条指令是依次执行的没有其它跳转指令打断他们。x0的值是从x19指向的内存load的查看相关寄存器和内存 (gdb) i register x19 x19 0x17bb988e0 6370724064 (gdb) x 0x17bb988e0 0x17bb988e0: 0x7998e758 (gdb) x 0x7998e758 0x7998e758: 0x79989f40 可以看到x19中存的是0x17bb988e0对这个地址取内容得到0x7998e758正常这个值应该存入x0但实际上x0中存储的却是非法地址0x2e002e而0x7998e758是一个合法地址可以正常取到它内容0x79989f40。 3.2 疑似原因一踩内存 问题就发生在这三行汇编指令之间首先我们怀疑是否是一个踩内存问题。 x0中存储的是x19中存储的值作为地址该地址中的内存崩溃时看到的是最终形态虽然最终x19指向的内存可以被访问但是否有可能ldr x0 [x19]时这块内存的值还是0x2e002e另外虽然x19指向的内存最终可以访问但是可以访问未必代表符合预期这块内存会不会是乱的 3.2.1 链表节点指针内存符合预期 首先我们来确认下第二个问题看看最终崩溃时x19存的地址对应的内存是什么 (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; }; 3.2.2 类内存布局符合预期 进一步查看observer内容 (gdb) p *(node-observer) $4 {_vptr.Observer 0x79989f40} 可见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指向的内存符合预期。 3.2.3 排除踩内存的可能性 再来看第一个问题x0中存储的是x19中存储的地址指向的内存崩溃时看到的是最终形态虽然最终x19指向的内存可以被访问但是否有可能ldr x0 [x19]时这块内存的值还是0x2e002e由于内存被踩踏导致x0的值与[x19]最终值不一致 回头来看崩溃前的三行指令 0x0000000078432d60 48: ldr x0, [x19]0x0000000078432d64 52: ldr x1, [x20,#1160]0x0000000078432d68 56: ldr x2, [x0] 刚才已经确认最终崩溃时x19指向的内存正常但是x0内容不正常如果是踩内存则需要在ldr x0 [x19]时将x19指向的内存踩坏在崩溃时将其恢复正常因此第一种假设不太可能。 3.3 疑似原因二未初始化变量访问 原因猜测x19指向的内存一开始是野指针(0x2e002e)该值赋给了x0但是后来(异步线程)进行了正确赋值导致崩溃最终现场x19指向的内存布局正常但是x0中存入的是野指针地址触发崩溃。 3.3.1 业务源码分析 针对该假设则需要进一步查看源码这三条指令已经进入了asl::LooperObserverMan::notifyIdle()函数的while循环中即node不为空那么是否存在node不为空但是node-observer为野指针的时间空档正好进入while(node)后ldr x0 [x19]将还没有初始化的node-observer地址给了x0呢 ​ 查看给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;。 3.3.2 排除未初始化变量访问 如果notifyIdle()在addObserver之前调用则ObserverNode * node _observers;中的_observers的初值为NULL在其所属类的构造函数中进行了初始化 LooperObserverMan::LooperObserverMan() : _observers(NULL) { } 而3.3.1的源码显示_observers赋值成new_node之前new_node-observer已经完成赋值。 因此node不为空时x19指向的node-observer内存未初始化时load到x0的假设也不成立。 3.4 初步分析结论 综上最终x0内容不符合预期则更可能是由于系统级别的稳定性问题导致了。 例如中断或进程抢占导致ldr x0 [x19]后当前任务被打断等到恢复上下文回到当前任务继续执行时x0寄存器值没有得到正确恢复导致崩溃。 当然是否真的如此当前证据已经不足了需要整机dump才能进一步分析。 3.5 问题复现闪电再次劈中 3.5.1 相同崩溃栈复现 不久前另一个客户也报了相同的问题客户反馈的崩溃调用栈是一样的。如果说是硬件或系统级别的问题那么这就是被闪电劈中了两次基本可以排除系统级别或硬件的问题。我们应该更多地审视为啥这块(用户态)代码被劈中。 3.5.2 崩溃原因再讨论 重新审视之前的分析。发现3.3中我们排除疑点二的一个重要依据为变量_observers初值为NULL后续赋值顺序为 new_node-observer xxxx; _observers new_node; 即另一个线程读的时候_observers要么是NULL要么是成员变量new_node-observer已经赋好值的new_node。 拆分出来就是两个依据 1指针_observers赋值是原子的读线程要么读到NULL要么读到好的_observers 2new_node-observer的赋值在_observers赋值之前进行。 3.5.3 指针赋值原子性讨论 对此大家产生了分歧。 一种观点认为指针、int等基础类型的赋值不是原子的否则C为什么还要搞std::atomic来保障基础类型读写原子性。 另一种观点认为在同一个cacheline中的操作是原子的(inter手册中有相关表述arm的还没找到)而本例中的指针没做特殊对齐限制所以地址是cacheline size(64bit系统为8字节)对齐的因此是原子的。 3.5.4 赋值顺序讨论 再次看这两个赋值语句 new_node-observer xxxx; _observers new_node; 发现其实这两个赋值是没有依赖的即交换顺序后结果是不变的。那么就存在被编译器以及CPU reorder的可能而此处并没有设置内存屏障来保障内存序。 因此存在这样一种可能性写线程由于reoder的存在先执行了_observers new_node与此同时读线程判空逻辑命中并将此时尚未初始化的_observers-observerload到了寄存器x0中这之后写线程完成_observers-observer的赋值读线程走到x0内存的访问发生崩溃。 3.6 show me the codedemo验证 3.6.1 demo构造 首先将addObserver代码原封不动从基础库复制过来 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; } 然后将读线程调用的notifyIdle函数稍作改造去掉更深层次的调用实现便于debug 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的构造函数中保证成员变量_observers初值为NULL 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; }; 在main函数中做如下测试构造与高精SDK中类似的只add一个observer的场景 #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; } 此处我们起了一个线程调用addObserver将变量Observer ob的地址作为实参传入主线程则调用notifyIdle()接口notifyIdle()的实现中会判断node为空则return falsenode不为空则比较node-observer的值并调用node-observer-onLooperIdle()接口。只要notifyIdle()返回一次truemain函数就会结束。notifyIdle()的入参也是变量Observer ob的地址正常内存序下如果node不为空则node-observer已经完成了赋值其值与变量Observer ob的地址应该相等。异常时将会打印出相关error日志。 使用脚本进行压测模拟每次只添加一个observer的场景反复启动测试进程test_reordershell脚本如下 num0; while true; do sleep 1; date; ./test_reorder; numexpr $num 1; echo $num; done 3.6.2 压测结果 在客户环境下压测了217258次出现了10次error日志如下所示 Sun Feb 15 09:20:29 GMT 1970 error: observer not match!!! observer: 100c7878, node-observer: 100c7878 onLooperIdle() 191229 说明存在万分之0.5的概率当node值不为空时node-observer ! ob。但是日志打印时node-observer的值跟变量ob的地址已经相等了。 3.6.3 demo压测结果分析 从结果中看3.5.2中我们提炼的第2点依据被推翻。实际情况下node不为空时指令乱序可能导致node-observer还未赋值。 指令乱序分为硬件和软件两个层面我们重点排查软件层面即编译器优化。正如3.5.4所分析node和node-observer的赋值是不存在相互依赖的因此满足指令乱序优化条件是否进行了编译优化我们只需要查看汇编即可。查看demo代码的汇编如下。 为了减少看汇编码的成本我们直接看逆向工具根据汇编生成的反编译代码即可如上图右侧窗口所示。 其中pOVar1 this-observers;即将LooperObserverMan的成员变量_observers赋值给pOVar1因为我们只压测插入第一个节点的场景因此只需关注pOVar1为空的分支即 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; 发现这里将operator.new分配的内存地址赋值给了pOVar1对应源码的ObserverNode * new_node new ObserverNode();但此处把new分配的地址赋值给pOVar1后紧接着把pOVar1赋值给了成员变量_observers即this-_observers pOVar这之后才对pOVar1-observer这个分量进行赋值。对比源码 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; ... } 可以看到将_observers new_node做的事情提前到了new_node-observer observer之前说明确实进行了reorder那么当读线程判断_observers不为空就立刻使用_observers-observer时就存在_observers-observer尚未初始化的情况导致崩溃。 3.6.4 其他平台编译结果对比 相同代码编译其他平台可执行程序对比汇编内容。 Android平台 发现android平台的编译结果并没有将_observers和_observers-observer赋值做reorder的优化(只是将new_node-next和new_node-observer这两个赋值语句做了reorder)几行核心反编译代码如下 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;} 将new分配的内存地址赋值给变量ppOVar3*ppOVar3表示struct ObserverNode的第一个成员observer, 因此*ppOVar3 param_1表示将入参ob赋值给ppOvar3-observer;接着ppOVar3[1]表示struct ObserverNode的第二个成员next指针ppOVar3[1] (Observer*)0x0表示ppOVar3-next NULL。因此变量ppOVar3就是addObserver源码中的new_node变量。这之后*(Observer ***)this ppOVar3对应的就是将成员变量_observers赋值成ppOVar3。因此android平台的赋值顺序是没有被优化的。 Mac平台 mac平台同样没有优化反编译得到的变量pauVar3就是源码中的new_node变量。 备注 1即使是相同的平台不同的编译选项结果也不同例如-O3和-O0 2struct ObserverNode定义如下 struct ObserverNode {Observer * observer;ObserverNode * next; }; 3.6.5 增加内存屏障 既然是编译器进行了reorder优化我们就可以使用内存屏障禁止编译器相关优化可以在addObserver代码中插入一行表示内存屏障的汇编__asm__ __volatile__(:::memory)进行测试 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分配的内存赋值给pOVar1pOVar1-observer完成赋值后才会将this-_observers赋值成pOVar1。赋值顺序得到了保障。 四、水落石出最终结论 至此终于水落石出。崩溃的直接原因是非法内存访问非法的内存为结构体变量node的分量node-observer。有两个线程分别对该变量进行读和写操作其中读线程对node进行判空后使用了node-observer分量的内存。其内部逻辑认为node不为空时node-observer一定合法而写线程代码中对临时变量new_node分配内存后对其分量new_node-observer进行赋值然后将new_node赋值给node即new_node-observer xxx; node new_node;想利用这样的设计保障读线程判断node不为空时读到合法的node-observer。但实际上qnx平台编译结果的汇编指出编译器在此处进行了内存序优化调整了这两个赋值语句的顺序打破了上述假设。导致读线程判断node不为空后调用node-observer-onLooperIdle()接口时由于node-observer变量还未初始化导致崩溃。 一句话总结编译器reorder优化导致指令顺序改变进而导致异步读线程使用了未初始化的变量触发崩溃。 优化方案 方案1基础库addObserver中增加内存屏障。 方案2业务封装的TimerCtrl将addObserver操作绑定到消息队列回调函数(notifyIdle)的线程上避免读写异步。 五、知识点回顾 本次崩溃问题分析中用到了很多以前书本上学习的知识例如我们查看虚表内存其实就是C多态实现机制和类内存布局相关知识。这些知识点让我们更加精准的看到了代码的内部也帮助我们印证了一些推断。 5.1 C多态实现类内存布局 5.1.1 C虚函数多态原理 这里说的多态特指C的动态多态虚函数。虚函数的多态实现离不开虚函数表后面简称虚表虚表不属于类的对象它属于整个类是一个全局变量是编译时就生成在data段的一张表表里面就是各个虚函数的函数指针这些指针指向各个函数的代码段。 类对象构造时编译器生成vptr指针指向虚表相同类的所有对象指向全局唯一虚表。虚表内容为各个虚函数的函数指针。子类则会拷贝一张虚表并将自己override的接口替换成override后函数的指针。这就是多态实现的关键。当我们取一个Base的指针指向子类对象时 Base *p new Driver(); new Driver()构造的是子类对象因此生成的vptr指向的是子类的虚表这样当使用指针p调用子类override的函数时就能从虚表中找到override后的函数指针了。 5.1.2 多态必须使用指针或引用的原因 我们使用C多态时通常是使用父类指针指向子类对象或者父类引用(Base)子类对象但是直接对象赋值则无法调用到子类方法例如 Base b; Driver d; b static_castBase(d); 原因是这种强转赋值时vptr指针并不会做拷贝动作因此赋值后对象b中的vptr还是指向的Base类的虚表因此无法调用子类方法即无法达到多态的效果的。 关于C多态实现的相关资料很多此处不再赘述。 5.1.3 子类虚表编译优化 本次分析问题时我们查看observer类的虚表内容如下 (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源码虚函数不止上面虚表内存中显示的5个 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) {} }; 实际上我们打印的node-observer指向的是classTimerMessageObserver 对象它是asl::IMessageLooper::Observer的子类而虚表中显示的几个函数指针都是这个子类没有override的函数。此处可能是编译器的优化。这一点可以从notifyIdle函数的汇编中看出一些端倪。 0x0000000078432d40 16: cbz x19, 0x78432d8c asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)920x0000000078432d44 20: mov x22, x10x0000000078432d48 24: mov w21, w20x0000000078432d4c 28: adrp x20, 0x786b00000x0000000078432d50 32: b 0x78432d60 asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)480x0000000078432d54 36: nop0x0000000078432d58 40: ldr x19, [x19,#8]0x0000000078432d5c 44: cbz x19, 0x78432d8c asl::LooperObserverMan::notifyIdle(asl::IMessageLooper*, int)920x0000000078432d60 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)400x0000000078432d78 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)480x0000000078432d8c 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。 adrpx20, 0x786b0000表示取0x786b0000所在4KB内存页首地址存入x20这之后跳转到0x0000000078432d60 48注意到x1的内容如下 (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, [x0] 此处实际上取到了Observer的this指针即子类对象的this指针它指向的就是子类的虚表 (gdb) i register x19 x19 0x17bb988e0 6370724064 (gdb) x 0x17bb988e0 0x17bb988e0: 0x7998e758 (gdb) x 0x7998e758 0x7998e758: 0x79989f40 // 虚表地址 (gdb) p *node-observer $6 {_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继续循环。 5.1.4 虚表中函数指针分布 虚表中的函数指针是按照虚函数声明顺序排列的但此处有一个小疑问按照虚函数声明顺序算上析构函数onLooperIdle()是第7个声明的虚函数应该是虚表偏移6*8 48个字节才对为什么这里差一个我们找一个没有编译优化的demo看一下虚表内存布局 (gdb) p *pa $1 {_vptr.A 0x400d30 vtable for A16} (gdb) x /16a 0x400d30 0x400d30 _ZTV1A16: 0x400ab6 A::~A() 0x400ae4 A::~A() 0x400d40 _ZTV1A32: 0x400b0a A::func1() 0x400b34 A::func2() 0x400d50 _ZTV1A48: 0x400b5e A::func3(int, int) 0x4231 可以看到这个虚表中有两个析构函数A::~A()这是因为gcc实现了两个虚析构函数(msvc只有一个)。许多编译器为一个类生成两个不同的析构函数一个用于销毁动态分配的对象另一个用于销毁非动态对象(静态对象、局部对象、基子对象或成员子对象称为complete object destructor)。前者从内部调用operator delete后者则不调用。有些编译器通过向一个析构函数添加隐藏参数来实现这一点(较老版本的GCC是这样做的msvc是这样做的)有些编译器只是生成两个独立的析构函数(较新版本的GCC是这样做的)。 至此多偏移的8字节就合理了。 5.2 理解pc指针与芯片异常处理 本次问题组内讨论时有同学提出了一个疑问pc指针是program counter指向的是下一条待执行的指令而arm指令又是三级流水线pc指向的只是“正在取指”的指令并不是指向的“正在执行”或“正在译码”的指令所以崩溃处是否不是反编译后pc的位置而是pc - 4或者pc -8处呢 虽然本次的问题我们可以通过打印寄存器和相关内存内容确认pc - 8和pc - 4处不会发生段错误崩溃但是这个问题还是一下子问住了我。之前无论是分析内核dump还是用户态进程dump都是默认调用栈pc指针处就是发生崩溃处确实没有认真想过这个问题。一下子让我怀疑了人生难道之前dump分析的都有问题应该看pc之前的代码可是这又跟历史经验不符难道使用gdb单步调试时看到的pc也不是正在执行的代码吗那我为啥能够在pc经过一行赋值语句后看到了内存赋值结果 会不会正常执行时pc指向的是尚未执行的指令但是发生异常时有不一样的处理呢ARM开发文档中给出了答案。 The ELR_ELn register is used to store the return address from an exception. The value in this register is automatically written on entry to an exception and is written to the PC as one of the effects of executing the ERET instruction that is used to return from exceptions. 发生异常时ELR_ELn寄存器中会存储异常返回后执行指令的地址待异常返回时再将其填入PC。 ELR_ELn contains the return address, which depends upon the specific exception type. Typically, this is the address of the instruction after the one that generated the exception. For example, when an SVC (system call) instruction is executed, you want to return to the following instruction in the application. In other cases, however, you might want to re-execute the instruction that generated the exception. 但是具体是存储触发异常的指令还是下一条待执行的指令由异常类型决定。 通常有如下规律 对于异步异常它是中断发生时的下一条指令或没有执行的第一条指令 对于不是system call的同步异常它是触发同步异常的那一条指令 对于system call, 它是svc指令的下一条指令。 关于同步异常、异步异常可以参考《ARM异常处理》常见的同步异常有 尝试访问异常等级不恰当的寄存器 尝试执行被关闭或没有定义UNDEFINED的指令 使用没有对齐的SP 尝试执行PC没有对齐的指令 软件产生的异常比如执行系统调用SVC、HVC或SMC指令 因地址翻译或权限等导致的数据异常 因地址翻译或权限等导致的指令异常 调试导致的异常比如断点异常、观察点异常、软件单步异常等 我们常见的段错误其实就是“因地址翻译或权限等导致的数据异常”属于一种数据中止的同步异常类似的还有缺页中断不同的是缺页中断会在中断处理函数中修复该地址即所谓按需分配page使得该地址可用因此这类异常返回时pc会指向触发异常的指令重新执行相关指令或退出。 因此我们分析段错误时直接看frame 0中pc的代码就是触发问题的地方。同理gdb单步调试时bt命令看到的pc也是程序暂停前执行的指令。 更多参考How to use ARM’s data-abort exception https://www.embedded.com/how-to-use-arms-data-abort-exception/ 5.3 内存乱序与内存屏障 本次问题的本质其实是一个内存乱序编译优化问题。我们的赋值语句没有强制禁止编译器优化那么编译器就可以在满足规则的前提下性能优先做一些reorder的优化。上文的demo代码其实就是经典的store-store乱序。相关知识有很多文章都写得比较好此处不再赘述。 六、小结 本文详细回放了一个崩溃案例的分析过程。 回顾了C多态和类内存布局相关知识了解原理后查看内存让我们看到了更多代码内部的细节。 回顾了pc指针的含义并了解了更多arm异常处理机制解释了一些日常认为理所当然的结论背后的原理。 回顾了内存屏障相关知识并构造了demo对理论分析进行了实践验证。 6.1 启发 该案例非常经典对我们后续分析问题和编码设计都有一定的启发。 分析问题方面 汇编码是高级语言源码的放大版当在高级语言层面看不出问题时不妨试一下查看汇编因为它更接近机器执行的“源码”具有更高的“分辨率”。 编码设计方面 无锁设计的代码尤其是我们“精心”设计依赖赋值顺序的代码不要忘记内存序优化的存在。 编码设计除了coding部分还要与编译器和谐相处明确编译器行为确保最终的编译产物符合设计预期避免编译器“自由发挥”。 6.2 感悟 “学而时习之不亦说乎。”有两种解释一种是说学习后经常复习很快乐。我更喜欢另一种解释学习后在适当的时机实践、使用很快乐。强调的是学以致用。复习有什么可快乐的真正的乐趣在于学习了书本知识后能够在实践中得以应用。
http://www.hkea.cn/news/14539651/

相关文章:

  • 网站建设有那几个类型adsl服务器建网站
  • php做网站主要怎么布局网站的费用可以做无形资产吗
  • 医院网站建设趋势响应式网站的原理
  • wordpress怎样建立二级菜单seo网页优化工具
  • 企业宣传模板图片上海seo推广整站
  • 网站建设推广注册公司在线画流程图的网站
  • 在什么网站做调查问卷wordpress搜索插件
  • 百度收录网站电话对电子商务网站建设与管理的理解
  • 网站开发国内外研究背景网站管理登录系统
  • 盘锦网站制作企业微信官网
  • 手表网站 二手不会做网站能做网络销售吗
  • 企业网站成功案例网络舆情处置公司
  • 主题公园旅游景区网站建设成都网站开发培训多少钱
  • asp.net网站开发项目化教程wordpress会员管理插件
  • 免费驾校网站模板龙炎电商软件
  • 广州增城区门户网站免费的网站在哪里下载
  • 旅游网站建设的重要性中国核工业第五建设
  • 比较好的商城网站设计西安专业做网站建设
  • 上海哪家公司做网站好山东省威海市文登区建设局网站
  • 电子商务网站建设与维护的考试高端网站建设知识
  • 贵阳58同城做网站公司有哪些返佣网站都是自己做的
  • 万站群cms云南省住房与城乡建设厅网站
  • 哪些网站可以免费看剧phpcms网站转移
  • 邯郸装修网站建设有什么网站可以帮人做模具吗
  • 用KEGG网站做KEGG富集分析修改wordpress中附件上传大小
  • 高明骏域网站建设泉州app开发
  • cms企业网站系统城乡住房建设厅网站
  • 装修公司网站设计专做品牌的网站
  • 青岛网上房地产网站软件开发全过程
  • 亿藤互联网站建设开发网站pr怎么提升