导读:程序core是指应用程序无奈放弃失常running状态而产生的解体行为。程序core时会生成相干的core-dump文件,是程序解体时程序状态的数据备份。core-dump文件中蕴含内存、处理器、寄存器、程序计数器、栈指针等状态信息。本文将介绍一些利用core-dump文件定位程序core起因的办法和技巧。
全文7023字,预计浏览工夫 13分钟。
一、程序Core定义及分类
程序core是指应用程序无奈放弃失常running状态而产生的解体行为。程序core时会生成相干的core-dump文件,core-dump文件是程序解体时程序状态的状态数据备份。core-dump文件蕴含内存、处理器、寄存器、程序计数器、栈指针等状态信息。咱们能够借助core-dump文件来剖析定位程序Core的起因。
这里咱们从三个方面对程序Core进行分类:机器、资源、程序Bug。下表对常见的Core起因进行了分类:
二、函数栈介绍
当咱们关上core文件时,首先关注的是程序解体时的函数调用栈状态,为了不便了解后续定位core的一些技巧,这里先简略介绍一下函数栈。
2.1 寄存器介绍
目前生产环境都为64位机,这里只介绍64位机的寄存器,如下:
对于x86-64架构,共有16个64位寄存器,每个寄存器的用处并不繁多,如%rax通常保留函数返回后果,但也被利用于imul和idiv指令。这里重点关注%rsp(栈顶指针寄存器)、%rbp(栈底指针寄存器)、%rdi、%rsi、%rdx、%rcx、%r8、%r9(别离对应第1~6函数参数)。
Callee Save阐明是否须要被调用者保留寄存器的值。
2.2 函数调用
2.2.1 调用函数栈帧:
在调用一个函数时首先进行的是参数压栈,参数压栈的程序跟参数定义的程序相同。留神,并不是参数肯定会压栈,在x86-64架构中会针对能够应用寄存器传递的变量,间接通过寄存器传值,如数字、指针、援用等。
接着是返回地址压栈,返回地址为被调用函数执行完后,调用函数执行的下一个指令地址。这里牢记返回地址的地位,后续章节会利用到这个返回地址的个性。
针对下面的介绍举个例子阐明:
如上图,在main函数中调用了foo函数,首先对参数压栈,三个参数都能够间接用寄存器传递(别离对应%edi、%esi、%edx),而后call指令将下一个指令压栈。
2.2.2 被调用函数栈帧:
被调用函数首先会将上一个函数的栈底指针(%rbp)保留,即%rbp压栈。而后再保留须要被保留的寄存器值,即Callee Save为True的寄存器。接着为长期变量、局部变量申请栈空间。
针对被调用函数,举个例子阐明:
如上图,在foo函数执行时,先对main函数的%rbp压栈,再把寄存器中的参数值寄存到局部变量(a, b, c)中。
2.3 总结
通过对函数调用的简略介绍,咱们能够发现函数栈是一个周密且软弱的构造,内存构造必须依照严格的形式被拜访,如稍有不慎就可能导致程序解体。
三、GDB定位Core
这一节将介绍从core文件关上到定位全流程中可能会遇到的问题以及解决技巧。
3.1 Core文件
core文件在哪里?
查看“/proc/sys/kernel/core\_pattern”确定core文件生成规定。
3.2 变量打印
程序debug过程中经常要查看各种变量(内存、寄存器、函数表等)的值是否正确,维持独自用一节介绍下罕用的变量打印办法以及一些冷门小技巧。
3.2.1 print命令
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 - 字符串
3.2.2 x命令
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>:示意内存地址。
3.2.3 容器对象打印
利用下面的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_lengthx /ncb s._M_dataplus._M_p
运行成果如下:
为了不便,这里举荐一个不便的脚本:stl-views.gdb(链接:https://sourceware.org/gdb/wi...,间接在gdb终端source stl-views.gdb即可,反对常见的容器打印,如vector、map、list、string等。
3.2.4 动态变量打印
程序中常常会应用到动态变量,有时咱们须要查看某个动态对象的值是否正确,就波及到动态对象的打印。看如下例子:
void foo() { static std::string s_foo("foo");}
这里能够借助nm -C ./bin | grep xx找到动态变量的内存地址,再通过gdb的print打印。
3.2.5 内存dump
dump [format] memory filename start_addr end_addrdump [format] value filename exprformat个别应用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内存数据到文件的例子:
3.3 定位代码行
定位core的起因,首先要定位解体时正在执行的代码行,这一节次要介绍一些定位代码行的办法。通常状况下间接通过gdb的breaktrace即可一览整个函数栈,但有时候函数栈信息并非如此清晰明了,这时就可利用一些小技巧来查看函数栈。
3.3.1 去编译优化
有时候会发现core的函数栈跟理论的代码行不匹配,如果是在线下环境中,能够尝试把编译优化设置成-O0,而后再从新复现core问题。
3.3.2 程序计数器 + addr2line
对于线上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) | varied0(%rbp) | previous stack frame address8(%rbp) | return address-----------------------------------High addresses
从下面的栈示意图能够发现,利用%rbp寄存器即可找到上一个函数的返回地址和栈底指针,再利用addr2line命令找到对应的代码行。这里举一个例子:
#首先找到以后被调用栈上一个栈的栈底指针值和返回地址x /2ag $rbp # 2个单位,a=十六进制,g=8字节单元 #应用上一条命令失去的栈底指针值顺次递归x /2ag address
3.3.4 无规律core栈
无规律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。
#设置编译参数CXXFLAGSCXXFLAGS="-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的具体起因提供线索。
3.4 定位Core起因
这一节次要介绍定位Core起因的办法以及一些常见起因的介绍。
3.4.1 确认信号量
从下面的Core分类咱们能够发现某些场景的core是因为机器故障导致的,如SIGBUS,因而能够先通过信号量排除掉一些core起因。
3.4.2 定位异样汇编指令
通过下面的代码行定位咱们能够大抵找到程序core在哪一行,比较简单的core间接print程序上下文即可找到core的起因。
但有些场景下,通过排查上下文无任何异样,这个时候就须要精确定位具体的异样汇编指令,依据指令找起因。
查看汇编指令比较简单的办法是应用layout asm命令,frame指向那个栈,就显示对应栈的汇编。这里举个core例子,如下:
程序显示core在start函数,查看相干上下文变量均无异样。应用layout asm关上正在执行的汇编指令,如下:
查看汇编定位到程序core在mov指令,mov指令上一个指令为sub,为栈申请了3M空间,狐疑是栈空间有余。采纳frame 0的%rsp - frame N的%rbp排查为栈空间有余。
通过下面的例子,能够发现定位异样汇编指令地位后,咱们可能把异样点进一步压缩,定位到是哪个指令、变量、地址导致的core问题。
3.4.3 排查异样变量
通过下面的操作咱们能够精确定位到具体是哪一行代码的哪一条指令呈现了问题,依据异样指令咱们能够排查相干的变量,确定变量值是否合乎预期。
这里举一个比拟经典的空指针例子,如下:
int main(int argc, char* argv[]) { int* a = nullptr; *a = 1; return *a;}
通过汇编指令咱们能够发现是movl $0x1, (%rax)呈现了问题,%rax的值来自于0x8(%rbp),x命令打印相干的地址就能够发现为空指针谬误。
3.4.4 查看被优化变量
通常状况下程序都是开启了编译优化的,就会呈现变量无奈被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的值了。
比较复杂的场景可能没法间接找到被优化变量,这时能够采纳汇编回溯的形式找到变量。
3.4.5 异样函数地址排查
有时的core问题是因为数据异样导致,有时也可能是优化函数地址导致,如调用虚函数地址谬误、函数返回地址谬误、函数指针值谬误。
异样函数地址排查同理于异样变量排查,依据汇编指令确认调用是否异样即可。这里举一个虚函数地址异样的例子,如下:
classA {public: virtual ~A() = default; virtual void foo() = 0;};classB : 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。
3.4.6 总结
定位core的根本流程可总结为以下几步:
- 明确core的大抵触发起因。机器问题?本身程序问题?
- 定位代码行。哪一行代码呈现了问题。
- 定位执行指令。哪一行指令干了什么事。
- 定位异样变量。指令不会有问题,是指令操作的变量不合乎预期。
长于利用汇编指令以及打印指令(x、print、display)能够更无效的定位Core。
参考资料:
汇编查看工具:https://godbolt.org/ https://cppinsights.io/
规范GDB文档:https://sourceware.org/gdb/cu...
招聘信息:
欢送退出百度挪动生态事业群内容中台架构团队,咱们长年需要后端、C++、模型架构、大数据、性能调优的同学、社招,实习,校招都要哦
简历投递邮箱:geektalk@baidu.com (投递备注【内容架构】)
举荐浏览:
|面向大规模商业系统的数据库设计和实际
|百度爱番番挪动端网页秒开实际
|解密百TB数据分析如何跑进45秒
---------- END ----------
百度Geek说
百度官网技术公众号上线啦!
技术干货 · 行业资讯 · 线上沙龙 · 行业大会
招聘信息 · 内推信息 · 技术书籍 · 百度周边
欢送各位同学关注