如何给嵌入式Linux构建一个支持多特性的perf

本文涉及环境为Ubuntu 20.04 x86_64、Ubuntu 20.04 ARM64、Linux Kernel源码Tag v5.9.11。

Perf工具对于Linux性能分析非常有用,在PC的Linux发行版上,只需要使用相应系统的包管理命令安装即可,例如,在Ubuntu系统上,直接使用sudo apt-get install -y linux-tools-common linux-tools-generic命令安装,其中linux-tools-common提供了各种linux tools的包装器,如acpidbg、perf等,它们会在运行时引用到适合当前内核版本的工具,而linux-tools-generic是个虚拟包,指向当前发行版最新的linux tools版本,这里面包含了真正要执行的命令。

常规交叉编译方法及其问题

对于嵌入式系统,通常需要自己编译perf,perf的源码已经集成到Linux Kernel中,以ARM64位体系架构为例,常规的交叉编译方法是执行如下命令:

1
2
3
4
5
$ sudo apt-get install gcc-aarch64-linux-gnu flex libssl-dev
$ cd KERNEL_SRC/tools/perf
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- V=1 VF=1 |& tee build.log
$ file perf
perf: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=eebe3fc457239da83b56a90b27b251df10160b3b, for GNU/Linux 3.7.0, with debug_info, not stripped

从上面的file命令输出可以看到这个perf程序是动态链接的,如此就要求交叉编译时引用的动态库与目标板上运行时的动态库是兼容的,省事的做法是,编译perf时的环境和构建目标系统的环境是一致的,这包括宿主机操作系统版本和工具链版本等。否则在目标板上运行时可能出现类似下面的错误:

1
2
$ perf
perf: /lib/libc.so.6: version `GLIBC_2.28' not found (required by perf)

然而,make的输出可能还给出了其它信息,查看make命令最开始的输出,可以看到类似以下的内容:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Auto-detecting system features:
... dwarf: [ OFF ]
... dwarf_getlocations: [ OFF ]
... glibc: [ on ]
... libbfd: [ OFF ]
... libcap: [ OFF ]
... libelf: [ OFF ]
... libnuma: [ OFF ]
... numa_num_possible_cpus: [ OFF ]
... libperl: [ OFF ]
... libpython: [ OFF ]
... libcrypto: [ OFF ]
... libunwind: [ OFF ]
... libdw-dwarf-unwind: [ OFF ]
... zlib: [ OFF ]
... lzma: [ OFF ]
... get_cpuid: [ OFF ]
... bpf: [ on ]
... libaio: [ on ]
... libzstd: [ OFF ]
... disassembler-four-args: [ OFF ]
... backtrace: [ on ]
... eventfd: [ on ]
... fortify-source: [ on ]
... sync-compare-and-swap: [ on ]
... get_current_dir_name: [ on ]
... gettid: [ on ]
... libelf-getphdrnum: [ OFF ]
... libelf-gelf_getnote: [ OFF ]
... libelf-getshdrstrndx: [ OFF ]
... libelf-mmap: [ OFF ]
... libpython-version: [ OFF ]
... libslang: [ OFF ]
... libslang-include-subdir: [ OFF ]
... pthread-attr-setaffinity-np: [ on ]
... pthread-barrier: [ on ]
... reallocarray: [ on ]
... stackprotector-all: [ on ]
... timerfd: [ on ]
... sched_getcpu: [ on ]
... sdt: [ OFF ]
... setns: [ on ]
... file-handle: [ on ]
Makefile.config:389: No libelf found. Disables 'probe' tool, jvmti and BPF support in 'perf record'. Please install libelf-dev, libelf-devel or elfutils-libelf-devel
Makefile.config:556: No sys/sdt.h found, no SDT events are defined, please install systemtap-sdt-devel or systemtap-sdt-dev
Makefile.config:631: Disabling post unwind, no support found.
Makefile.config:697: No libcrypto.h found, disables jitted code injection, please install openssl-devel or libssl-dev
Makefile.config:713: slang not found, disables TUI support. Please install slang-devel, libslang-dev or libslang2-dev
Makefile.config:759: Missing perl devel files. Disabling perl scripting support, please install perl-ExtUtils-Embed/libperl-dev
Makefile.config:791: No 'python-config' tool was found: disables Python support - please install python-devel/python-dev
Makefile.config:875: No liblzma found, disables xz kernel module decompression, please install xz-devel/liblzma-dev
Makefile.config:888: No libzstd found, disables trace compression, please install libzstd-dev[el] and/or set LIBZSTD_DIR
Makefile.config:899: No libcap found, disables capability support, please install libcap-devel/libcap-dev
Makefile.config:912: No numa.h found, disables 'perf bench numa mem' benchmark, please install numactl-devel/libnuma-devel/libnuma-dev
Makefile.config:967: No libbabeltrace found, disables 'perf data' CTF format support, please install libbabeltrace-dev[el]/libbabeltrace-ctf-dev

以上表明,有多个库找不到,以致相应的特性得到支持,这可能导致最后使用时出现问题,例如:所捕获数据用来生成火焰图时有些函数名是unknown(这个问题详见后文参考资料)。

要生成一个能在目标系统上运行的,支持所有特性的perf版本,基本上有这些要求:

  • 编译时与运行时内核版本一致,这个一般不难满足,直接使用构建目标系统时用到的内核源码或者使用一个相同版本号的标准内核源码即可
  • 编译成静态链接的版本,这样就无需处理目标系统上已有的支持库是否缺失或者与编译时链接库版本不一致的问题
  • 编译时尽可能提供所有特性需要依赖的库

这里面最难的就是提供所有特性需要依赖的静态库,手工编译源码安装一些工具的开发者可能都会有印象,支持库的依赖解决是很麻烦的,各种库的版本要匹配,对编译器的版本可能还存在依赖。正是因为这样,各大Linux发行版通过各自的方式解决了包依赖,使得安装软件非常方便。那么,在嵌入式开发中,如何处理这一情况?Buildroot及Yocto项目都提供了构建一个完整的、定制化的嵌入式Linux系统,包括了根文件系统、工具链等。然而,在perf这个实例里,Buildroot目前还缺少上述日志中列出的多个包,而Yocto至少已知构建速度很慢、学习曲线很陡峭。其它的类似项目还包括Cross Linux From Scratch,可能也会有类似的问题。

利用Ubuntu ARM64 Docker的解决方案

Ubuntu是开发人员中常用的系统,提供了非常丰富的软件包,这里面包括许多的支持库,这些库不仅有交叉编译的版本,还有静态链接的版本(包名通常是PackageName-dev-ARCH-cross),如果能够利用Ubuntu丰富的库来生成静态链接的perf,可以大大减少我们自己去生成这些依赖库的时间。

平时,我们在x86或x86_64体系架构上运行Ubuntu,虽然可以安装各种以-cross结尾的库来交叉编译,但逐一安装就很繁琐了,事实上Ubuntu如果已经支持某个包,想要自己编译安装它,是很简单的,以i2c-tools为例,大概执行以下几步就能搞定

1
2
3
4
5
6
# 下载源代码并打上Ubuntu特定patch
apt-get source i2c-tools
# 安装构建i2c-tools的依赖包
sudo apt-get build-dep i2c-tools
# 编译源码并生成deb包
apt-get source --compile i2c-tools

当然我们平时不需要这么做,因为直接sudo apt-get install i2c-tools就能下载预编译好的包安装了。这里对我们有用的是apt-get build-dep命令可以轻而易举地帮我们解决大部分依赖问题,通过一定的配置(sudo dpkg --add-architecture arm64),还可以使用sudo apt-get build-dep -a arm64 PackageName来安装其它体系架构的依赖包。然而,Ubuntu同样提供了其它架构的版本,查看Ubuntu 21.04 Release页面,已经支持ARM、ARM64、PowerPC、System Z等多种体系架构。因此,如果我们在ARM64位版本的Ubuntu里安装相应的依赖库,并执行编译,就不存在交叉编译的问题,而是直接生成ARM64位版本。所以,现在的首要问题是能够运行一个ARM64位版本的Ubuntu系统,从头安装这样的一个系统当然很麻烦,好在我们可以使用docker技术,在x86_64位Ubuntu中,执行ARM64位版本的Ubuntu系统。在Docker Hubarm64v8/ubuntu仓库中,已经提供了Ubuntu的多个ARM64位版本,这些容器不能直接在x86_64的Linux版本上运行,而是需要使用qemu模拟器来执行它,简单地来说,就是如下的操作:

1
2
3
4
5
6
7
sudo apt-get install -y qemu-user-static
docker run -it -v /usr/bin/qemu-aarch64-static:/usr/bin/qemu-aarch64-static \
-v /home:/home \
-v /etc/passwd:/etc/passwd \
-v /etc/group:/etc/group \
-v /etc/shadow:/etc/shadow \
arm64v8/ubuntu:20.04 bash

现在我们已经获得一个Ubuntu 20.04的ARM64位容器,可以通过在容器中运行下列的命令来确认当前体系架构:

1
2
# uname -m
aarch64

接下来,可以在这个容器里安装好编译linux-tools-generic的必要依赖并执行编译:

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
40
41
42
43
44
45
# 启用sources.list中的源代码下载路径,以便apt-get build-dep可以运行
sed -i "s/# deb-src/deb-src/g" /etc/apt/sources.list
apt-get update -y
# 先安装一些必要的工具,如果不需要内建jvmti引擎的支持,可不安装default-jdk
apt-get install -y apt-utils bc bison dialog flex python3 python2-minimal default-jdk
# 自动安装构建linux-tools-generic包的大部分工具和依赖库
apt-get build-dep -y linux-tools-generic
# 使用下面的命令尝试静态编译,然后从日志中再查看还缺少的库,并逐一安装
# cd KERNEL_SRC/tools/perf
# make V=1 VF=1 FEATURE_TESTS=all LDFLAGS=-static |& tee build.log
# 根据上条命令的输出提示,安装缺少的包,反复尝试此过程
apt-get install -y libelf-dev
apt-get install -y libdw-dev
apt-get install -y systemtap-sdt-dev
apt-get install -y libssl-dev
apt-get install -y libslang2-dev
apt-get install -y libperl-dev
apt-get install -y python-dev
apt-get install -y liblzma-dev
apt-get install -y libzstd-dev
apt-get install -y libcap-dev
apt-get install -y libnuma-dev
apt-get install -y libiberty-dev
apt-get install -y libbabeltrace-dev
apt-get install -y libbabeltrace-ctf-dev
apt-get install -y libunwind-dev
# 即使安装了相应包后,仍旧会有No libdw DWARF unwind found这种提示,此时需要调试Makefile来判断真实原因
# make -n -d V=1 VF=1 FEATURE_TESTS=all LDFLAGS=-static |& tee build.log
# /usr/bin/make -f Makefile.perf -n -d V=1 VF=1 FEATURE_TESTS=all LDFLAGS=-static all |& tee build.log
# 通过上面命令的调试日志,可以看出来,是由KERNEL_SRC/tools/build/Makefile.feature来检测feature是否支持的
# 再具体到Makefile.feature,又可调试feature_check_code,将之添加$(warning)函数来输出检查feature时执行的命令
# 从命令的输出可以看到测试日志都在KERNEL_SRC/tools/build/feature/test-FEATURE.make.output中
# 针对每一个为OFF的feature,查看这些output文件,才会知道检测失败的真实原因
# 例如,从tools/build/feature/test-dwarf.make.output中看到的dwarf检测失败原因其实是/usr/bin/ld: cannot find -lbz2
apt-get install -y libbz2-dev
apt-get install -y binutils-dev
apt-get install -y libglib2.0-dev
apt-get install -y libopencsd-dev
apt-get install -y libclang-9-dev clang
apt-get install -y llvm-9-dev llvm llvm-9
apt-get install -y libpfm4-dev
# 为何最终使用下面的命令编译,查看后文问题说明
make V=1 VF=1 FEATURE_TESTS=all NO_LIBBABELTRACE=1 NO_JVMTI=1 |& tee shared.log
rm perf
make V=1 VF=1 FEATURE_TESTS=all NO_LIBBABELTRACE=1 NO_JVMTI=1 LDFLAGS=-static |& tee static.log

有些在ARM64架构上确实还不支持的,只能保留OFF了,为OFF的feature可能有:

  • libnuma:较低版本的Ubuntu(如16.04)没有libnuma-dev库,20.04是有的
  • numa_num_possible_cpus:同上
  • get_cpuid:尽管在2019年12月13日的一个内核patch中(提交ID:df5a5f3cf24608457bb5e57297dd9f0d528be58f)已经增加了ARM64版本的get_cpuid支持,但是,Ubuntu 20.04的内核头文件及libgcc-X-dev包尚未提供支持
  • disassembler-four-args:较早期的binutils-dev(至少 2.26.1前)提供的disassembler函数接口不同于新版本的,导致编译出错
  • gettid:早期版本的libc6-dev(如2.23)可能并没有提供此函数
  • libslang-include-subdir:测试libslang的头文件目录布局,并不太影响功能,libslang是用于TUI的,可以不支持
  • reallocarray:早期版本的libc6-dev(如2.23)可能并没有提供此函数
  • bionic:这是Android的C库,可以不支持,如果是用于Android,可考虑安装NDK来支持
  • compile-[x]32:这两个都以生成32位版的perf有关,并不需要
  • hello:简单的测试程序,这个为OFF,通常只是因为没有对所有feature进行测试而已,如果需要测试所有的,可以给make指定FEATURE_TESTS=all,也就是make V=1 VF=1 LDFLAGS=-static FEATURE_TESTS=all
  • gtk2/gtk2-infobar:GTK+ GUI支持,嵌入式系统一般不需要
  • libbabeltrace:静态链接时,链接参数指定的库不够导致编译错误,可以使用NO_LIBBABELTRACE=1禁止这一支持,以免引起编译错误,或使用下面的补丁
  • libopencsd:Ubuntu 20.04上的OpenCSD版本较低,为0.12.2,不满足v5.9.11版Kernel对OpenCSD 1.0.0以上的要求,如果需要可以手工下载源码编译安装
  • libunwind-x86:这里是编译ARM64架构的perf,自然是不支持的
  • libunwind-x86_64:这里是编译ARM64架构的perf,自然是不支持的
  • libunwind-arm:这里是编译ARM64架构的perf,自然是不支持的
  • libunwind-debug-frame-arm:这里是编译ARM64架构的perf,自然是不支持的
  • llvm:首先,使用llvm-10时编译会出错,原因是函数定义不匹配,需要将/usr/bin/llvm-config指向llvm9(rm /usr/bin/llvm-config && ln -s ../lib/llvm-9/bin/llvm-config /usr/bin/llvm-config)。其次,改为llvm9后静态编译,Ubuntu 20.04没有LLVM-9.a这个静态库,若是动态链接,无此问题
  • clang:同上
  • libbpf:Ubuntu 20.04尚未提供此库,但在编译过程中,会使用内核源码中生成的tools/lib/bpf/libbpf.a
  • libdebuginfod:Ubuntu 20.04的libelf-dev尚未提供debuginfod的支持
  • clang-bpf-co-re:与clang版本有关,已知clang-10是没有问题的,clang-9是有问题的

其它的feature检测,则可能是由于tools/build/feature/Makefile本身定义的规则有问题导致,通常是链接库缺少,在使用Ubuntu 20.04 ARM64版本编译v5.9.11 tag对应的版本时,至少要对内核源码做如下做如下修改,才能使相应的feature检测正确:

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
diff --git a/tools/build/feature/Makefile b/tools/build/feature/Makefile
index 8da2556cdbfa..af8a4caff3ef 100644
--- a/tools/build/feature/Makefile
+++ b/tools/build/feature/Makefile
@@ -137,7 +137,7 @@ $(OUTPUT)test-libopencsd.bin:

DWARFLIBS := -ldw
ifeq ($(findstring -static,${LDFLAGS}),-static)
-DWARFLIBS += -lelf -lebl -lz -llzma -lbz2
+DWARFLIBS += -lelf -lebl -ldl -lz -llzma -lbz2
endif

$(OUTPUT)test-dwarf.bin:
diff --git a/tools/perf/Makefile.config b/tools/perf/Makefile.config
index 2d6690b30856..123df1d500ff 100644
--- a/tools/perf/Makefile.config
+++ b/tools/perf/Makefile.config
@@ -962,6 +962,9 @@ ifndef NO_LIBBABELTRACE
CFLAGS += -DHAVE_LIBBABELTRACE_SUPPORT $(LIBBABELTRACE_CFLAGS)
LDFLAGS += $(LIBBABELTRACE_LDFLAGS)
EXTLIBS += -lbabeltrace-ctf
+ ifeq ($(findstring -static,${LDFLAGS}),-static)
+ EXTLIBS += -lbabeltrace -lglib-2.0 -luuid
+ endif
$(call detected,CONFIG_LIBBABELTRACE)
else
msg := $(warning No libbabeltrace found, disables 'perf data' CTF format support, please install libbabeltrace-dev[el]/libbabeltrace-ctf-dev);

在做完上述修改后,重新执行

1
2
3
4
5
$ make V=1 VF=1 FEATURE_TESTS=all NO_JVMTI=1 |& tee shared.log
$ rm perf
$ make V=1 VF=1 FEATURE_TESTS=all NO_JVMTI=1 LDFLAGS=-static |& tee static.log
$ ls -lh perf
-rwxr-xr-x 1 root root 38M May 26 18:49 perf

看见的将是大片的[ on ]而不再是大片的[ OFF ],如果编译顺利完成的话,得到的就是一个较完整的静态链接版perf了,而其文件大小将远远大于未启用这些特性的版本。尽管本例中并不完全支持所有特性,但剩余的通过编译源码的方式来处理,已经不再具备很大的工作量了。同时需要注意的:这些只是在用户空间的perf程序中集成了相应的特性,具体到某些功能能不能支持,还依赖于内核版本。这也是为什么Ubuntu系统对一些Linux tools做了包装脚本的原因,包装脚本会在运行时检测当前内核版本来确定运行哪一个目录下的程序。

可能遇到的问题

编译过程中提示如下错误:

1
2
3
/usr/bin/make FIXDEP=1 -f Makefile.perf 
/usr/bin/sh: line 0: command: -c: invalid option
command: usage: command [-pVv] command [arg ...]

当没有安装python2时,tools/perf/Makefile.config$(if $(call get-executable,$(PYTHON)-config),$(PYTHON)-config,python-config)将执行出错,产生这条提示,使用apt-get install -y python2-minimal安装好python2后即可解决。

编译过程中提示如下错误:

1
2
dangerous relocation: unsupported relocation
/usr/bin/ld: BFD (GNU Binutils for Ubuntu) 2.24 internal error, aborting at ../../bfd/elfnn-aarch64.c line 4331 in elf64_aarch64_final_link_relocate

这个是binutils包的问题,2.31版本后已经解决,这就是为什么使用Ubuntu 20.04的原因,这一版本里自带的binutils是2.34版本。

编译过程中提示如下错误:

1
2
gcc -g -Wall -fPIC  -I. -I.. -I /home/oak/work/perf/tools/include -D_GNU_SOURCE -shared -static -Wl,-z,noexecstack  -lunwind-aarch64 -lunwind -lunwind-aarch64 -Wl,--allow-multiple-definition -Wl,-E -fstack-protector-strong -L/usr/local/lib -L/usr/lib/aarch64-linux-gnu/perl/5.30/CORE -L/usr/lib/python2.7/config-aarch64-linux-gnu -L/usr/lib -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions  -nostartfiles -o plugin_kmem.so plugin_kmem-in.o
/usr/bin/ld: /usr/lib/gcc/aarch64-linux-gnu/9/../../../aarch64-linux-gnu/libc.a(malloc.o): relocation R_AARCH64_ADR_PREL_PG_HI21 against symbol `__free_hook' which may bind externally can not be used when making a shared object; recompile with -fPIC

这里的原因在于生成各个插件时,Makefile只定义了编译成共享库的方式,不单是plugin_kmem.so,其它的插件也一样,包括libperf-jvmti.so等,此时指定了-static选项,而它原本又具有-shared选项,冲突的选项导致了生成这些库失败,解决办法是先编译一次动态链接版本的,把这些共享库都给生成了,而jvmti总是会重新编译,直接使用NO_JVMTI=1禁止,然后再指定-static选项编译perf:

1
2
3
make V=1 VF=1 FEATURE_TESTS=all NO_JVMTI=1 |& tee shared.log
rm perf
make V=1 VF=1 FEATURE_TESTS=all NO_JVMTI=1 LDFLAGS=-static |& tee static.log

执行过程中提示如下错误:

1
2
3
4
5
6
$ perf record -F 99 -p 1913 -g -- sleep 5
(process:9417): GLib-CRITICAL **: 05:32:39.141: g_hash_table_insert_internal: assertion 'hash_table != NULL' failed
(process:9417): GLib-CRITICAL **: 05:32:39.141: g_hash_table_lookup: assertion 'hash_table != NULL' failed
(process:9417): GLib-CRITICAL **: 05:32:39.141: g_hash_table_insert_internal: assertion 'hash_table != NULL' failed
(process:9417): GLib-CRITICAL **: 05:32:39.141: g_hash_table_lookup: assertion 'hash_table != NULL' failed
(process:9417): GLib-CRITICAL **: 05:32:39.141: g_hash_table_insert_internal: assertion 'hash_table != NULL' failed

如果出现这个错误,则说明系统安装的libglib2.0-dev有问题,libglib2.0-dev是GTK的库,简单的办法是不使用这个库,但是,如此一来,Ubuntu 20.04 ARM64版本上编译v5.9.11时需要禁止libbabeltrace,因为系统安装的libbabeltrace使用了libglib2.0-dev中定义的函数,可用的编译命令如下:

1
2
3
make V=1 VF=1 FEATURE_TESTS=all NO_LIBBABELTRACE=1 NO_JVMTI=1 |& tee shared.log
rm perf
make V=1 VF=1 FEATURE_TESTS=all NO_LIBBABELTRACE=1 NO_JVMTI=1 LDFLAGS=-static |& tee static.log

参考资料