关于后端:如何快速定位程序Core

7次阅读

共计 6829 个字符,预计需要花费 18 分钟才能阅读完成。

导读:程序 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_length
x /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_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/cu…

招聘信息:

欢送退出百度挪动生态事业群内容中台架构团队,咱们长年需要后端、C++、模型架构、大数据、性能调优的同学、社招,实习,校招都要哦

简历投递邮箱:geektalk@baidu.com(投递备注【内容架构】)

举荐浏览

|面向大规模商业系统的数据库设计和实际

|百度爱番番挪动端网页秒开实际

|解密百 TB 数据分析如何跑进 45 秒

———- END ———-

百度 Geek 说

百度官网技术公众号上线啦!

技术干货 · 行业资讯 · 线上沙龙 · 行业大会

招聘信息 · 内推信息 · 技术书籍 · 百度周边

欢送各位同学关注

正文完
 0