导读:程序core是指应用程序没法保持正常running状态而发生的崩溃行为。程序core时会生成相关的core-dump文件,是程序崩溃时程序状态的数据备份。core-dump文件中包含内存、处理器、寄存器、程序计数器、栈指针等状态信息。本文将介绍一些利用core-dump文件定位程序core缘由的方法和技巧。 shell
全文7023字,预计阅读时间 13分钟。 数据库
程序core是指应用程序没法保持正常running状态而发生的崩溃行为。程序core时会生成相关的core-dump文件,core-dump文件是程序崩溃时程序状态的状态数据备份。core-dump文件包含内存、处理器、寄存器、程序计数器、栈指针等状态信息。咱们能够借助core-dump文件来分析定位程序Core的缘由。vim
这里咱们从三个方面对程序Core进行分类:机器、资源、程序Bug。下表对常见的Core缘由进行了分类:后端
当咱们打开core文件时,首先关注的是程序崩溃时的函数调用栈状态,为了方便理解后续定位core的一些技巧,这里先简单介绍一下函数栈。markdown
目前生产环境都为64位机,这里只介绍64位机的寄存器,以下:数据结构
对于x86-64架构,共有16个64位寄存器,每一个寄存器的用途并不单一,如%rax一般保存函数返回结果,但也被应用于imul和idiv指令。这里重点关注%rsp(栈顶指针寄存器)、%rbp(栈底指针寄存器)、%rdi、%rsi、%rdx、%rcx、%r八、%r9(分别对应第1~6函数参数)。架构
Callee Save说明是否须要被调用者保存寄存器的值。数据库设计
在调用一个函数时首先进行的是参数压栈,参数压栈的顺序跟参数定义的顺序相反。注意,并非参数必定会压栈,在x86-64架构中会针对可使用寄存器传递的变量,直接经过寄存器传值,如数字、指针、引用等。ide
接着是返回地址压栈,返回地址为被调用函数执行完后,调用函数执行的下一个指令地址。这里牢记返回地址的位置,后续章节会利用到这个返回地址的特性。函数
针对上面的介绍举个例子说明:
如上图,在main函数中调用了foo函数,首先对参数压栈,三个参数均可以直接用寄存器传递(分别对应%edi、%esi、%edx),而后call指令将下一个指令压栈。
被调用函数首先会将上一个函数的栈底指针(%rbp)保存,即%rbp压栈。而后再保存须要被保存的寄存器值,即Callee Save为True的寄存器。接着为临时变量、局部变量申请栈空间。
针对被调用函数,举个例子说明:
如上图,在foo函数执行时,先对main函数的%rbp压栈,再把寄存器中的参数值存放到局部变量(a, b, c)中。
经过对函数调用的简单介绍,咱们能够发现函数栈是一个缜密且脆弱的结构,内存结构必须按照严格的方式被访问,如稍有不慎就可能致使程序崩溃。
这一节将介绍从core文件打开到定位全流程中可能会遇到的问题以及解决技巧。
core文件在哪里?
查看“/proc/sys/kernel/core_pattern”肯定core文件生成规则。
程序debug过程当中经常要查看各类变量(内存、寄存器、函数表等)的值是否正确,维持单独用一节介绍下经常使用的变量打印方法以及一些冷门小技巧。
print [Expression] print $[Previous value number] print {[Type]}[Address] print [First element]@[Element count] print /[Format] [Expression] Format格式: o - 8进制 x - 16进制 u - 无符号十进制 t - 二进制 f - 浮点数 a - 地址 c - 字符 s - 字符串
x /<n/f/u> <addr> n:是正整数,表示须要显示的内存单元的个数,即从当前地址向后显示n个内存单元的内容,一个内存单元的大小由第三个参数u定义。 f:表示addr指向的内存内容的输出格式, s对应输出字符串,此处需特别注意输出整型数据的格式: x 按十六进制格式显示变量. d 按十进制格式显示变量。 u 按十进制格式显示无符号整型。 o 按八进制格式显示变量。 t 按二进制格式显示变量。 a 按十六进制格式显示变量。 c 按字符格式显示变量。 f 按浮点数格式显示变量。 u:就是指以多少个字节做为一个内存单元-unit,默认为4。 u还能够用被一些字符表示: 如b=1 byte, h=2 bytes,w=4 bytes,g=8 bytes.<addr>:表示内存地址。
利用上面的print和x命令,再结合容器的数据结构,咱们就能知道容器的详细信息。这里举个完整打印二进制string的例子,string的数据结构以下:
string为空时,_M_dataplus._M_p是指向nullptr的。当赋值后会在堆上申请一段内存,分为两段,前半段是meta信息(类型为std::string::_Rep),如length、capacity、refcount,后半段为数据区,_M_p指向数据区。
一般状况下非二进制的string,直接print便可显示数据内容,但当数据为二进制时,'\0'会截断打印内容。所以,打印二进制string的首要任务是确认string的size。
string的size信息保存在std::string::_Rep结构体中,根据上面的数据结构能够发现,_Rep与_M_dataplus._M_p相差一个结构体大小,所以打印_Rep结构体的命令为:
#先把_M_p转成_Rep指针,再让指针向低地址偏移一个结构体大小 p *((std::string::_Rep*)(s._M_dataplus._M_p) - 1)
找到string的size(_M_length)后,再经过x命令打印相关的内存区便可,命令为:
#这里的n是_Rep._M_length x /ncb s._M_dataplus._M_p
运行效果以下:
为了方便,这里推荐一个方便的脚本:stl-views.gdb(连接:https://sourceware.org/gdb/wiki/STLSupport?action=AttachFile&do=view&target=stl-views-1.0.3.gdb,直接在gdb终端**source stl-views.gdb**便可,支持常见的容器打印,如vector、map、list、string等。
程序中常常会使用到静态变量,有时咱们须要查看某个静态对象的值是否正确,就涉及到静态对象的打印。看以下例子:
void foo() { static std::string s_foo("foo"); }
这里能够借助nm -C ./bin | grep xx找到静态变量的内存地址,再经过gdb的print打印。
dump [format] memory filename start_addr end_addr dump [format] value filename expr format通常使用binary,其余的能够查看gdb手册。 好比咱们能够结合上面查看string内容的例子dump整个string数据到文件中。 dump binary memory file1 s._M_dataplus._M_p s._M_dataplus._M_p + length 若是想查看文件内容的话可把vim -b和xxd结合使用。
接上面string的例子,举一个dump string内存数据到文件的例子:
定位core的缘由,首先要定位崩溃时正在执行的代码行,这一节主要介绍一些定位代码行的方法。一般状况下直接经过gdb的breaktrace便可一览整个函数栈,但有时候函数栈信息并不是如此清晰明了,这时就可利用一些小技巧来查看函数栈。
有时候会发现core的函数栈跟实际的代码行不匹配,若是是在线下环境中,能够尝试把编译优化设置成-O0,而后再从新复现core问题。
对于线上core问题,通常无法再对程序进行去编译优化操做,只能在现有的core文件基础上进行代码定位。这一节咱们采用一个例子来介绍如何使用程序计数器 + addr2line来定位代码行。
从截图能够发现frame 20指示的代码行与实际的代码行是不匹配的,定位步骤以下:
# 跳转到第20号栈 frame 20 # 使用display命令显示程序计数器 display /i $rip # 使用addrline工具作地址转换 shell /opt/compiler/gcc-8.2/bin/addr2line -e bin address
3.3.3 函数栈修复
有时候咱们会发现函数调用栈里面会出现不少??的状况,这常发生于栈被写花,某些状况下手动进行修复。函数栈的修复利用的函数栈内存分布知识,见第一节。
----------------------------------- Low addresses ----------------------------------- 0(%rsp) | top of the stack frame | (this is the same as -n(%rbp)) ---------|-------------------------- n(%rbp) | variable sized stack frame- 8(%rbp) | varied 0(%rbp) | previous stack frame address 8(%rbp) | return address ----------------------------------- High addresses
从上面的栈示意图能够发现,利用%rbp寄存器便可找到上一个函数的返回地址和栈底指针,再利用addr2line命令找到对应的代码行。这里举一个例子:
#首先找到当前被调用栈上一个栈的栈底指针值和返回地址 x /2ag $rbp # 2个单位,a=十六进制,g=8字节单元 #使用上一条命令获得的栈底指针值依次递归 x /2ag address
无规律core栈问题通常发生于堆内存写坏。函数调用是一个很是精密的过程,任何一个位置发生非预期的读写都会致使程序崩溃。这里能够举个小例子来讲明:
int main(int argc, char* argv[]) { std::string s("abcd"); *reinterpret_cast<uint64_t*>(&s) = 0x11; return 0; }
上面的例子core在string析构上,缘由是由于string的_M_ptr被改写成了0x11,析构流程变成了非法内存操做。
同理,因为进程堆空间是共享的,一个线程对堆的非法操做就可能会影响另外一个线程的正常操做,因为堆分配的随机性,表现出来的现象就是无规律core栈。
针对无规律core栈最好的方式仍是借助AddressSanitizer。
#设置编译参数CXXFLAGS CXXFLAGS="-fPIC -fsanitize=address -fno-omit-frame-pointer" #设置连接参数 LDFLAGS="-lasan" # 设置启动环境变量 export ASAN_OPTIONS=halt_on_error=0:abort_on_error=1:disable_coredump=0 # 启动 LD_PRELOAD=/opt/compiler/gcc-8.2/lib/libasan.so ./bin/xxx
3.3.5 总结
上面提到的几种方法都是为了找到具体的问题代码行,为后续分析core的具体缘由提供线索。
这一节主要介绍定位Core缘由的方法以及一些常见缘由的介绍。
从上面的Core分类咱们能够发现某些场景的core是因为机器故障致使的,如SIGBUS,所以能够先经过信号量排除掉一些core缘由。
经过上面的代码行定位咱们能够大体找到程序core在哪一行,比较简单的core直接print程序上下文便可找到core的缘由。
但有些场景下,经过排查上下文无任何异常,这个时候就须要准肯定位具体的异常汇编指令,根据指令找缘由。
查看汇编指令比较简单的方法是使用layout asm命令,frame指向那个栈,就显示对应栈的汇编。这里举个core例子,以下:
程序显示core在start函数,查看相关上下文变量均无异常。使用layout asm打开正在执行的汇编指令,以下:
查看汇编定位到程序core在mov指令,mov指令上一个指令为sub,为栈申请了3M空间,怀疑是栈空间不足。采用frame 0的%rsp - frame N的%rbp排查为栈空间不足。
经过上面的例子,能够发现定位异常汇编指令位置后,咱们可以把异常点进一步压缩,定位到是哪一个指令、变量、地址致使的core问题。
经过上面的操做咱们能够准肯定位到具体是哪一行代码的哪一条指令出现了问题,根据异常指令咱们能够排查相关的变量,肯定变量值是否符合预期。
这里举一个比较经典的空指针例子,以下:
int main(int argc, char* argv[]) { int* a = nullptr; *a = 1; return *a; }
经过汇编指令咱们能够发现是movl $0x1, (%rax)出现了问题,%rax的值来自于0x8(%rbp),x命令打印相关的地址就能够发现为空指针错误。
一般状况下程序都是开启了编译优化的,就会出现变量没法被print,提示变量被优化,有时可利用汇编 + 寄存器的方式查看被优化的变量。
这里举一个例子说明下:
void foo(char const* str) { char buf[1024] = {'\0'}; memcpy(buf, str, sizeof(buf)); } int main(int argc, char* argv[]) { foo("abcd"); return 0; }
一般状况下在foo函数内部,str变量是会直接别优化掉的,由于能够直接利用%rdi寄存器传递参数。为了可以打印出str的值,这个时候咱们能够借助汇编 + 寄存器的方式找到具体的变量值,以下:
首先找到main函数调用foo函数的参数压栈汇编:mov $0x402011, %edi,这里的0x402011即为str的内存地址,经过x命令便可显示str的值了。
比较复杂的场景可能无法直接找到被优化变量,这时能够采用汇编回溯的方式找到变量。
有时的core问题是由于数据异常致使,有时也多是优化函数地址致使,如调用虚函数地址错误、函数返回地址错误、函数指针值错误。
异常函数地址排查同理于异常变量排查,根据汇编指令确认调用是否异常便可。这里举一个虚函数地址异常的例子,以下:
class A { public: virtual ~A() = default; virtual void foo() = 0; }; class B : public A { public: void foo() {} }; int main(int argc, char* argv[]) { A* a = new B; a->foo(); A* b = new B; *reinterpret_cast<void**>(b) = 0x0; b->foo(); return 0; }
从汇编指令看是core在了mov (%rax), %rax,结合指令上下文可发现是在虚函数地址寻址操做,对比两个变量的虚函数表便可发现是函数地址load错误致使的core。
定位core的基本流程可总结为如下几步:
明确core的大体触发缘由。机器问题?自身程序问题?
定位代码行。哪一行代码出现了问题。
定位执行指令。哪一行指令干了什么事。
善于利用汇编指令以及打印指令(x、print、display)能够更有效的定位Core。
汇编查看工具:https://godbolt.org/ https://cppinsights.io/
标准GDB文档:https://sourceware.org/gdb/current/onlinedocs/gdb/
招聘信息:
欢迎加入百度移动生态事业群内容中台架构团队,咱们常年需求后端、C++、模型架构、大数据、性能调优的同窗、社招,实习,校招都要哦
简历投递邮箱:geektalk@baidu.com (投递备注【内容架构】)
推荐阅读:
---------- END ----------
百度Geek说
百度官方技术公众号上线啦!
技术干货 · 行业资讯 · 线上沙龙 · 行业大会
招聘信息 · 内推信息 · 技术书籍 · 百度周边
欢迎各位同窗关注