百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

深入理解Android NDK日志符号化

bigegpt 2024-09-03 11:01 3 浏览

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

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

  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)。

相关推荐

当Frida来“敲”门(frida是什么)

0x1渗透测试瓶颈目前,碰到越来越多的大客户都会将核心资产业务集中在统一的APP上,或者对自己比较重要的APP,如自己的主业务,办公APP进行加壳,流量加密,投入了很多精力在移动端的防护上。而现在挖...

服务端性能测试实战3-性能测试脚本开发

前言在前面的两篇文章中,我们分别介绍了性能测试的理论知识以及性能测试计划制定,本篇文章将重点介绍性能测试脚本开发。脚本开发将分为两个阶段:阶段一:了解各个接口的入参、出参,使用Python代码模拟前端...

Springboot整合Apache Ftpserver拓展功能及业务讲解(三)

今日分享每天分享技术实战干货,技术在于积累和收藏,希望可以帮助到您,同时也希望获得您的支持和关注。架构开源地址:https://gitee.com/msxyspringboot整合Ftpserver参...

Linux和Windows下:Python Crypto模块安装方式区别

一、Linux环境下:fromCrypto.SignatureimportPKCS1_v1_5如果导包报错:ImportError:Nomodulenamed'Crypt...

Python 3 加密简介(python des加密解密)

Python3的标准库中是没多少用来解决加密的,不过却有用于处理哈希的库。在这里我们会对其进行一个简单的介绍,但重点会放在两个第三方的软件包:PyCrypto和cryptography上,我...

怎样从零开始编译一个魔兽世界开源服务端Windows

第二章:编译和安装我是艾西,上期我们讲述到编译一个魔兽世界开源服务端环境准备,那么今天跟大家聊聊怎么编译和安装我们直接进入正题(上一章没有看到的小伙伴可以点我主页查看)编译服务端:在D盘新建一个文件夹...

附1-Conda部署安装及基本使用(conda安装教程)

Windows环境安装安装介质下载下载地址:https://www.anaconda.com/products/individual安装Anaconda安装时,选择自定义安装,选择自定义安装路径:配置...

如何配置全世界最小的 MySQL 服务器

配置全世界最小的MySQL服务器——如何在一块IntelEdison为控制板上安装一个MySQL服务器。介绍在我最近的一篇博文中,物联网,消息以及MySQL,我展示了如果Partic...

如何使用Github Action来自动化编译PolarDB-PG数据库

随着PolarDB在国产数据库领域荣膺桂冠并持续获得广泛认可,越来越多的学生和技术爱好者开始关注并涉足这款由阿里巴巴集团倾力打造且性能卓越的关系型云原生数据库。有很多同学想要上手尝试,却卡在了编译数据...

面向NDK开发者的Android 7.0变更(ndk android.mk)

订阅Google官方微信公众号:谷歌开发者。与谷歌一起创造未来!受Android平台其他改进的影响,为了方便加载本机代码,AndroidM和N中的动态链接器对编写整洁且跨平台兼容的本机...

信创改造--人大金仓(Kingbase)数据库安装、备份恢复的问题纪要

问题一:在安装KingbaseES时,安装用户对于安装路径需有“读”、“写”、“执行”的权限。在Linux系统中,需要以非root用户执行安装程序,且该用户要有标准的home目录,您可...

OpenSSH 安全漏洞,修补操作一手掌握

1.漏洞概述近日,国家信息安全漏洞库(CNNVD)收到关于OpenSSH安全漏洞(CNNVD-202407-017、CVE-2024-6387)情况的报送。攻击者可以利用该漏洞在无需认证的情况下,通...

Linux:lsof命令详解(linux lsof命令详解)

介绍欢迎来到这篇博客。在这篇博客中,我们将学习Unix/Linux系统上的lsof命令行工具。命令行工具是您使用CLI(命令行界面)而不是GUI(图形用户界面)运行的程序或工具。lsoflsof代表&...

幻隐说固态第一期:固态硬盘接口类别

前排声明所有信息来源于网络收集,如有错误请评论区指出更正。废话不多说,目前固态硬盘接口按速度由慢到快分有这几类:SATA、mSATA、SATAExpress、PCI-E、m.2、u.2。下面我们来...

新品轰炸 影驰SSD多款产品登Computex

分享泡泡网SSD固态硬盘频道6月6日台北电脑展作为全球第二、亚洲最大的3C/IT产业链专业展,吸引了众多IT厂商和全球各地媒体的热烈关注,全球存储新势力—影驰,也积极参与其中,为广大玩家朋友带来了...