导读:程序core是指应用程序无法保持正常running状态而发生的崩溃行为。程序core时会生成相关的core-dump文件,是程序崩溃时程序状态的数据备份。core-dump文件中包含内存、处理器、寄存器、程序计数器、栈指针等状态信息。本文将介绍一些利用core-dump文件定位程序core原因的方法和技巧。

一、程序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_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等。

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_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内存数据到文件的例子:

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) | 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

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。

#设置编译参数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的具体原因提供线索。

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问题是因为数据异常导致,有时也可能是优化函数地址导致,如调用虚函数地址错误、函数返回地址错误、函数指针值错误。

异常函数地址排查同理于异常变量排查,根据汇编指令确认调用是否异常即可。这里举一个虚函数地址异常的例子,如下:

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。

3.4.6 总结
定位core的基本流程可总结为以下几步:

  1. 明确core的大致触发原因。机器问题?自身程序问题?
  2. 定位代码行。哪一行代码出现了问题。
  3. 定位执行指令。哪一行指令干了什么事。
  4. 定位异常变量。指令不会有问题,是指令操作的变量不符合预期。

善于利用汇编指令以及打印指令(x、print、display)可以更有效的定位Core。

参考资料:
汇编查看工具:https://godbolt.org/ https://cppinsights.io/
标准GDB文档:https://sourceware.org/gdb/current/onlinedocs/gdb/

原文链接:
如何快速定位程序Core?

如何快速定位程序Core?相关推荐

  1. 【iOS】iOS 调试快速定位程序在哪崩溃

    iOS 开发过程中经常遇到程序崩溃.快速定位程序在哪崩溃的步骤如下: 1. 2. 3. 这样设置后,程序崩溃时会定位到崩溃的语句,如下: 原文链接:iOS开发何如在调试的时候轻松找到程序在哪里崩溃 转 ...

  2. 使用MAP文件快速定位程序崩溃代码行

    作为程序员,平时最担心见到的事情就是程序发生了崩溃,无论是指针越界还是非法操作,都将给我们的应用系统 造成巨大的损失.但在一个大型系统的测试过程中,初期出现程序崩溃似乎成了不可避免的事.其实测试中出现 ...

  3. 使用MAP文件快速定位程序崩溃代码行(转)

    作为程序员,平时最担心见到的事情就是程序发生了崩溃,无论是指针越界还是非法操作,都将给我们的应用系统造成巨大的损失.但在一个大型系统的测试过程中,初期出现程序崩溃似乎成了不可避免的事.其实测试中出现程 ...

  4. 嵌入式linux应用程序崩溃,嵌入式Linux gdb core dump快速定位程序crash问题

    指定生成 core dump 文件: echo "/tmp/core-%e-%p-%t" > /proc/sys/kernel/core_pattern ulimit -c ...

  5. .net程序调试一:快速定位异常

    .net程序调试一:快速定位异常 参考文章: (1).net程序调试一:快速定位异常 (2)https://www.cnblogs.com/yuilin/p/3788796.html 备忘一下.

  6. [原]调试实战——程序CPU占用率飙升,你知道如何快速定位吗?

    前言 如果我们自己的程序的CPU Usage(CPU占用率)飙升,并且居高不下,很有可能陷入了死循环.你知道怎么快速定位并解决吗?今天跟大家分享几种定位方法,希望对你有所帮助. 如何判断是否有死循环? ...

  7. Go程序内存泄露问题快速定位 | Gopher Daily (2021.09.01) ʕ◔ϖ◔ʔ

    每日一谚:Less is more. Go技术生态 github针对go仓库提供免费的持续benchmark服务 - https://github.com/marketplace/gobencher ...

  8. dSYM-如何获取dSYM UUID并快速定位到已发布程序的crash位置(二)

    上一篇介绍到使用友盟统计分析,结合日志信息和.xcarchive文件和.dSYM工具快速定位crash位置.但如果项目中没有集成友盟统计分析或者其他第三方统计分析,我们便可以在项目中自己做一个日志的类 ...

  9. 推荐一个快速定位深度学习代码bug的炼丹神器!

    文 | McGL 源 | 知乎 写深度学习网络代码,最大的挑战之一,尤其对新手来说,就是把所有的张量维度正确对齐.如果以前就有TensorSensor这个工具,相信我的头发一定比现在更浓密茂盛! Te ...

最新文章

  1. void*与int互转
  2. 菜鸟学前端之遍寻名师
  3. vs2012使用64位IIS EXPRESS调试
  4. 国产Dhyana禅定x86处理器开始启动生产
  5. Boost:boost :: mem_fn等式运算符的测试程序
  6. PC 上访问设备数据库的方法
  7. shell结合expect写的批量scp脚本工具
  8. VGA光端机技术原理及应用领域介绍
  9. JAVA刷题方法整理
  10. android abrc 9.png,android从sdcard加载.9.png图片
  11. Enabled AWE
  12. Spring8中lambda表达式的学习(Function接口、BiFunction接口、Consumer接口)
  13. 菜鸟升级记——模板方法模式
  14. 体彩大乐透模拟机选随机号码_模拟
  15. html设置表格列宽百 分比,WPS解决实现单页显示 高分辨率显示器百分之百比例下双页改单页方法...
  16. gif动图怎么制作?gif动图制作教程大全
  17. 微信小程序通过BLE低功耗蓝牙向USB HID键盘设备输出汉字(内含GBK编码转汉字)
  18. 计算机表格做八折怎么辛,原来古人的书信常用语那么美
  19. u盘中毒文件为html文档,U盘u盘中毒,文件被隐藏了怎么办 – 手机爱问
  20. DC-DC电源设计[基于MP2315]

热门文章

  1. 将SQL Server中所有表的列信息显示出来
  2. 【Pycharm】专业版连接xshell 远程服务器
  3. 浅析数据库case when 用法
  4. Hbase 预写日志WAL处理源码分析之 LogCleaner
  5. Python3字符串的编码
  6. Java客户端操作HBase:插入数据(逐条插入与批量插入)代码示例
  7. zuul默认的路由规则及禁用路由规则
  8. JVM 调优实战--jvisualvm远程连接使用教程
  9. Java客户端操作elasticsearch--向索引库添加mappings映射数据
  10. Mybatis执行select语句无匹配对象时返回集为Empty还是null