zipflinger导致的UnsatisfiedLinkError分析
笔者在安卓源码环境下做一些开发工作。几日前碰到了一个奇怪的问题,预装的APP突然报了一个UnsatisfiedLinkError的崩溃。查了一下最近的改动记录,只是将AGP(Androidd gradle plugin) 从3.6.1版本升级到了4.1.0版本。
源码环境为Android 9.0,app预装在 /system/priv-app下,且app中包含有so。为了简化问题,写了一个极简的 ,将这个app预装在 /system/priv-app下,使用AGP 4.0及其以下的版本都正常,一旦使用AGP 4.1及其以上的版本打出来的apk包,就会报 UnsatisfiedLinkError的错误。
app预装的配置
include $(CLEAR_VARS)
LOCAL_MODULE := MyTestApp.apk
LOCAL_SRC_FILES := $(LOCAL_MODULE).apk
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
LOCAL_CERTIFICATE := PRESIGNED
LOCAL_PRIVILEGED_MODULE := true
LOCAL_DEX_PREOPT := nostrippinginclude $(BUILD_PREBUILT)
报错信息
E AndroidRuntime: java.lang.UnsatisfiedLinkError: dlopen failed: library "/system/priv-app/MyTestApp/MyTestApp.apk!/lib/armeabi-v7a/libmytest.so" not foundE AndroidRuntime: at java.lang.Runtime.loadLibrary0(Runtime.java:1016)E AndroidRuntime: at java.lang.System.loadLibrary(System.java:1669)...
问题原因很明显是由于升级了AGP到4.1导致的,不过查看了一下官方的changelog,没发现有什么明显的跟这个问题相关的改动。 分析log发现是so加载失败了,可是把MyTestApp.apk pull出来解压,发现so文件是存在的,路径也没问题,那问题出现在哪呢,这个时候只能先分析一下系统加载so的流程,看看问题出在哪了。
So加载的流程网上文章很多,就不逐一分析了,这里列出调用栈
ojluni/src/main/java/java/lang/System.java --> System.loadLibrary
ojluni/src/main/java/java/lang/Runtime.java --> Runtime.loadLibrary0 -> nativeLoad
ojluni/src/main/native/Runtime.c --> Runtime_nativeLoad
art/openjdkjvm/OpenjdkJvm.cc --> JVM_NativeLoad
art/runtime/java_vm_ext.cc --> JavaVMExt::LoadNativeLibrary
system/core/libnativeloader/native_loader.cpp --> OpenNativeLibrary
bionic/libdl/libdl.cpp --> android_dlopen_ext
bionic/linker/dlfcn.cpp --> __loader_android_dlopen_ext
bionic/linker/dlfcn.cpp --> dlopen_ext
bionic/linker/linker.cpp --> do_dlopen
bionic/linker/linker.cpp --> find_library
bionic/linker/linker.cpp --> find_libraries
bionic/linker/linker.cpp --> find_library_internal
bionic/linker/linker.cpp --> load_library
bionic/linker/linker.cpp --> open_library
bionic/linker/linker.cpp --> open_library_in_zipfile
经过大量的debug,最终发现系统会使用 "!/" 这个分隔符来分隔路径 /system/priv-app/MyTestApp/MyTestApp.apk!/lib/armeabi-v7a/libmytest.so,然后在 /system/priv-app/MyTestApp/MyTestApp.apk这个apk文件(apk其实就是一个zip文件)中搜索name为 lib/armeabi-v7a/libmytest.so的entry。这部分逻辑在 bionic/linker/linker.cpp --> open_library_in_zipfile 中。 导致加载失败的是以下条件 entry.offset % PAGE_SIZE != 0
if (entry.method != kCompressStored || (entry.offset % PAGE_SIZE) != 0) {
close(fd);
return -1;}
由此我们可以推测出问题应该发生在zipalign相关的事情上。根据官方文档的描述,zipalign的目的是确保所有未压缩数据的开头均相对于文件开头部分执行特定的对齐。具体来说,它会使 APK 中的所有未压缩数据(例如图片或原始文件)在 4 字节边界上对齐。这样一来,即可使用 mmap() 直接访问所有部分,即使其中包含具有对齐限制的二进制数据也没关系。这样做的好处是可以减少运行应用时消耗的 RAM 容量。
很显然/system/priv-app/MyTestApp/MyTestApp.apk这个apk的对齐处理应该是有问题的,我们来做一下验证。将这个apk pull出来,执行以下命令,发现确实有问题。
xxx@debian:~/workspace$ zipalign -c -v -p 4 MyTestApp.apk Verifying alignment of out.apk (4)...
3964 lib/armeabi-v7a/libmytest.so (BAD - 3964)
108038 META-INF/CERT.SF (OK - compressed)
108568 AndroidManifest.xml (OK - compressed)
109583 META-INF/CERT.RSA (OK - compressed)
110676 res/layout/activity_main.xml (OK - compressed)
111012 res/mipmap-xhdpi-v4/ic_launcher.png (OK)
115704 resources.arsc (OK)
116722 META-INF/MANIFEST.MF (OK - compressed)
117196 classes.dex (OK)Verification FAILED
那么gradle打包生成的apk是否有问题呢,我们按照相同的方法验证一下源文件,发现是没问题的!那么问题就很明显了,安卓系统在编译的时候一定是对这个apk做了一些处理,导致出现了问题。于是我们需要来看一下编译相关的处理。
Android系统对应 BUILT_PREBUILT 的脚本在 build/core/prebuilt_internal.mk 中,其中
ifeq (true, $(LOCAL_UNCOMPRESS_DEX))
$(uncompress-dexs)
endif # LOCAL_UNCOMPRESS_DEX
也就是说如果LOCAL_UNCOMPRESS_DEX 为true,那么会对apk进行一个 uncompress-dexs 的处理,uncompress-dexs定义在 build/core/definitions.mk 中
# Uncompress dex files embedded in an apk.#
define uncompress-dexs
$(hide) if (zipinfo $@ '*.dex' 2>/dev/null | grep -v ' stor ' >/dev/null) ; then \
tmpdir=$@.tmpdir; \
rm -rf $$tmpdir && mkdir $$tmpdir; \
unzip -q $@ '*.dex' -d $$tmpdir && \
zip -qd $@ '*.dex' && \
( cd $$tmpdir && find . -type f | sort | zip -qD -X -0 ../$(notdir $@) -@ ) && \
rm -rf $$tmpdir; \
fi
endef
分析发现,这个处理就是判断apk中的 dex 后缀的文件是否是压缩存储的,如果不是压缩存储的那么不做任何操作,如果是压缩存储的,那么将其变为不压缩存储的方式。(zip文件中的文件项目的存储方式分为不压缩存储(stored)和压缩存储(deflated))
继续分析发现经过uncompress-dexs之后,编译系统对这个apk还进行了一步 align-package的操作,定义还是在 build/core/definitions.mk 中
# Align STORED entries of a package on 4-byte boundaries to make them easier to mmap.#define align-package$(hide) if ! $(ZIPALIGN) -c $(ZIPALIGN_PAGE_ALIGN_FLAGS) 4 $@ >/dev/null ; then \
mv $@ $@.unaligned; \
$(ZIPALIGN) \
-f \
$(ZIPALIGN_PAGE_ALIGN_FLAGS) \
4 \
$@.unaligned $@.aligned; \
mv $@.aligned $@; \
fiendef
那现在问题比较明显了,就是对于AGP4.1打出来的apk包,经过uncompress-dexs操作后,再 网站监控[?]重新执行zipalign,生成的apk文件的对齐是有问题的。为了方便debug,将uncompress-dexs对应的操作写了一个shell脚本
那么为什么系统会对apk做这样的处理呢,LOCAL_UNCOMPRESS_DEX 这个参数我们似乎也没有定义呀,查看LOCAL_UNCOMPRESS_DEX这个参数的定义和用法,在 build/core/dex_preopt_odex_install.mk 中
# We explicitly uncompress APKs of privileged apps, and used by# privileged apps
LOCAL_UNCOMPRESS_DEX := false
ifneq (true,$(DONT_UNCOMPRESS_PRIV_APPS_DEXS))
ifeq (true,$(LOCAL_PRIVILEGED_MODULE))
LOCAL_UNCOMPRESS_DEX := trueelse
ifneq (,$(filter $(PRODUCT_LOADED_BY_PRIVILEGED_MODULES), $(LOCAL_MODULE)))
LOCAL_UNCOMPRESS_DEX := true
endif # PRODUCT_LOADED_BY_PRIVILEGED_MODULES
endif # LOCAL_PRIVILEGED_MODULE
endif # DONT_UNCOMPRESS_PRIV_APPS_DEXS
分析发现,如果DONT_UNCOMPRESS_PRIV_APPS_DEXS为默认值false,那么系统会对privileged app,也就是 /system/priv-app/下的app执行uncompress-dexs操作。
那么现在就需要去调研AGP4.1到底有什么改动,导致uncompress-dexs这个操作会对zipalign造成影响。
经过一些搜索最终发现,google从AGP 3.6版本开始加入了一个新的打包工具zipflinger,不过只在构建调试版本的时候生效,但是从AGP4.1开始,构建release版本默认也会启用zipflinger。通过在gradle.properties中加入以下属性可禁用zipflinger
android.useNewApkCreator=false
我们使用AGP4.1,加入这个配置打包测试,发现问题果然解决了。
虽然问题解决了,不过难道我们就不能在系统集成的时候,集成启用了zipflinger工具打包的apk吗?google既然推出了这个工具想必是做了充分的测试的吧。由于我们目前使用的是Android 9.0的源码,那我们看看最新的master上的这部分代码是如何处理的,查看代码果然发现了一些改动。在最新的aosp源码中,uncompress-dexs的实现如下
# Uncompress dex files embedded in an apk.#
define uncompress-dexs
if (zipinfo $@ '*.dex' 2>/dev/null | grep -v ' stor ' >/dev/null) ; then \
$(ZIP2ZIP) -i $@ -o $@.tmp -0 "classes*.dex" && \
mv -f $@.tmp $@ ; \
fi
endef
我们发现google使用了一个新的工具zip2zip来处理apk中dex文件的解压缩。我们找到这个工具的源码,其实就是一个单独的文件,使用go语言编写的 ,我们将源码下载下来,使用 go build 命令编译成可执行文件,这里我编译了一个,我们来使用这个工具对启用zipflinger生成的apk进行测试,发现果然没问题
zip2zip -i MyTestApp.apk -o out.apk -0 classes.dexzipalign -f -p 4 out.apk MyTestApp.apkzipalign -c -v -p 4 MyTest.apk
至此这个问题终于算是解决了,总结来看就是一个旧版本的AOSP不兼容新的zipflinger打包工具的问题。根据分析过程解决办法有如下几个:
解决办法1
在BoardConfig.mk文件中声明
DONT_UNCOMPRESS_PRIV_APPS_DEXS := true
(不推荐这种方式,只有在分区空间不足的情况下,才会声明这个属性,以牺牲一点dex的加载速度来换取空间)
解决办法2
预装APP的时候设置不为privileged app
LOCAL_PRIVILEGED_MODULE := false
这种处理方式不通用,有些app必须是privileged
解决办法3
修改 build/core/definitions.mk 中的 uncompress-dexs方法,使用新的zip2zip方案来适配
这种方法可行,不过需要修改的地方有点多,需要更新很多AOSP的新代码过去,比较麻烦
解决办法4
回退AGP版本到4.0或其以下 (显然这不是一个好办法)