llvm学习(九):再启程,llvm-8.0.0+ndkr19的环境搭建
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 | lld-8.0.0.src.tar.xz -> tools/lld |
先把llvm主工程解压出来:
1 | xz -d llvm-8.0.0.src.tar.xz |
工作目录是 llvm-8.0.0.src
然后把对应的文件全都放到对应的位置就行,见上面的文本,至于为什么要这样做,可以看对应父目录下的CMakeLists.txt,有个编译选项是:“当存在该目录时,编译该目录(大概这个意思)”
1 | mkdir b |
然后喝杯茶,lldb 编译爆炸了,有两个方案,一是舍弃lldb,二是看第二步。 舍弃lldb的配置是
-DLLDB_CODESIGN_IDENTITY=""
install到我想要的地方
cmake -DCMAKE_INSTALL_PREFIX=/Users/leadroyal/pllvm/r -P cmake_install.cmake
二、Mac上为lldb配置codesign
其实说明文件里是有的,就下面这段,按照描述操作一遍就可以编译通过了,非常稳。
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
Issue 780: Standalone toolchains are now unnecessary. Clang, binutils, the sysroot, and other toolchain pieces are now all installed to $NDK/toolchains/llvm/prebuilt/
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/
/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 | ➜ bin cat armv7a-linux-androideabi16-clang |
总之,平时使用时候更加友好,替换工程也更加方便了!
然后我们对比一下 llvm-8.0.0和 ndk-llvm之间的区别。
第一步编译出来的llvm-8.0.0,进行一下 install,目录如下
1 | ➜ pllvm ls r |
而ndkr19里llvm的目录是
1 | ➜ darwin-x86_64 ls |
去除掉无关的东西,主要关注这三个路径 bin、lib、lib64,因为其他基本都不影响。
列一个表格,展示一下
file | llvm-8.0.0 | ndkr19-llvm |
---|---|---|
bin | llvm 的 binary | 除了 llvm 的 binary,还有自己封装的一些脚本,本体在 clang 和 clang++ |
lib | 存放llvm的library,有.a和.dylib | 存放交叉编译相关的一些文件,与llvm无关 |
lib64 | 不拥有 | 存放llvm的library,只有.dylib |
1 | ➜ android-ndk-r19c/.../darwin-x86_64 tree lib |
再对比一下二者存放 llvm 库的目录,llvm-8.0.0里既有静态链接库,也有动态链接库,而ndk-llvm里只有动态链接库。
而且版本号也不一样,会引起头文件寻找路径不一样,因为寻找路径是硬编码在clang里面的,一个是 lib/clang/8.0.0/
,另一个是 · 。
还有一个细节,ndk-llvm 里是整合在一起的、叫libLLVM.dylib
和libclang.dylib
,这个与编译配置有关,所以 google 应该是主动设置过 llvm 的编译选项的,于是我准备去寻找google 对 llvm 到底做了什么。
根据clang -v的结果,我们看下仓库的 master 分支
1 | 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) |
打开 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 | main |
在build_stage1里,编译整个llvm的框架,不包括子项目(似乎包括 clang),里面有各种各样的配置; 在build_stage2里,编译lld、lldb等子项目,里面有各种各样的配置; 在package_toolchain里,将前两步编译出来的东西打包一下,删去无关的东西。
1 | for bin_filename in bin_files: |
删去无用的二进制文件,并且strip 掉来缩小体积。
remove_static_libraries(os.path.join(install_dir, lib_dir))
之后组织头文件、写 License、设置一些细枝末节的东西。
那么回到最开始的问题,是哪个编译选项控制llvm 的输出是 lib64呢?这里搜两个字符串,分别是 lib64 和 64。前者一般是硬编码拼接字符串的,没有看到主动配置,后者却是是主动的,代码是:
1 | def base_cmake_defines(): |
正是因为这个选项,才导致编译出来的 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 | rm /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 |
这个错误很眼熟,之前用llvm7也是这个报错,于是我开始怀疑并不是版本不一致的问题,开始探索。
有个命令很好用,叫 nm -gU
,用来获取导出函数,这里放个结论:
1 | ➜ bin nm -gU ~/pllvm/r/bin/clang grep __ZN4llvm10ModulePass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE |
结论是:我们自己编译的clang和libLLVM.dylib可以找到符号,ndk的clang找不到符号,ndk的libLLVM.dylib可以找到符号。
也就是说,ndk的clang是坏掉的!我这里对比了一下二者的大小,发现差的也太多了吧
1 | ➜ lsa bin/clang-8.0.0 |
这里我做了另一个正确的决定,直接把 clang 文件给换了,看看会发生什么事。经过测试,一次性成功!
那么问题就来了,估计又是哪个编译选项我没有注意到,开始下面的探索,为什么这个 ndk-clang 是坏的。
主要就阅读这个 python 文件,并且编译 llvm 进行测试,第一步就是下载相关的环境,经过多次测试,下面这个是完成build_stage1的集合,其实已经够我们用了,下载一份 master 代码回来。 这些工程放好就行,因为在 llvm 里,google 已经放好了软连接,将各个子项目连起来了。
1 | ➜ tc tree -L 3 |
这个列表主要是根据这段代码抠出来的:
1 | def install_license_files(install_dir): |
放好之后,python 直接就可以直接跑了,编译过程会比较慢,大概一个小时,不要急。
流程是:使用系统编译器,获得stage1;使用stage1再次编译,获得stage2。stage2 就是NDK的前身。
对 llvm-8.0.0
的代码 和 ndk-llvm
的源文件的配置项进行 diff,tools下和tools/opt下都没有什么变化,这时候有两种猜测,一个是在 stage2里再生成opt,一个是配置项里主动关闭了 opt。
经过阅读,暂时可以排除掉stage2里的编译内容,opt 是 llvm 本身的东西,不大会放在第二步执行。于是可以继续锁定到编译选项里,直到我找到了下面这句话
1 | if build_llvm_tools: |
在llvm-8.0.0里,默认是开着的;而在 ndk-llvm里,默认是关着的。从名字上来看,像是针对 tools 下的文件是否进行编译,所以这个参数可以测试一下。 修改 python 里的文件,改为 ON,这时候果然出现了opt,于是我用这个clang 去加载我们的 Pass:
开启之前:
1 | ➜ stage1-install bin/clang -m32 -Xclang -load -Xclang /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so /tmp/test.c |
开启之后:
1 | ➜ stage1-install bin/clang -m32 -Xclang -load -Xclang /Users/leadroyal/Pdd/online/pllvm/cmake-build-debug/pllvm/libpllvm.so /tmp/test.c |
果然,是 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 | ➜ llvm-8.0.0.src grep "LLVM_ENABLE_ABI_BREAKING_CHECKS" * -R |
LLVM_ENABLE_ABI_BREAKING_CHECKS 很可能是与LLVM_ENABLE_ASSERTIONS 有关的,所以这里再搜一下,找到了下面的内容
1 | cmake/modules/HandleLLVMOptions.cmake |
破案了,这时候没有任何遗留的问题了。
回归主题!
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 的配置。可以说学到非常多的东西了。