深入理解Android NDK日志符号化
ztj100 2024-12-06 20:54 15 浏览 0 评论
CSDN移动将持续为您优选移动开发的精华内容,共同探讨移动开发的技术热点话题,涵盖移动应用、开发工具、移动游戏及引擎、智能硬件、物联网等方方面面。如果您想投稿、参与内容翻译工作,或寻求近匠报道,请发送邮件至tangxy#csdn.net(请把#改成@)。
前言
为了进行代码及产品保护,几乎所有的非开源App都会进行代码混淆。这样,当收集到崩溃信息后,就需要进行符号化来还原代码信息,以便开发者可以定位Bug。基于使用SDK和NDK的不同,Android的崩溃分为两类:Java崩溃和C/C++崩溃。Java崩溃通过mapping.txt文件进行符号化,比较简单直观。而C/C++崩溃的符号化则需要使用Google自带的一些NDK工具,比如ndk-stack、addr2line、objdump等。本文不去讨论如何使用这些工具,有兴趣的朋友可以参考之前尹春鹏写的另一篇文章《 如何定位Android NDK开发中遇到的错误》,里面做了详细的描述。
基于NDK的Android开发都会生成一个动态链接库(so),它是基于C/C++编译生成的。动态链接库在Linux系统下广泛使用,而Android系统底层是基于Linux的,所以NDK so库的编译生成遵循相同的规则,只不过Google NDK把相关的交叉编译工具都封装了。
Ndk-build编译时会生成的两个同名的so库,位于不同的目录/project path/libs/armeabi/xxx.so和/project path/obj/local/armeabi/xxx.so,比较两个so文件会发现体积相差很大。前者会跟随App一起发布,所以尽可能地小,而后者包含了很多调试信息,主要为了gdb调试的时候使用,当然,NDK的日志符号化信息也包含其中。
深入分析so动态库组成结构
本文主要针对这个包含调试信息的so动态库,深入分析它的组成结构。在开始之前,先来说说这样做的目的或者好处。现在的App基本都会采集上报崩溃时的日志信息,无论是采用第三方云平台,还是自己搭建云服务,都要将含调试信息的so动态库上传,实现云端日志符号化以及云端可视化管理。
移动App的快速迭代,使得我们必须存储管理每一个版本的debug so库,而其包含了很多与符号化无关的信息。如果我们只提取出符号化需要的信息,那么符号化文件的体积将会呈现数量级的减少。同时可以在自定义的符号化文件中添加App的版本号等定制化信息,实现符号化提取、上传到云端、云端解析及可视化等自动化部署。另外,从技术角度讲,开发者将不再害怕看到“unresolved symbol” linking errors,可以更从容地debugging C/C++ crash或进行一些hacking操作。
首先通过readelf来看看两个不同目录下的so库有什么不同。
从中可以清楚看到,包含调试信息的so库多了8个.debug_开头的条目以及.symtab和.strtab条目。符号化的本质,是通过堆栈中的地址信息,还原代码本来的语句以及相应的行号,所以这里只需解析.debug_ line和.symtab,最终获取到如下的信息就可以实现符号化了。
c85 c8b willCrash jni/hello-jni.c:27-29 c8b c8d willCrash jni/hello-jni.c:32 c8d c8f JNI_OnLoad jni/hello-jni.c:34 c8f c93 JNI_OnLoad jni/hello-jni.c:35 c93 c9d JNI_OnLoad jni/hello-jni.c:37
通常,目标文件分为三类:relocatable文件、executable文件和shared object文件,它们格式称为ELF(Executable and Linking Format),so动态库属于第三类shared object,它的整体组织结构如下:
ELF Header
ELF Header文件头的结构如下,记录了文件其他内容在文件中的偏移以及大小信息。这里以32bit为例:
typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; // 目标文件类型,如relocatable、executable和shared object Elf32_Half e_machine; // 指定需要的特定架构,如Intel 80386,Motorola 68000 Elf32_Word e_version; // 目标文件版本,通e_ident中的EI_VERSION Elf32_Addr e_entry; // 指定入口点地址,如C可执行文件的入口是_start,而不是main Elf32_Off e_phoff; // program header table 的偏移量 Elf32_Off e_shoff; // section header table的偏移量 Elf32_Word e_flags; // 处理器相关的标志 Elf32_Half e_ehsize; // 代表ELF Header部分的大小 Elf32_Half e_phentsize; // program header table中每一项的大小 Elf32_Half e_phnum; // program header table包含多少项 Elf32_Half e_shentsize; // section header table中每一项的大小 Elf32_Half e_shnum; // section header table包含多少项 Elf32_Half e_shstrndx; //section header table中某一子项的index,该子项包含了所有section的字符串名称 } Elf32_Ehdr;
其中e_ident为固定16个字节大小的数组,称为ELF Identification,包含了处理器类型、文件编码格式、机器类型等,具体结构如下:
Sections
该部分包含了除ELF Header、program header table以及section header table之外的所有信息。通过section header table可以找到每一个section的基本信息,如名称、类型、偏移量等。
先来看看Section Header的内容,仍以32-bit为例:
typedef struct { Elf32_Word sh_name; // 指定section的名称,该值为String Table字符串表中的索引 Elf32_Word sh_type; // 指定section的分类 Elf32_Word sh_flags; // 该字段的bit代表不同的section属性 Elf32_Addr sh_addr; // 如果section出现在内存镜像中,该字段表示section第一个字节的地址 Elf32_Off sh_offset; // 指定section在文件中的偏移量 Elf32_Word sh_size; // 指定section占用的字节大小 Elf32_Word sh_link; // 相关联的section header table的index Elf32_Word sh_info; // 附加信息,意义依赖于section的类型 Elf32_Word sh_addralign; // 指定地址对其约束 Elf32_Word sh_entsize; // 如果section包含一个table,该值指定table中每一个子项的大小 } Elf32_Shdr;
通过Section Header的sh_name可以找到指定的section,比如.debug_line、.symbol、.strtab。
String Table
String Table包含一系列以\0结束的字符序列,最后一个字节设置为\0,表明所有字符序列的结束,比如:
String Table也属于section,只不过它的偏移量直接在ELF Header中的e_shstrndx字段指定。String Table的读取方法是,从指定的index开始,直到遇到休止符。比如要section header中sh_name获取section的名称,假设sh_name = 7, 则从string table字节流的第7个index开始(注意这里从0开始),一直读到第一个休止符(index=18),读取到的名称为.debug_line。
Symbol Table
该部分包含了程序符号化的定义相关信息,比如函数定义、变量定义等,每一项的定义如下:
# Symbol Table Entry typedef struct { Elf32_Word st_name; //symbol字符串表的索引 Elf32_Addr st_value; //symbol相关的值,依赖于symbol的类型 Elf32_Word st_size; //symbol内容的大小 unsigned char st_info; //symbol的类型及其属性 unsigned char st_other; //symbol的可见性,比如类的public等属性 Elf32_Half st_shndx; //与此symbol相关的section header的索引 } Elf32_Sym;
Symbol的类型包含以下几种:
其中STT_FUNC就是我们要找的函数symbol。然后通过st_name从symbol字符串表中获取到相应的函数名(如JNI_OnLoad)。当symbol类型为STT_FUNC时,st_value代表该symbol的起始地址,而(st_value+st_size)代表该symbol的结束地址。
回顾之前提到的.symtab和.strtab两个部分,对应的便是Symbol Section和Symbol String Section。
DWARF(Debugging With Attributed Record Formats)
DWARF是一种调试文件格式,很多编译器和调试器都通过它进行源码调试(gdb等)。尽管它是一种独立的目标文件格式,但往往嵌入在ELF文件中。前面通过readelf看到的8个.debug_* Section全部都属于DWARF格式。本文将只讨论与符号化相关的.debug_line部分,更多的DWARF信息请查看参考文献的内容。
.debug_line部分包含了行号信息,通过它可以将代码语句和机器指令地址对应,从而进行源码调试。.debug_line由很多子项组成,每个子项都包含类似数据块头的描述,称为Statement Program Prologue。Prologue提供了解码程序指令和跳转到其他语句的信息,它包含如下字段,这些字段是以二进制格式顺序存在的:
这里用到的机器指令可以分为三类:
这里不做机器指令的解析说明,感兴趣的,可以查看参考文献的内容。
通过.debug_line,我们最终可以获得如下信息:文件路径、文件名、行号以及起始地址。
最后,我们汇总一下整个符号化提取的过程:
- 从ELF Header中获知32bit或者64bit,以及大端还是小端,基于此读取后面的内容;
- 从ELF Header中获得Section Header Table在文件中的位置;
- 读取Section Header Table,从中获得.debug_line、.symtab以及.strtab三个section在文中的位置;
- 读取.symtab和.strtab两个section,最后获得所有function symbol的名称、起始地址以及结束地址;
- 读取.debug_line,按照DWARF格式解析获取文件名称、路径、行号以及起始地址;
- 对比步骤4和5中获取的结果,进行对比合并,形成最终的结果。
参考文献
作者简介:
贾志凯 Testin技术总监,主要负责崩溃分析项目Android平台架构设计、性能及稳定性优化。
第一时间掌握最新移动开发相关信息和技术,请关注mobilehub公众微信号(ID: mobilehub)。
- 上一篇:Java17运算符看这一篇就够了
- 下一篇:第8天|Java入门有野,运算符
相关推荐
- Vue3非兼容变更——函数式组件(vue 兼容)
-
在Vue2.X中,函数式组件有两个主要应用场景:作为性能优化,因为它们的初始化速度比有状态组件快得多;返回多个根节点。然而在Vue3.X中,有状态组件的性能已经提高到可以忽略不计的程度。此外,有状态组...
- 利用vue.js进行组件化开发,一学就会(一)
-
组件原理/组成组件(Component)扩展HTML元素,封装可重用的代码,核心目标是为了可重用性高,减少重复性的开发。组件预先定义好行为的ViewModel类。代码按照template\styl...
- Vue3 新趋势:10 个最强 X 操作!(vue.3)
-
Vue3为前端开发带来了诸多革新,它不仅提升了性能,还提供了...
- 总结 Vue3 组件管理 12 种高级写法,灵活使用才能提高效率
-
SFC单文件组件顾名思义,就是一个.vue文件只写一个组件...
- 前端流行框架Vue3教程:17. _组件数据传递
-
_组件数据传递我们之前讲解过了组件之间的数据传递,...
- 前端流行框架Vue3教程:14. 组件传递Props效验
-
组件传递Props效验Vue组件可以更细致地声明对传入的props的校验要求...
- 前端流行框架Vue3教程:25. 组件保持存活
-
25.组件保持存活当使用...
- 5 个被低估的 Vue3 实战技巧,让你的项目性能提升 300%?
-
前端圈最近都在卷性能优化和工程化,你还在用老一套的Vue3开发方法?作为摸爬滚打多年的老前端,今天就把私藏的几个Vue3实战技巧分享出来,帮你在开发效率、代码质量和项目性能上实现弯道超车!一、...
- 绝望!Vue3 组件频繁崩溃?7 个硬核技巧让性能暴涨 400%!
-
前端的兄弟姐妹们五一假期快乐,谁还没在Vue3项目上栽过跟头?满心欢喜写好的组件,一到实际场景就频频崩溃,页面加载慢得像蜗牛,操作卡顿到让人想砸电脑。用户疯狂吐槽,领导脸色难看,自己改代码改到怀疑...
- 前端流行框架Vue3教程:15. 组件事件
-
组件事件在组件的模板表达式中,可以直接使用...
- Vue3,看这篇就够了(vue3 从入门到实战)
-
一、前言最近很多技术网站,讨论的最多的无非就是Vue3了,大多数都是CompositionAPI和基于Proxy的原理分析。但是今天想着跟大家聊聊,Vue3对于一个低代码平台的前端更深层次意味着什么...
- 前端流行框架Vue3教程:24.动态组件
-
24.动态组件有些场景会需要在两个组件间来回切换,比如Tab界面...
- 前端流行框架Vue3教程:12. 组件的注册方式
-
组件的注册方式一个Vue组件在使用前需要先被“注册”,这样Vue才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册...
- 焦虑!Vue3 组件频繁假死?6 个奇招让页面流畅度狂飙 500%!
-
前端圈的朋友们,谁还没在Vue3项目上踩过性能的坑?满心期待开发出的组件,一到高并发场景就频繁假死,用户反馈页面点不动,产品经理追着问进度,自己调试到心态炸裂!别以为这是个例,不少人在电商大促、数...
- 前端流行框架Vue3教程:26. 异步组件
-
根据上节课的代码,我们在切换到B组件的时候,发现并没有网络请求:异步组件:...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)