[APM翻译]支持从APK加载的本地安卓库

220 阅读9分钟

原文地址:blog.sentry.io/2021/05/13/…

原文作者:

发布时间:2021年5月13日

本文由 简悦SimpRead 转码,原文地址 blog.sentry.io

就像机械师修复自己的汽车或整形外科医生自我隆鼻一样,我们的开发人员pu......。

_像机械师修复自己的汽车或整形外科医生自我隆鼻一样,我们的开发人员在空闲时间将他们的技能用于有趣的用途。在这里,原生平台工程师Arpad Borsos阐述了内存映射和动态库加载的工作原理,以及它与从APK加载的原生Android库的关系。

库是模块化编程的关键,因为它们在一个单元中提供功能,可以与其他开发者共享。毫无疑问,你知道有两种类型的库:静态和动态。静态库在构建时直接嵌入到你的应用程序中,而动态库则在启动应用程序时,或在以后的任何时候链接。

这个概念很重要,因为动态库可以在不修改应用程序本身的情况下进行更新,例如修复安全问题或提高性能。出于组织上的考虑,或者当一个应用程序由一个用户界面和一个在后台运行的独立服务组成时,应用程序也可以被分割成多个动态加载的库。

在使用Android NDK时,用C、Rust或类似的低级语言编写的本地库被Java层动态加载。这就是 "sentry-native "被加载到使用NDK的Android应用中的方式。

通常情况下,这些库是磁盘上的独立文件。你可能在Windows应用程序旁边看到过一些这样的".dll "文件,在其他操作系统上也是如此。安卓的动态加载器本身就是一个系统库,它能够直接从安卓".apk "包中加载库,而不需要先将它们解压到磁盘上。这是相当有益的,因为它节省了你的移动设备上宝贵的磁盘空间。

到目前为止,sentry-native,以及我们的Android SDK的NDK支持,都依赖于这些文件被提取到磁盘。这造成了很多摩擦--特别是对于新客户--因为新的安卓版本不再默认提取".apk "包。为了解决这个问题,应用程序开发人员必须设置一些明确的配置标志,其中许多在为Android设置Sentry时经常被忽略。

从我们的Android SDK的第5个版本开始,我们增加了对从".apk "加载的库的支持,这将消除改变配置标志的需要,同时也改善了使用Sentry SDK的应用程序的磁盘空间使用率。

让我们深入了解这一切是如何运作的,以及为了支持这种使用情况,我们需要改变什么。

哪些库被加载?

在大多数平台上,你可以通过API直接从动态加载器中查询已加载的库的列表。例如,Windows有工具帮助库,在苹果平台上有一些dyld函数可用。不幸的是,Linux没有标准化的用户空间工具。虽然GNU/Linux有dl_iterate_phdr函数,但它在较早的Android系统上明显不可用(Bionic Status列出该API从API 21开始可用,也就是Android 5,在2014年底发布)。这意味着为了支持古老的Android版本,你需要从其他地方获得加载的库列表。

这里的标准做法是通过解析/proc/XXX/maps的内存映射信息来找到映射的ELF文件。这就是Breakpad在两个地方),CrashpadAndroid的libunwindstack,以及LLDB都是如此。在我看来,他们采取这种方式是因为他们是外部观察者,即他们不能从进程内部查询动态加载器。

这种方法是有道理的:我就是这样为sentry-native加载库的。也就是说,你必须小心翼翼地涵盖所有情况--特别是直接从Android .apk包内加载的.so文件。因此,我开始寻找方法来支持这些.apk的情况。

/proc/X/maps格式

在Linux中,所有的可执行文件和库都是ELF文件。Cloudflare有一个关于加载器如何解析和处理这些ELF文件的伟大教程。/proc/X/maps输出格式的文档在这个manpage中描述。该格式包括虚拟地址空间的开始/结束,权限信息,inode(文件)的信息,以及该文件内的偏移。

虽然有些情况下,一个库只需要一个映射,但大多数时候,它被分成两个或更多的映射。通常这包括一个包括ELF头文件和元数据的只读映射,以及一个保存实际程序代码的可执行映射。在我的Linux系统上,我看到一个文件有多达六个映射。

7f8cd3467000-7f8cd3475000 r--p 00000000 00:1c 7597971 /usr/lib/libcurl.so.4.7.0
7f8cd3475000-7f8cd34da000 r-xp 0000e000 00:1c 7597971 /usr/lib/libcurl.so.4.7.0
7f8cd34da000-7f8cd34f6000 r--p 00073000 00:1c 7597971 /usr/lib/libcurl.so.4.7.0
7f8cd34f6000-7f8cd34f7000 ---p 0008f000 00:1c 7597971 /usr/lib/libcurl.so.4.7.0
7f8cd34f7000-7f8cd34fa000 r--p 0008f000 00:1c 7597971 /usr/lib/libcurl.so.4.7.0
7f8cd34fa000-7f8cd34fc000 rw-p 00092000 00:1c 7597971 /usr/lib/libcurl.so.4.7.0

这里有趣的情况是,第四个映射是不可读的,基本上在地址空间中创造了一个缺口。

这两个映射加载了完全相同的库,一次是提取到磁盘,一次是直接从apk加载。

77a85dbda000-77a85dbdd000 r-xp 00000000 fd:05 40992 /data/app/x/y/lib/x86_64/libsentry-android.so
77a85dbdd000-77a85dbde000 --p 00000000 00:00 0
77a85dbde000-77a85dbdf000 r--p 00003000 fd:05 40992 /data/app/x/y/lib/x86_64/libsentry-android.so
77a85dc15000-77a85dd6c000 r-xp 00000000 fd:05 40991 /data/app/x/y/lib/x86_64/libsentry.so
77a85dd6c000-77a85dd6d000 --p 00000000 00:00 0
77a85dd6d000-77a85dd79000 r--p 00157000 fd:05 40991 /data/app/x/y/lib/x86_64/libsentry.so
77a85dd79000-77a85dd7a000 rw-p 00163000 fd:05 40991 /data/app/x/y/lib/x86_64/libsentry.so
77a85dbf0000-77a85dbf3000 r-xp 00001000 fd:05 40977 /data/app/x/y/base.apk
77a85dbf3000-77a85dbf4000 --p 00000000 00:00 0
77a85dbf4000-77a85dbf5000 r--p 00004000 fd:05 40977 /data/app/x/y/base.apk
77a85dc15000-77a85dd6c000 r--xp 00006000 fd:05 40977 /data/app/x/y/base.apk
77a85dd6c000-77a85dd6d000 --p 00000000 00:00 0
77a85dd6d000-77a85dd79000 r--p 0015d000 fd:05 40977 /data/app/x/y/base.apk
77a85dd79000-77a85dd7a000 rw-p 00169000 fd:05 40977 /data/app/x/y/base.apk

这些映射基本上是相同的--只是在base.apk的情况下,文件的偏移量不同。而安卓加载器又在中间插入了一个不可读的空隙。

那么我们如何从那里获得库列表呢?

到目前为止,sentry-native modulefinder的实现有点过于保守了。由于担心读取任意内存,我们把文件mmap到内存中,并试图提取ELF头文件。不幸的是,这种方法对那些直接从apk文件加载的库不起作用,因为ELF头文件在该文件的某个偏移处。另外,正如我们上面所展示的,在旧的实现中存在一些与非连续映射和双重映射有关的问题,因为它是根据它看到的文件名来工作的。

因此,我的新方法是跟踪可读映射,它们的文件偏移量,以及它们之间的间隙。对于每个可读映射,我正在寻找神奇的ELF签名。如果我找到了,我就处理以前保存的映射,同时也处理可能的重复。

这种方法仍然有未解决的问题。一个是试图读取任意的内存。我认为我很安全,因为我只考虑可读映射,但一个改进是在这里使用process_vm_readv。也就是说,我也看到了在Android上使用这种映射的问题。另一个潜在的问题是如何正确处理那些有空隙的映射,甚至是那些出现多次的映射。ELF文件可能会指示加载器在RAM中的ELF头的偏移量与磁盘上的偏移量不同的地方加载可执行代码--也可能不会。这在很大程度上取决于我们如何利用这些信息来对崩溃报告进行后期处理。

这个问题不是sentry-native用来读取库列表的唯一方式。我们也看到一些breakpad工具在这个问题上犯了错误,它们创建的minidumps带有无效的映射,在后处理管道中失败了。

加载库是一个非微不足道的问题,我相信我不是唯一一个与之斗争的人。不要搞错了:为这样一个特定的用例调查故障和修补相关的代码是一项工作。但随着安卓系统的采用越来越多,这是必要的工作,可以为你的用户节省空间--也为你自己节省压力。


www.deepl.com 翻译