Android 15- 16kb页对齐适配大扫盲

4,225 阅读9分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

通过本文,你将会了解到如何适配Android15中16kb页大小,同时我们也有一个实战例子,通过一个真实的例子:shadowhook的mprotect适配,让大家快速上手。希望这篇文章能让读者少走弯路~

Android 15 PageSize 大更新

Android 15已经迎来beta了,其中有一个更新让开发者们必须要注意,就是google决定在Android15上可以配置16kb的pagesize。(负责适配的厂商们,是可以选择开启与不开启16kb对齐适配的)

image.png 当然,作为开发者,我们还是要尽快适配,避免后续开启16kb对齐后产生Crash。PageSize的使用在Android Native开发比较多,之前的pagesize为4kb,在Android15后扩充至16kb。这也就意味着,CPU高速缓存或者内存缓存等以页为单位的核心驱动能够进一步提高速度(其实就是页大了能够命中的同一页缓存的概率大了) image.png

pagesize会在Native中使用,也就是有so的应用,我们需要留意一下,虽然pagesize目前有一些文章,但是为了让开发者们更加了解到什么样的so才需要适配,我这里给大家总结一下。

需要适配的so分为两部分,一部分是本身没有进行16kb对齐的so,一部分是native代码中以固定硬编码写死了4kb,这部分可能在部分系统调用中产生异常。值得注意的是,这两部分是互相独立的,也就是说即使你的so是16kb对齐的,依旧有可能在native代码中用到4kb硬编码。因此大家需要注意!

适配过程

环境准备

根据官方文档 下载合适的模拟器,如果你的公司刚好有跟google进行商务合作,你也可以刷入google提供的android15进行配置,这里环境比较简单,我们就不再啰嗦,跟着官网走即可

image.png image.png

按照好环境后,就可以启动模拟器,进行app验证了,下面我们将详细介绍需要适配的两部分

so本身需要进行16kb对齐边界对齐

如果没有进行16kb边界对齐的so,那么我们安装好android15环境并启动16kb对齐时,就会在运行时出现以下错误:

java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH/DT_GNU_HASH in "/data/app/~~RMVOpAp3vbZNQkgobvqssg==/xxxx-vdmh-Q0a7ybWc-n9C_YZIw==/lib/arm64/libxxxx.so" (new hash type from the future?)
                                                                                                            at java.lang.Runtime.loadLibrary0(Runtime.java:1081)
                                                                                                            at java.lang.Runtime.loadLibrary0(Runtime.java:1003)
                                                                                                            at java.lang.System.loadLibrary(System.java:1765)`

google非常贴心的给出了报告提示,如果你的so运行时出现上面错误,这就意味着你需要重新编译该so进行16kb对齐

当然,我们也可以通过官方提供的脚本分析,用来查看某个文件下的so是否对齐,官网链接在这

#!/bin/bash

# usage: alignment.sh path to search for *.so files

dir="$1"

RED="\e[31m"
GREEN="\e[32m"
ENDCOLOR="\e[0m"

matches="$(find $dir -name "*.so" -type f)"
IFS=$'\n'
for match in $matches; do
  res="$(objdump -p ${match} | grep LOAD | awk '{ print $NF }' | head -1)"
  if [[ $res =~ "2**14" ]] || [[ $res =~ "2**16" ]]; then
    echo -e "${match}: ${GREEN}ALIGNED${ENDCOLOR} ($res)"
  else
    echo -e "${match}: ${RED}UNALIGNED${ENDCOLOR} ($res)"
  fi
done

如果运行脚本出现了UNALIGNED的项目,那么这就是需要进行适配的。

这里我们值得一提,并非所有so都是边界不对齐的,取决于当时编译so时的cmake配置。

如果你的so出现了边界不对齐,你可以对so进行重新打包,我们以cmake为例子,现在的native项目基本都是cmake配置了,我们可以通过配置target_link_options,并添加-Wl,-z,max-page-size=16384 即可

Cmake 
#添加pagesize标识
set(ARCH_LINK_FLAGS "-Wl,-z,max-page-size=16384" )
#添加编译选项
target_link_options(自己的项目名 PUBLIC $ { ARCH_LINK_FLAGS } )

添加好上面的配置项目后,你的so就可以重新编译为16kb对齐了,当然,如果你的so依赖了其他so,那么也得确保其他so处于16kb边界对齐。

固定编码4kb在部分系统调用异常

虽然官网浅浅提了一下mmap,以及固定编码4096可能产生的影响, image.png 但是这里我们需要注意,即使开启了16kb对齐,也并不是所有4096的硬编码都会有问题的,这里也是很多开发者误解的点,我们并不需要无脑替换4kb为16kb

页对齐需要根据具体的系统调用,我们还是拿官网提到mmap举例子,mmap函数定义如下:

void* mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset);

mmap限制的是addroffset需要与页对齐,对于size是不限制的,因此以下代码无论是不是开启16kb对齐,都没有问题

这里size写死4096,也是没有问题的
void *buf = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (MAP_FAILED == buf){
  __android_log_print(ANDROID_LOG_ERROR, "hello" , "mmap fail" );
  return 0;
}
__android_log_print(ANDROID_LOG_ERROR, "hello" , "mmap success %p" ,buf);

mmap返回是成功的,即MAP_FAILED == buf为false。

相反,影响最终地址对齐的,比如offset,以下代码会在16kb对齐开启时返回MAP_FAILED, 非16kb则是成功调用

修改了offset,从0->4096
void *buf = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 4096);
if (MAP_FAILED == buf){
  __android_log_print(ANDROID_LOG_ERROR, "hello" , "mmap fail" );
  return 0;
}
__android_log_print(ANDROID_LOG_ERROR, "hello" , "mmap success %p" ,buf);

这里想要说清楚的是,并不是所有写死4096的地方都有问题,关键在于你所调用的系统调用特定的限制,mmap size为4kb的内存块如果能够满足我们的业务需求,那么也不必替换为16kb大小,因为size是不限制的。

pagesize在某些Native库上非常常见,比如我们需要与内存打交道的场景就会经常使用mprotect

最后,我们来实战一下android15的16kb pagesize适配场景,我们以shadowhook为例子,在shadowhook中,我们经常需要对内存进行读写权限改写,以便我们进行hook操作。

当前main分支的shadowhook产生的so是还没有进行16kb对齐的,因此我们可以运用上小节学到的知识,让shadowhook适配后并跑起来。

如果你按照上面我说的知识点适配完,你是可以进入到demo页面,这时候我们点击一个hook例子

如果我们是在android15机子上并开启16kb对齐,那么我们应该可以看到以下crash


Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x784a80a20864 in tid 6104 (adowhook.sample), pid 6104 (adowhook.sample)
Cmdline: com.bytedance.shadowhook.sample
pid: 6104, tid: 6104, name: adowhook.sample >>> com.bytedance.shadowhook.sample <<<
and: shadowhook: hook_sym_name(linker64, __dl__Z9do_dlopenPKciPK17android_dlextinfoPKv, 0x784a8099dff8) FAILED. 5 - MProtect failed

这里发生了MProtect failed,对应的源码如下

#define SH_UTIL_PAGE_START(x) SH_UTIL_ALIGN_START(x, 0x1000)
#define SH_UTIL_PAGE_END(x)   SH_UTIL_ALIGN_END(x, 0x1000)
int sh_util_mprotect(uintptr_t addr, size_t len, int prot) {
  uintptr_t start = SH_UTIL_PAGE_START(addr);
  uintptr_t end = SH_UTIL_PAGE_END(addr + len - 1);

  return mprotect((void *)start, end - start, prot);
}

这是因为对齐是写死了0x1000,即4096,mprotect系统调用需要以页为单位对齐,因此4096在4kb页下没有问题,而升级到16kb后才会有问题

image.png

像mprotect这种系统调用需要页对齐的地方,我们传入的地址也必须按照页对齐,这也就意味着即使我们需要对齐的地址是4097,在4kb下我们也需要从4096开始进行内存权限设置

image.png

知道原因后,我们只需要把0x1000的宏替换为调用系统pagesize函数,如getpagesize即可

#define SH_UTIL_PAGE_START(x) SH_UTIL_ALIGN_START(x, 0x1000) => SH_UTIL_ALIGN_START(x, getpagesize())
#define SH_UTIL_PAGE_END(x)   SH_UTIL_ALIGN_END(x, 0x1000) =>SH_UTIL_ALIGN_END(x, getpagesize())

适配完成后,再次点击hook应该能看到下面日志:

shadowhook: hook_sym_name(libhookee2.so, test_hook_before_dlopen_1, 0x784a809d9ce8) OK. return: 0xb400784c3894e3c0. 1 - Pending task
shadowhook: hook_sym_name(libhookee2.so, test_hook_before_dlopen_2, 0x784a809d9eb0) ...
shadowhook: hook_sym_name(libhookee2.so, test_hook_before_dlopen_2, 0x784a809d9eb0) OK. return: 0xb400784c389441e0. 1 - Pending task

16kb页大小对于Native代码来说,更多在于系统调用失败。系统调用失败后果:如果编码习惯好,比如关注返回值、mmap返回值,那么你可以及时发现异常地方,如果没有关注返回值,如mprotect后不关注返回值,那么很有可能你后面再用这块内存的时候,比如你以为申请可读可写生效了,但其实没有生效导致产生Crash

mprotect((void *)start, end - start, prot);
use start (error)

当然,有可能依赖的三方库众多,你有可能检索不出来。那么对于此类问题,你可以全局hook一些关键函数,比如mprotect,调用时先对addr进行校验,判断是不是按照pagesize对齐即可,校验不通过则立马抛出crash,也能够让你在测试环境快速找到一些问题。

总结

最后,我们要意识到3个小结论:

  1. so 16kb 与 固定编码4kb在部分系统调用异常是互相独立的,即使so通过脚本查看到已经处于16kb边界对齐,在native代码上也以及存在硬编码的错误存在。
  2. 并非所有4096硬编码都会影响,需要看具体的系统调用是否支持,一刀切也不可取。
  3. 查看是否硬编码pagesize时,也要留意其他进制的数字,比如4096 0x1000等

通过本文,相信大家能够了解到如何适配Android15的16kb页了,那么,让子弹飞一会,动起来。