[llvm] [BOLT] Preserve interprocedural fall-throughs (PR #219430)

via llvm-commits llvm-commits at lists.llvm.org
Fri Aug 28 02:55:06 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-bolt

Author: wangjue (WangJee)

<details>
<summary>Changes</summary>

BOLT only represents fall-through edges within a BinaryFunction. Reordering a function that implicitly falls through into the next function can therefore silently change program behavior.

Detect this pattern before function post-processing and preserve both functions in the original text. Add RISC-V and X86 regression coverage.

---
Full diff: https://github.com/llvm/llvm-project/pull/219430.diff


3 Files Affected:

- (modified) bolt/lib/Rewrite/RewriteInstance.cpp (+50) 
- (added) bolt/test/RISCV/interprocedural-fallthrough.s (+65) 
- (added) bolt/test/X86/interprocedural-fallthrough.s (+66) 


``````````diff
diff --git a/bolt/lib/Rewrite/RewriteInstance.cpp b/bolt/lib/Rewrite/RewriteInstance.cpp
index 4486efe926d01..bf4efdd31489e 100644
--- a/bolt/lib/Rewrite/RewriteInstance.cpp
+++ b/bolt/lib/Rewrite/RewriteInstance.cpp
@@ -4098,12 +4098,62 @@ void RewriteInstance::buildFunctionsCFG() {
   BC->postProcessSymbolTable();
 }
 
+namespace {
+
+BinaryFunction *getInterproceduralFallThroughTarget(BinaryContext &BC,
+                                                    BinaryFunction &Function) {
+  if (!Function.hasCFG() || Function.getLayout().block_empty())
+    return nullptr;
+
+  const BinaryBasicBlock *LastBB = Function.getLayout().block_back();
+  const MCInst *LastInst = LastBB->getLastNonPseudoInstr();
+  if (!LastInst || LastBB->succ_size() != 0 ||
+      BC.MIB->isTerminator(*LastInst) || BC.MIB->isCall(*LastInst))
+    return nullptr;
+
+  BinaryFunction *Target = BC.getBinaryFunctionAtAddress(Function.getAddress() +
+                                                         Function.getMaxSize());
+  if (!Target || Target == &Function || Target->isPseudo() ||
+      Target->isFragment() ||
+      Target->getOriginSection() != Function.getOriginSection())
+    return nullptr;
+
+  return Target;
+}
+
+} // namespace
+
 void RewriteInstance::postProcessFunctions() {
   // We mark fragments as non-simple here, not during disassembly,
   // So we can build their CFGs.
   BC->skipMarkedFragments();
   BC->clearFragmentsToSkip();
 
+  // Function boundaries are not represented by CFG edges. If a function
+  // reaches the end of its body without a terminator, execution continues in
+  // the next function in the input layout. Preserve both functions in place,
+  // since moving either one independently would change program semantics.
+  //
+  // setIgnored() cannot be used after disassembly in process-all-functions
+  // mode. That mode is intended to emit every function, so leave its existing
+  // behavior unchanged.
+  if (!opts::processAllFunctions()) {
+    for (auto &BFI : BC->getBinaryFunctions()) {
+      BinaryFunction &Function = BFI.second;
+      BinaryFunction *Target =
+          getInterproceduralFallThroughTarget(*BC, Function);
+      if (!Target)
+        continue;
+
+      BC->errs() << "BOLT-WARNING: interprocedural fall-through detected "
+                    "from "
+                 << Function.getPrintName() << " to " << Target->getPrintName()
+                 << "; preserving both functions in the original text\n";
+      Function.setIgnored();
+      Target->setIgnored();
+    }
+  }
+
   BC->TotalScore = 0;
   BC->SumExecutionCount = 0;
   for (auto &BFI : BC->getBinaryFunctions()) {
diff --git a/bolt/test/RISCV/interprocedural-fallthrough.s b/bolt/test/RISCV/interprocedural-fallthrough.s
new file mode 100644
index 0000000000000..0bd1db9580649
--- /dev/null
+++ b/bolt/test/RISCV/interprocedural-fallthrough.s
@@ -0,0 +1,65 @@
+// Check that BOLT preserves functions connected by an implicit fall-through.
+// Moving fallthrough_source away from fallthrough_target changes the exit code
+// from zero to one because it starts falling through into poison instead.
+
+// RUN: split-file %s %t.dir
+// RUN: llvm-mc -triple=riscv64 -mattr=+c -filetype=obj \
+// RUN:   -o %t.dir/input.o %t.dir/input.s
+// RUN: ld.lld -q -e _start -o %t.dir/input.exe %t.dir/input.o
+// RUN: llvm-bolt %t.dir/input.exe -o %t.dir/output.exe \
+// RUN:   --reorder-functions=user --function-order=%t.dir/order.txt 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=WARNING
+// RUN: llvm-objdump -d --no-show-raw-insn %t.dir/output.exe \
+// RUN:   | FileCheck %s --check-prefix=DISASM
+
+// WARNING: BOLT-WARNING: interprocedural fall-through detected from fallthrough_source to fallthrough_target; preserving both functions in the original text
+
+// DISASM-LABEL: <fallthrough_source>:
+// DISASM:       li a0, 0x0
+// DISASM-NEXT:  nop
+// DISASM-LABEL: <fallthrough_target>:
+// DISASM-NEXT:  ret
+
+//--- input.s
+  .text
+  .option rvc
+
+  .globl _start
+  .type _start, @function
+  .p2align 2
+_start:
+  call fallthrough_source
+  li a7, 93
+  ecall
+  j .
+  .size _start, .-_start
+
+  .globl fallthrough_source
+  .type fallthrough_source, @function
+  .p2align 2
+fallthrough_source:
+  li a0, 0
+  .size fallthrough_source, .-fallthrough_source
+
+  // Keep this alignment padding outside fallthrough_source's symbol size.
+  // Execution intentionally crosses it to reach fallthrough_target.
+  .p2align 2
+  .globl fallthrough_target
+  .type fallthrough_target, @function
+fallthrough_target:
+  ret
+  .size fallthrough_target, .-fallthrough_target
+
+  .globl poison
+  .type poison, @function
+  .p2align 2
+poison:
+  li a0, 1
+  ret
+  .size poison, .-poison
+
+//--- order.txt
+_start
+fallthrough_source
+poison
+fallthrough_target
diff --git a/bolt/test/X86/interprocedural-fallthrough.s b/bolt/test/X86/interprocedural-fallthrough.s
new file mode 100644
index 0000000000000..edce242342629
--- /dev/null
+++ b/bolt/test/X86/interprocedural-fallthrough.s
@@ -0,0 +1,66 @@
+# REQUIRES: x86_64-linux
+
+## Check that the generic BOLT rewrite pipeline preserves functions connected
+## by an implicit fall-through. If fallthrough_source is moved away from
+## fallthrough_target, it falls through into poison and changes the exit code
+## from zero to one.
+
+# RUN: split-file %s %t.dir
+# RUN: llvm-mc -filetype=obj -triple x86_64-unknown-linux-gnu \
+# RUN:   %t.dir/input.s -o %t.dir/input.o
+# RUN: %clang %cflags -no-pie %t.dir/input.o -o %t.dir/input.exe \
+# RUN:   -Wl,-q -Wl,-e,_start
+# RUN: %t.dir/input.exe
+# RUN: llvm-bolt %t.dir/input.exe -o %t.dir/output.exe \
+# RUN:   --reorder-functions=user --function-order=%t.dir/order.txt 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=WARNING
+# RUN: %t.dir/output.exe
+# RUN: llvm-nm -n %t.dir/output.exe | FileCheck %s --check-prefix=NM
+
+# WARNING: BOLT-WARNING: interprocedural fall-through detected from fallthrough_source to fallthrough_target; preserving both functions in the original text
+
+# NM:      T fallthrough_source
+# NM-NEXT: T fallthrough_target
+
+#--- input.s
+  .text
+
+  .globl _start
+  .type _start, @function
+  .p2align 4
+_start:
+  callq fallthrough_source
+  movq $60, %rax
+  syscall
+  ud2
+  .size _start, .-_start
+
+  .globl fallthrough_source
+  .type fallthrough_source, @function
+  .p2align 4
+fallthrough_source:
+  xorl %edi, %edi
+  .size fallthrough_source, .-fallthrough_source
+
+  # Keep this alignment padding outside fallthrough_source's symbol size.
+  # Execution intentionally crosses it to reach fallthrough_target.
+  .p2align 4
+  .globl fallthrough_target
+  .type fallthrough_target, @function
+fallthrough_target:
+  retq
+  .size fallthrough_target, .-fallthrough_target
+
+  .globl poison
+  .type poison, @function
+  .p2align 4
+poison:
+  movl $1, %edi
+  retq
+  .size poison, .-poison
+
+#--- order.txt
+_start
+fallthrough_source
+poison
+fallthrough_target

``````````

</details>


https://github.com/llvm/llvm-project/pull/219430


More information about the llvm-commits mailing list