ndkr19默认用的是llvm-8.0.2,而今天才发布的llvm-8.0.0,之前是用7.0.0将就的,今天终于不用将就了,重新搭建了一下环境,对 ndk 使用 llvm 的理解更加深刻。本文介绍一下开发环境的搭建。
2019年10月26日17:21:10:本文介绍的是覆盖 NDK 中的clang的实现方式,在第十一篇中介绍了更优雅的实现。
一、编译llvm-8.0.0及其附加组件
下载一下,我使用了这些组件,并且放到正确的位置:
1 2 3 4 5 6 7 |
lld-8.0.0.src.tar.xz -> tools/lld lldb-8.0.0.src.tar.xz -> tools/lldb cfe-8.0.0.src.tar.xz -> tools/clang clang-tools-extra-8.0.0.src.tar.xz -> tools/clang/tools/extra compiler-rt-8.0.0.src.tar.xz -> projects/compiler-rt libcxxabi-8.0.0.src.tar.xz -> projects/libcxxabi libcxx-8.0.0.src.tar.xz -> projects/libcxx |
先把llvm主工程解压出来:
1 2 |
xz -d llvm-8.0.0.src.tar.xz tar xvf llvm-8.0.0.src.tar |
工作目录是 llvm-8.0.0.src
然后把对应的文件全都放到对应的位置就行,见上面的文本,至于为什么要这样做,可以看对应父目录下的CMakeLists.txt,有个编译选项是:“当存在该目录时,编译该目录(大概这个意思)”
1 2 3 4 5 |
mkdir b cd b # 如果是为了 android-ndk 的话,建议添加 -DLLVM_LIBDIR_SUFFIX=64 让lib自动重命名为lib64 cmake ../llvm-8.0.0.src -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON make -j8 |
然后喝杯茶,lldb 编译爆炸了,有两个方案,一是舍弃lldb,二是看第二步。
舍弃lldb的配置是
1 |
-DLLDB_CODESIGN_IDENTITY="" |
install到我想要的地方
1 |
cmake -DCMAKE_INSTALL_PREFIX=/Users/leadroyal/pllvm/r -P cmake_install.cmake |
二、Mac上为lldb配置codesign
其实说明文件里是有的,就下面这段,按照描述操作一遍就可以编译通过了,非常稳。
1 |
cat llvm-8.0.0.src/tools/lldb/docs/code-signing.txt |
不会的话,网上搜吧,懒得写了,就是自己创建一个证书并且信任它。
三、ndk对llvm的处理
先说一下ndkr19和ndkr18的区别,关于 standard-toolchains 我认为这个是个很大的区别。
https://github.com/android-ndk/ndk/wiki/Changelog-r19
1 2 3 4 5 6 7 8 |
Issue 780: Standalone toolchains are now unnecessary. Clang, binutils, the sysroot, and other toolchain pieces are now all installed to $NDK/toolchains/llvm/prebuilt/<host-tag> and Clang will automatically find them. Instead of creating a standalone toolchain for API 26 ARM, instead invoke the compiler directly from the NDK: $ $NDK/toolchains/llvm/prebuilt/<host-tag>/bin/armv7a-linux-androideabi26-clang++ src.cpp For r19 the toolchain is also installed to the old path to give build systems a chance to adapt to the new layout. The old paths will be removed in r20. The make_standalone_toolchain.py script will not be removed. It is now unnecessary and will emit a warning with the above information, but the script will remain to preserve existing workflows. If you're using ndk-build, CMake, or a standalone toolchain, there should be no change to your workflow. This change is meaningful for maintainers of third-party build systems, who should now be able to delete some Android-specific code. For more information, see the Build System Maintainers guide. |
r19直接把这个功能砍了,说使用了更加友好的功能,经过体验,确实非常非常非常非常友好!强烈推荐这个大版本!
从原理上将,是将各个 Android 版本分别包装了一层,例如这个
1 2 3 4 5 6 7 8 |
➜ bin cat armv7a-linux-androideabi16-clang #!/bin/bash if [ "$1" != "-cc1" ]; then `dirname $0`/clang --target=armv7a-linux-androideabi16 -fno-addrsig "$@" else # Target is already an argument. `dirname $0`/clang "$@" fi |
总之,平时使用时候更加友好,替换工程也更加方便了!
然后我们对比一下 llvm-8.0.0和 ndk-llvm之间的区别。
第一步编译出来的llvm-8.0.0,进行一下 install,目录如下
1 2 |
➜ pllvm ls r bin include lib libexec share |
而ndkr19里llvm的目录是
1 2 3 4 5 6 7 |
➜ darwin-x86_64 ls AndroidVersion.txt bin manifest_5058415.xml MODULE_LICENSE_BSD_LIKE i686-linux-android share MODULE_LICENSE_MIT include sysroot NOTICE lib x86_64-linux-android aarch64-linux-android lib64 arm-linux-androideabi libexec |
去除掉无关的东西,主要关注这三个路径 bin、lib、lib64,因为其他基本都不影响。
列一个表格,展示一下
llvm-8.0.0 | ndkr19-llvm | |
---|---|---|
bin | llvm 的 binary | 除了 llvm 的 binary,还有自己封装的一些脚本,本体在 clang 和 clang++ |
lib | 存放llvm的library,有.a和.dylib | 存放交叉编译相关的一些文件,与llvm无关 |
lib64 | 不拥有 | 存放llvm的library,只有.dylib |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
➜ android-ndk-r19c/.../darwin-x86_64 tree lib lib ├── bfd-plugins │ └── LLVMgold.dylib ├── gcc │ ├── aarch64-linux-android │ │ └── 4.9.x │ │ ├── crtbegin.o │ │ ├── libgcc.a │ │ └── libgcc_real.a │ ├── arm-linux-androideabi │ │ └── 4.9.x │ │ ├── armv7-a │ │ │ ├── crtbegin.o │ │ │ ├── libgcc.a │ │ │ ├── libgcc_real.a │ │ │ └── thumb │ │ │ ├── crtbegin.o │ │ │ ├── libgcc.a │ │ │ └── libgcc_real.a │ │ ├── crtbegin.o │ │ ├── libgcc.a │ │ ├── libgcc_real.a │ │ └── thumb │ │ ├── crtbegin.o │ │ ├── libgcc.a │ │ └── libgcc_real.a │ ├── i686-linux-android │ │ └── 4.9.x │ │ ├── crtbegin.o │ │ ├── libgcc.a │ │ └── libgcc_real.a │ └── x86_64-linux-android │ └── 4.9.x │ ├── crtbegin.o │ ├── libgcc.a │ └── libgcc_real.a └── lib64 └── libc++.1.dylib |
再对比一下二者存放 llvm 库的目录,llvm-8.0.0里既有静态链接库,也有动态链接库,而ndk-llvm里只有动态链接库。而且版本号也不一样,会引起头文件寻找路径不一样,因为寻找路径是硬编码在clang里面的,一个是 lib/clang/8.0.0/,另一个是 lib64/clang/8.0.2/ 。
还有一个细节,ndk-llvm 里是整合在一起的、叫libLLVM.dylib和libclang.dylib,这个与编译配置有关,所以 google 应该是主动设置过 llvm 的编译选项的,于是我准备去寻找google 对 llvm 到底做了什么。
根据clang -v的结果,我们看下仓库的 master 分支
1 2 3 4 |
Android (5058415 based on r339409) clang version 8.0.2 (https://android.googlesource.com/toolchain/clang 40173bab62ec746213857d083c0e8b0abb568790) (https://android.googlesource.com/toolchain/llvm 7a6618d69e7e8111e1d49dc9e7813767c5ca756a) (based on LLVM 8.0.2svn) Target: x86_64-apple-darwin18.2.0 Thread model: posix InstalledDir: /tmp/android-ndk-r19c/toolchains/llvm/prebuilt/darwin-x86_64/bin |
打开 https://android.googlesource.com/toolchain/llvm ,里面没有主动编译它,似乎也没有配置文件,逛了一圈,不小心找到一个这个东西,如图
点进去一看,我去,踏破铁鞋无觅处,也就是这个仓库 https://android.googlesource.com/toolchain/llvm_androd/ 。
里面写着 Android Clang/LLVM Toolchain 和 编译流程,直接一个 python 文件就搞定了
1 |
python toolchain/llvm_android/build.py |
所以,所有的谜题都在 build.py 里解开了,我们仔细阅读一下,就知道ndk对llvm做了什么。
这里简单介绍一下,以 darwin 为例,主要执行逻辑如下:
1 2 3 4 5 6 7 8 9 |
main \->build_stage1 \--->build_llvm \----->base_cmake_defines \----->invoke_cmake \-------> \->build_stage2 \--->build_llvm....... \->package_toolchain |
在build_stage1里,编译整个llvm的框架,不包括子项目(似乎包括 clang),里面有各种各样的配置;
在build_stage2里,编译lld、lldb等子项目,里面有各种各样的配置;
在package_toolchain里,将前两步编译出来的东西打包一下,删去无关的东西。
1 2 3 4 5 6 7 8 |
for bin_filename in bin_files: binary = os.path.join(bin_dir, bin_filename) if os.path.isfile(binary): if bin_filename not in necessary_bin_files: remove(binary) elif strip: if bin_filename not in script_bins: check_call(['strip', binary]) |
删去无用的二进制文件,并且strip 掉来缩小体积。
1 |
remove_static_libraries(os.path.join(install_dir, lib_dir)) |
之后组织头文件、写 License、设置一些细枝末节的东西。
那么回到最开始的问题,是哪个编译选项控制llvm 的输出是 lib64呢?这里搜两个字符串,分别是 lib64 和 64。前者一般是硬编码拼接字符串的,没有看到主动配置,后者却是是主动的,代码是:
1 2 3 4 5 |
def base_cmake_defines(): defines = {} .......... defines['LLVM_LIBDIR_SUFFIX'] = '64' .......... |
正是因为这个选项,才导致编译出来的 lib 被命名为了 lib64。所有的配置都在这个 python 文件里写着,所以要想复现一个一模一样配置的环境出来,也是很简单的事情。
结论是:llvm-8.0.0 和 ndk-llvm 主要的不一致在于编译选项的不一致,次要的不一致在于可能在于某些细节上 google 做了 patch。
四、让 ndk-llvm 加载本地的 Pass
既然原理都说清楚了,之前暴力替换掉 ndk-llvm 整个工程的方法也可以不用了,当然,这里两条路都可以走,都介绍一下吧。
方案1:使用llvm-8.0.0替换掉ndk-llvm(之前一直在用的老方案)
直接用软连接,连到我们编译好的 llvm-8.0.0 上面即可。
第一步:删掉 android-ndk-r19c/toolchains/llvm/prebuilt/darwin-x86_64/lib64 ,删掉 android-ndk-r19c/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang 和 android-ndk-r19c/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++
第二步:修复lib64、clang、clang++(其实还有一些别的文件例如lld之类的需要替换,但跟我们无关)
主要代码如下:
1 2 3 4 5 6 7 |
rm /tmp/android-ndk-r19c/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang rm /tmp/android-ndk-r19c/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++ rm -rf /tmp/android-ndk-r19c/toolchains/llvm/prebuilt/darwin-x86_64/lib64 # $LLVM_HOME/lib64 requires LLVM_LIBDIR_SUFFIX=64 cp -r $LLVM_HOME/lib64 /tmp/android-ndk-r19c/toolchains/llvm/prebuilt/darwin-x86_64/lib64 cp $LLVM_HOME/bin/clang /tmp/android-ndk-r19c/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang cp $LLVM_HOME/bin/clang++ /tmp/android-ndk-r19c/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++ |
将 llvm 完全替换掉,自己的 clang 加载自己的 pass,没毛病,相信这种办法大家已经都会了,下面介绍另一种办法,不修改NDK、让 NDK 加载我们的 Pass。
方案2:用相同配置编译一份 llvm 的环境,使用它编译 Pass。(虽然失败了,但发现了比较有意思的内容)
先试试看,既然都是llvm8直接加载,发现有个符号找不到,
1 |
__ZN4llvm10ModulePass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE |
1 2 3 4 5 6 7 8 9 10 |
➜ darwin-x86_64 bin/clang -m32 -Xclang -load -Xclang /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so /tmp/test.c clang++: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated] error: unable to load plugin '/Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so': 'dlopen(/Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so, 9): Symbol not found: __ZN4llvm10ModulePass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE Referenced from: /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so Expected in: flat namespace in /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so' |
这个错误很眼熟,之前用llvm7也是这个报错,于是我开始怀疑并不是版本不一致的问题,开始探索。
有个命令很好用,叫 nm -gU ,用来获取导出函数,这里放个结论:
1 2 3 4 5 6 |
➜ bin nm -gU ~/pllvm/r/bin/clang | grep __ZN4llvm10ModulePass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE 00000001015b0670 T __ZN4llvm10ModulePass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE ➜ bin nm -gU ~/pllvm/r2/lib/libLLVM.dylib | grep __ZN4llvm10ModulePass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE 00000000002aa200 T __ZN4llvm10ModulePass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE ➜ bin nm -gU $ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang | grep __ZN4llvm10ModulePass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE NOT FOUND! |
结论是:我们自己编译的clang和libLLVM.dylib可以找到符号,ndk的clang找不到符号,ndk的libLLVM.dylib可以找到符号。
也就是说,ndk的clang是坏掉的!我这里对比了一下二者的大小,发现差的也太多了吧
1 2 3 4 |
➜ lsa bin/clang-8.0.0 -rwxr-xr-x@ 1 leadroyal wheel 94M 3 21 20:55 bin/clang ➜ lsa bin/clang-ndk -rwxr-xr-x@ 1 leadroyal wheel 48M 3 1 00:41 bin/clang |
这里我做了另一个正确的决定,直接把 clang 文件给换了,看看会发生什么事。经过测试,一次性成功!
那么问题就来了,估计又是哪个编译选项我没有注意到,开始下面的探索,为什么这个 ndk-clang 是坏的。
主要就阅读这个 python 文件,并且编译 llvm 进行测试,第一步就是下载相关的环境,经过多次测试,下面这个是完成build_stage1的集合,其实已经够我们用了,下载一份 master 代码回来。
这些工程放好就行,因为在 llvm 里,google 已经放好了软连接,将各个子项目连起来了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
➜ tc tree -L 3 . └── android ├── prebuilts ├── clang │ └── host │ └── darwin-x86 ├── cmake │ └── darwin-x86 └── toolchain ├── binutils ├── clang ├── compiler-rt ├── libcxx ├── libcxxabi ├── lld ├── llvm ├── llvm_android └── openmp_llvm |
这个列表主要是根据这段代码抠出来的:
1 2 3 4 5 6 7 8 9 10 11 |
def install_license_files(install_dir): projects = ( 'llvm', 'llvm/projects/compiler-rt', 'llvm/projects/libcxx', 'llvm/projects/libcxxabi', 'llvm/projects/openmp', 'llvm/tools/clang', 'llvm/tools/clang/tools/extra', 'llvm/tools/lld', ) |
放好之后,python 直接就可以直接跑了,编译过程会比较慢,大概一个小时,不要急。
编译完成之后,我看了一眼输出目录,居然少很多文件:
1 2 3 4 5 6 7 8 9 10 |
➜ android ls out/stage1/bin FileCheck clang-import-test ld64.lld llvm-isel-fuzzer llvm-readobj c-index-test clang-offload-bundler lld llvm-itanium-demangle-fuzzer llvm-special-case-list-fuzzer clang clang-refactor lld-link llvm-lib llvm-strip clang++ clang-rename lli-child-target llvm-lit llvm-tblgen clang-9 clang-tblgen llvm-PerfectShuffle llvm-microsoft-demangle-fuzzer llvm-yaml-numeric-parser-fuzzer clang-cl count llvm-ar llvm-objcopy not clang-cpp diagtool llvm-config llvm-opt-fuzzer wasm-ld clang-diff hmaptool llvm-dlltool llvm-ranlib yaml-bench clang-format ld.lld llvm-go llvm-readelf |
我们最最最关心的,opt 文件是不存在的!还有一些别的文件也不存在!
更新:和 opt没关系,只是clang符号缺失了。
对llvm-8.0.0的代码 和 ndk-llvm 的源文件的配置项进行 diff,tools下和tools/opt下都没有什么变化,这时候有两种猜测,一个是在 stage2里再生成opt,一个是配置项里主动关闭了 opt。
经过阅读,暂时可以排除掉stage2里的编译内容,opt 是 llvm 本身的东西,不大会放在第二步执行。于是可以继续锁定到编译选项里,直到我找到了下面这句话
1 2 3 4 5 |
<del> if build_llvm_tools: stage1_extra_defines['LLVM_BUILD_TOOLS'] = 'ON' else: stage1_extra_defines['LLVM_BUILD_TOOLS'] = 'OFF' </del> |
在llvm-8.0.0里,默认是开着的;而在 ndk-llvm里,默认是关着的。从名字上来看,像是针对 tools 下的文件是否进行编译,所以这个参数可以测试一下。
修改 python 里的文件,改为 ON,这时候果然出现了opt,于是我用这个clang 去加载我们的 Pass:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<del>开启之前: ➜ stage1-install bin/clang -m32 -Xclang -load -Xclang /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so /tmp/test.c clang++: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated] error: unable to load plugin '/Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so': 'dlopen(/Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so, 9): Symbol not found: __ZN4llvm10ModulePass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE Referenced from: /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so Expected in: flat namespace in /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so' 开启之后: ➜ stage1-install bin/clang -m32 -Xclang -load -Xclang /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so /tmp/test.c error: unable to load plugin '/Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so': 'dlopen(/Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so, 9): Symbol not found: __ZN4llvm23EnableABIBreakingChecksE Referenced from: /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so Expected in: flat namespace in /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so'</del> |
果然,是
LLVM_BUILD_TOOLS 引起的!
这时候遇到了老问题,是 __ZN4llvm23EnableABIBreakingChecksE 还是 __ZN4llvm24DisableABIBreakingChecksE 的问题,之前认为是 Release 版和 Debug 版的区别(详见第四篇文章)。这里我们编译的都是 Release 版,为什么也会出现符号不一致的情况呢?
这里让我想起另一个配置项,我编译 Release 版的时候,是有
-DLLVM_ENABLE_ASSERTIONS=ON,而ndk里这个是关掉的!
另一个细节是,Debug 版默认是 -DLLVM_ENABLE_ASSERTIONS=ON,而 Release 版默认是 OFF 的!
之后搜索代码,发现这个标记由
LLVM_ENABLE_ABI_BREAKING_CHECKS 直接决定。
1 2 3 4 |
<del>➜ llvm-8.0.0.src grep "LLVM_ENABLE_ABI_BREAKING_CHECKS" * -R cmake/modules/HandleLLVMOptions.cmake: set( LLVM_ENABLE_ABI_BREAKING_CHECKS 1 ) cmake/modules/HandleLLVMOptions.cmake: set( LLVM_ENABLE_ABI_BREAKING_CHECKS 1 ) </del> |
LLVM_ENABLE_ABI_BREAKING_CHECKS 很可能是与
LLVM_ENABLE_ASSERTIONS 有关的,所以这里再搜一下,找到了下面的内容
1 2 3 4 5 |
<del>cmake/modules/HandleLLVMOptions.cmake 82 if( LLVM_ENABLE_ASSERTIONS ) 83 set( LLVM_ENABLE_ABI_BREAKING_CHECKS 1 ) 84 endif() </del> |
破案了,这时候没有任何遗留的问题了。
回归主题!
ndk无法加载Pass的原因是:LLVM_BUILD_TOOLS 默认是 false,在这种情况下,本来就没有 opt 和编译时优化,是永远无法加载 Pass 的!
2019年10月23日16:56:34:这里描述有误,clang加载pass和opt是无关的,mac上无法加载是因为那个clang被strip过了,确确实实没有符号。
__ZN4llvm23EnableABIBreakingChecksE 和 __ZN4llvm24DisableABIBreakingChecksE 是由 LLVM_ENABLE_ASSERTIONS 决定的,需要保持一致。
五、总结
整个过程编译了十来遍 llvm,花了不少时间操作和思考,加深了对 llvm 整个体系的了解,加深了 ndk 里对 llvm 的配置。可以说学到非常多的东西了。
不太对。
LLVM_ABI_BREAKING_CHECKS这个cmake选项才是决定abi breaking的。
感谢,没有太注意这个细节,后来一直直接盖掉ndk的llvm,就不在意它们的区别了。
然后大小不一样的话。我记得strip会strip掉10m左右,其他的话确定一下是不是一样的build mode,release跟minsizerelease会差很多。然后如果一个启用了动态编译一个静态链接的话也有可能
您好,移植后如果想让ndk-llvm加载本地的 Pass,我是通过为clang指定 -Xclang -load -Xclang ollvm.so 参数加载混淆pass的,那么请问这种方式怎么能为pass传递参数,如ollvm的-bcf_loop=3这种参数?
和原先一样,使用 [-mllvm -bcf_loop=3]