Learn COMDAT with inline functions
本文通过一个 inline 函数的例子学习 COMDAT 的作用。
考虑以下例子:
//--- a.cpp
inline const char* hello() { return "Hello,World!"; }
void foo() { printf("%s: %s\n", __FILE__, hello()); }
int main() {
return 0;
}
//--- b.cpp
inline const char* hello() { return "Hello,World!"; }
void bar() { printf("%s: %s\n", __FILE__, hello()); }
// clang a.cpp b.cpp -o a.out
该例子,在 a.cpp 中定义了 inline 函数 hello()
,在 b.cpp 中同样定义了 inline 函数 hello()
,最终在编译链接产物 a.out 中只会保留一份 inline 函数 hello()
的定义。
那么在 a.out 中只保留一份 inline 函数 hello()
的定义是如何实现呢?
注:对于一个 inline 函数,如果其在不同编译单元的定义不一样,则是未定义行为。
Weak definition
使用 readelf
可以看到 a.out 中函数 hello()
的符号是 weak 的。
$ readelf -sW a.out | grep hello
50: 0000000000401170 16 FUNC WEAK DEFAULT 13 _Z5hellov
对于在多个编译单元都有定义的 weak 符号,链接器是不会报错该符号存在重复定义的,并且链接器会将所有对该符号的引用指向该符号的第一处定义。
那么 inline 函数是否就是单纯将函数设置为 weak 符号呢?
可以直接写个测试用例验证下:
//--- weak_a.cpp
__attribute__((weak)) const char* hello() { return "Hello,World!"; }
void foo() { printf("%s: %s\n", __FILE__, hello()); }
int main() {
return 0;
}
//--- weak_b.cpp
__attribute__((weak)) const char* hello() { return "Hello,World!"; }
void bar() { printf("%s: %s\n", __FILE__, hello()); }
// clang weak_a.cpp weak_b.cpp -o a.out.weak
虽然使用 readelf
可以看到 a.out.weak 中函数 hello()
的符号属性是一模一样的,但是仔细看 objdump
反汇编 a.out.weak 的结果:
0000000000401130 <_Z5hellov>:
401130: 55 push %rbp
401131: 48 89 e5 mov %rsp,%rbp
401134: 48 b8 04 20 40 00 00 movabs $0x402004,%rax
40113b: 00 00 00
40113e: 5d pop %rbp
40113f: c3 retq
0000000000401170 <main>:
401170: 55 push %rbp
401171: 48 89 e5 mov %rsp,%rbp
401174: 31 c0 xor %eax,%eax
401176: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
40117d: 5d pop %rbp
40117e: c3 retq
40117f: 90 nop
401180: 55 push %rbp
401181: 48 89 e5 mov %rsp,%rbp
401184: 48 b8 04 20 40 00 00 movabs $0x402004,%rax
40118b: 00 00 00
40118e: 5d pop %rbp
40118f: c3 retq
在 a.out.weak 中只有一个函数 hello
的符号,但是仔细看 main
函数的反汇编 ,40117f
处就是 main
函数的结束,401180 - 40118f
这段汇编指令与 401130 - 40113f
这段函数 hello
的汇编指令是一模一样的,401180 - 40118f
这段汇编指令其实就是定义在 b.cpp 的函数 hello
的实现。
这说明单纯将函数 hello
设置为 weak 符号,虽然使得最终链接产物只有一个 hello
符号,但是 .text section 中实际上还是保留了定义在不同编译单元的 hello
的代码/汇编指令。
对比 objdump
反汇编 a.out 的结果,将函数 hello
设置为 inline 使得 .text section 只保留了一份 hello
代码/汇编指令。
0000000000401160 <main>:
401160: 55 push %rbp
401161: 48 89 e5 mov %rsp,%rbp
401164: 31 c0 xor %eax,%eax
401166: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
40116d: 5d pop %rbp
40116e: c3 retq
40116f: 90 nop
0000000000401170 <_Z5hellov>:
401170: 55 push %rbp
401171: 48 89 e5 mov %rsp,%rbp
401174: 48 b8 12 20 40 00 00 movabs $0x402012,%rax
40117b: 00 00 00
40117e: 5d pop %rbp
40117f: c3 retq
COMDAT/section group
那么在 a.out 中只保留一份 inline 函数 hello()
的定义是如何实现呢?
对比 a.cpp 与 weak_a.cpp、b.cpp 与 weak_b.cpp 对应的 LLVM IR:
; clang -S -emit-llvm a.cpp -o -
$_Z5hellov = comdat any
@.str.2 = private unnamed_addr constant [13 x i8] c"Hello,World!\00", align 1
; Function Attrs: noinline nounwind optnone uwtable
define linkonce_odr dso_local i8* @_Z5hellov() #2 comdat {
entry:
ret i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.2, i64 0, i64 0)
}
; clang -S -emit-llvm weak_a.cpp -o -
@.str = private unnamed_addr constant [13 x i8] c"Hello,World!\00", align 1
; Function Attrs: noinline optnone uwtable
define weak dso_local i8* @_Z5hellov() #0 {
entry:
ret i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i64 0, i64 0)
}
; clang -S -emit-llvm b.cpp -o -
$_Z5hellov = comdat any
@.str.2 = private unnamed_addr constant [13 x i8] c"Hello,World!\00", align 1
; Function Attrs: noinline nounwind optnone uwtable
define linkonce_odr dso_local i8* @_Z5hellov() #2 comdat {
entry:
ret i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str.2, i64 0, i64 0)
}
; clang -S -emit-llvm weak_b.cpp -o -
@.str = private unnamed_addr constant [13 x i8] c"Hello,World!\00", align 1
; Function Attrs: noinline optnone uwtable
define weak dso_local i8* @_Z5hellov() #0 {
entry:
ret i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i64 0, i64 0)
}
a.cpp 与 weak_a.cpp、b.cpp 与 weak_b.cpp 对应的 LLVM IR 之间的关键区别在于 $_Z5hellov = comdat any
!
根据 https://llvm.org/docs/LangRef.html#comdats
Comdat IR provides access to object file COMDAT/section group functionality which represents interrelated sections.
Comdats have a name which represents the COMDAT key and a selection kind to provide input on how the linker deduplicates comdats with the same key in two different object files. A comdat must be included or omitted as a unit. Discarding the whole comdat is allowed but discarding a subset is not.
A global object may be a member of at most one comdat.
For selection kinds other than
nodeduplicate
, only one of the duplicate comdats may be retained by the linker and the members of the remaining comdats must be discarded.For selection kind
any
, the linker may choose any COMDAT key, the choice is arbitrary.
a.cpp 和 b.cpp 对应的 LLVM IR 中都创建了一个 key 为 _Z5hellov
的 COMDAT,并且 a.cpp 中定义的函数 hello
属于 a.cpp 中的 _Z5hellov
COMDAT,b.cpp 中定义的函数 hello
属于 b.cpp 中的 _Z5hellov
COMDAT。在链接时,对于 a.cpp 和 b.cpp 中的 _Z5hellov
COMDAT,链接器会任意选择其中一个 _Z5hellov
COMDAT 保留,并丢弃另外的 _Z5hellov
COMDAT。假设链接器选择保留 a.cpp 的 _Z5hellov
COMDAT,那么会丢弃掉 b.cpp 的 _Z5hellov
COMDAT,b.cpp 的 _Z5hellov
COMDAT 中的函数 hello
也就会被丢弃。
readelf
提供了一个选项 -g --section-groups
查看 section groups,查看 a.cpp 和 b.cpp 对应的目标文件 a.o 和 b.o 的 section groups:
$ readelf -g a.o
COMDAT group section [ 4] `.group' [_Z5hellov] contains 2 sections:
[Index] Name
[ 5] .text._Z5hellov
[ 6] .rela.text._Z5hellov
$ readelf -g b.o
COMDAT group section [ 4] `.group' [_Z5hellov] contains 2 sections:
[Index] Name
[ 5] .text._Z5hellov
[ 6] .rela.text._Z5hellov
通过 objdump
查看 a.o 的 .text._Z5hellov section 的内容,可以看到 .text._Z5hellov section 中就是函数 hello
的定义:
$ objdump -d a.o
Disassembly of section .text._Z5hellov:
0000000000000000 <_Z5hellov>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 b8 00 00 00 00 00 movabs $0x0,%rax
b: 00 00 00
e: 5d pop %rbp
f: c3 retq
作为对比,weak_a.cpp 对应的目标文件 weak_a.o 则没有任何 section group:
$ readelf -g weak_a.o
There are no section groups in this file.
除了 COMDAT 外,a.cpp 与 weak_a.cpp、b.cpp 与 weak_b.cpp 对应的 LLVM IR 还有一点区分:a.cpp/b.cpp 对应的 LLVM IR 中函数 hello
的 linkage type 是 linkonce_odr,而 weak_a.cpp/weak_b.cpp 对应的 LLVM IR 中函数 hello
的 linkage type 是 weak。关于 linkage type 的详细解释见 https://llvm.org/docs/LangRef.html#linkage-types
。
Usage of COMDAT
如果我们想通过 LLVM IR 层面插桩的方式实现为链接产物添加一个函数定义 foo
,那么可以编写一个 ModulePass,为每一个 Module 都插入一个 foo
函数定义。因为 C++ 项目会有很多个编译单元,所以为每个编译单元执行该 ModulePass 会为每个编译单元都插入一个 foo
函数定义。尽管我们可以将函数 foo
的 linkage type 设置为 linkonce_odr,但是还是会使得最终链接产物中有多份 foo
函数的副本,增加 .text section 的大小。这个时候就可以利用 COMDAT,在 ModulePass 中创建函数foo
时同时创建一个 key 为 foo 的 COMDAT,将函数foo
添加至该 COMDAT 中,这样在最终链接产物中就只会保留一份 foo
函数了。
void insertFunctionFoo(llvm::Module &M) {
llvm::FunctionType *FTT = llvm::FunctionType::get(Int8PtrTy, false);
llvm::FunctionCallee FC = M.getOrInsertFunction("foo", FTT);
llvm::Function *F = dyn_cast<llvm::Function>(FC.getCallee());
if (F && F->isDeclaration()) {
F->setComdat(M.getOrInsertComdat("foo"));
F->setLinkage(llvm::GlobalValue::LinkOnceODRLinkage);
llvm::BasicBlock *BB =
llvm::BasicBlock::Create(M.getContext(), "entry", F);
// Create a constant string "Hello,World"
// We use private linkage for module-local string, and set the unnamed_addr
// attribute to make it can be merged with another one.
llvm::Constant *StrConst = llvm::ConstantDataArray::getString(
M.getContext(), "Hello,World");
llvm::GlobalVariable *GV = new llvm::GlobalVariable(
M, StrConst->getType(), true, llvm::GlobalValue::PrivateLinkage,
StrConst, "hello_world_string_constant");
GV->setUnnamedAddr(llvm::GlobalValue::UnnamedAddr::Global);
// Strings may not be merged w/o setting alignment explicitly.
GV->setAlignment(llvm::Align(1));
llvm::ReturnInst::Create(
M.getContext(),
llvm::ConstantExpr::getPointerCast(GV, Int8PtrTy), BB);
}
}