From 35386fc18339de07b2894befbe3db113be775365 Mon Sep 17 00:00:00 2001 From: AkshayK Date: Tue, 18 Aug 2026 12:08:28 -0400 Subject: [PATCH 1/3] [ARM] Clear the call-used registers at every exit Implement emitZeroCallUsedRegs and buildClearRegister for ARM, Thumb-1 and Thumb-2 over the core, VFP, NEON and MVE registers. Until now the hook's empty default ran and the request was dropped without a word. The requested set is reduced to its leaves and coalesced back up only where every part of a wider register was asked for. Unlike AArch64, where the registers nest in one chain per number, D0 here holds two S registers, so clearing D0 to discharge a request for S1 would destroy an S0 return value. The instruction is the flags-free immediate move where one exists: MOVi, t2MOVi, t2MOVi16. Classic Thumb-1 has only tMOVi8, so it materializes one zero and copies it out with tMOVr, and reports rather than emit if the flags are live at the exit. Vector registers take a vector immediate under NEON and MVE and are otherwise written from a zeroed general-purpose register, because the VFP 8-bit immediate cannot encode +0.0. t2CLRM is not used: it writes APSR where a flags-free instruction is available. The zero-holding register is taken from the set being cleared, or found by liveness. Not the register scavenger: the frame is finalized by the time this runs, so a scavenge that must spill aborts the compiler. The driver still rejects -fzero-call-used-regs on ARM through its hard-coded target list, so this is reachable through the function attribute only. This is trailofbits/vspells-ct-internal-notes#52, under the umbrella trailofbits/vspells-ct-internal-notes#30. --- llvm/lib/Target/ARM/ARMBaseInstrInfo.cpp | 76 ++++++ llvm/lib/Target/ARM/ARMBaseInstrInfo.h | 16 ++ llvm/lib/Target/ARM/ARMFrameLowering.cpp | 244 ++++++++++++++++++ llvm/lib/Target/ARM/ARMFrameLowering.h | 6 + .../CodeGen/ARM/zero-call-used-regs-fp.ll | 67 +++++ llvm/test/CodeGen/ARM/zero-call-used-regs.ll | 202 +++++++++++++++ 6 files changed, 611 insertions(+) create mode 100644 llvm/test/CodeGen/ARM/zero-call-used-regs-fp.ll create mode 100644 llvm/test/CodeGen/ARM/zero-call-used-regs.ll diff --git a/llvm/lib/Target/ARM/ARMBaseInstrInfo.cpp b/llvm/lib/Target/ARM/ARMBaseInstrInfo.cpp index a922024b032db..7d60fbdf256ba 100644 --- a/llvm/lib/Target/ARM/ARMBaseInstrInfo.cpp +++ b/llvm/lib/Target/ARM/ARMBaseInstrInfo.cpp @@ -877,6 +877,82 @@ void ARMBaseInstrInfo::copyPhysReg(MachineBasicBlock &MBB, Mov->addRegisterKilled(SrcReg, TRI); } +void ARMBaseInstrInfo::buildClearRegister(Register Reg, + MachineBasicBlock &MBB, + MachineBasicBlock::iterator Iter, + DebugLoc &DL, + bool AllowSideEffects) const { + if (ARM::GPRRegClass.contains(Reg)) { + if (!Subtarget.isThumb()) { + // The S bit is an operand rather than a separate opcode, and passing no + // register for it is what makes this a MOV and not a MOVS. + BuildMI(MBB, Iter, DL, get(ARM::MOVi), Reg) + .addImm(0) + .add(predOps(ARMCC::AL)) + .add(condCodeOp()); + return; + } + + // Thumb-2 reaches R0-R12 and LR with a flags-free move of an immediate. + if (Subtarget.isThumb2()) { + BuildMI(MBB, Iter, DL, get(ARM::t2MOVi), Reg) + .addImm(0) + .add(predOps(ARMCC::AL)) + .add(condCodeOp()); + return; + } + + // On v8-M Baseline the 16-bit-immediate move is the flags-free one, and it + // reaches the same registers, so prefer it over tMOVi8 whether or not side + // effects are allowed: it is the only Thumb-1 form that reaches a high + // register at all. + if (Subtarget.hasV8MBaselineOps()) { + BuildMI(MBB, Iter, DL, get(ARM::t2MOVi16), Reg) + .addImm(0) + .add(predOps(ARMCC::AL)); + return; + } + + // Classic Thumb-1 has only tMOVi8, which reaches R0-R7 and always writes + // the flags. The CPSR definition is an out operand of the instruction, so + // it comes before the immediate. + if (ARM::tGPRRegClass.contains(Reg) && AllowSideEffects) { + BuildMI(MBB, Iter, DL, get(ARM::tMOVi8), Reg) + .add(t1CondCodeOp()) + .addImm(0) + .add(predOps(ARMCC::AL)); + return; + } + } else if (ARM::DPRRegClass.contains(Reg)) { + if (Subtarget.hasNEON()) { + BuildMI(MBB, Iter, DL, get(ARM::VMOVv2i32), Reg) + .addImm(0) + .add(predOps(ARMCC::AL)); + return; + } + } else if (ARM::QPRRegClass.contains(Reg)) { + if (Subtarget.hasNEON()) { + BuildMI(MBB, Iter, DL, get(ARM::VMOVv4i32), Reg) + .addImm(0) + .add(predOps(ARMCC::AL)); + return; + } + if (Subtarget.hasMVEIntegerOps() && ARM::MQPRRegClass.contains(Reg)) { + MachineInstrBuilder MIB = + BuildMI(MBB, Iter, DL, get(ARM::MVE_VMOVimmi32), Reg).addImm(0); + addUnpredicatedMveVpredROp(MIB, Reg); + return; + } + } + + // Everything else needs a register already holding zero to read from, which + // this cannot obtain, or has no zeroing form on this subtarget at all. Say + // so rather than return having emitted nothing: a caller cannot tell the + // difference between a register that was cleared and one that was skipped. + reportFatalInternalError("buildClearRegister is not implemented for " + + getRegisterInfo().getRegAsmName(Reg)); +} + std::optional ARMBaseInstrInfo::isCopyInstrImpl(const MachineInstr &MI) const { // VMOVRRD is also a copy instruction but it requires diff --git a/llvm/lib/Target/ARM/ARMBaseInstrInfo.h b/llvm/lib/Target/ARM/ARMBaseInstrInfo.h index 94595ab2b338b..a24e1c26dc1b4 100644 --- a/llvm/lib/Target/ARM/ARMBaseInstrInfo.h +++ b/llvm/lib/Target/ARM/ARMBaseInstrInfo.h @@ -214,6 +214,22 @@ class ARMBaseInstrInfo : public ARMGenInstrInfo { bool KillSrc, bool RenamableDest = false, bool RenamableSrc = false) const override; + /// Write zero to \p Reg with a single instruction. + /// + /// This covers the registers ARM can zero without a second register to read + /// a zero out of, which is every general-purpose register except a Thumb-1 + /// high register on a subtarget without the 16-bit-immediate move, and the + /// D and Q registers on a subtarget with NEON or MVE. A register outside + /// that set is reported rather than skipped, because a caller that asked for + /// it to be cleared and got nothing would have no way to tell. + /// + /// The cases left out are the ones that need a register already holding + /// zero, and a caller that has one clears them itself; see + /// ARMFrameLowering::emitZeroCallUsedRegs. + void buildClearRegister(Register Reg, MachineBasicBlock &MBB, + MachineBasicBlock::iterator Iter, DebugLoc &DL, + bool AllowSideEffects = true) const override; + void storeRegToStackSlot( MachineBasicBlock &MBB, MachineBasicBlock::iterator MBBI, Register SrcReg, bool isKill, int FrameIndex, const TargetRegisterClass *RC, Register VReg, diff --git a/llvm/lib/Target/ARM/ARMFrameLowering.cpp b/llvm/lib/Target/ARM/ARMFrameLowering.cpp index 553e4ed6e03d5..bed0bbbccd72d 100644 --- a/llvm/lib/Target/ARM/ARMFrameLowering.cpp +++ b/llvm/lib/Target/ARM/ARMFrameLowering.cpp @@ -121,6 +121,7 @@ #include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallVector.h" #include "llvm/CodeGen/CFIInstBuilder.h" +#include "llvm/CodeGen/LiveRegUnits.h" #include "llvm/CodeGen/MachineBasicBlock.h" #include "llvm/CodeGen/MachineConstantPool.h" #include "llvm/CodeGen/MachineFrameInfo.h" @@ -138,6 +139,7 @@ #include "llvm/IR/Attributes.h" #include "llvm/IR/CallingConv.h" #include "llvm/IR/DebugLoc.h" +#include "llvm/IR/DiagnosticInfo.h" #include "llvm/IR/Function.h" #include "llvm/IR/Module.h" #include "llvm/MC/MCAsmInfo.h" @@ -1576,6 +1578,248 @@ void ARMFrameLowering::emitEpilogue(MachineFunction &MF, } } +//===----------------------------------------------------------------------===// +// Clearing the call-used registers. +// +// The set arrives holding every allocatable register the exit does not need, +// named through every class that contains it, so R0 arrives alongside R0_R1 +// and S0 alongside D0 and Q0. The first job is to reduce that to one register +// per piece of state, and the second is to pick an instruction for each. +// +// Widening is where ARM differs from the targets that already do this. On +// AArch64 the floating-point registers nest in a single chain per number, so a +// request for S0 can be discharged by clearing Q0 and nothing else is touched. +// Here D0 holds two S registers, so clearing D0 to discharge a request for S1 +// would also destroy S0 -- which the exit may be returning in. So a wider +// register is only used when every part of it was asked for. +//===----------------------------------------------------------------------===// + +/// Whether \p Reg is one of the registers this step sorts into the +/// floating-point half of the work. +/// +/// Asked of the leaves rather than of a list of classes, because the tuple +/// classes the vector load and store instructions need are numerous, are +/// spaced as well as consecutive, and are not what anything else here is +/// written in terms of. What makes a register floating-point is what it is +/// made of. +static bool isFPOrNEONReg(const TargetRegisterInfo &TRI, MCRegister Reg) { + bool AnyLeaf = false; + for (MCPhysReg Sub : TRI.subregs_inclusive(Reg)) { + if (!TRI.subregs(Sub).empty()) + continue; + AnyLeaf = true; + if (!ARM::SPRRegClass.contains(Sub) && !ARM::DPRRegClass.contains(Sub)) + return false; + } + return AnyLeaf; +} + +/// Record in \p Used what is live at \p MBBI, which is what the clearing +/// sequence at this exit may not write. +static void computeLiveUnitsAt(LiveRegUnits &Used, const MachineBasicBlock &MBB, + MachineBasicBlock::const_iterator MBBI) { + // addLiveOuts rather than addLiveOutsNoPristines: a callee-saved register the + // function never touched holds the caller's value here, and writing it would + // hand the caller something else back. + Used.addLiveOuts(MBB); + for (auto I = MBB.end(); I != MBBI;) { + --I; + if (I->isDebugInstr()) + continue; + Used.stepBackward(*I); + } +} + +void ARMFrameLowering::emitZeroCallUsedRegs(BitVector RegsToZero, + MachineBasicBlock &MBB, + RegScavenger *) const { + MachineFunction &MF = *MBB.getParent(); + const Function &F = MF.getFunction(); + const ARMBaseRegisterInfo &TRI = *STI.getRegisterInfo(); + const ARMBaseInstrInfo &TII = *STI.getInstrInfo(); + + MachineBasicBlock::iterator MBBI = MBB.getFirstTerminator(); + DebugLoc DL; + if (MBBI != MBB.end()) + DL = MBBI->getDebugLoc(); + + auto report = [&](const Twine &Message) { + F.getContext().diagnose(DiagnosticInfoUnsupported{F, Message}); + }; + + // Sort the request into the general-purpose registers and the leaves of the + // floating-point register file. A leaf is a register with no sub-registers, + // which is what identifies one piece of state exactly once: S0 through S31, + // and D16 through D31 on a subtarget whose D registers go that far. + BitVector GPRs(TRI.getNumRegs()); + BitVector FPLeaves(TRI.getNumRegs()); + for (MCRegister Reg : RegsToZero.set_bits()) { + if (TRI.isGeneralPurposeRegister(MF, Reg)) { + GPRs.set(Reg.id()); + } else if (ARM::GPRPairRegClass.contains(Reg)) { + for (MCPhysReg Sub : TRI.subregs(Reg)) + GPRs.set(Sub); + } else if (isFPOrNEONReg(TRI, Reg)) { + // Without floating-point registers there is no instruction that can put + // a value in one, so there is nothing in them to destroy and nothing to + // report. This is the one skip here that is not a gap. + if (!STI.hasFPRegs()) + continue; + for (MCPhysReg Sub : TRI.subregs_inclusive(Reg)) + if (TRI.subregs(Sub).empty()) + FPLeaves.set(Sub); + } else if (Reg != ARM::VPR) { + report("clearing the call-used registers reached " + + Twine(TRI.getRegAsmName(Reg)) + + ", which this target does not know how to clear"); + } + } + const bool ClearVPR = RegsToZero.test(ARM::VPR) && STI.hasMVEIntegerOps(); + + // Reduce the leaves to the widest register that covers only leaves that were + // asked for. Q first, then D, and whatever is left stays an S. + auto coversOnlyRequested = [&](MCRegister Reg) { + bool AnyLeaf = false; + for (MCPhysReg Sub : TRI.subregs_inclusive(Reg)) { + if (!TRI.subregs(Sub).empty()) + continue; + AnyLeaf = true; + if (!FPLeaves.test(Sub)) + return false; + } + return AnyLeaf; + }; + auto takeLeaves = [&](MCRegister Reg) { + for (MCPhysReg Sub : TRI.subregs_inclusive(Reg)) + if (TRI.subregs(Sub).empty()) + FPLeaves.reset(Sub); + }; + + SmallVector WideFPRegs; // cleared by one instruction + SmallVector PairedFPRegs; // cleared from two zeroed halves + SmallVector SingleFPRegs; // cleared from one zeroed half + + const bool HasVectorImm = STI.hasNEON() || STI.hasMVEIntegerOps(); + if (HasVectorImm) + for (MCRegister Reg : ARM::QPRRegClass) + if ((STI.hasNEON() || ARM::MQPRRegClass.contains(Reg)) && + coversOnlyRequested(Reg)) { + WideFPRegs.push_back(Reg); + takeLeaves(Reg); + } + for (MCRegister Reg : ARM::DPRRegClass) + if (coversOnlyRequested(Reg)) { + if (STI.hasNEON()) + WideFPRegs.push_back(Reg); + else + PairedFPRegs.push_back(Reg); + takeLeaves(Reg); + } + for (MCRegister Reg : FPLeaves.set_bits()) { + // Anything still here is an S register: a D register above D15 has no S + // sub-registers, so the loop above took it whole or not at all. + assert(ARM::SPRRegClass.contains(Reg) && "unreduced floating-point leaf"); + SingleFPRegs.push_back(Reg); + } + + // A register holding zero is needed where the instruction that writes the + // destination reads one: a Thumb-1 high register, a floating-point register + // on a subtarget with no vector immediate, and the vector predicate. + const bool NeedThumb1ZeroSrc = + STI.isThumb1Only() && !STI.hasV8MBaselineOps() && GPRs.any(); + const bool NeedZeroSrc = NeedThumb1ZeroSrc || !PairedFPRegs.empty() || + !SingleFPRegs.empty() || ClearVPR; + + // In Thumb-1 the source has to be one tMOVi8 can write, which is a low + // register; everywhere else any general-purpose register will do. + const TargetRegisterClass &ZeroSrcRC = + STI.isThumb1Only() ? ARM::tGPRRegClass : ARM::GPRRegClass; + MCRegister ZeroSrc; + if (NeedZeroSrc) { + // Prefer one that is being cleared anyway. It is dead by construction, and + // it ends holding what it was asked to hold, so it costs nothing. + for (MCRegister Reg : GPRs.set_bits()) + if (ZeroSrcRC.contains(Reg)) { + ZeroSrc = Reg; + break; + } + + // Otherwise take one that is free here. Not the register scavenger: the + // frame is finalized by the time this runs, so a scavenge that has to spill + // has nowhere to spill to and aborts. What is wanted is a register that + // needs no spill, which is what asking for the live ones and taking a + // register that is not among them gives. + if (!ZeroSrc) { + const MachineRegisterInfo &MRI = MF.getRegInfo(); + LiveRegUnits Used(TRI); + computeLiveUnitsAt(Used, MBB, MBBI); + for (MCRegister Reg : ZeroSrcRC) + if (!MRI.isReserved(Reg) && Used.available(Reg)) { + ZeroSrc = Reg; + break; + } + } + + if (!ZeroSrc) { + report("clearing the call-used registers needs a register to hold zero " + "and none is free at this exit"); + return; + } + } + + // Thumb-1 below v8-M Baseline writes the flags whichever instruction it uses + // to materialize the zero, and there is no form that does not. Refuse rather + // than change what a predicated return does. + if (NeedThumb1ZeroSrc) { + LiveRegUnits Used(TRI); + computeLiveUnitsAt(Used, MBB, MBBI); + if (!Used.available(ARM::CPSR)) { + report("clearing the call-used registers writes the condition flags on " + "this subtarget, and they are live at this exit"); + return; + } + } + + // The general-purpose registers first, so that the zero source is in place + // for the steps below that read it. + if (NeedThumb1ZeroSrc) { + // One materialized zero, then a flags-free copy of it into everything + // else. This is the only Thumb-1 sequence that reaches a high register, + // and using it for the low ones too keeps the flag writes down to one. + TII.buildClearRegister(ZeroSrc, MBB, MBBI, DL); + for (MCRegister Reg : GPRs.set_bits()) { + if (Reg == ZeroSrc) + continue; + BuildMI(MBB, MBBI, DL, TII.get(ARM::tMOVr), Reg) + .addReg(ZeroSrc) + .add(predOps(ARMCC::AL)); + } + } else { + for (MCRegister Reg : GPRs.set_bits()) + TII.buildClearRegister(Reg, MBB, MBBI, DL); + // A scavenged source is not in the set, so it has not been zeroed yet. + if (NeedZeroSrc && !GPRs.test(ZeroSrc.id())) + TII.buildClearRegister(ZeroSrc, MBB, MBBI, DL); + } + + for (MCRegister Reg : WideFPRegs) + TII.buildClearRegister(Reg, MBB, MBBI, DL); + for (MCRegister Reg : PairedFPRegs) + BuildMI(MBB, MBBI, DL, TII.get(ARM::VMOVDRR), Reg) + .addReg(ZeroSrc) + .addReg(ZeroSrc) + .add(predOps(ARMCC::AL)); + for (MCRegister Reg : SingleFPRegs) + BuildMI(MBB, MBBI, DL, TII.get(ARM::VMOVSR), Reg) + .addReg(ZeroSrc) + .add(predOps(ARMCC::AL)); + + if (ClearVPR) + BuildMI(MBB, MBBI, DL, TII.get(ARM::VMSR_VPR)) + .addReg(ZeroSrc) + .add(predOps(ARMCC::AL)); +} + /// getFrameIndexReference - Provide a base+offset reference to an FI slot for /// debug info. It's the same as what we use for resolving the code-gen /// references for now. FIXME: This can go wrong when references are diff --git a/llvm/lib/Target/ARM/ARMFrameLowering.h b/llvm/lib/Target/ARM/ARMFrameLowering.h index 7021af0d9fbdd..d3e96e729322f 100644 --- a/llvm/lib/Target/ARM/ARMFrameLowering.h +++ b/llvm/lib/Target/ARM/ARMFrameLowering.h @@ -9,6 +9,7 @@ #ifndef LLVM_LIB_TARGET_ARM_ARMFRAMELOWERING_H #define LLVM_LIB_TARGET_ARM_ARMFRAMELOWERING_H +#include "llvm/ADT/BitVector.h" #include "llvm/CodeGen/TargetFrameLowering.h" #include "llvm/Support/TypeSize.h" @@ -17,6 +18,7 @@ namespace llvm { class ARMSubtarget; class CalleeSavedInfo; class MachineFunction; +class RegScavenger; class ARMFrameLowering : public TargetFrameLowering { protected: @@ -92,6 +94,10 @@ class ARMFrameLowering : public TargetFrameLowering { bool hasFPImpl(const MachineFunction &MF) const override; private: + /// Emit target zero call-used regs. + void emitZeroCallUsedRegs(BitVector RegsToZero, MachineBasicBlock &MBB, + RegScavenger *RS) const override; + void emitPushInst(MachineBasicBlock &MBB, MachineBasicBlock::iterator MI, ArrayRef CSI, unsigned StmOpc, unsigned StrOpc, bool NoGap, diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs-fp.ll b/llvm/test/CodeGen/ARM/zero-call-used-regs-fp.ll new file mode 100644 index 0000000000000..b1b54b869a789 --- /dev/null +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs-fp.ll @@ -0,0 +1,67 @@ +; What the floating-point half of the clear can use depends on the subtarget, +; and the four configurations here are the four answers: a vector immediate +; from NEON, a vector immediate from MVE, a move from a zeroed general-purpose +; register when there is neither, and nothing at all when the registers do not +; exist. + +; RUN: llc -mtriple=armv7-unknown-linux-gnueabihf %s -o - | FileCheck %s --check-prefix=NEON +; RUN: llc -mtriple=thumbv8m.main -mattr=+fp-armv8d16sp %s -o - | FileCheck %s --check-prefix=VFP +; RUN: llc -mtriple=thumbv8.1m.main -mattr=+mve %s -o - | FileCheck %s --check-prefix=MVE +; RUN: llc -mtriple=thumbv7m-none-eabi %s -o - | FileCheck %s --check-prefix=NOFP + +; D8-D15 are callee-saved, so the vector registers built out of them are the +; caller's and are not cleared: on NEON that leaves q0-q3 and q8-q15, and the +; gap where q4-q7 would be is the point. +; NEON-LABEL: all_regs: +; NEON: mov r0, #0 +; NEON: mov r12, #0 +; NEON-NEXT: vmov.i32 q0, #0x0 +; NEON: vmov.i32 q3, #0x0 +; NEON-NEXT: vmov.i32 q8, #0x0 +; NEON: vmov.i32 q15, #0x0 +; NEON-NEXT: bx lr +; +; With floating-point registers but no vector immediate, each register is +; written from one that has been zeroed already. A d register takes two halves +; of it, which is the same instruction the security extension's own clearing +; sequence uses. +; VFP-LABEL: all_regs: +; VFP: movs r0, #0 +; VFP: vmov d0, r0, r0 +; VFP: vmov d7, r0, r0 +; VFP-NEXT: bx lr +; +; MVE reaches q0-q7 and, unlike NEON, has a predicate register that is neither +; general-purpose nor part of the vector file. It is data, so it is cleared. +; MVE-LABEL: all_regs: +; MVE: movs r0, #0 +; MVE: vmov.i32 q0, #0x0 +; MVE: vmov.i32 q3, #0x0 +; MVE-NEXT: vmsr vpr, r0 +; MVE-NEXT: bx lr +; +; Without floating-point registers there is nothing in them to destroy, because +; no instruction exists that could have put anything there. +; NOFP-LABEL: all_regs: +; NOFP: movs r0, #0 +; NOFP: mov.w r12, #0 +; NOFP-NEXT: bx lr +; NOFP-NOT: vmov +define void @all_regs() "zero-call-used-regs"="all" { + ret void +} + +; s0 is the return value and s1 is not, and they are the two halves of d0. The +; wider register cannot stand in for the half that was asked for: clearing d0 +; here would destroy the value being returned. So the clear stays at the width +; it was asked for, and pays for a zeroed general-purpose register to do it. +; NEON-LABEL: half_a_pair: +; NEON: vmul.f32 s0, s0, s1 +; NEON-NEXT: mov r0, #0 +; NEON-NEXT: vmov s1, r0 +; NEON-NEXT: bx lr +; NEON-NOT: vmov.i32 d0 +define float @half_a_pair(float %a, float %b) noinline optnone "zero-call-used-regs"="used" { + %r = fmul float %a, %b + ret float %r +} diff --git a/llvm/test/CodeGen/ARM/zero-call-used-regs.ll b/llvm/test/CodeGen/ARM/zero-call-used-regs.ll new file mode 100644 index 0000000000000..e962ef4c92875 --- /dev/null +++ b/llvm/test/CodeGen/ARM/zero-call-used-regs.ll @@ -0,0 +1,202 @@ +; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6 +; RUN: llc < %s -verify-machineinstrs -mtriple=armv7-unknown-linux-gnueabi | FileCheck %s --check-prefixes=ARM +; RUN: llc < %s -verify-machineinstrs -mtriple=thumbv7m-none-eabi | FileCheck %s --check-prefixes=THUMB2 +; RUN: llc < %s -verify-machineinstrs -mtriple=thumbv6m-none-eabi | FileCheck %s --check-prefixes=THUMB1 + +define dso_local i32 @skip(i32 noundef %a, i32 noundef %b, i32 noundef %c) local_unnamed_addr "zero-call-used-regs"="skip" { +; ARM-LABEL: skip: +; ARM: @ %bb.0: @ %entry +; ARM-NEXT: mul r0, r1, r0 +; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: bx lr +; +; THUMB2-LABEL: skip: +; THUMB2: @ %bb.0: @ %entry +; THUMB2-NEXT: muls r0, r1, r0 +; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: bx lr +; +; THUMB1-LABEL: skip: +; THUMB1: @ %bb.0: @ %entry +; THUMB1-NEXT: muls r0, r1, r0 +; THUMB1-NEXT: orrs r0, r2 +; THUMB1-NEXT: bx lr +entry: + %mul = mul nsw i32 %b, %a + %or = or i32 %mul, %c + ret i32 %or +} + +define dso_local i32 @used_gpr_arg(i32 noundef %a, i32 noundef %b, i32 noundef %c) local_unnamed_addr noinline optnone "zero-call-used-regs"="used-gpr-arg" { +; ARM-LABEL: used_gpr_arg: +; ARM: @ %bb.0: @ %entry +; ARM-NEXT: mul r0, r1, r0 +; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: mov r2, #0 +; ARM-NEXT: bx lr +; +; THUMB2-LABEL: used_gpr_arg: +; THUMB2: @ %bb.0: @ %entry +; THUMB2-NEXT: muls r0, r1, r0 +; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: movs r2, #0 +; THUMB2-NEXT: bx lr +; +; THUMB1-LABEL: used_gpr_arg: +; THUMB1: @ %bb.0: @ %entry +; THUMB1-NEXT: muls r0, r1, r0 +; THUMB1-NEXT: orrs r0, r2 +; THUMB1-NEXT: movs r2, #0 +; THUMB1-NEXT: bx lr +entry: + %mul = mul nsw i32 %b, %a + %or = or i32 %mul, %c + ret i32 %or +} + +define dso_local i32 @used_gpr(i32 noundef %a, i32 noundef %b, i32 noundef %c) local_unnamed_addr noinline optnone "zero-call-used-regs"="used-gpr" { +; ARM-LABEL: used_gpr: +; ARM: @ %bb.0: @ %entry +; ARM-NEXT: mul r0, r1, r0 +; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: mov r2, #0 +; ARM-NEXT: bx lr +; +; THUMB2-LABEL: used_gpr: +; THUMB2: @ %bb.0: @ %entry +; THUMB2-NEXT: muls r0, r1, r0 +; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: movs r2, #0 +; THUMB2-NEXT: bx lr +; +; THUMB1-LABEL: used_gpr: +; THUMB1: @ %bb.0: @ %entry +; THUMB1-NEXT: muls r0, r1, r0 +; THUMB1-NEXT: orrs r0, r2 +; THUMB1-NEXT: movs r2, #0 +; THUMB1-NEXT: bx lr +entry: + %mul = mul nsw i32 %b, %a + %or = or i32 %mul, %c + ret i32 %or +} + +define dso_local i32 @used_arg(i32 noundef %a, i32 noundef %b, i32 noundef %c) local_unnamed_addr noinline optnone "zero-call-used-regs"="used-arg" { +; ARM-LABEL: used_arg: +; ARM: @ %bb.0: @ %entry +; ARM-NEXT: mul r0, r1, r0 +; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: mov r2, #0 +; ARM-NEXT: bx lr +; +; THUMB2-LABEL: used_arg: +; THUMB2: @ %bb.0: @ %entry +; THUMB2-NEXT: muls r0, r1, r0 +; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: movs r2, #0 +; THUMB2-NEXT: bx lr +; +; THUMB1-LABEL: used_arg: +; THUMB1: @ %bb.0: @ %entry +; THUMB1-NEXT: muls r0, r1, r0 +; THUMB1-NEXT: orrs r0, r2 +; THUMB1-NEXT: movs r2, #0 +; THUMB1-NEXT: bx lr +entry: + %mul = mul nsw i32 %b, %a + %or = or i32 %mul, %c + ret i32 %or +} + +define dso_local i32 @used(i32 noundef %a, i32 noundef %b, i32 noundef %c) local_unnamed_addr noinline optnone "zero-call-used-regs"="used" { +; ARM-LABEL: used: +; ARM: @ %bb.0: @ %entry +; ARM-NEXT: mul r0, r1, r0 +; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: mov r2, #0 +; ARM-NEXT: bx lr +; +; THUMB2-LABEL: used: +; THUMB2: @ %bb.0: @ %entry +; THUMB2-NEXT: muls r0, r1, r0 +; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: movs r2, #0 +; THUMB2-NEXT: bx lr +; +; THUMB1-LABEL: used: +; THUMB1: @ %bb.0: @ %entry +; THUMB1-NEXT: muls r0, r1, r0 +; THUMB1-NEXT: orrs r0, r2 +; THUMB1-NEXT: movs r2, #0 +; THUMB1-NEXT: bx lr +entry: + %mul = mul nsw i32 %b, %a + %or = or i32 %mul, %c + ret i32 %or +} + +define dso_local i32 @all_gpr_arg(i32 noundef %a, i32 noundef %b, i32 noundef %c) local_unnamed_addr "zero-call-used-regs"="all-gpr-arg" { +; ARM-LABEL: all_gpr_arg: +; ARM: @ %bb.0: @ %entry +; ARM-NEXT: mul r0, r1, r0 +; ARM-NEXT: mov r3, #0 +; ARM-NEXT: mov r12, #0 +; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: mov r2, #0 +; ARM-NEXT: bx lr +; +; THUMB2-LABEL: all_gpr_arg: +; THUMB2: @ %bb.0: @ %entry +; THUMB2-NEXT: muls r0, r1, r0 +; THUMB2-NEXT: movs r3, #0 +; THUMB2-NEXT: mov.w r12, #0 +; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: movs r2, #0 +; THUMB2-NEXT: bx lr +; +; THUMB1-LABEL: all_gpr_arg: +; THUMB1: @ %bb.0: @ %entry +; THUMB1-NEXT: muls r0, r1, r0 +; THUMB1-NEXT: orrs r0, r2 +; THUMB1-NEXT: movs r2, #0 +; THUMB1-NEXT: mov r3, r2 +; THUMB1-NEXT: mov r12, r2 +; THUMB1-NEXT: bx lr +entry: + %mul = mul nsw i32 %b, %a + %or = or i32 %mul, %c + ret i32 %or +} + +define dso_local i32 @all_gpr(i32 noundef %a, i32 noundef %b, i32 noundef %c) local_unnamed_addr "zero-call-used-regs"="all-gpr" { +; ARM-LABEL: all_gpr: +; ARM: @ %bb.0: @ %entry +; ARM-NEXT: mul r0, r1, r0 +; ARM-NEXT: mov r3, #0 +; ARM-NEXT: mov r12, #0 +; ARM-NEXT: orr r0, r0, r2 +; ARM-NEXT: mov r2, #0 +; ARM-NEXT: bx lr +; +; THUMB2-LABEL: all_gpr: +; THUMB2: @ %bb.0: @ %entry +; THUMB2-NEXT: muls r0, r1, r0 +; THUMB2-NEXT: movs r3, #0 +; THUMB2-NEXT: mov.w r12, #0 +; THUMB2-NEXT: orrs r0, r2 +; THUMB2-NEXT: movs r2, #0 +; THUMB2-NEXT: bx lr +; +; THUMB1-LABEL: all_gpr: +; THUMB1: @ %bb.0: @ %entry +; THUMB1-NEXT: muls r0, r1, r0 +; THUMB1-NEXT: orrs r0, r2 +; THUMB1-NEXT: movs r2, #0 +; THUMB1-NEXT: mov r3, r2 +; THUMB1-NEXT: mov r12, r2 +; THUMB1-NEXT: bx lr +entry: + %mul = mul nsw i32 %b, %a + %or = or i32 %mul, %c + ret i32 %or +} From 4da8993a72392c82fd71ce67a499edfa573bab83 Mon Sep 17 00:00:00 2001 From: AkshayK Date: Tue, 18 Aug 2026 16:12:24 -0400 Subject: [PATCH 2/3] [Driver] Allow -fzero-call-used-regs on 32-bit Arm The driver rejected the flag on Arm through a hard-coded list of targets, carrying a FIXME that the restriction had no good reason beyond the backend work not being done. The backend now clears the call-used registers on Arm, Thumb-1 and Thumb-2, so the reason is gone and the two request paths agree: the flag and the function attribute reach the same place. A target whose backend still does not implement the clear is refused as before, which the test pins with PowerPC. This is trailofbits/vspells-ct-internal-notes#52, under the umbrella trailofbits/vspells-ct-internal-notes#30. --- clang/include/clang/Options/Options.td | 2 +- clang/lib/Driver/ToolChains/Clang.cpp | 3 ++- clang/test/Driver/arm-zero-call-used-regs.c | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 clang/test/Driver/arm-zero-call-used-regs.c diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td index b354a475346df..f8d93a35aae16 100644 --- a/clang/include/clang/Options/Options.td +++ b/clang/include/clang/Options/Options.td @@ -5121,7 +5121,7 @@ defm raw_string_literals : BoolFOption<"raw-string-literals", def fzero_call_used_regs_EQ : Joined<["-"], "fzero-call-used-regs=">, Group, Visibility<[ClangOption, CC1Option]>, - HelpText<"Clear call-used registers upon function return (AArch64/RISC-V/x86 only)">, + HelpText<"Clear call-used registers upon function return (AArch64/Arm/RISC-V/x86 only)">, Values<"skip,used-gpr-arg,used-gpr,used-arg,used,all-gpr-arg,all-gpr,all-arg,all">, NormalizedValues<["Skip", "UsedGPRArg", "UsedGPR", "UsedArg", "Used", "AllGPRArg", "AllGPR", "AllArg", "All"]>, diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp index d2e22920aa432..b616fba65ab3a 100644 --- a/clang/lib/Driver/ToolChains/Clang.cpp +++ b/clang/lib/Driver/ToolChains/Clang.cpp @@ -7034,7 +7034,8 @@ void Clang::ConstructJob(Compilation &C, const JobAction &JA, // FIXME: There's no reason for this to be restricted to some backend. // The backend code needs to be changed to include the appropriate function // calls automatically. - if (!Triple.isX86() && !Triple.isAArch64() && !Triple.isRISCV()) + if (!Triple.isX86() && !Triple.isAArch64() && !Triple.isRISCV() && + !Triple.isARM() && !Triple.isThumb()) D.Diag(diag::err_drv_unsupported_opt_for_target) << A->getAsString(Args) << TripleStr; } diff --git a/clang/test/Driver/arm-zero-call-used-regs.c b/clang/test/Driver/arm-zero-call-used-regs.c new file mode 100644 index 0000000000000..b6c45ee86e8d8 --- /dev/null +++ b/clang/test/Driver/arm-zero-call-used-regs.c @@ -0,0 +1,16 @@ +// The backend clears the call-used registers on 32-bit Arm, in each of its +// three instruction-set modes, so the driver no longer refuses the request. + +// RUN: %clang -### --target=arm-none-eabi -fzero-call-used-regs=used-gpr -S %s 2>&1 | FileCheck %s +// RUN: %clang -### --target=armeb-none-eabi -fzero-call-used-regs=all -S %s 2>&1 | FileCheck %s +// RUN: %clang -### --target=thumbv7m-none-eabi -fzero-call-used-regs=all -S %s 2>&1 | FileCheck %s +// RUN: %clang -### --target=thumbv6m-none-eabi -fzero-call-used-regs=used -S %s 2>&1 | FileCheck %s + +// CHECK-NOT: error: unsupported option +// CHECK: "-fzero-call-used-regs= + +// A target whose backend does not implement the clear is still refused, which +// is what keeps the check above from passing for the wrong reason. +// RUN: not %clang -### --target=powerpc64-unknown-linux-gnu -fzero-call-used-regs=all -S %s 2>&1 | \ +// RUN: FileCheck %s --check-prefix=REFUSED +// REFUSED: error: unsupported option '-fzero-call-used-regs=all' for target From 6df9e03e70ba08076b3fb6666e34f7dac90a6ef5 Mon Sep 17 00:00:00 2001 From: AkshayK Date: Mon, 14 Sep 2026 16:47:47 -0400 Subject: [PATCH 3/3] [ARM] Match the coordinated register-clearing hook Accept the insertion point supplied by the zeroization coordinator in ARMFrameLowering::emitZeroCallUsedRegs. This fixes the three-argument override of the four-argument base hook and ensures emission and liveness checks use the coordinated exit position. Validated by compiling ARMFrameLowering.cpp, Thumb1FrameLowering.cpp, and ARMBaseInstrInfo.cpp with the PR source headers and regenerated ARM register information. Changed-line formatting and git diff --check pass. --- llvm/lib/Target/ARM/ARMFrameLowering.cpp | 2 +- llvm/lib/Target/ARM/ARMFrameLowering.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/llvm/lib/Target/ARM/ARMFrameLowering.cpp b/llvm/lib/Target/ARM/ARMFrameLowering.cpp index bed0bbbccd72d..5a41077860ee5 100644 --- a/llvm/lib/Target/ARM/ARMFrameLowering.cpp +++ b/llvm/lib/Target/ARM/ARMFrameLowering.cpp @@ -1632,13 +1632,13 @@ static void computeLiveUnitsAt(LiveRegUnits &Used, const MachineBasicBlock &MBB, void ARMFrameLowering::emitZeroCallUsedRegs(BitVector RegsToZero, MachineBasicBlock &MBB, + MachineBasicBlock::iterator MBBI, RegScavenger *) const { MachineFunction &MF = *MBB.getParent(); const Function &F = MF.getFunction(); const ARMBaseRegisterInfo &TRI = *STI.getRegisterInfo(); const ARMBaseInstrInfo &TII = *STI.getInstrInfo(); - MachineBasicBlock::iterator MBBI = MBB.getFirstTerminator(); DebugLoc DL; if (MBBI != MBB.end()) DL = MBBI->getDebugLoc(); diff --git a/llvm/lib/Target/ARM/ARMFrameLowering.h b/llvm/lib/Target/ARM/ARMFrameLowering.h index d3e96e729322f..16950d54d8329 100644 --- a/llvm/lib/Target/ARM/ARMFrameLowering.h +++ b/llvm/lib/Target/ARM/ARMFrameLowering.h @@ -96,6 +96,7 @@ class ARMFrameLowering : public TargetFrameLowering { private: /// Emit target zero call-used regs. void emitZeroCallUsedRegs(BitVector RegsToZero, MachineBasicBlock &MBB, + MachineBasicBlock::iterator MBBI, RegScavenger *RS) const override; void emitPushInst(MachineBasicBlock &MBB, MachineBasicBlock::iterator MI,