当前位置: 首页 > Linux

如何快速定位程序Core?

时间:2023-04-06 23:50:29 Linux

简介:程序内核是指应用程序无法维持正常运行状态时发生的崩溃行为。程序执行core时会生成相关的core-dump文件,是程序崩溃时程序状态的数据备份。核心转储文件包含内存、处理器、寄存器、程序计数器和堆栈指针等状态信息。本文将介绍一些使用core-dump文件定位程序核心原因的方法和技巧。全文7023字,预计阅读时间13分钟。一、程序内核定义及分类程序内核是指应用程序无法维持正常运行状态时发生的崩溃行为。程序执行core时会生成相关的core-dump文件,core-dump文件是程序崩溃时程序状态的状态数据备份。core-dump文件包含了内存、处理器、寄存器、程序计数器、堆栈指针等状态信息,我们可以通过core-dump文件来分析定位程序Core的原因。这里我们从三个方面对程序核心进行分类:机器、资源、程序bug。下表对常见的内核原因进行了分类:2.函数栈介绍当我们打开内核文件时,首先关注的是程序崩溃时的函数调用栈状态。为了方便理解后续的一些核心定位技巧,这里简单介绍一下函数栈。2.1寄存器介绍当前生产环境是64位机器。这里只介绍64位机的寄存器,如下:对于x86-64架构,有16个64位寄存器。每个寄存器的用途不是单一的。例如,%rax通常保存函数返回结果,但也适用于imul和idiv指令。这里重点关注%rsp(栈顶指针寄存器)、%rbp(栈底指针寄存器)、%rdi、%rsi、%rdx、%rcx、%r8、%r9(分别对应第1~6个函数参数).CalleeSave表示被调用者是否需要保存寄存器的值。2.2函数调用2.2.1调用函数栈帧:调用函数时,首先要做的是将参数入栈,参数入栈的顺序与参数入栈的顺序相反定义。请注意,参数不一定被压入堆栈。在x86-64架构中,可以通过寄存器传递的变量直接通过寄存器传递,比如数字、指针、引用等。然后将返回地址压栈,返回地址就是被调用函数执行完后调用函数执行的下一条指令的地址。记住这里返回地址的位置,后面的章节会利用这个返回地址的特点。给上面的介绍举个例子:如上图,在main函数中调用foo函数,先把参数压栈,三个参数可以直接用寄存器传递(对应%edi,%esi,%edx),然后call指令将下一条指令压入栈中。2.2.2被调用函数栈帧:被调用函数首先保存前一个函数的栈底指针(%rbp),即将%rbp压入栈中。然后保存需要保存的寄存器值,即CalleeSave为True的寄存器。然后为临时变量和局部变量申请栈空间。对于被调用的函数,举个例子:如上图所示,执行foo函数时,先将main函数的%rbp压栈,然后将寄存器中的参数值存入局部变量(a、b、c)。2.3小结通过对函数调用的简单介绍,我们可以发现函数栈是一个细致而脆弱的结构,内存结构必须严格访问,稍有不慎可能导致程序崩溃。3、GDB定位核心本节将介绍从打开核心文件到定位整个过程中可能遇到的问题及解决方法。3.1核心文件核心文件在哪里?检查“/proc/sys/kernel/core\_pattern”以确定核心文件生成规则。3.2变量打印在程序调试过程中,经常需要检查各种变量(内存、寄存器、函数表等)的取值是否正确,单独维护一节介绍常用的变量打印方法以及一些冷门的把戏。3.2.1打印命令print[表达式]print$[前值个数]print{[类型]}[地址]print[第一个元素]@[元素个数]print/[格式][表达式]格式格式:o-8成systemx-十六进制u-无符号十进制t-二进制f-浮点数a-地址c-字符s-字符串3.2.2x命令x/n:为正整数,表示显示的内存单元个数,即从当前地址向后显示n个内存单元的内容,一个内存单元的大小由第三个参数u定义。f:表示addr指向的内存内容的输出格式,s对应输出字符串,这里要特别注意输出整数数据的格式:x以十六进制格式显示变量。d以十进制格式显示变量。u以十进制格式显示无符号整数。o以八进制格式显示变量。t以二进制格式显示变量。a以十六进制格式显示变量。c以字符格式显示变量。f以浮点格式显示变量。u:指以多少字节为一个内存单位-unit,默认为4。u也可以用一些字符来表示:如b=1字节,h=2字节,w=4字节,g=8字节。:表示内存地址。3.2.3容器对象打印使用上面的print和x命令,结合容器的数据结构,我们可以知道容器的详细信息。这是一个完整打印二进制字符串的示例。字符串的数据结构如下:当字符串为空时,\_M\_dataplus.\_M\_p指向nullptr。赋值后会在堆上申请一段内存,分为两段。前半部分是元信息(类型:std::string::\_Rep),比如长度,容量,refcount,后半部分是数据区,\_M\_p指向数据区。通常,对于非二进制的字符串,可以通过打印直接显示数据内容,但是当数据为二进制时,'\0'会截断打印的内容。因此,打印二进制字符串的首要任务是确认字符串的大小。字符串的大小信息存储在std::string::\_Rep结构中。根据上面的数据结构,可以发现\_Rep与\_M\_dataplus.\_M\_p差一个结构体大小,所以打印\_Rep结构体命令为:#先将_M_p转换为_Rep指针,然后让指针偏移一个结构体大小p*((std::string::_Rep*)(s._M_dataplus._M_p)-1)求出字符串的大小(\_M\_length)后,通过打印相关内存区x命令。命令为:#herenis_Rep._M_lengthx/ncbs._M_dataplus._M_p运行效果如下:为了方便,这里推荐一个方便的脚本:stl-views.gdb(链接:https://sourceware.org/gdb/wi...,gdb终端直接sourcestl-views.gdb即可,支持常用容器打印,如vector、map、list、string等。3.2.4静态变量打印静态变量在程序中经常使用。有时候我们需要检查一个静态对象的值是否正确,这就涉及到静态对象的打印。请参见以下示例:voidfoo(){staticstd::strings_foo("foo");}在这里您可以使用nm-C./bin|grepxx找到静态变量的内存地址,然后通过gdb的print打印出来。3.2.5Memorydumpdump[format]memoryfilenamestart_addrend_addrdump[format]valuefilenameexprformat一般使用二进制,其他可以参考gdb手册。比如我们可以结合上面查看字符串内容的例子,将整个字符串数据dump到一个文件中。dumpbinarymemoryfile1s._M_dataplus._M_ps._M_dataplus._M_p+length如果想查看文件内容可以用vim-b和xxd一起使用。继上面的字符串例子,我们再举一个将字符串内存数据转存到文件的例子:3.3定位代码行定位核心的原因是首先定位崩溃发生时正在执行的代码行。本节主要介绍一些定位代码行的方法。通常情况下,可以直接通过gdb的breaktrace查看整个函数栈,但是有时候函数栈信息并不是那么清晰,这时可以使用一些技巧来查看函数栈。3.3.1去编译优化有时会发现核心函数栈与实际代码行不匹配。如果是在离线环境下,可以尝试将编译优化设置为-O0,然后再次重现核心问题。3.3.2Programcounter+addr2line对于线上的core问题,一般无法对程序进行反编译优化,只能在已有core文件的基础上进行代码定位。本节我们通过一个例子来介绍如何使用程序计数器+addr2line来定位代码行。从截图中可以发现第20帧指示的代码行与实际代码行不符。定位步骤如下:#跳转到第20个栈帧20#使用display命令显示程序计数器display/i$rip#使用addrline工具做地址转换shell/opt/compiler/gcc-8.2/bin/addr2line-ebinaddress3.3.3函数栈修复有时候我们发现函数调用栈很多??在这种情况下,这通常发生在堆栈被覆盖时,并且在某些情况下是手动修复的。函数栈的修复所用到的函数栈的内存分布知识,见第一节。----------------------------------低地址----------------------------------0(%rsp)|栈顶|(这与-n(%rbp)相同)---------|------------------------n(%rbp)|可变大小堆栈帧8(%rbp)|变化0(%rbp)|前一个堆栈帧地址8(%rbp)|returnaddress----------------------------------Highaddresses从上面的栈图中可以发现通过%rbp寄存器可以找到上一个函数的返回地址和栈底指针,然后通过addr2line命令可以找到对应的代码行。这里举个例子:#先在当前调用的栈上找到一个栈的栈底指针值和返回地址x/2ag$rbp#2个单位,a=16进制,g=8字节单位#用上一个栈底指针该命令得到的栈值递归x/2ag地址3.3.4不规则核心栈不规则核心栈问题一般发生在堆内存损坏时。函数调用是一个非常微妙的过程,任何位置的意外读写都会导致程序崩溃。这里有一个小例子来说明:intmain(intargc,char*argv[]){std::strings("abcd");*重新解释_cast(&s)=0x11;return0;}上面的例子核心在字符串销毁上,因为字符串的\_M\_ptr被改写为0x11,销毁过程变成了非法的内存操作。同样,由于进程堆空间是共享的,一个线程对堆的非法操作可能会影响另一个线程的正常运行。由于堆分配的随机性,表现出来的现象就是核心栈不规则。处理不规则核心堆栈的最佳方法是使用AddressSanitizer。#设置编译参数CXXFLAGSCXXFLAGS="-fPIC-fsanitize=address-fno-omit-frame-pointer"#设置链接参数LDFLAGS="-lasan"#设置启动环境变量exportASAN_OPTIONS=halt_on_error=0:abort_on_error=1:disable_coredump=0#StartLD_PRELOAD=/opt/compiler/gcc-8.2/lib/libasan.so./bin/xxx3.3.5综上所述,上面提到的方法都是为了找到具体的问题代码行,具体分析内核原因提供了线索。3.4定位核心原因本节主要介绍定位核心原因的方法,并介绍一些常见的原因。3.4.1确认信号量从上面的Core分类我们可以发现,有些场景下的core是机器故障引起的,比如SIGBUS,那么我们可以先通过信号量来排除一些core原因。3.4.2定位异常汇编指令通过定位上面的代码行,我们可以大致找出程序内核在哪一行。对于比较简单的内核,可以直接打印程序上下文来查找内核的原因。但在某些场景下,检查上下文后并没有异常。这时候就需要准确定位到具体异常的汇编指令,并根据指令查找原因。查看汇编指令更简单的方法是使用layoutasm命令,frame指向堆栈,显示对应堆栈的汇编。这里以core为例,如下:程序显示core在start函数中,检查相关上下文变量没有异常。使用layoutasm打开正在执行的汇编指令,如下:查看汇编,将程序核心定位在mov指令中。mov指令的前一条指令为sub,栈申请3M空间。怀疑是栈空间不足。使用frame0的%rsp-frameN的%rbp检查堆栈空间是否不足。通过上面的例子,我们可以发现,定位到异常汇编指令的位置后,我们可以进一步压缩异常点,定位到是哪条指令、变量或地址导致了核心问题。3.4.3异常变量排查通过以上操作,我们可以准确定位出哪一行代码的哪条指令有问题。根据异常指令排查相关变量,判断变量值是否符合预期。这里有一个比较经典的空指针例子,如下:intmain(intargc,char*argv[]){int*a=nullptr;*一个=1;return*a;}通过汇编指令可以发现是movl$0x1,(%rax)有问题,%rax的值来自0x8(%rbp),x命令打印相关地址,可以查到是空指针错误。3.4.4查看优化后的变量通常程序编译时打开了优化,无法打印变量,提示变量已优化,有时可以使用汇编+寄存器的方式查看优化后的变量。这里有一个例子来说明:voidfoo(charconst*str){charbuf[1024]={'\0'};memcpy(buf,str,sizeof(buf));}intmain(intargc,char*argv[]){foo("abcd");return0;}通常在foo函数内部,str变量不会直接优化,因为可以直接使用%rdi寄存器来传递参数。为了能够打印出str的值,这时候我们可以使用汇编+寄存器的方式来查找具体的变量值,如下:首先找到调用foo函数的main函数的参数,将其pushstackassembly:mov$0x402011,%edi,其中0x402011是str的内存地址,可以通过x命令显示str的值。在更复杂的场景中,可能无法直接找到优化变量。在这种情况下,可以通过程序集回溯找到变量。3.4.5函数地址异常排查核心问题有时是数据异常引起的,有时是优化函数地址引起的,如调用虚函数地址错误、函数返回地址错误、函数指针值错误等。检查异常函数地址和检查异常变量是一样的,只是根据汇编指令确认调用是否异常。下面是一个虚函数地址异常的例子,如下:classA{public:virtual~A()=default;virtualvoidfoo()=0;};classB:publicA{public:voidfoo(){}};intmain(intargc,char*argv[]){A*a=newB;一个->富();A*b=新B;*reinterpret_cast(b)=0x0;b->foo();return0;}从汇编指令看,核心在mov(%rax),%rax。结合指令上下文可以发现,它寻址的是虚函数的地址。对比两个变量的虚函数表,可以发现是函数地址加载错误导致的核心。3.4.6小结定位核心的基本过程可以概括为以下步骤:确定核心的大致触发原因。机器问题?你自己的程序有问题吗?找到代码行。哪一行代码有问题。定位执行指令。哪一行命令做什么。找到异常变量。指令没有问题,但是指令操作的变量不符合预期。善用装配指令和打印指令(x、print、display)可以更有效地定位Core。参考资料:组装查看工具:https://godbolt.org/https://cppinsights.io/标准GDB文档:https://sourceware.org/gdb/cu...招聘信息:欢迎加入百度移动生态群内容中泰架构团队,我们常年需要后端,C++,模型架构,大数据,性能调优同学,社招,实习,校招。简历投递邮箱:geektalk@baidu.com(投递须知【内容架构】)推荐阅读:|大型商业系统数据库设计与实践|百度爱饭饭手机网页秒开实践|如何解密100TB数据分析45秒------------END------------百度极客说,百度官方技术公众号上线啦!技术干货·行业资讯·在线沙龙·行业会议招聘信息·介绍资料·技术书籍·百度周边欢迎各位同学关注