前言

stackplz在v3.0.3以及之前,为了实现堆栈回溯功能,用到了一个libstackplz.so的预编译库,项目位于unwinddaemon

这个so的实现会依赖于libunwindstack,这是参考定制bcc/ebpf在android平台上实现基于dwarf的用户态栈回溯后改进实现的,本质上是因为simpleperf是用libunwindstack实现的

于是libstackplz编译需要在AOSP的环境下完成,随着stackplz的更新,每次改动libstackplz时就需要在AOSP下编译一次,如果代码写得有问题,又是等上好几分钟再来一次

如果是在已经编译过一次的情况下再次进行编译,使用mmm不会从头编译一轮,但是AOSP的构建系统扫描代码的相当费时,即使很小的改动,在我的电脑上仍然要至少五六分钟的样子(记不太清了

最近终于在参考了以下两个项目之后,成功在CMAKE + NDK + 部分AOSP代码的条件下实现了libstackplz的编译,并且将所有需要的代码编译为单个so

实现

简单来说就是拉取必要的AOSP代码,编写CMakeLists.txt,配置NDK的环境,然后make即可

同步必要AOSP代码

这里选择了android14-release分支,作为子模块添加

以下是init-submodules.sh的内容

BRANCH=android14-release

git submodule add -b "$BRANCH" https://android.googlesource.com/platform/art external/art
git submodule add -b "$BRANCH" https://android.googlesource.com/platform/system/libbase external/libbase
# git submodule add -b "$BRANCH" https://android.googlesource.com/platform/system/logging external/logging
git submodule add -b "$BRANCH" https://android.googlesource.com/platform/libnativehelper external/libnativehelper
git submodule add -b "$BRANCH" https://android.googlesource.com/platform/system/libprocinfo external/libprocinfo
git submodule add -b "$BRANCH" https://android.googlesource.com/platform/system/unwinding external/unwinding
git submodule add -b "$BRANCH" https://android.googlesource.com/platform/external/lzma external/lzma
git submodule add -b "$BRANCH" https://android.googlesource.com/platform/bionic external/bionic

这部分主要是参考的libunwindstack-standalone

这里logging注释掉的原因是:最终编译是提取头文件 + 依赖ndk的log库,这个其实和libunwindstack中的做法差不多

也就是说如果想自己手动提取一下头文件,就同步,不然就不需要,用提取好的就行

另外提一下,如果想直接把log库静态编译到自己的so里面,依赖的其他模块还有点多,写起来可能很麻烦,我尝试了下还是放弃了


如果是自己写项目,执行了上面的命令,也就是init-submodules.sh,接着就会拉取代码(记得上代理),一般情况下拉取了之后就会自己切好分支

以及这里需要补充的是,貌似没办法在git submodule里面用--depth,所以这个是同步的完整commit记录,所以下载的大小也不小,不过比起AOSP的整个代码还是小太多了

如果是拉取unwinddaemon,那么应该执行下面的命令(记得上代理),这样就会同步好代码

git submodule init
git submodule update --remote

这里额外提一下,有的地方会让你git submodule update,不过可能会出现说某commit不存在之类的,一种可能是因为原项目子模块有修改,这样就和官方,这里也就是googlesource的commit不一样了,所以这个时候应该加上--remote,把子模块的commit与官方保持一致

到这个时候,代码就算同步完成了

编写CMakeLists.txt

已经写好的CMakeLists.txt

我对cmake有一点了解但不多,所以还是选择参考libunwindstack,然后稍作改动以适应本项目需求

要编译之前已经写好的lib.cpp,最简单的写法就是下面这样

add_library(${PROJECT_NAME} SHARED lib.cpp)

当然接着就会出现各种错误,最直接的就是头文件找不到,于是挨个补齐头文件

target_include_directories(${PROJECT_NAME} PUBLIC ${EXTERNAL_SRC}/libbase/include)
target_include_directories(${PROJECT_NAME} PUBLIC ${EXTERNAL_SRC}/bionic/libc/kernel/uapi)
target_include_directories(${PROJECT_NAME} PUBLIC ${EXTERNAL_SRC}/unwinding/libunwindstack/include)

最开始编译出来发现文件很大,于是下面这样可以把产物做strip,生成的so就会很小了

SET_TARGET_PROPERTIES(${PROJECT_NAME} PROPERTIES LINK_FLAGS "-Wl,-s")

上面当然不是直接就能编译出来的,根据引用的头文件这些,就知道还需要其他依赖,主要是下面这些

target_link_libraries(${PROJECT_NAME} base procinfo lzma unwindstack log)

对于这些依赖,除了log库,其他的都是静态编译的,以base为例,也就是设置好源代码,头文件目录,编译选项

add_library(base STATIC
        "${EXTERNAL_SRC}/libbase/chrono_utils.cpp"
        "${EXTERNAL_SRC}/libbase/file.cpp"
        "${EXTERNAL_SRC}/libbase/hex.cpp"
        "${EXTERNAL_SRC}/libbase/mapped_file.cpp"
        "${EXTERNAL_SRC}/libbase/parsebool.cpp"
        "${EXTERNAL_SRC}/libbase/parsenetaddress.cpp"
        "${EXTERNAL_SRC}/libbase/logging.cpp"
        "${EXTERNAL_SRC}/libbase/posix_strerror_r.cpp"
        "${EXTERNAL_SRC}/libbase/process.cpp"
        "${EXTERNAL_SRC}/libbase/stringprintf.cpp"
        "${EXTERNAL_SRC}/libbase/strings.cpp"
        "${EXTERNAL_SRC}/libbase/threads.cpp"
)
target_include_directories(base PUBLIC "${EXTERNAL_SRC}/libbase/include" "${SHIM_SRC}/liblog/include")
target_compile_options(base PRIVATE -Wno-c99-designator)

复制粘贴命令当然是不方便的,所以需要写一个编译脚本,也就是build.sh,具体如下:

#/bin/bash

export ANDROID_NDK=/home/kali/Desktop/android-ndk-r25b

rm -r build
mkdir build && cd build 

cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
    -DANDROID=1 \
    -DANDROID_ABI="arm64-v8a" \
    -DANDROID_NDK=$ANDROID_NDK \
    -DANDROID_PLATFORM=android-30 \
    ..

make

cd ..

其他

关于shim_files

在测试过程中,出现rustc_demangle.h头文件说找不到,也就是unwinding/libunwindstack/Demangle.cpp这个文件

rust什么的...暂时只好不管了,所以将这个文件提取出来,按照去掉了rust的部分,因为以前也没这个

为了不改动子模块的东西,于是把涉及到改动了的,都放到了shim_files下面

  • shim_files/libunwindstack/Demangle.cpp

另外还有下面这个,这里面的改动主要是struct sigaction的初始化,原本的写法会有warning,强迫症怎么能忍

这个文件本身内容不算多,感觉也不会怎么变化,所以也改了拿出来放到shim_files下面,这些文件也对应在CMakeLists.txt

  • shim_files/libunwindstack/ThreadUnwinder.cpp

关于Android 10/ANDROID_PLATFORM=android-29

最近刚好遇到一个issue反馈,内核是5.10的,但是安卓版本是10,然而unwinddaemon本身一开始就是基于安卓13的aosp代码编译的,虽然Android.bp中有设置min_sdk_version: "29",,不过好像还是有些问题,于是堆栈功能用不了

既然cmake+ndk走通了,那就试试编译安卓10的版本,于是将ANDROID_PLATFORM指定为android-29进行编译

很快就会出现报错,提示如下:

error: '__android_log_set_default_tag' is unavailable: introduced in Android 30

这是因为相关的头文件中定义是下面这样的,也就是这个API是SDK 30即Android 11引入的,Android 10是没法用的

void __android_log_set_default_tag(const char* tag) __INTRODUCED_IN(30);

这个的解决办法,最合理的当然是找到具体的实现,自己适配一下

次之就是让它编译通过就行,比如返回void的就给个空函数,返回int的直接返回0或者1

cinit/libunwindstack有一个android_log_impl.cc可以作为参考,不过我尝试了下感觉还是怪麻烦的

秉承着尽可能不改动aosp里面的代码,我决定下面这样写,也就是让编译器以为Android 10上真有这些函数...

然后测试了下发现竟然不崩(也就是没有实际用到这些函数,所以这样也行吧...

#if __ANDROID_API__ >= 30

// 原来的代码

#else

void __android_log_set_default_tag(const char* tag);

#endif

总结

脱离AOSP的构建环境的核心其实就是做好Android.bpCMakeLists.txt的转换,然后搞清楚依赖关系

当然这是对于简单引用一些AOSP里面的代码的情况,如果是太复杂,那也不好说...

有了这个经验之后,有一个用处就是,可以试试简单引用ArtMethod之类的头文件,然后可以比较方便编译出来,得到它们的偏移(大概