dmp文件分析(一)- 概述


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

Overview

dmp文件分析(一)

前面的章节已经将dmp文件读取分析完成,读取过程将dmp中的信息加载到对象中,用于后续分析,这篇文章开始介绍分析流程。

dmp读取流程回顾

如下源码已经做了详细注释,总结如下:

  1. 读取头文件中的文件创建时间
  2. 读取进程运行时长
  3. 读取cpu信息
  4. 读取操作系统信息
  5. 读取崩溃的线程id
  6. 读取崩溃原因
  7. 读取断言信息
  8. 读取模块信息,并拷贝一份,用于后续分析(分析时会修改原始数据)
  9. 读取内存信息
  10. 读取线程信息
  11. for循环,依次分析每个线程的调用栈

分析调用栈需要的数据

  • 操作系统信息
  • 上下文信息
  • 线程内存信息
  • 模块列表
  • 符号相关参数信息
  1ProcessResult MinidumpProcessor::Process(
  2    Minidump *dump, ProcessState *process_state) {
  3
  4  // 读取dmp文件头
  5  const MDRawHeader *header = dump->header();
  6  // 取出创建的时间戳
  7  process_state->time_date_stamp_ = header->time_date_stamp;
  8
  9  // 读取misc stream中的进程运行时长
 10  bool has_process_create_time =
 11      GetProcessCreateTime(dump, &process_state->process_create_time_);
 12
 13  // 读取system stream中的cpu、处理器架构信息
 14  bool has_cpu_info = GetCPUInfo(dump, &process_state->system_info_);
 15  // 读取system stream中的操作系统信息
 16  bool has_os_info = GetOSInfo(dump, &process_state->system_info_);
 17
 18  // 读取breakpad stream中的自定义崩溃信息
 19  MinidumpBreakpadInfo *breakpad_info = dump->GetBreakpadInfo();
 20  if (breakpad_info) {
 21    // 如果breakpad中有崩溃线程id,以这个线程id为准
 22    has_dump_thread = breakpad_info->GetDumpThreadID(&dump_thread_id);
 23    has_requesting_thread =
 24        breakpad_info->GetRequestingThreadID(&requesting_thread_id);
 25  }
 26  // 读取exception中的异常描述信息
 27  MinidumpException *exception = dump->GetException();
 28  if (exception) {
 29    process_state->crashed_ = true;
 30    has_requesting_thread = exception->GetThreadID(&requesting_thread_id);
 31    // 获取进程崩溃原因
 32    process_state->crash_reason_ = GetCrashReason(
 33        dump, &process_state->crash_address_);
 34  }
 35
 36  // 获取assert stream中的断言信息
 37  process_state->assertion_ = GetAssertion(dump);
 38  // 获取module stream中的模块列表信息
 39  MinidumpModuleList *module_list = dump->GetModuleList();
 40  if (module_list) {
 41    // 将模块信息拷贝一份,后面分析时使用拷贝的副本进行分析
 42    // 原因是:后续在分析调用栈时,会动态的修改这个字段,不能在原有的字段上操作
 43    process_state->modules_ = module_list->Copy();
 44  }
 45  // 获取未加载的模块列表信息(这个模块跟module stream类似,没有专门介绍)
 46  MinidumpUnloadedModuleList *unloaded_module_list =
 47      dump->GetUnloadedModuleList();
 48  // 获取 memory stream中的内存信息
 49  MinidumpMemoryList *memory_list = dump->GetMemoryList();
 50  // 获取 thread stream中的线程列表信息
 51  MinidumpThreadList *threads = dump->GetThreadList();
 52
 53  // 遍历所有的线程
 54  for (unsigned int thread_index = 0;
 55       thread_index < thread_count;
 56       ++thread_index) {
 57
 58    MinidumpThread *thread = threads->GetThreadAtIndex(thread_index);
 59
 60    // breakpad中记录的线程,表示是崩溃线程,这个线程跳过分析
 61    // 因为这个崩溃的线程的堆栈和上下文信息有可能被损坏了,分析它并不会给我们提供有用信息
 62    if (has_dump_thread && thread_id == dump_thread_id) {
 63      continue;
 64    }
 65
 66    // 获取线程的上下文信息
 67    MinidumpContext *context = thread->GetContext();
 68
 69    if (has_requesting_thread && thread_id == requesting_thread_id) {
 70      // 如果找到两个崩溃线程,出错
 71      if (found_requesting_thread) {
 72        BPLOG(ERROR) << "Duplicate requesting thread: " << thread_string;
 73        return PROCESS_ERROR_DUPLICATE_REQUESTING_THREADS;
 74      }
 75      // 如果exception stream中有上下文信息,就用这个上下文信息代替线程本身的上下文信息
 76      if (process_state->crashed_) {
 77        MinidumpContext *ctx = exception->GetContext();
 78        context = ctx ? ctx : thread->GetContext();
 79      }
 80    }
 81
 82    // 读取线程运行时的内存信息
 83    MinidumpMemoryRegion *thread_memory = thread->GetMemory();
 84    // 如果读取不到线程中存储的内存信息
 85    if (!thread_memory && memory_list) {
 86      // 尝试获取线程堆栈的起始内存地址
 87      uint64_t start_stack_memory_range = thread->GetStartOfStackMemoryRange();
 88      if (start_stack_memory_range) {
 89        // 根据线程栈中的起始内存地址,在进程内存列表中找到一个比较符合的内存信息
 90        thread_memory = memory_list->GetMemoryRegionForAddress(
 91           start_stack_memory_range);
 92      }
 93    }
 94
 95    /**
 96    * 这里是分析的入口函数,前面都是读取dmp文件的各种数据初始化操作
 97    * 外面套了一个for循环,依次对dmp中的每个线程做堆栈的提取和分析
 98    * 从这个函数可以看出,分析每个线程的堆栈需要的参数有:
 99    *   1. 操作系统信息
100        1. 上下文信息
101    *   3. 线程内存信息
102    *   4. 模块列表
103    *   5. 未加载的模块列表
104    *   6. 符号相关参数信息(最后一个参数frame_symbolizer_)
105    */
106    scoped_ptr<Stackwalker> stackwalker(
107        Stackwalker::StackwalkerForCPU(process_state->system_info(),
108                                       context,
109                                       thread_memory,
110                                       process_state->modules_,
111                                       process_state->unloaded_modules_,
112                                       frame_symbolizer_));
113
114    scoped_ptr<CallStack> stack(new CallStack());
115    if (stackwalker.get()) {
116      if (!stackwalker->Walk(stack.get(),
117                             &process_state->modules_without_symbols_,
118                             &process_state->modules_with_corrupt_symbols_)) {
119        BPLOG(INFO) << "Stackwalker interrupt (missing symbols?) at "
120                    << thread_string;
121        interrupted = true;
122      }
123    } else {
124      // 线程如果找不到cpu上下文信息,将会走到这一步
125      // 但是并不会终止分析,只是认为只是一个被损坏的线程,会继续进行下一个线程的分析,
126      BPLOG(ERROR) << "No stackwalker for " << thread_string;
127    }
128    // 重置栈的线程id
129    stack->set_tid(thread_id);
130    // 保存栈帧信息到堆栈列表
131    process_state->threads_.push_back(stack.release());
132    // 保存线程内存信息到内存列表
133    process_state->thread_memory_regions_.push_back(thread_memory);
134  }
135  ...
136}

线程堆栈分析核心流程

  • Stackwalker::StackwalkerForCPU函数根据不同的cpu类型实例化不同的类
  • 构造stackwalker对象
  • 调用stackwalker的Walk方法开始分析
 1ProcessResult MinidumpProcessor::Process(
 2    Minidump *dump, ProcessState *process_state) {
 3
 4  ...
 5  for (unsigned int thread_index = 0;
 6       thread_index < thread_count;
 7       ++thread_index) {
 8    ...
 9    /**
10    * 这里是分析的入口函数,前面都是读取dmp文件的各种数据初始化操作
11    * 外面套了一个for循环,依次对dmp中的每个线程做堆栈的提取和分析
12    * 从这个函数可以看出,分析每个线程的堆栈需要的参数有:
13    *   1. 操作系统信息
14        1. 上下文信息
15    *   3. 线程内存信息
16    *   4. 模块列表
17    *   5. 未加载的模块列表
18    *   6. 符号相关参数信息(最后一个参数frame_symbolizer_)
19    */
20    scoped_ptr<Stackwalker> stackwalker(
21        // 根据CPU信息,实例化不同的类
22        Stackwalker::StackwalkerForCPU(process_state->system_info(),
23                                       context,
24                                       thread_memory,
25                                       process_state->modules_,
26                                       process_state->unloaded_modules_,
27                                       frame_symbolizer_));
28
29    scoped_ptr<CallStack> stack(new CallStack());
30    if (stackwalker.get()) {
31      // 分析的核心方法
32      if (!stackwalker->Walk(stack.get(),
33                             &process_state->modules_without_symbols_,
34                             &process_state->modules_with_corrupt_symbols_)) {
35        BPLOG(INFO) << "Stackwalker interrupt (missing symbols?) at "
36                    << thread_string;
37        interrupted = true;
38      }
39    }
40  ...
41}

构造Stackwalker

  • Stackwalker是一个基类,不同类型的cpu,都继承这个类,实现自己的堆栈分析方法
  • 调用context中的方法,得到cpu信息,初始化不同的子类
  • 不同的子类,重写基类的Walk方法
 1Stackwalker* Stackwalker::StackwalkerForCPU(
 2    const SystemInfo* system_info,
 3    DumpContext* context,
 4    MemoryRegion* memory,
 5    const CodeModules* modules,
 6    const CodeModules* unloaded_modules,
 7    StackFrameSymbolizer* frame_symbolizer) {
 8  // 获取cpu信息
 9  uint32_t cpu = context->GetContextCPU();
10  // 根据不同的cpu类型,初始化不同的子类
11  // 可以看到,不同的cpu类型,获取context(上下文)的方法也不同
12  switch (cpu) {
13    case MD_CONTEXT_X86:
14      cpu_stackwalker = new StackwalkerX86(system_info,
15                                           context->GetContextX86(),
16                                           memory, modules, frame_symbolizer);
17      break;
18
19    case MD_CONTEXT_PPC:
20      cpu_stackwalker = new StackwalkerPPC(system_info,
21                                           context->GetContextPPC(),
22                                           memory, modules, frame_symbolizer);
23      break;
24
25    ...
26  }
27
28  return cpu_stackwalker;
29}

不同cpu的context

  • context记录的线程堆栈分析的核心数据
  • context使用union(联合体)记录这些数据
  • 写入时,根据cpu类型,只会给cpu相匹配的字段赋值
  • 读取时,根据cpu类型,只会获取指定字段的值
 1union {
 2    MDRawContextBase*  base;
 3    MDRawContextX86*   x86;
 4    MDRawContextPPC*   ppc;
 5    MDRawContextPPC64* ppc64;
 6    MDRawContextAMD64* amd64;
 7    // on Solaris SPARC, sparc is defined as a numeric constant,
 8    // so variables can NOT be named as sparc
 9    MDRawContextSPARC* ctx_sparc;
10    MDRawContextARM*   arm;
11    MDRawContextARM64* arm64;
12    MDRawContextMIPS*  ctx_mips;
13  } context_;

Walk方法概述

  • 根据不同cpu构造好Stackwalker的子类后,开始调用Walk方法进行堆栈分析
  • 分析每一个栈帧时,会先读取匹配的符号信息,协助分析
  • 每一个栈帧中保存着寄存器的指令地址:eip
  • 分析完一个栈帧,会查找它的调用者(上一级)栈帧,直到分析到栈结束
  • 或者调用者的栈帧方法为:GetCallerFrame,下一章专门介绍
 1bool Stackwalker::Walk(
 2    CallStack* stack,
 3    vector<const CodeModule*>* modules_without_symbols,
 4    vector<const CodeModule*>* modules_with_corrupt_symbols) {
 5
 6  // 先清空堆栈信息
 7  stack->Clear();
 8
 9  // 开始根据栈帧的上下文信息,找到上层调用者,直到调用结束
10  // 记录调用栈的层数,防止递归调用死循环的存在
11  uint32_t scanned_frames = 0;
12
13  // 构造一个栈帧,并取出context中的寄存器的eip,并缓存下来
14  scoped_ptr<StackFrame> frame(GetContextFrame());
15
16  // 这里是一个死循环,一直分析并提取堆栈,直到下一个栈帧为空
17  while (frame.get()) {
18    // 栈帧始终包含一个有效的调用栈信息,并设置指令地址
19    // 指令地址来自context中的eip,或者被调用者
20
21    // 提取symbol信息,协助分析
22    StackFrameSymbolizer::SymbolizerResult symbolizer_result =
23        frame_symbolizer_->FillSourceLineInfo(modules_, unloaded_modules_,
24                                              system_info_,
25                                              frame.get());
26    // 匹配的符号文件的结果
27    switch (symbolizer_result) {
28      case StackFrameSymbolizer::kInterrupt:
29        BPLOG(INFO) << "Stack walk is interrupted.";
30        return false;
31        break;
32      case StackFrameSymbolizer::kError:
33        InsertSpecialAttentionModule(symbolizer_result, frame->module,
34                                     modules_without_symbols);
35        break;
36      case StackFrameSymbolizer::kWarningCorruptSymbols:
37        InsertSpecialAttentionModule(symbolizer_result, frame->module,
38                                     modules_with_corrupt_symbols);
39        break;
40      case StackFrameSymbolizer::kNoError:
41        break;
42      default:
43        assert(false);
44        break;
45    }
46
47    // 调用层数 +1
48    switch (frame.get()->trust) {
49       case StackFrame::FRAME_TRUST_NONE:
50       case StackFrame::FRAME_TRUST_SCAN:
51       case StackFrame::FRAME_TRUST_CFI_SCAN:
52         scanned_frames++;
53         break;
54      default:
55        break;
56    }
57
58    // 添加分析完毕的栈帧信息到堆栈列表中
59    stack->frames_.push_back(frame.release());
60
61    // 获取下一个栈帧(调用者的栈帧)
62    bool stack_scan_allowed = scanned_frames < max_frames_scanned_;
63
64    // 栈帧回溯的核心方法:GetCallerFrame
65    frame.reset(GetCallerFrame(stack, stack_scan_allowed));
66  }
67
68  return true;
69}

GetContextFrame

  • GetContextFrame是基类的一个虚方法,子类必须重新
  • 这里以x86的cpu架构为例,分析实现逻辑
  • 内部实现:
    • 构造一个StackFrame对象
    • 取出寄存器中的eip,放入StackFrame的成员变量中
 1StackFrame* StackwalkerX86::GetContextFrame() {
 2
 3  StackFrameX86* frame = new StackFrameX86();
 4
 5  // context中记录了寄存器的信息,
 6  // 其中最核心的是指令指针:eip
 7  // eip寄存器,用来存储CPU要读取指令的地址
 8  // 崩溃的时候,eip存放的地址,就是崩溃时的指令地址
 9  // 将eip传给栈帧的instruction字段,用于后续分析
10  frame->context = *context_;
11  frame->context_validity = StackFrameX86::CONTEXT_VALID_ALL;
12  frame->trust = StackFrame::FRAME_TRUST_CONTEXT;
13  frame->instruction = frame->context.eip;
14
15  return frame;
16}