[BOLT] Split functions: support fragments with multiple parents

Summary:
Gracefully handle binaries with split functions where two fragments are folded
into one, resulting in a fragment with two parent functions.

This behavior is expected in GCC8+ with -O2 optimization level, where both
function splitting and ICF are enabled by default.

On the BOLT side, the changes are:
- BinaryFunction: allow multiple parent fragments:
  - `ParentFragment` --> `ParentFragments`,
  - `setParentFragment` --> `addParentFragment`.
- BinaryContext:
  - `populateJumpTables`: mark fragments to be skipped later,
  - `registerFragment`: add a name heuristic check, return false if it failed,
  - `processInterproceduralReferences`: check if `registerFragment`
succeeded, otherwise issue a warning,
  - `skipMarkedFragments`: move out fragment traversal and skipping from
  `populateJumpTables` into a separate function.

This change fixes an issue where unrelated functions might be registered
as fragments:

```
BOLT-WARNING: interprocedural reference between unrelated fragments:
bad_gs/1(*2) and amd_decode_mce.cold.27/1(*2)
```

(Linux kernel binary)

(cherry picked from FBD32786688)
This commit is contained in:
Amir Ayupov 2021-12-01 21:14:56 -08:00 committed by Maksim Panchenko
parent 69706eafab
commit 6aa735ceaf
6 changed files with 189 additions and 84 deletions

View File

@ -195,6 +195,9 @@ class BinaryContext {
/// with size won't overflow
uint32_t DuplicatedJumpTables{0x10000000};
/// Function fragments to skip.
std::vector<BinaryFunction *> FragmentsToSkip;
/// The runtime library.
std::unique_ptr<RuntimeLibrary> RtLibrary;
@ -864,13 +867,19 @@ public:
/// @}
/// Register \p TargetFunction as fragment of \p Function.
void registerFragment(BinaryFunction &TargetFunction,
/// Register \p TargetFunction as a fragment of \p Function if checks pass:
/// - if \p TargetFunction name matches \p Function name with a suffix:
/// fragment_name == parent_name.cold(.\d+)?
/// True if the Function is registered, false if the check failed.
bool registerFragment(BinaryFunction &TargetFunction,
BinaryFunction &Function) const;
/// Resolve inter-procedural dependencies from \p Function.
void processInterproceduralReferences(BinaryFunction &Function);
/// Skip functions with all parent and child fragments transitively.
void skipMarkedFragments();
/// Perform any necessary post processing on the symbol table after
/// function disassembly is complete. This processing fixes top
/// level data holes and makes sure the symbol table is valid.

View File

@ -354,7 +354,7 @@ private:
std::string ColdCodeSectionName;
/// Parent function fragment for split function fragments.
BinaryFunction *ParentFragment{nullptr};
SmallPtrSet<BinaryFunction *, 1> ParentFragments;
/// Indicate if the function body was folded into another function.
/// Used by ICF optimization.
@ -633,15 +633,15 @@ private:
/// If the function represents a secondary split function fragment, set its
/// parent fragment to \p BF.
void setParentFragment(BinaryFunction &BF) {
void addParentFragment(BinaryFunction &BF) {
assert(this != &BF);
assert(IsFragment && "function must be a fragment to have a parent");
assert((!ParentFragment || ParentFragment == &BF) &&
"cannot have more than one parent function");
ParentFragment = &BF;
ParentFragments.insert(&BF);
}
/// Register a child fragment for the main fragment of a split function.
void addFragment(BinaryFunction &BF) {
assert(this != &BF);
Fragments.insert(&BF);
}
@ -2000,20 +2000,9 @@ public:
return IsFragment;
}
/// Return parent function fragment if this function is a secondary (child)
/// fragment of another function.
BinaryFunction *getParentFragment() const {
return ParentFragment;
}
/// If the function is a nested child fragment of another function, return its
/// topmost parent fragment.
const BinaryFunction *getTopmostFragment() const {
const BinaryFunction *BF = this;
while (BF->getParentFragment())
BF = BF->getParentFragment();
return BF;
/// Returns if the given function is a parent fragment of this function.
bool isParentFragment(BinaryFunction *Parent) const {
return ParentFragments.count(Parent);
}
/// Set the profile data for the number of times the function was called.
@ -2558,12 +2547,6 @@ public:
FragmentInfo &cold() { return ColdFragment; }
const FragmentInfo &cold() const { return ColdFragment; }
/// Mark child fragments as ignored.
void ignoreFragments() {
for (BinaryFunction *Fragment : Fragments)
Fragment->setIgnored();
}
};
inline raw_ostream &operator<<(raw_ostream &OS,

View File

@ -29,8 +29,10 @@
#include "llvm/MC/MCSymbol.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Regex.h"
#include <algorithm>
#include <functional>
#include <iterator>
#include <unordered_set>
using namespace llvm;
@ -483,6 +485,18 @@ BinaryContext::analyzeMemoryAt(uint64_t Address, BinaryFunction &BF) {
return MemoryContentsType::UNKNOWN;
}
/// Check if <fragment restored name> == <parent restored name>.cold(.\d+)?
bool isPotentialFragmentByName(BinaryFunction &Fragment,
BinaryFunction &Parent) {
for (StringRef Name : Parent.getNames()) {
std::string NamePrefix = Regex::escape(NameResolver::restore(Name));
std::string NameRegex = Twine(NamePrefix, "\\.cold(\\.[0-9]+)?").str();
if (Fragment.hasRestoredNameRegex(NameRegex))
return true;
}
return false;
}
bool BinaryContext::analyzeJumpTable(const uint64_t Address,
const JumpTable::JumpTableType Type,
BinaryFunction &BF,
@ -500,19 +514,6 @@ bool BinaryContext::analyzeJumpTable(const uint64_t Address,
Offsets->emplace_back(Offset);
};
auto isFragment = [](BinaryFunction &Fragment,
BinaryFunction &Parent) -> bool {
// Check if <fragment restored name> == <parent restored name>.cold(.\d+)?
for (StringRef BFName : Parent.getNames()) {
std::string BFNamePrefix = Regex::escape(NameResolver::restore(BFName));
std::string BFNameRegex =
Twine(BFNamePrefix, "\\.cold(\\.[0-9]+)?").str();
if (Fragment.hasRestoredNameRegex(BFNameRegex))
return true;
}
return false;
};
auto doesBelongToFunction = [&](const uint64_t Addr,
BinaryFunction *TargetBF) -> bool {
if (BF.containsAddress(Addr))
@ -522,25 +523,12 @@ bool BinaryContext::analyzeJumpTable(const uint64_t Address,
return false;
// Case 1: check if BF is a fragment and TargetBF is its parent.
if (BF.isFragment()) {
// BF is a fragment, but parent function is not registered.
// This means there's no direct jump between parent and fragment.
// Set parent link here in jump table analysis, based on function name
// matching heuristic.
if (!BF.getParentFragment() && isFragment(BF, *TargetBF))
registerFragment(BF, *TargetBF);
return BF.getParentFragment() == TargetBF;
// Parent function may or may not be already registered.
// Set parent link based on function name matching heuristic.
return registerFragment(BF, *TargetBF);
}
// Case 2: check if TargetBF is a fragment and BF is its parent.
if (TargetBF->isFragment()) {
// TargetBF is a fragment, but parent function is not registered.
// This means there's no direct jump between parent and fragment.
// Set parent link here in jump table analysis, based on function name
// matching heuristic.
if (!TargetBF->getParentFragment() && isFragment(*TargetBF, BF))
registerFragment(*TargetBF, BF);
return TargetBF->getParentFragment() == &BF;
}
return false;
return TargetBF->isFragment() && registerFragment(*TargetBF, BF);
};
ErrorOr<BinarySection &> Section = getSectionForAddress(Address);
@ -608,10 +596,10 @@ bool BinaryContext::analyzeJumpTable(const uint64_t Address,
<< TargetBF->getPrintName() << '\n';
if (TargetBF->isFragment())
dbgs() << " ! is a fragment\n";
BinaryFunction *TargetParent = TargetBF->getParentFragment();
dbgs() << " ! its parent is "
<< (TargetParent ? TargetParent->getPrintName() : "(none)")
<< '\n';
for (BinaryFunction *TargetParent : TargetBF->ParentFragments)
dbgs() << " ! its parent is "
<< (TargetParent ? TargetParent->getPrintName() : "(none)")
<< '\n';
}
}
if (Value == BF.getAddress())
@ -638,7 +626,8 @@ bool BinaryContext::analyzeJumpTable(const uint64_t Address,
BF.setHasSplitJumpTable(true);
// Add invalid offset for proper identification of jump table size.
addOffset(INVALID_OFFSET);
LLVM_DEBUG(dbgs() << "OK: address in split fragment\n");
LLVM_DEBUG(dbgs() << "OK: address in split fragment "
<< TargetBF->getPrintName() << '\n');
}
}
@ -649,7 +638,6 @@ bool BinaryContext::analyzeJumpTable(const uint64_t Address,
}
void BinaryContext::populateJumpTables() {
std::vector<BinaryFunction *> FuncsToSkip;
LLVM_DEBUG(dbgs() << "DataPCRelocations: " << DataPCRelocations.size()
<< '\n');
for (auto JTI = JumpTables.begin(), JTE = JumpTables.end(); JTI != JTE;
@ -699,7 +687,7 @@ void BinaryContext::populateJumpTables() {
// Mark to skip the function and all its fragments.
if (BF.hasSplitJumpTable())
FuncsToSkip.push_back(&BF);
FragmentsToSkip.push_back(&BF);
}
if (opts::StrictMode && DataPCRelocations.size()) {
@ -712,16 +700,36 @@ void BinaryContext::populateJumpTables() {
assert(0 && "unclaimed PC-relative relocations left in data\n");
}
clearList(DataPCRelocations);
}
void BinaryContext::skipMarkedFragments() {
// Unique functions in the vector.
std::unordered_set<BinaryFunction *> UniqueFunctions(FragmentsToSkip.begin(),
FragmentsToSkip.end());
// Copy the functions back to FragmentsToSkip.
FragmentsToSkip.assign(UniqueFunctions.begin(), UniqueFunctions.end());
auto addToWorklist = [&](BinaryFunction *Function) -> void {
if (UniqueFunctions.count(Function))
return;
FragmentsToSkip.push_back(Function);
UniqueFunctions.insert(Function);
};
// Functions containing split jump tables need to be skipped with all
// fragments.
for (BinaryFunction *BF : FuncsToSkip) {
BinaryFunction *ParentBF =
const_cast<BinaryFunction *>(BF->getTopmostFragment());
LLVM_DEBUG(dbgs() << "Skipping " << ParentBF->getPrintName()
<< " family\n");
ParentBF->setIgnored();
ParentBF->ignoreFragments();
// fragments (transitively).
for (size_t I = 0; I != FragmentsToSkip.size(); I++) {
BinaryFunction *BF = FragmentsToSkip[I];
assert(UniqueFunctions.count(BF) &&
"internal error in traversing function fragments");
if (opts::Verbosity >= 1)
errs() << "BOLT-WARNING: Ignoring " << BF->getPrintName() << '\n';
BF->setIgnored();
std::for_each(BF->Fragments.begin(), BF->Fragments.end(), addToWorklist);
std::for_each(BF->ParentFragments.begin(), BF->ParentFragments.end(),
addToWorklist);
}
errs() << "BOLT-WARNING: Ignored " << FragmentsToSkip.size() << " functions "
<< "due to cold fragments.\n";
FragmentsToSkip.clear();
}
MCSymbol *BinaryContext::getOrCreateGlobalSymbol(uint64_t Address,
@ -1093,16 +1101,14 @@ void BinaryContext::generateSymbolHashes() {
}
}
void BinaryContext::registerFragment(BinaryFunction &TargetFunction,
bool BinaryContext::registerFragment(BinaryFunction &TargetFunction,
BinaryFunction &Function) const {
// Only a parent function (or a sibling) can reach its fragment.
assert(!Function.IsFragment &&
"only one cold fragment is supported at this time");
if (BinaryFunction *TargetParent = TargetFunction.getParentFragment()) {
assert(TargetParent == &Function && "mismatching parent function");
return;
}
TargetFunction.setParentFragment(Function);
if (!isPotentialFragmentByName(TargetFunction, Function))
return false;
assert(TargetFunction.isFragment() && "TargetFunction must be a fragment");
if (TargetFunction.isParentFragment(&Function))
return true;
TargetFunction.addParentFragment(Function);
Function.addFragment(TargetFunction);
if (!HasRelocations) {
TargetFunction.setSimple(false);
@ -1112,6 +1118,7 @@ void BinaryContext::registerFragment(BinaryFunction &TargetFunction,
outs() << "BOLT-INFO: marking " << TargetFunction
<< " as a fragment of " << Function << '\n';
}
return true;
}
void BinaryContext::processInterproceduralReferences(BinaryFunction &Function) {
@ -1125,8 +1132,13 @@ void BinaryContext::processInterproceduralReferences(BinaryFunction &Function) {
continue;
if (TargetFunction) {
if (TargetFunction->IsFragment)
registerFragment(*TargetFunction, Function);
if (TargetFunction->IsFragment &&
!registerFragment(*TargetFunction, Function)) {
errs() << "BOLT-WARNING: interprocedural reference between unrelated "
"fragments: "
<< Function.getPrintName() << " and "
<< TargetFunction->getPrintName() << '\n';
}
if (uint64_t Offset = Address - TargetFunction->getAddress())
TargetFunction->addEntryPointAtOffset(Offset);

View File

@ -442,7 +442,7 @@ void BinaryFunction::print(raw_ostream &OS, std::string Annotation,
if (isFolded()) {
OS << "\n FoldedInto : " << *getFoldedIntoFunction();
}
if (ParentFragment) {
for (BinaryFunction *ParentFragment : ParentFragments) {
OS << "\n Parent : " << *ParentFragment;
}
if (!Fragments.empty()) {

View File

@ -2789,6 +2789,7 @@ void RewriteInstance::disassembleFunctions() {
}
BC->populateJumpTables();
BC->skipMarkedFragments();
for (auto &BFI : BC->getBinaryFunctions()) {
BinaryFunction &Function = BFI.second;

View File

@ -0,0 +1,100 @@
# This reproduces an issue where two cold fragments are folded into one, so the
# fragment has two parents.
# The fragment is only reachable through a jump table, so all functions must be
# ignored.
# REQUIRES: system-linux
# RUN: llvm-mc -filetype=obj -triple x86_64-unknown-unknown %s -o %t.o
# RUN: llvm-strip --strip-unneeded %t.o
# RUN: %clang %cflags %t.o -o %t.exe -Wl,-q
# RUN: llvm-bolt %t.exe -o %t.out -lite=0 -v=1 2>&1 | FileCheck %s
# CHECK-NOT: unclaimed PC-relative relocations left in data
# CHECK-DAG: BOLT-INFO: marking main2.cold.1(*2) as a fragment of main
# CHECK-DAG: BOLT-INFO: marking main2.cold.1(*2) as a fragment of main2
# CHECK-DAG: BOLT-WARNING: Ignoring main2
# CHECK-DAG: BOLT-WARNING: Ignoring main
# CHECK-DAG: BOLT-WARNING: Ignoring main2.cold.1(*2)
# CHECK: BOLT-WARNING: Ignored 3 functions due to cold fragments.
.text
.globl main
.type main, %function
.p2align 2
main:
LBB0:
andl $0xf, %ecx
cmpb $0x4, %cl
# exit through ret
ja LBB3
# jump table dispatch, jumping to label indexed by val in %ecx
LBB1:
leaq JUMP_TABLE1(%rip), %r8
movzbl %cl, %ecx
movslq (%r8,%rcx,4), %rax
addq %rax, %r8
jmpq *%r8
LBB2:
xorq %rax, %rax
LBB3:
addq $0x8, %rsp
ret
.size main, .-main
.globl main2
.type main2, %function
.p2align 2
main2:
LBB20:
andl $0xb, %ebx
cmpb $0x1, %cl
# exit through ret
ja LBB23
# jump table dispatch, jumping to label indexed by val in %ecx
LBB21:
leaq JUMP_TABLE2(%rip), %r8
movzbl %cl, %ecx
movslq (%r8,%rcx,4), %rax
addq %rax, %r8
jmpq *%r8
LBB22:
xorq %rax, %rax
LBB23:
addq $0x8, %rsp
ret
.size main2, .-main2
# cold fragment is only reachable through jump table
.globl main2.cold.1
.type main2.cold.1, %function
main2.cold.1:
.globl main.cold.1
.type main.cold.1, %function
.p2align 2
main.cold.1:
# load bearing nop: pad LBB4 so that it can't be treated
# as __builtin_unreachable by analyzeJumpTable
nop
LBB4:
callq abort
.size main.cold.1, .-main.cold.1
.rodata
# jmp table, entries must be R_X86_64_PC32 relocs
.globl JUMP_TABLE1
JUMP_TABLE1:
.long LBB2-JUMP_TABLE1
.long LBB3-JUMP_TABLE1
.long LBB4-JUMP_TABLE1
.long LBB3-JUMP_TABLE1
.globl JUMP_TABLE2
JUMP_TABLE2:
.long LBB22-JUMP_TABLE2
.long LBB23-JUMP_TABLE2
.long LBB4-JUMP_TABLE2
.long LBB23-JUMP_TABLE2