一、背景
Chromium 源码中使用了大量的第三方开源库,其中部分做了定制或裁减,部分对库的版本有明确的要求,而平台也常常会提供同名的共享库,当不同版本的库同时被使用的时候,就会发生冲突。
工作中遇到一个典型的例子是 boringssl。boringssl 是 google 从 openssl fork 的分支,与标准的 openssl 库之间存在着大量的同名函数。在某个嵌入式平台,我们的 SDK 会使用 chromium 里的 boringssl,同时又间接地依赖系统的 openssl,这就导致了运行时的符号冲突,发生 crash。
针对这种情况,之前的解决方案是将 boringssl 做成静态库,并隐藏符号。这样运行时,我们的模块就会使用 boringssl,平台的模块会使用系统提供的 openssl。这样看似解决了问题,但是存在一个严重的弊端,就是会导致 size 膨胀。在我们的 SDK 中,boringssl 会被 8 个动态模块(共享库或可执行程序)所依赖。如果做成静态库,意味着这 8 个动态模块中都存在着 boringssl 的实现,这会导致 SDK size 显著增大。如果遇到类似的共享库冲突问题都这样来解决,显然是不合理的。最近找到了一种解决类似问题的方案,并做成一套机制,下面简要做一下介绍。
二、原理
要让不同版本的共享库共存而不发生符号冲突,最根本的解决之道就是修改共享库里的符号。提到修改符号,很多人的第一反应是修改源码。如果修改源码的话,不仅需要修改定义符号的源文件,并且所有引用到相关符号的源文件同样要做修改,这种工作极其繁琐。以 boringssl 为例,里面定义了上千个函数,如果手工更改的话,显然是要出人命的;即便写脚本去替换,由于宏定义、跨行等各种写法的存在,难免会导致很多疏漏,难以覆盖所有符号,再者编写脚本的工作量也不小。
既然修改源码的方式举步维艰,何不尝试去修改二进制文件呢?首先想到的方案是直接替换 ELF 文件中符号,但由于 ELF 文件中的符号已经被分配地址,导致这种工作几乎无法完成。这样似乎要绝望了。但仔细一想,编译器从源码生成最终的 ELF 文件,中间不是还会生成目标文件嘛,既然修改源码和 ELF 文件都不可行,是否可以修改编译中间阶段的目标文件呢?答案是肯定的。
考虑一个 C/C++ 程序编译链接过程:编译阶段,每个源文件(.c/.cc)会被编译成目标文件(.o),相关的符号并不会被实际分配地址,而是存放于目标文件的符号表中;如果引用的符号未在该源文件中定义,会在符号表中标记为 UND。链接阶段,linker 接收输入的目标文件,为每个目标文件中的符号分配地址,最终生成 ELF 文件。如果我们在目标文件中将符号替换,那么最终链接生成的 ELF 中的符号也就是替换后的版本了。
如何去修改目标文件中的符号呢?这里使用到了 objdump 和 objcopy 这两个利器。其中,objdump 可以用来导出 ELF 文件中的符号表,objcopy 可用于重命名符号。
1 | $ objcopy --redefine-sym old_symbol=new_symbol xxx.o |
如果要替换多个符号,可以使用 objcopy 的 –redefine-syms 参数,后面跟一个文件,其每一行的第一列是旧的符号,第二列是新的符号。
1 | $ objcopy --redefine-syms symbols_in_file xxx.o |
三、实现
要实现编译时对目标文件的符号替换,先要梳理构建工具和构建流程。Chromium 的构建系统主要使用了 gyp 和 ninja 这两套工具,其中 gyp 生成 .ninja 文件,ninja 负责最终的构建工作。而衔接 gyp 和 ninja 的桥梁,就是 tools/gyp/pylib/gyp/generator/ninja.py 这个脚本,它也是这里的工作重点。
Chromium 的构建工作,是从 out/Release/build.ninja 开始的,它是 ninja 工具最早的输入,也被称为 master ninja,里面定义着构建过程中会使用到的工具、规则(rule),以及依赖的子模块(subninja)等类内容。而 ninja.py 这个工具就是用来产生 mater ninja 和 sub ninja 文件的。
ninja 的规则(rule),简单来说就是针对输入的一系列处理流程,过程被称 command。如果将 ninja 的整个构建视作一张图,每一个 target 都是一个包括 input、output 以及 rule 的子图,其中 input 和 output 是节点,rule 就是从 input 到 output 的有向边,这和 Makefile 的语法非常类似,但更加灵活和简洁。比如,我们在构建过程常常看到的 cc 和 cxx 就是两个典型的 rule,分别用来处理 .c 和 .cpp 源文件,另外一个 cc_s 则用来处理汇编源码,它们都定义在 out/Release/build.ninja 这个 master ninja 文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 | rule cc command = $cc -MMD -MF $out.d $defines $includes $cflags $cflags_c $cflags_pch_c -c $in -o $out description = CC $out depfile = $out.d deps = gcc rule cc_s command = $cc $defines $includes $cflags $cflags_c $cflags_pch_c -c $in -o $out description = CC $out rule cxx command = $cxx -MMD -MF $out.d $defines $includes $cflags $cflags_cc $cflags_pch_cc -c $in -o $out description = CXX $out depfile = $out.d deps = gcc |
如果要定义 rule 来处理符号替换,那么输入自然是每个目标文件,command 就是我们使用 objcopy 替换符号的命令,输出就是新的目标文件。仔细一想,如果针对每个目标文件,都附加这么一套 rule,代价肯定是巨大的,会显著地增加每个 ninja 文件的 size,进而影响构建速度。这里想到的一个解决方案是改造既有的 rule 来产生我们期望的 rule,因为 cc、cxx、cc_s 这三个 rule 的 output 都是目标文件,我们只需要在 command 后面直接追加 objcopy 替换符号的命令就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | master_ninja.rule( 'cc_and_redefine_symbols', description='CC $out and redefine symbols in $redefine_symbols_in_files', command=('$cc -MMD -MF $out.d $defines $includes $cflags $cflags_c ' '$cflags_pch_c -c $in -o $out; ' '$objcopy --redefine-syms $redefine_symbols_in_files $out'), depfile='$out.d', deps=deps) master_ninja.rule( 'cc_s_and_redefine_symbols', description='CC $out and redefine symbols in $redefine_symbols_in_files', command=('$cc $defines $includes $cflags $cflags_c ' '$cflags_pch_c -c $in -o $out; ' '$objcopy --redefine-syms $redefine_symbols_in_files $out')) master_ninja.rule( 'cxx_and_redefine_symbols', description='CXX $out and redefine symbols in $redefine_symbols_in_files', command=('$cxx -MMD -MF $out.d $defines $includes $cflags $cflags_cc ' '$cflags_pch_cc -c $in -o $out; ' '$objcopy --redefine-syms $redefine_symbols_in_files $out'), depfile='$out.d', deps=deps) |
改造后的形式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | rule cc_and_redefine_symbols command = $cc -MMD -MF $out.d $defines $includes $cflags $cflags_c $cflags_pch_c -c $in -o $out; $objcopy $ --redefine-syms $redefine_symbols_in_files $out description = CC $out and redefine symbols in $redefine_symbols_in_files depfile = $out.d deps = gcc rule cc_s_and_redefine_symbols command = $cc $defines $includes $cflags $cflags_c $cflags_pch_c -c $in -o $out; $objcopy --redefine-syms $ $redefine_symbols_in_files $out description = CC $out and redefine symbols in $redefine_symbols_in_files rule cxx_and_redefine_symbols command = $cxx -MMD -MF $out.d $defines $includes $cflags $cflags_cc $cflags_pch_cc -c $in -o $out; $objcopy $ --redefine-syms $redefine_symbols_in_files $out description = CXX $out and redefine symbols in $redefine_symbols_in_files depfile = $out.d deps = gcc |
新的 rule 里,objcopy 直接作用于编译生成的目标文件,redefine_symbols_in_files 这个变量就是上面提到的记录着所需替换符号的文件。为支持同时指定多个输入文件,这里使用了 list,通过 join 的方式转成 string 导入到 ninja 文件中:
然后,在对源码文件定义 rule 的地方,检查 target 的配置项中 $redefine_symbols_in_files 这个 list 是否为空,如果不为空,说明有符号需要替换,那么就使用我们改造后的 rule,否则就使用原来的 rule:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | for source in sources: filename, ext = os.path.splitext(source) ext = ext[1:] obj_ext = self.obj_ext if ext in ('cc', 'cpp', 'cxx'): if redefine_symbols_in_files: command = 'cxx_and_redefine_symbols' else: command = 'cxx' self.uses_cpp = True elif ext == 'c' or (ext == 'S' and self.flavor != 'win'): if redefine_symbols_in_files: command = 'cc_and_redefine_symbols' else: command = 'cc' elif ext == 's' and self.flavor != 'win': # Doesn't generate .o.d files. if redefine_symbols_in_files: command = 'cc_s_and_redefine_symbols' else: command = 'cc_s' |
四、使用
通过以上方式建立的机制,适用于所有与一开始提到的 boringssl 符号冲突问题相类似的场景。一旦我们依赖的某个 third party 模块(共享库)与系统的 library 间存在冲突,就可以通过该机制来解决。下面以 boringssl 为例来介绍其使用方法。
首先,导出目标模块(boringssl)的动态符号到文件:
1 | objdump -T out/Release/lib/libboringssl.so | grep 'DF .text\|DO' | grep -v '*UND*' | awk '{print $NF " chromium_"$NF}' | sort | uniq > thirdparty/boringssl/boringss |
以上命令会将 boringssl 中所有的动态函数符号导出,输入到一个文件,其中每一行的第一列是原来的符号,第二列是附加前缀 “chromium_” 后产生的新符号。
1 2 3 4 5 6 | $ head -n 5 third_party/boringssl/boringssl_redefine_symbols.sigs a2d_ASN1_OBJECT chromium_a2d_ASN1_OBJECT a2i_ASN1_ENUMERATED chromium_a2i_ASN1_ENUMERATED a2i_ASN1_INTEGER chromium_a2i_ASN1_INTEGER a2i_ASN1_STRING chromium_a2i_ASN1_STRING a2i_GENERAL_NAME chromium_a2i_GENERAL_NAME |
然后,我们需要修改相关模块的 gyp 文件,在相应的 target 配置下添加如下配置项:
1 2 3 4 5 6 7 8 | 'redefine_symbols_in_files': [ '<!(pwd)/boringssl_redefine_symbols.sigs', ], 'direct_dependent_settings': { 'redefine_symbols_in_files': [ '<!(pwd)/boringssl_redefine_symbols.sigs', ], }, |
其中 redefine_symbols_in_files 这个列表里面,存放着上面导出符号生成的文件的路径,可以同时指定多个文件;direct_dependent_settings 这项配置也是必须的,它会作用于所有依赖于 boringssl 的模块,这就保证了所有引用到 boringssl 里面符号的模块,所引用的符号也都被正确替换。
N
请问对于C++ ,objcopy –redefine-sym如何替换?
例如,对于C的函数OPENSSL_cpuid_setup,可以直接替换为XXX_OPENSSL_cpuid_setup,命令:objcopy –redefine-sym OPENSSL_cpuid_setup=XXX_OPENSSL_cpuid_setup …,
但是对于C++ 的函数google::protobuf::internal::WireFormatLite::SkipMessage(),该如何操作?objcopy –redefine-sym google::protobuf::internal::WireFormatLite::SkipMessage()=XXX_google::protobuf::internal::WireFormatLite::SkipMessage()并不起作用。
darkgod
对于c++的函数 google::protobuf::internal::WireFormatLite::SkipMessage(), 编译器默认将其进行名字改编(Name mangling),改编后的名字形如 _ZN6google8protobuf8inter。
因此,你可以将改编后的名字替换为目标符号名: objcopy –redefine-sym _ZN6google8protobuf8inter=target_symbol …
gongxifu
您好,文中提到“在对源码文件定义 rule 的地方,检查 target 的配置项中 $redefine_symbols_in_files 这个 list 是否为空,如果不为空,说明有符号需要替换,那么就使用我们改造后的 rule,否则就使用原来的 rule”,这个具体修改哪个文件,能举个例子么?没找到对应的文件。谢谢
darkgod
这个是对于所有定义在 gyp 文件中的编译目标(target)而言的;比如 [//src/third_party/boringssl/boringssl.gyp], 可以在 target(“boringssl”) 下使用 redefine_symbols_in_files 这个 list 配置符号列表文件。