CSDN移动继续青睐移动开发的精髓,讨论移动应用、开发工具、移动游戏和引擎、智能硬件、物联网等移动开发的技术问题。如果您想投稿、参与内容翻译工作或得到附近工匠的报道,请发送电子邮件到tangxy # c(#至@)。

为了保护

前言

代码和产品,几乎所有开源应用程序都会引起代码混淆。这样,一旦收集了冲突信息,开发人员就需要对代码信息进行编码,以便能够找到bug。根据SDK和NDK的使用情况,Android的冲突分为两类:Java冲突和C/C冲突。Java冲突通过ma文件编码,相对简单直观。C/C冲突的符号化需要Google附带的NDK工具(例如,ndk-stack、addr2line、objdump等)。这篇文章不讨论如何使用这些工具。感兴趣的朋友可以参考尹春鹏写的另一篇文章《如何定位Android NDK开发中遇到的错误》进行详细说明。

基于NDK的Android开发生成基于C/C编译生成的动态链接库(so)。动态链接库在Linux系统中广泛使用,Android系统的基本层基于Linux,因此NDK so库的编译生成遵循相同的规则。但是,Google NDK封装了相关的交叉编译工具。

编译Ndk-build时生成的两个同名的so库位于不同的目录/project path/libs/armeabi和/project path/obj/local/armeabi中,前者尽可能小,因为它们是随App一起发布的。当然,它还包含NDK的日志符号化信息。

深入分析so动态库组成结构

本文主要对包含调试信息的该so动态库的配置结构进行了深入分析。开始之前,先谈谈这样做的目的或好处。现在,App基本上可以上传包含调试信息的so动态库,无论是利用第三方云平台还是自己构建云服务,实现云日志符号化和云可视化管理。

移动App的快速迭代要求存储每个版本的debug so库。其中包含很多与符号化无关的信息。如果只提取编码所需的信息,编码文件中的卷将出现大小减少。您还可以将用户定义的信息(如App版本号)添加到用户定义的符号文件中,以执行符号提取、上载到云、云分析和可视化等自动分发。此外,从技术角度来看,开发人员不再害怕查看“未解决的symbol”链接错误,更容易执行调试C/C崩溃或某些hacking操作。

首先,让我们通过readelf了解两个不同目录中的so库有何不同。

如所示,包含调试信息的so库包含。以debug_开头的八个项目和。symtab和。strtab项目更多。符号化的本质是通过堆栈中的地址信息、还原代码的原始语句和对应的行号。debug_ line和。只要解析symtab,最终就可以获得以下信息:

C85 c8b willCrash JNI

C8b c8d willCrash JNI

C8d c8f JNI_OnLoad JNI

C8f c93 JNI_OnLoad JNI

C93 c9d JNI_OnLoad JNI通常将大象文件分为三类:可执行和链接格式(ELF)的可重新定位文件、可执行文件和shared object文件

ELF Header

elf header标头结构记录文件中其他内容的偏移和大小信息,如下所示:以32位为例。

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包含一系列以结束的字符序列,最后一个字节设置为,表明所有字符序列的结束,比如:

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,我们最终可以获得如下信息:文件路径、文件名、行号以及起始地址。

最后,我们汇总一下整个符号化提取的过程:

  1. 从ELF Header中获知32bit或者64bit,以及大端还是小端,基于此读取后面的内容;
  2. 从ELF Header中获得Section Header Table在文件中的位置;
  3. 读取Section Header Table,从中获得.debug_line、.symtab以及.strtab三个section在文中的位置;
  4. 读取.symtab和.strtab两个section,最后获得所有function symbol的名称、起始地址以及结束地址;
  5. 读取.debug_line,按照DWARF格式解析获取文件名称、路径、行号以及起始地址;
  6. 对比步骤4和5中获取的结果,进行对比合并,形成最终的结果。

参考文献

作者简介:

贾志凯 Testin技术总监,主要负责崩溃分析项目Android平台架构设计、性能及稳定性优化。

第一时间掌握最新移动开发相关信息和技术,请关注mobilehub公众微信号(ID: mobilehub)。

相关推荐