dmp文件分析(二)- symbol


| 阅读 |,阅读约 11 分钟
| 复制链接:

Overview

dmp文件分析(二)- symbol

上一篇回顾了dmp的解析,并介绍了dmp分析的入口,通过不同的cpu架构,构造Stackwalker的不同子类,并调用Walk方法分析调用栈,其中Walk方法中包含两步:

  • 读取寄存器中的eip,构造初始化栈帧
  • 提取symbol信息,用于协助分析,使分析结果能准确到代码行数级别
  • 根据eip,分析顶层栈调用情况
  • 根据栈帧查找原理,获取caller(上层调用者)的栈帧信息,直到栈帧结束

第一步上一篇已经介绍过,这篇文章主要介绍内容包括:

  • symbol是什么
  • symbol文件格式以及存储格式规范
  • 如何通过寄存器eip,快速定位到哪个模块崩溃
  • 如何通过某种查找规则,找到模块对应的symbol

Walk方法回顾

核心方法包括以下三步

  • GetContextFrame:填充eip,构造顶层栈帧(上一篇已经介绍)
  • FillSourceLineInfo:获取符号信息,协助准确定位崩溃代码行
  • GetCallerFrame:获取调用者的栈帧
 1bool Stackwalker::Walk(
 2    CallStack* stack,
 3    vector<const CodeModule*>* modules_without_symbols,
 4    vector<const CodeModule*>* modules_with_corrupt_symbols) {
 5  ...
 6  // 构造一个栈帧,并取出context中的寄存器的eip,并缓存下来
 7  scoped_ptr<StackFrame> frame(GetContextFrame());
 8
 9  // 这里是一个死循环,一直分析并提取堆栈,直到下一个栈帧为空
10  while (frame.get()) {
11    // 栈帧始终包含一个有效的调用栈信息,并设置指令地址
12    // 指令地址来自context中的eip,或者被调用者
13
14    // 提取symbol信息,协助分析
15    StackFrameSymbolizer::SymbolizerResult symbolizer_result =
16        frame_symbolizer_->FillSourceLineInfo(modules_, unloaded_modules_,
17                                              system_info_,
18    // 添加分析完毕的栈帧信息到堆栈列表中
19    stack->frames_.push_back(frame.release());
20
21    // 获取下一个栈帧(调用者的栈帧)
22    bool stack_scan_allowed = scanned_frames < max_frames_scanned_;
23
24    // 栈帧回溯的核心方法:GetCallerFrame
25    frame.reset(GetCallerFrame(stack, stack_scan_allowed));
26  }
27
28  return true;
29}

symbol概述

为什么会有symbol

应用程序在编译时,如果开启某些编译选型,编译器通常会将带有符号相关的信息保存。比如:windows下的pdb文件,linux下的so文件。便于后续定位崩溃的代码位置。breakpad作为一个跨平台的分析工具,不应该跟特定平台的符号文件关联,于是规定了一种symbol文件格式,屏蔽各个平台的差异。

symbol的特点

  • 纯文本格式,不是二进制,可直接打开查看
  • 文件很小。通常pdb、so等文件非常大

如何生成symbol文件

  • breakpad提供的三大组件之一:symbole dumper,专门用于将各个平台的调试文件生成symbol文件

symbole文件格式

官方文档

样例:

1MODULE windows x86 3AA3B2229C144C24AEBEF3D971F32D711 GMTSJWorker.pdb
2FILE 18430 d:\winmain.public.x86fre\sdk\inc\rpcdce.h
3FUNC b7990 49 0 `dynamic initializer for 'std::_Error_objects<int>::_Generic_object''
4b7990 49 611 282
5FUNC c2a70 327 0 boost::algorithm::detail::process_segment_helper<0>::operator()<std::deque<wchar_t,std::allocator<wchar_t> >,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >,std::_String_iterator<std::_String_val<std::_Simple_types<wchar_t> > > >(std::deque<wchar_t,std::allocator<wchar_t> > &,std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> > &,std::_String_iterator<std::_String_val<std::_Simple_types<wchar_t> > >,std::_String_iterator<std::_String_val<std::_Simple_types<wchar_t> > >,std::_String_iterator<std::_String_val<std::_Simple_types<wchar_t> > >)
6c2a70 53 63 6337
7c2ac3 62 71 6337
8STACK WIN 4 207410 f 3 0 0 0 0 0 1 $T0 $ebp = $eip $T0 4 + ^ = $ebp $T0 ^ = $esp $T0 8 + =
  • MODULE:第一条记录,用来描述当前这个模块文件
  • FILE:记录源文件,包含有文件名及路径信息。会被分配一个整形符号来作标记,然后在别的记录中可能会引用它。
  • FUNC:这种记录用来描述一个函数,包含函数名,函数在可执行文件中的地址等信息
  • Line:这种记录没有类型,描述一个给定范围的机器指令对应哪个源文件的哪一行。行记录总是跟在FUNC记录后面,从而描述每个函数里的指令对应在源码里的位置。
  • PUBLIC: 这种记录用来描述每一个链接符号的地址,如汇编函数里的各个入口点
  • STACK WIN: 这种记录用来描述函数调用时,函数帧(stack frame)的布局。有了这个记录,给定一特定的函数帧F,就可以找到哪个函数帧调用了F
  • STACK CFI:CFI, 就是Call Frame Info,这种记录用来表述当执行到某条指令的时候,怎样去查看当前的函数调用栈。

symbol文件存放格式

symbol文件被dmp分析工具分析时,需要按照一定的存放格式存储,才能被分析工具找到,比如windows平台下:

后面介绍源码的时候,就会看到它为什么需要按照这个格式存储

1msvcp100.amd64.pdb/FAE62411E45D4218AB9761C4E55F64791/msvcp100.amd64.sym

格式为:

{模块名.pdb}/{模块构建时的UUID}/{模块名.sym}

symbol信息填充

FillSourceLineInfo方法提取相关的符号信息,并填充符号信息到分析的调用栈中

  • 先通过寄存器的eip地址,去module列表中匹配内存地址,确定是在运行那个module崩溃了
  • 通过module去匹配符号信息
    • 如果匹配到,填充源码信息
    • 如果匹配不到,直接返回(只知道崩溃在哪个模块,无法知道崩溃在哪行代码)
 1StackFrameSymbolizer::SymbolizerResult StackFrameSymbolizer::FillSourceLineInfo(
 2    const CodeModules* modules,
 3    const CodeModules* unloaded_modules,
 4    const SystemInfo* system_info,
 5    StackFrame* frame) {
 6
 7  const CodeModule* module = NULL;
 8  if (modules) {
 9    // frame->instruction就是上一篇介绍到的,从寄存器中获取的eip指令地址
10    // eip寄存器,用来存储CPU要读取指令的地址
11    // 崩溃的时候,eip存放的地址,就是崩溃时的指令地址
12    // 这里根据eip指令的地址,去modules列表中去匹配相关的module,确定在哪个module崩溃了
13    // 分析module的读取时,我们说过,modules中根据模块的内存地址,建立了RangeMap的索引
14    //     在这里,就能快速定位到当前指令是在哪个模块执行时崩溃的
15    module = modules->GetModuleForAddress(frame->instruction);
16  }
17  // 如果加载的模块内存,匹配不到eip指令,就去未加载的模块中去匹配
18  if (!module && unloaded_modules) {
19    module = unloaded_modules->GetModuleForAddress(frame->instruction);
20  }
21
22  // 找到模块后,保存到当前分析的栈帧中
23  frame->module = module;
24
25  // 每个module,都会尝试去匹配module相关的symbol信息,
26  // 如果找不到symbol信息,就放到无symbol的名单中,下次就不用再尝试去找了
27  if (no_symbol_modules_.find(module->code_file()) !=
28      no_symbol_modules_.end()) {
29    return kError;
30  }
31
32  // resolver_保存了所有已经和symbol信息关联上的module列表
33  // 首先判断该模块是否加载过symol了,如果加载过,直接将symbol中的代码行数信息进行填充
34  if (resolver_->HasModule(frame->module)) {
35    resolver_->FillSourceLineInfo(frame);
36    return resolver_->IsModuleCorrupt(frame->module) ?
37        kWarningCorruptSymbols : kNoError;
38  }
39
40  // 如果module和symbol还没有做过关联,开始尝试去提取symbol
41  string symbol_file;
42  char* symbol_data = NULL;
43  size_t symbol_data_size;
44
45  // 这里是提取symbol的核心方法
46  // 判断给定的symbol路径下,有没有和moduel匹配的sym信息
47  // 并不是暴力搜索,而是按照规则去匹配
48  SymbolSupplier::SymbolResult symbol_result = supplier_->GetCStringSymbolData(
49      module, system_info, &symbol_file, &symbol_data, &symbol_data_size);
50  // 处理匹配结果
51  switch (symbol_result) {
52    case SymbolSupplier::FOUND: {
53      // 成功找到symbol信息,symbol_data中保存的是读取到symbol文本内容
54      // 使用symbol内容做下一步操作
55      bool load_success = resolver_->LoadModuleUsingMemoryBuffer(
56          frame->module,
57          symbol_data,
58          symbol_data_size);
59      if (resolver_->ShouldDeleteMemoryBufferAfterLoadModule()) {
60        supplier_->FreeSymbolData(module);
61      }
62      // 加载成功,填充源码信息到结果中
63      if (load_success) {
64        resolver_->FillSourceLineInfo(frame);
65        return resolver_->IsModuleCorrupt(frame->module) ?
66            kWarningCorruptSymbols : kNoError;
67      } else {
68        // 加载失败,认为该模块是无法匹配symbol的,下次就不用再分析这个module了
69        no_symbol_modules_.insert(module->code_file());
70        return kError;
71      }
72    }
73
74    case SymbolSupplier::NOT_FOUND:
75    // 匹配不到符号文件,认为该模块是无法匹配symbol的,下次就不用再分析这个module了
76      no_symbol_modules_.insert(module->code_file());
77      return kError;
78
79    case SymbolSupplier::INTERRUPT:
80      return kInterrupt;
81
82    default:
83      BPLOG(ERROR) << "Unknown SymbolResult enum: " << symbol_result;
84      return kError;
85  }
86  return kError;
87}

定位某个module的symbol

前面源码注释中提到一个很重要的方法,如何定位魔都个module的symbol,源码如下, 源码位置:src/processor/SimpleSymbolSupplier.cc

 1SymbolSupplier::SymbolResult SimpleSymbolSupplier::GetCStringSymbolData(
 2    const CodeModule *module,
 3    const SystemInfo *system_info,
 4    string *symbol_file,
 5    char **symbol_data,
 6    size_t *symbol_data_size) {
 7
 8  // 调用GetSymbolFile方法去找符号信息
 9  string symbol_data_string;
10  SymbolSupplier::SymbolResult s =
11      GetSymbolFile(module, system_info, symbol_file, &symbol_data_string);
12
13  // 如果找到符号信息,将读取的字符串保存
14  if (s == FOUND) {
15    *symbol_data_size = symbol_data_string.size() + 1;
16    *symbol_data = new char[*symbol_data_size];
17    if (*symbol_data == NULL) {
18      BPLOG(ERROR) << "Memory allocation for size " << *symbol_data_size
19                   << " failed";
20      return INTERRUPT;
21    }
22    memcpy(*symbol_data, symbol_data_string.c_str(), symbol_data_string.size());
23    (*symbol_data)[symbol_data_string.size()] = '\0';
24    // 将符号信息缓存下来,key为模块的完整路径名,value为symbol信息
25    memory_buffers_.insert(make_pair(module->code_file(), *symbol_data));
26  }
27  return s;
28}

GetSymbolFile

 1SymbolSupplier::SymbolResult SimpleSymbolSupplier::GetSymbolFile(
 2    const CodeModule *module, const SystemInfo *system_info,
 3    string *symbol_file) {
 4
 5  // minidump_stackwalk传symbol路径时,可以传入多个
 6  // 遍历所有的symbol路径
 7  for (unsigned int path_index = 0; path_index < paths_.size(); ++path_index) {
 8    SymbolResult result;
 9    // 针对每个路径,调用符号查找方法
10    // 如果找到了,直接返回不再继续查找
11    if ((result = GetSymbolFileAtPathFromRoot(module, system_info,
12                                              paths_[path_index],
13                                              symbol_file)) != NOT_FOUND) {
14      return result;
15    }
16  }
17  return NOT_FOUND;
18}

GetSymbolFileAtPathFromRoot

这个方法中,根据一定的规则去查找符号文件,了解完这个方法后,就能知道为什么symbol存储时,必须遵照前面的规则

  • 源码路径查找规则:{module.pdb}/{uuid}/{moduel.sym},这也解释了为什么前面说到symbol保存的路径有一定要求
  • 其中uuid是一个很重要的信息,在源码中叫做debug_identifier
  • 源码编译时,会自动生成一个debug_identifier,分别写到pdb文件和dll文件
  • dmp分析时,二者必须完全对应上,否则无法分析(路径查找不到)
 1SymbolSupplier::SymbolResult SimpleSymbolSupplier::GetSymbolFileAtPathFromRoot(
 2    const CodeModule *module, const SystemInfo *system_info,
 3    const string &root_path, string *symbol_file) {
 4
 5  // 拼接查找路径
 6  string path = root_path;
 7  path.append("/");
 8  // 获取模块的debug_file,具体到windows平台,就是module.pdb, 比如msvcp100.pdb
 9  string debug_file_name = PathnameStripper::File(module->debug_file());
10  // 获取模块的唯一标识
11  string identifier = module->debug_identifier();
12  if (debug_file_name.empty()) {
13    BPLOG(ERROR) << "Can't construct symbol file path without debug_file "
14                    "(code_file = " <<
15                    PathnameStripper::File(module->code_file()) << ")";
16    return NOT_FOUND;
17  }
18  path.append(debug_file_name);
19
20  // Append the identifier as a directory name.
21  path.append("/");
22
23  // 如果唯一标识为空,直接返回错误,认为这个symbol找不到
24  if (identifier.empty()) {
25    return NOT_FOUND;
26  }
27  path.append(identifier);
28
29  // 获取debug_file名称,如果后缀为.pdb,将后缀改为.sym
30  path.append("/");
31  string debug_file_extension;
32  if (debug_file_name.size() > 4)
33    debug_file_extension = debug_file_name.substr(debug_file_name.size() - 4);
34  std::transform(debug_file_extension.begin(), debug_file_extension.end(),
35                 debug_file_extension.begin(), tolower);
36  if (debug_file_extension == ".pdb") {
37    path.append(debug_file_name.substr(0, debug_file_name.size() - 4));
38  } else {
39    path.append(debug_file_name);
40  }
41  path.append(".sym");
42
43  // 判断文件是否存在,不存在直接返回找不到
44  if (!file_exists(path)) {
45    BPLOG(INFO) << "No symbol file at " << path;
46    return NOT_FOUND;
47  }
48
49  // 路径存在,说明成功找到symbol
50  *symbol_file = path;
51  return FOUND;
52}

读取symbol文件的后续操作

前一个方法读取到symbol文本,并保存到字符串中,之后调用以下方法进行后续处理

  • LoadModuleUsingMemoryBuffer:使用内存缓存加载模块
  • FillSourceLineInfo:填充崩溃处栈帧对应的源码信息
LoadModuleUsingMemoryBuffer
 1bool SourceLineResolverBase::LoadModuleUsingMemoryBuffer(
 2    const CodeModule *module,
 3    char *memory_buffer,
 4    size_t memory_buffer_size) {
 5  // 工厂方法构造一个Module子类,用于接收symbol读取后的文本,并转换成对象
 6  Module *basic_module = module_factory_->CreateModule(module->code_file());
 7
 8  // 将symbol文本格式转换为特定的模型
 9  if (!basic_module->LoadMapFromMemory(memory_buffer, memory_buffer_size)) {
10    BPLOG(ERROR) << "Too many error while parsing symbol data for module "
11                 << module->code_file();
12    assert(basic_module->IsCorrupt());
13  }
14
15  modules_->insert(make_pair(module->code_file(), basic_module));
16}
解析symbol文本
  • LoadMapFromMemory方法解析symbol文件
  • 依次读取前面介绍过的symbol中的各种行
    • MODULE
    • FILE
    • PUBLIC
    • STACK
    • INFO
  1bool BasicSourceLineResolver::Module::LoadMapFromMemory(
  2    char *memory_buffer,
  3    size_t memory_buffer_size) {
  4
  5  // 首先确保字符串末尾以‘\n`结尾
  6  size_t last_null_terminator = memory_buffer_size - 1;
  7  if (memory_buffer[last_null_terminator] != '\0') {
  8    memory_buffer[last_null_terminator] = '\0';
  9  }
 10
 11  // 跳过所有的'\n'字符(除了末尾的)
 12  // 并且确保读取的symbol字符串没有'\n',有的都替换为‘_'
 13  bool has_null_terminator_in_the_middle = false;
 14  while (last_null_terminator > 0 &&
 15         memory_buffer[last_null_terminator - 1] == '\0') {
 16    last_null_terminator--;
 17  }
 18  for (size_t i = 0; i < last_null_terminator; i++) {
 19    if (memory_buffer[i] == '\0') {
 20      memory_buffer[i] = '_';
 21      has_null_terminator_in_the_middle = true;
 22    }
 23  }
 24  // 如果有‘\n’字符在中间,抛出错误提示信息
 25  if (has_null_terminator_in_the_middle) {
 26    LogParseError(
 27       "Null terminator is not expected in the middle of the symbol data",
 28       line_number,
 29       &num_errors);
 30  }
 31
 32  // 将文本字符串按照'\r\n'做切割,就是按行切割成字符串列表
 33  char *buffer;
 34  buffer = strtok_r(memory_buffer, "\r\n", &save_ptr);
 35
 36  // 遍历所有行
 37  while (buffer != NULL) {
 38    ++line_number;
 39
 40    // 解析symbol中的 FILE(前5个字符是FILE)
 41    if (strncmp(buffer, "FILE ", 5) == 0) {
 42      // 将FILE字符串转换为对象,并保存到一个保存File信息的RangeMap中
 43      if (!ParseFile(buffer)) {
 44        LogParseError("ParseFile on buffer failed", line_number, &num_errors);
 45      }
 46    }else if (strncmp(buffer, "STACK ", 6) == 0) {
 47      // 处理 symbole中的 STACK(前六个字符是STACK)
 48      if (!ParseStackInfo(buffer)) {
 49        LogParseError("ParseStackInfo failed", line_number, &num_errors);
 50      }
 51    } else if (strncmp(buffer, "FUNC ", 5) == 0) {
 52      // 处理 symbole中的 FUNC(前五个字符是STACK)
 53      cur_func.reset(ParseFunction(buffer));
 54      if (!cur_func.get()) {
 55        LogParseError("ParseFunction failed", line_number, &num_errors);
 56      } else {
 57        // StoreRange will fail if the function has an invalid address or size.
 58        // We'll silently ignore this, the function and any corresponding lines
 59        // will be destroyed when cur_func is released.
 60        functions_.StoreRange(cur_func->address, cur_func->size, cur_func);
 61      }
 62    } else if (strncmp(buffer, "PUBLIC ", 7) == 0) {
 63      // 处理 symbole中的 FUNC(前五个字符是STACK)
 64      // Clear cur_func: public symbols don't contain line number information.
 65      cur_func.reset();
 66
 67      if (!ParsePublicSymbol(buffer)) {
 68        LogParseError("ParsePublicSymbol failed", line_number, &num_errors);
 69      }
 70    } else if (strncmp(buffer, "MODULE ", 7) == 0) {
 71      // 处理 symbole中的 MODULE(前7个字符是MODULE)
 72      // 忽略不处理,没有崩溃的有用信息
 73      // 主要用来做唯一标识,格式如下:
 74      // MODULE <guid> <age> <filename>
 75    } else if (strncmp(buffer, "INFO ", 5) == 0) {
 76      // 处理 symbole中的 INFO(前5个字符是INFO)
 77      // 忽略不处理,没有崩溃的有用信息,格式如下:
 78      // INFO CODE_ID <code id> <filename>
 79    } else {
 80      if (!cur_func.get()) {
 81        LogParseError("Found source line data without a function",
 82                       line_number, &num_errors);
 83      } else {
 84        Line *line = ParseLine(buffer);
 85        if (!line) {
 86          LogParseError("ParseLine failed", line_number, &num_errors);
 87        } else {
 88          cur_func->lines.StoreRange(line->address, line->size,
 89                                     linked_ptr<Line>(line));
 90        }
 91      }
 92    }
 93    if (num_errors > kMaxErrorsBeforeBailing) {
 94      break;
 95    }
 96    buffer = strtok_r(NULL, "\r\n", &save_ptr);
 97  }
 98  is_corrupt_ = num_errors > 0;
 99  return true;
100}
FillSourceLineInfo
  • 该函数将加载的符号信息和栈帧的信息做匹配
  • 然后将符号信息中匹配到的源代码信息赋值给栈帧对象
 1void SourceLineResolverBase::FillSourceLineInfo(StackFrame *frame) {
 2  if (frame->module) {
 3    ModuleMap::const_iterator it = modules_->find(frame->module->code_file());
 4    if (it != modules_->end()) {
 5      it->second->LookupAddress(frame);
 6    }
 7  }
 8}
 9
10void BasicSourceLineResolver::Module::LookupAddress(StackFrame *frame) const {
11  MemAddr address = frame->instruction - frame->module->base_address();
12
13  // First, look for a FUNC record that covers address. Use
14  // RetrieveNearestRange instead of RetrieveRange so that, if there
15  // is no such function, we can use the next function to bound the
16  // extent of the PUBLIC symbol we find, below. This does mean we
17  // need to check that address indeed falls within the function we
18  // find; do the range comparison in an overflow-friendly way.
19  linked_ptr<Function> func;
20  linked_ptr<PublicSymbol> public_symbol;
21  MemAddr function_base;
22  MemAddr function_size;
23  MemAddr public_address;
24  if (functions_.RetrieveNearestRange(address, &func, &function_base,
25                                      NULL /* delta */, &function_size) &&
26      address >= function_base && address - function_base < function_size) {
27    frame->function_name = func->name;
28    frame->function_base = frame->module->base_address() + function_base;
29
30    linked_ptr<Line> line;
31    MemAddr line_base;
32    if (func->lines.RetrieveRange(address, &line, &line_base, NULL /* delta */,
33                                  NULL /* size */)) {
34      FileMap::const_iterator it = files_.find(line->source_file_id);
35      if (it != files_.end()) {
36        frame->source_file_name = files_.find(line->source_file_id)->second;
37      }
38      frame->source_line = line->line;
39      frame->source_line_base = frame->module->base_address() + line_base;
40    }
41  } else if (public_symbols_.Retrieve(address,
42                                      &public_symbol, &public_address) &&
43             (!func.get() || public_address > function_base)) {
44    frame->function_name = public_symbol->name;
45    frame->function_base = frame->module->base_address() + public_address;
46  }
47}