dmp文件分析(二)- symbol
| 阅读 | 共 5057 字,阅读约
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}