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
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的配置是

-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
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,因为其他基本都不影响。

列一个表格,展示一下

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
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/,另一个是 · 。

还有一个细节,ndk-llvm 里是整合在一起的、叫libLLVM.dyliblibclang.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 掉来缩小体积。

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/clangandroid-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
2
3
4
5
6
7
8
9
10
11
12
__ZN4llvm10ModulePass17assignPassManagerERNS_7PMStackENS_15PassManagerTypeE

➜ 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
if build_llvm_tools:
stage1_extra_defines['LLVM_BUILD_TOOLS'] = 'ON'
else:
stage1_extra_defines['LLVM_BUILD_TOOLS'] = 'OFF'

在llvm-8.0.0里,默认是开着的;而在 ndk-llvm里,默认是关着的。从名字上来看,像是针对 tools 下的文件是否进行编译,所以这个参数可以测试一下。 修改 python 里的文件,改为 ON,这时候果然出现了opt,于是我用这个clang 去加载我们的 Pass:

开启之前:

1
2
3
4
5
6
7
8
9
10
➜  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'

开启之后:

1
2
3
4
5
6
➜  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'

果然,是 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
➜  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 )

LLVM_ENABLE_ABI_BREAKING_CHECKS 很可能是与LLVM_ENABLE_ASSERTIONS 有关的,所以这里再搜一下,找到了下面的内容

1
2
3
4
cmake/modules/HandleLLVMOptions.cmake
82 if( LLVM_ENABLE_ASSERTIONS )
83 set( LLVM_ENABLE_ABI_BREAKING_CHECKS 1 )
84 endif()

破案了,这时候没有任何遗留的问题了。

回归主题!

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 的配置。可以说学到非常多的东西了。