作者:王森(天作)
C/C++ 具有天然的跨平台特性,丰富的构建工具、Native 的性能以及成熟的社区生态,近年来移动端也越来越多的集成了一些使用 C/C++ 开发一些逻辑内聚且对性能要求较高的模块,特别是各类引擎模块例如音视频编解码、RPC 网络库、数据库、神经网络库等。
但并不完美的是,C/C++ 技术栈在获得上述收益的同时,也使得原本用原生语言进行开发的体验产生了割裂,导致了不小的“隐性成本”,这点 Dropbox 在其分享的一系列 C++ 文章中也有提到。
https://dropbox.tech/mobile/the-not-so-hidden-cost-of-sharing-code-between-ios-and-android
C/C++ 工具链和构建环境比较复杂,Makefile、cmake、gn、ninja 等构建工具繁多,对环境的要求极高,因此这些库通常都是以链接后的静态库或者动态库的方式进行二进制分发,app 再进行二次链接之后进行调用。
在模块层和接入层都保障自己 Unit 不出问题的情况下,这一切看似都井井有条。但一旦出现问题需要排查的时候,C/C++ 二进制库对双方来说都是个黑盒:
因此导致出现问题后的排查效率极低,甚至出现过一个问题来来回回排查好几天,影响项目进展。
LLDB 源码调试的原理是根据二进制中的 DWARF 调试信息找到对应源代码路径和行号等信息,在 Mach-O 格式文件中,这部分信息存放在 __DWARF 相关的 Section 中,我们可以使用 dwarfdump 命令查看结构化的调试信息,elf 格式文件可以使用 objdump 查看:
0x00000ed8: DW_TAG_inlined_subroutine DW_AT_abstract_origin (0x00000ec6 "AbcAppContext") DW_AT_ranges (0x000004e0 [0x00000506, 0x00000560) [0x00000576, 0x000005a2)) DW_AT_call_file ("/Users/abc/.abc/build/123456/workspace/ios_out/arm/../../abc_core/src/abc_engine_impl.cc") DW_AT_call_line (37) DW_AT_call_column (0x0a)0x00000ee4: DW_TAG_inlined_subroutine DW_AT_abstract_origin (0x00000ec1 "AbcAppContext") DW_AT_ranges (0x000004f8 [0x00000506, 0x00000560) [0x00000576, 0x000005a2)) DW_AT_call_file ("/Users/abc/.abc/build/123456/workspace/ios_out/arm/../../abc_core/public/abc_core/abc_app_context.h") DW_AT_call_line (12) DW_AT_call_column (0x08)
由于 DWARF 中定义的源码路径本地并不存在,因此 LLDB 并不能进入源码调试模式,这时候要实现本地源码调试有两个方案:
幸运的是 lldb 提供了命令可以修改路径,将二进制中的路径前缀映射到本地的真实路径,因此你可以在 Debug 控制台中输入以下指令实现本地调试:
settings set target.source-map [二进制中的源码路径] [本地代码路径]
再次点击单步调试后,我们就可以看到熟悉的源码调试界面:
但这,还是太麻烦了,首先这个命令不一定记得住,然后是需要从二进制中找到需要映射的路径地址,而且每次重新运行 app 之后又得重新输入一遍;因此,我们利用 lldb 提供的 lldbinit 和 Python API,这些步骤都自动化完成,真正实现一键调试体验。
lldbinit 是 LLDB 在启动调试的时候提供给开发者自定义调试命令的接口文件,我们可以在源码根目录下放置一个 lldbinit 用于自定义源码映射的逻辑,于是调试的流程就变成:
增加一个 python 文件,例如 debug.py ,存放在源码目录下,用 Python 实现以下伪代码步骤:
1、通过 Python API 查找指定的 Symbol 调试信息
# symbol_name 为这个库中的任意一个符号名funcs = target.FindGlobalFunctions(symbol_name, 0, lldb.eMatchTypeNormal)# 通过 lldb API 获取到这个符号的源码文件信息symbol = funcs.GetContextAtIndex(0)symbol_file = "%s" % symbol.GetCompileUnit().GetFileSpec()
2、约定规则,将 Python 文件默认认为存放在源代码的根目录,这样可以比较方便的获取本地源码路径:
local_source_path = os.path.dirname(__file__)
3、然后需要找到调试信息中存储的路径与本地路径之间的前缀映射关系,可以通过遍历路径目录的方式逐级查找本地对应的源码是否存在:
src_comps = symbol_file.split("/")for i in range(len(src_comps)): if len(src_comps[i]) == 0: continue suffix_path = "/".join(src_comps[i:]) detect_path = "/".join([local_source_path, suffix_path]) detect_file = Path(detect_path) if detect_file.is_file() == False: continue print("-> Matched:", detect_path) prefix_path = "/".join(src_comps[:i])
4、再调用 lldb 命令将源码路径映射到本地路径:
target.GetDebugger().HandleCommand("settings set target.source-map '%s' '%s'" % (prefix_path, local_source_path))
5、以上替换的主要逻辑实现,需要通过一个 lldbinit 将这个方法添加给 lldb 运行时,我们可以通过添加 stop-hook 的方式让上述脚本自动运行:
lldbinit:command script import -c debug.py# 这里通过指定一个 Library 中一定存在符号名用于查找调试符号中的源码目录target stop-hook add -P debug.DebugHook -k "symbol" -v "::FuncAbc"
最后,通过一些简单的工程改造,便可以将调试应用到 Android 和 iOS 的日常开发环境中:
通过这几十行的脚本,让 C/C++ 二进制库在各平台上能够使用默认的 IDE 快速进行源码调试,有助于提升问题排查效率和打破上下游边界。
除了应用在 C/C++ 模块,以上原理同样适用于 Swift 或者 ObjC、Rust 等其他任何能够生成 DWARF 调试信息并支持使用 lldb 调试的语言。
附上实现源码:
# LLDB commands for quickly debug C/C++ libraries with source codecommand script import -c debug.pytarget stop-hook add -P debug.DebugHook -k "symbol" -v "::FuncAbc"
import lldbimport osfrom pathlib import Pathclass DebugHook: def __init__(self, target, extra_args, internal_dict): self.source_mapped = False self.symbol_name = extra_args.GetValueForKey("symbol").GetStringValue(100) print("With Symbol:", self.symbol_name) def handle_stop(self, exe_ctx, stream): if self.source_mapped and exe_ctx != None: # 已经做过源码映射,无需再次映射 return target = exe_ctx.GetTarget() print("-> Target Stopped:", target) print("-> Searching Symbol:", self.symbol_name) funcs = target.FindGlobalFunctions(self.symbol_name, 0, lldb.eMatchTypeNormal) if funcs.GetSize() == 0: print("** No Symbol Found") return symbol = funcs.GetContextAtIndex(0) symbol_file = "%s" % symbol.GetCompileUnit().GetFileSpec() print("-> Symbol Source:
", symbol_file) local_source_path = os.path.dirname(__file__) print("-> Local Source:
", nest_path) # 便利二进制中源码路径的各层级目录,查找与本地源码匹配的目录路径 src_comps = symbol_file.split("/") source_mapped = False for i in range(len(src_comps)): if len(src_comps[i]) == 0: continue suffix_path = "/".join(src_comps[i:]) detect_path = "/".join([nest_path, suffix_path]) detect_file = Path(detect_path) if detect_file.is_file() == False: continue print("-> Matched:", detect_path) prefix_path = "/".join(src_comps[:i]) print("-> Mapping Source:
'%s' -> '%s'" % (prefix_path, nest_path)) target.GetDebugger().HandleCommand("settings set target.source-map '%s' '%s'" % (prefix_path, nest_path)) print("-> Done, Feel free to deubg now!") # Mark this time as mapped source_mapped = True break if source_mapped == False: print("-> Source Mapping Failed: No local source path matched")def __lldb_init_module(debugger, internal_dict): print('source-debug enabled')
留言与评论(共有 0 条评论) “” |