Skip to content

Conversation

atrosinenko
Copy link
Contributor

@atrosinenko atrosinenko commented May 7, 2025

Introduce a low-level instruction matching DSL to capture and/or match the operands of MCInst, single instruction at a time. Unlike the existing MCPlusBuilder::MCInstMatcher machinery, this DSL is intended for the use cases when the precise control over the instruction order is required. For example, when validating PtrAuth hardening, all registers are usually considered unsafe after a function call, even though callee-saved registers should preserve their old
values under normal operation.

Usage example:

// Bring the short names into the local scope:
using namespace LowLevelInstMatcherDSL;
// Declare the registers to capture:
Reg Xn, Xm;
// Capture the 0th and 1st operands, match the 2nd operand against the
// just captured Xm register, match the 3rd operand against literal 0:
if (!matchInst(MaybeAdd, AArch64::ADDXrs, Xm, Xn, Xm, Imm(0))
  return AArch64::NoRegister;
// Match the 0th operand against Xm:
if (!matchInst(MaybeBr, AArch64::BR, Xm))
  return AArch64::NoRegister;
// Manually check that Xm and Xn did not match the same register:
if (Xm.get() == Xn.get())
  return AArch64::NoRegister;
// Return the matched register:
return Xm.get();

Copy link
Contributor Author

atrosinenko commented May 7, 2025

@llvmbot
Copy link
Member

llvmbot commented May 7, 2025

@llvm/pr-subscribers-bolt

Author: Anatoly Trosinenko (atrosinenko)

Changes

Introduce matchInst helper function to capture and/or match the operands
of MCInst. Unlike the existing MCPlusBuilder::MCInstMatcher machinery,
matchInst is intended for the use cases when precise control over the
instruction order is required. For example, when validating PtrAuth
hardening, all registers are usually considered unsafe after a function
call, even though callee-saved registers should preserve their old
values under normal operation.

Usage example:

    // Bring the short names into the local scope:
    using namespace MCInstMatcher;
    // Declare the registers to capture:
    Reg Xn, Xm;
    // Capture the 0th and 1st operands, match the 2nd operand against the
    // just captured Xm register, match the 3rd operand against literal 0:
    if (!matchInst(MaybeAdd, AArch64::ADDXrs, Xm, Xn, Xm, Imm(0))
      return AArch64::NoRegister;
    // Match the 0th operand against Xm:
    if (!matchInst(MaybeBr, AArch64::BR, Xm))
      return AArch64::NoRegister;
    // Return the matched register:
    return Xm.get();

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

2 Files Affected:

  • (modified) bolt/include/bolt/Core/MCInstUtils.h (+128)
  • (modified) bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp (+34-56)
diff --git a/bolt/include/bolt/Core/MCInstUtils.h b/bolt/include/bolt/Core/MCInstUtils.h
index a3912a8fb265a..b495eb8ef5eec 100644
--- a/bolt/include/bolt/Core/MCInstUtils.h
+++ b/bolt/include/bolt/Core/MCInstUtils.h
@@ -166,6 +166,134 @@ static inline raw_ostream &operator<<(raw_ostream &OS,
   return Ref.print(OS);
 }
 
+/// Instruction-matching helpers operating on a single instruction at a time.
+///
+/// Unlike MCPlusBuilder::MCInstMatcher, this matchInst() function focuses on
+/// the cases where a precise control over the instruction order is important:
+///
+///     // Bring the short names into the local scope:
+///     using namespace MCInstMatcher;
+///     // Declare the registers to capture:
+///     Reg Xn, Xm;
+///     // Capture the 0th and 1st operands, match the 2nd operand against the
+///     // just captured Xm register, match the 3rd operand against literal 0:
+///     if (!matchInst(MaybeAdd, AArch64::ADDXrs, Xm, Xn, Xm, Imm(0))
+///       return AArch64::NoRegister;
+///     // Match the 0th operand against Xm:
+///     if (!matchInst(MaybeBr, AArch64::BR, Xm))
+///       return AArch64::NoRegister;
+///     // Return the matched register:
+///     return Xm.get();
+namespace MCInstMatcher {
+
+// The base class to match an operand of type T.
+//
+// The subclasses of OpMatcher are intended to be allocated on the stack and
+// to only be used by passing them to matchInst() and by calling their get()
+// function, thus the peculiar `mutable` specifiers: to make the calling code
+// compact and readable, the templated matchInst() function has to accept both
+// long-lived Imm/Reg wrappers declared as local variables (intended to capture
+// the first operand's value and match the subsequent operands, whether inside
+// a single instruction or across multiple instructions), as well as temporary
+// wrappers around literal values to match, f.e. Imm(42) or Reg(AArch64::XZR).
+template <typename T> class OpMatcher {
+  mutable std::optional<T> Value;
+  mutable std::optional<T> SavedValue;
+
+  // Remember/restore the last Value - to be called by matchInst.
+  void remember() const { SavedValue = Value; }
+  void restore() const { Value = SavedValue; }
+
+  template <class... OpMatchers>
+  friend bool matchInst(const MCInst &, unsigned, const OpMatchers &...);
+
+protected:
+  OpMatcher(std::optional<T> ValueToMatch) : Value(ValueToMatch) {}
+
+  bool matchValue(T OpValue) const {
+    // Check that OpValue does not contradict the existing Value.
+    bool MatchResult = !Value || *Value == OpValue;
+    // If MatchResult is false, all matchers will be reset before returning from
+    // matchInst, including this one, thus no need to assign conditionally.
+    Value = OpValue;
+
+    return MatchResult;
+  }
+
+public:
+  /// Returns the captured value.
+  T get() const {
+    assert(Value.has_value());
+    return *Value;
+  }
+};
+
+class Reg : public OpMatcher<MCPhysReg> {
+  bool matches(const MCOperand &Op) const {
+    if (!Op.isReg())
+      return false;
+
+    return matchValue(Op.getReg());
+  }
+
+  template <class... OpMatchers>
+  friend bool matchInst(const MCInst &, unsigned, const OpMatchers &...);
+
+public:
+  Reg(std::optional<MCPhysReg> RegToMatch = std::nullopt)
+      : OpMatcher<MCPhysReg>(RegToMatch) {}
+};
+
+class Imm : public OpMatcher<int64_t> {
+  bool matches(const MCOperand &Op) const {
+    if (!Op.isImm())
+      return false;
+
+    return matchValue(Op.getImm());
+  }
+
+  template <class... OpMatchers>
+  friend bool matchInst(const MCInst &, unsigned, const OpMatchers &...);
+
+public:
+  Imm(std::optional<int64_t> ImmToMatch = std::nullopt)
+      : OpMatcher<int64_t>(ImmToMatch) {}
+};
+
+/// Tries to match Inst and updates Ops on success.
+///
+/// If Inst has the specified Opcode and its operand list prefix matches Ops,
+/// this function returns true and updates Ops, otherwise false is returned and
+/// values of Ops are kept as before matchInst was called.
+///
+/// Please note that while Ops are technically passed by a const reference to
+/// make invocations like `matchInst(MI, Opcode, Imm(42))` possible, all their
+/// fields are marked mutable.
+template <class... OpMatchers>
+bool matchInst(const MCInst &Inst, unsigned Opcode, const OpMatchers &...Ops) {
+  if (Inst.getOpcode() != Opcode)
+    return false;
+  assert(sizeof...(Ops) <= Inst.getNumOperands() &&
+         "Too many operands are matched for the Opcode");
+
+  // Ask each matcher to remember its current value in case of rollback.
+  (Ops.remember(), ...);
+
+  // Check if all matchers match the corresponding operands.
+  auto It = Inst.begin();
+  auto AllMatched = (Ops.matches(*(It++)) && ... && true);
+
+  // If match failed, restore the original captured values.
+  if (!AllMatched) {
+    (Ops.restore(), ...);
+    return false;
+  }
+
+  return true;
+}
+
+} // namespace MCInstMatcher
+
 } // namespace bolt
 } // namespace llvm
 
diff --git a/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp b/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp
index 4d11c5b206eab..2522de7005c64 100644
--- a/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp
+++ b/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp
@@ -19,6 +19,7 @@
 #include "Utils/AArch64BaseInfo.h"
 #include "bolt/Core/BinaryBasicBlock.h"
 #include "bolt/Core/BinaryFunction.h"
+#include "bolt/Core/MCInstUtils.h"
 #include "bolt/Core/MCPlusBuilder.h"
 #include "llvm/BinaryFormat/ELF.h"
 #include "llvm/MC/MCContext.h"
@@ -393,81 +394,58 @@ class AArch64MCPlusBuilder : public MCPlusBuilder {
 
     // Iterate over the instructions of BB in reverse order, matching opcodes
     // and operands.
-    MCPhysReg TestedReg = 0;
-    MCPhysReg ScratchReg = 0;
+
     auto It = BB.end();
-    auto StepAndGetOpcode = [&It, &BB]() -> int {
-      if (It == BB.begin())
-        return -1;
-      --It;
-      return It->getOpcode();
+    auto StepBack = [&]() {
+      while (It != BB.begin()) {
+        --It;
+        if (!isCFI(*It))
+          return true;
+      }
+      return false;
     };
-
-    switch (StepAndGetOpcode()) {
-    default:
-      // Not matched the branch instruction.
+    // Step to the last non-CFI instruction.
+    if (!StepBack())
       return std::nullopt;
-    case AArch64::Bcc:
-      // Bcc EQ, .Lon_success
-      if (It->getOperand(0).getImm() != AArch64CC::EQ)
-        return std::nullopt;
-      // Not checking .Lon_success (see above).
 
-      // SUBSXrs XZR, TestedReg, ScratchReg, 0 (used by "CMP reg, reg" alias)
-      if (StepAndGetOpcode() != AArch64::SUBSXrs ||
-          It->getOperand(0).getReg() != AArch64::XZR ||
-          It->getOperand(3).getImm() != 0)
+    using namespace llvm::bolt::MCInstMatcher;
+    Reg TestedReg;
+    Reg ScratchReg;
+
+    if (matchInst(*It, AArch64::Bcc, Imm(AArch64CC::EQ) /*, .Lon_success*/)) {
+      if (!StepBack() || !matchInst(*It, AArch64::SUBSXrs, Reg(AArch64::XZR),
+                                    TestedReg, ScratchReg, Imm(0)))
         return std::nullopt;
-      TestedReg = It->getOperand(1).getReg();
-      ScratchReg = It->getOperand(2).getReg();
 
       // Either XPAC(I|D) ScratchReg, ScratchReg
       // or     XPACLRI
-      switch (StepAndGetOpcode()) {
-      default:
+      if (!StepBack())
         return std::nullopt;
-      case AArch64::XPACLRI:
+      if (matchInst(*It, AArch64::XPACLRI)) {
         // No operands to check, but using XPACLRI forces TestedReg to be X30.
-        if (TestedReg != AArch64::LR)
-          return std::nullopt;
-        break;
-      case AArch64::XPACI:
-      case AArch64::XPACD:
-        if (It->getOperand(0).getReg() != ScratchReg ||
-            It->getOperand(1).getReg() != ScratchReg)
+        if (TestedReg.get() != AArch64::LR)
           return std::nullopt;
-        break;
+      } else if (!matchInst(*It, AArch64::XPACI, ScratchReg, ScratchReg) &&
+                 !matchInst(*It, AArch64::XPACD, ScratchReg, ScratchReg)) {
+        return std::nullopt;
       }
 
-      // ORRXrs ScratchReg, XZR, TestedReg, 0 (used by "MOV reg, reg" alias)
-      if (StepAndGetOpcode() != AArch64::ORRXrs)
+      if (!StepBack() || !matchInst(*It, AArch64::ORRXrs, ScratchReg,
+                                    Reg(AArch64::XZR), TestedReg, Imm(0)))
         return std::nullopt;
-      if (It->getOperand(0).getReg() != ScratchReg ||
-          It->getOperand(1).getReg() != AArch64::XZR ||
-          It->getOperand(2).getReg() != TestedReg ||
-          It->getOperand(3).getImm() != 0)
-        return std::nullopt;
-
-      return std::make_pair(TestedReg, &*It);
 
-    case AArch64::TBZX:
-      // TBZX ScratchReg, 62, .Lon_success
-      ScratchReg = It->getOperand(0).getReg();
-      if (It->getOperand(1).getImm() != 62)
-        return std::nullopt;
-      // Not checking .Lon_success (see above).
+      return std::make_pair(TestedReg.get(), &*It);
+    }
 
-      // EORXrs ScratchReg, TestedReg, TestedReg, 1
-      if (StepAndGetOpcode() != AArch64::EORXrs)
-        return std::nullopt;
-      TestedReg = It->getOperand(1).getReg();
-      if (It->getOperand(0).getReg() != ScratchReg ||
-          It->getOperand(2).getReg() != TestedReg ||
-          It->getOperand(3).getImm() != 1)
+    if (matchInst(*It, AArch64::TBZX, ScratchReg, Imm(62) /*, .Lon_success*/)) {
+      if (!StepBack() || !matchInst(*It, AArch64::EORXrs, Reg(ScratchReg),
+                                    TestedReg, TestedReg, Imm(1)))
         return std::nullopt;
 
-      return std::make_pair(TestedReg, &*It);
+      return std::make_pair(TestedReg.get(), &*It);
     }
+
+    return std::nullopt;
   }
 
   std::optional<MCPhysReg> getAuthCheckedReg(const MCInst &Inst,

@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-factor-out-mcinstreference branch from 92a1e07 to cbcac1b Compare May 14, 2025 13:20
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-mcinstmatcher branch from 8acd2f8 to 1c135a1 Compare May 14, 2025 13:20
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-factor-out-mcinstreference branch from cbcac1b to a7a2eea Compare May 14, 2025 20:20
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-mcinstmatcher branch from d9aaf1b to 5214833 Compare July 31, 2025 19:08
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-factor-out-mcinstreference branch from e5a9182 to 847c39d Compare August 8, 2025 13:06
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-mcinstmatcher branch from 5214833 to 86d80e5 Compare August 8, 2025 13:06
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-factor-out-mcinstreference branch from 847c39d to 6ea7d73 Compare August 25, 2025 11:29
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-mcinstmatcher branch 2 times, most recently from 13fab5b to b5a5ea9 Compare August 26, 2025 15:34
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-factor-out-mcinstreference branch from 6ea7d73 to 5e906b0 Compare August 26, 2025 15:34
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-mcinstmatcher branch from b5a5ea9 to 10c6c25 Compare August 26, 2025 18:45
@atrosinenko atrosinenko changed the base branch from users/atrosinenko/bolt-factor-out-mcinstreference to users/atrosinenko/bolt-gs-modernize August 26, 2025 18:46
@atrosinenko atrosinenko changed the base branch from users/atrosinenko/bolt-gs-modernize to users/atrosinenko/bolt-factor-out-mcinstreference August 26, 2025 20:05
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-mcinstmatcher branch from 10c6c25 to b5a5ea9 Compare August 26, 2025 20:49
Base automatically changed from users/atrosinenko/bolt-factor-out-mcinstreference to main August 27, 2025 11:19
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-mcinstmatcher branch from b5a5ea9 to a1e344f Compare August 27, 2025 12:23
@kbeyls
Copy link
Collaborator

kbeyls commented Aug 27, 2025

This looks like it is core BOLT functionality, and therefore I think it's better for one of the core BOLT maintainers to review this, especially as this is introducing a second Instruction Matching functionality. @rafaelauler, @maksfb : would you agree?

Just a small note from my side: I think that if there is good reason to have 2 different instruction matching machineries in BOLT, wouldn't it be more logical if they lived closer together than one being in the MCPlusBuilder class and the other being in its own namespace MCInstMatcher namespace? It makes me wonder if it would be possible and better to try and get the functionality of both matchers into a single implementation that can serve both use cases?

@maksfb
Copy link
Contributor

maksfb commented Aug 27, 2025

Let me take a look.

Copy link
Contributor

@maksfb maksfb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat.

I'm in favor of renaming one of MCInstMatcher classes instead of just keeping them in different namespaces. If the functionality can be merged, it sounds good to me as well.

auto StepBack = [&]() {
while (It != BB.begin()) {
--It;
if (!isCFI(*It))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While virtually identical, you likely want isPseudo().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention was to control the instruction sequence as much as possible in ptrauth-related methods. While CFI pseudos were known to occur in the middle of the sequence (and they don't add any real instructions to the sequence being analyzed), I don't expect any other pseudo instructions to occur here. As I'm not absolutely sure any pseudo instruction must expand into exactly zero bytes of code (this is obviously not the case in the backend in general, but it is probably the case in the disassembled code), I kept isCFI so far and added a comment. Please feel free to correct me if I'm wrong.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Thanks for the context. In BOLT, at the moment CFIs are the only pseudo instructions widely used. I believe we have an assumption that pseudos don't directly produce any code.

bool matchInst(const MCInst &Inst, unsigned Opcode, const OpMatchers &...Ops) {
if (Inst.getOpcode() != Opcode)
return false;
assert(sizeof...(Ops) <= Inst.getNumOperands() &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use MCPlus::getNumPrimeOperands(Inst)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied, thanks.

Copy link
Contributor Author

@atrosinenko atrosinenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it is feasible to me to unify the two approaches right now, thus I tried to summarize the reasons not to do so in the description of the namespace :)

I renamed my namespace to LowLevelInstMatcherDSL, but kept it in MCInstUtils.cpp instead of MCPlusBuilder.cpp so far. My idea was that it is probably more expected by the user to find such utilities in a dedicated source file instead of the file defining target-specific callbacks. Honestly, I would rather move the existing MCInstMatcher to MCInstUtils.cpp, but even if it would be an acceptable solution, it should probably be done in a separate PR.

PS: This PR depends on #155846 (that reapplies the reverted patch from this stack), thus the conflicts are shown by Github. It is not always trivial to make Graphite reorder the PRs, so kept it as-is for now.

bool matchInst(const MCInst &Inst, unsigned Opcode, const OpMatchers &...Ops) {
if (Inst.getOpcode() != Opcode)
return false;
assert(sizeof...(Ops) <= Inst.getNumOperands() &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied, thanks.

auto StepBack = [&]() {
while (It != BB.begin()) {
--It;
if (!isCFI(*It))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention was to control the instruction sequence as much as possible in ptrauth-related methods. While CFI pseudos were known to occur in the middle of the sequence (and they don't add any real instructions to the sequence being analyzed), I don't expect any other pseudo instructions to occur here. As I'm not absolutely sure any pseudo instruction must expand into exactly zero bytes of code (this is obviously not the case in the backend in general, but it is probably the case in the disassembled code), I kept isCFI so far and added a comment. Please feel free to correct me if I'm wrong.

@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-mcinstmatcher branch from 5a3b71a to ec302b4 Compare September 30, 2025 12:43
@atrosinenko
Copy link
Contributor Author

Rebased the stack after #155846 was merged. I plan waiting a bit more to make sure I don't have to rollback the patch again and then merging this PR, then I will check if it is possible to reorder the remaining patches, to land already approved PRs #139778 and #141665.

Introduce matchInst helper function to capture and/or match the operands
of MCInst. Unlike the existing `MCPlusBuilder::MCInstMatcher` machinery,
matchInst is intended for the use cases when precise control over the
instruction order is required. For example, when validating PtrAuth
hardening, all registers are usually considered unsafe after a function
call, even though callee-saved registers should preserve their old
values *under normal operation*.
@atrosinenko atrosinenko force-pushed the users/atrosinenko/bolt-mcinstmatcher branch from ec302b4 to 99e01ae Compare September 30, 2025 18:17
@atrosinenko atrosinenko merged commit d884b55 into main Sep 30, 2025
9 checks passed
@atrosinenko atrosinenko deleted the users/atrosinenko/bolt-mcinstmatcher branch September 30, 2025 18:24
mahesh-attarde pushed a commit to mahesh-attarde/llvm-project that referenced this pull request Oct 3, 2025
…#138883)

Introduce a low-level instruction matching DSL to capture and/or match
the operands of MCInst, single instruction at a time. Unlike the
existing `MCPlusBuilder::MCInstMatcher` machinery, this DSL is intended
for the use cases when the precise control over the instruction order is
required. For example, when validating PtrAuth hardening, all registers
are usually considered unsafe after a function call, even though
callee-saved registers should preserve their old
values _under normal operation_.

Usage example:

    // Bring the short names into the local scope:
    using namespace LowLevelInstMatcherDSL;
    // Declare the registers to capture:
    Reg Xn, Xm;
    // Capture the 0th and 1st operands, match the 2nd operand against the
    // just captured Xm register, match the 3rd operand against literal 0:
    if (!matchInst(MaybeAdd, AArch64::ADDXrs, Xm, Xn, Xm, Imm(0))
      return AArch64::NoRegister;
    // Match the 0th operand against Xm:
    if (!matchInst(MaybeBr, AArch64::BR, Xm))
      return AArch64::NoRegister;
    // Manually check that Xm and Xn did not match the same register:
    if (Xm.get() == Xn.get())
      return AArch64::NoRegister;
    // Return the matched register:
    return Xm.get();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants