Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions clang/test/CodeGen/arm-zero-call-used-regs.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// REQUIRES: arm-registered-target
// RUN: %clang --target=armv7-none-eabi -O2 -S %s -o - | FileCheck %s --check-prefix=ATTR
// RUN: %clang --target=thumbv6m-none-eabi -O2 -S %s -o - | FileCheck %s --check-prefix=ATTR
// RUN: %clang --target=thumbv7m-none-eabi -O2 -S %s -o - | FileCheck %s --check-prefix=ATTR
// RUN: %clang --target=armv7-none-eabi -O2 -fzero-call-used-regs=all-gpr -S %s -o - | FileCheck %s --check-prefix=FLAG
// RUN: %clang --target=thumbv6m-none-eabi -O2 -fzero-call-used-regs=all-gpr -S %s -o - | FileCheck %s --check-prefix=FLAG
// RUN: %clang --target=thumbv7m-none-eabi -O2 -fzero-call-used-regs=all-gpr -S %s -o - | FileCheck %s --check-prefix=FLAG

// Exercise code generation, not just the driver's -### output. Both request
// paths must reach the target emitter, while an explicit skip still opts out.

// ATTR-LABEL: attributed:
// ATTR: mov{{.*}} r12,
// ATTR: bx lr
__attribute__((zero_call_used_regs("all-gpr")))
int attributed(int x) { return x; }

// FLAG-LABEL: flag_enabled:
// FLAG: mov{{.*}} r12,
// FLAG: bx lr
int flag_enabled(int x) { return x; }

// FLAG-LABEL: skipped:
// FLAG-NOT: mov
// FLAG: bx lr
__attribute__((zero_call_used_regs("skip")))
int skipped(int x) { return x; }
6 changes: 6 additions & 0 deletions llvm/include/llvm/CodeGen/TargetFrameLowering.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ class LLVM_ABI TargetFrameLowering {
return false;
}

/// Whether emitZeroCallUsedRegs avoids widening a clear into an unrequested
/// sibling register. Such targets do not need PEI's legacy sibling exclusion.
virtual bool zeroCallUsedRegsPreservesUnrequestedSiblings() const {
return false;
}

/// emitZeroCallUsedRegs - Zeros out call used registers. Only called on
/// targets whose supportsZeroCallUsedRegs returns true.
///
Expand Down
18 changes: 13 additions & 5 deletions llvm/lib/CodeGen/PrologEpilogInserter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1513,7 +1513,8 @@ getClearingInsertPoint(MachineBasicBlock &MBB, MachineInstr &ExitMI) {
/// registers uncleared at every other return, dead and holding a value.
static BitVector computeRegsToClearAtExit(
const BitVector &Candidates, const MachineBasicBlock &MBB,
MachineBasicBlock::const_iterator InsertPt, const TargetRegisterInfo &TRI) {
MachineBasicBlock::const_iterator InsertPt, const TargetRegisterInfo &TRI,
const TargetFrameLowering &TFI) {
// Only the rest of the block runs after the sequence, and only because the
// block does not continue in the function; getEnforceableExit() ensures that.
assert(MBB.succ_empty() && "exit block continues in the function");
Expand All @@ -1535,7 +1536,7 @@ static BitVector computeRegsToClearAtExit(
// the same: it also spares siblings a clear would widen into a live
// register (%ah into %al on x86), and no target-agnostic rule keeps both
// that and AArch64's independently-cleared register tuples correct.
if (MI.isReturn())
if (MI.isReturn() && !TFI.zeroCallUsedRegsPreservesUnrequestedSiblings())
for (MCRegUnit Unit : TRI.regunits(Reg))
RegsToZero.reset(static_cast<unsigned>(Unit));

Expand Down Expand Up @@ -1627,9 +1628,9 @@ void PEIImpl::emitClearingStep(ClearingStep Step, const ExitClearingPlan &Plan,
case ClearingStep::ClearRegisters:
// What to clear is settled here rather than in the plan, because it is the
// exit that decides it: see computeRegsToClearAtExit.
TFI.emitZeroCallUsedRegs(
computeRegsToClearAtExit(Plan.CandidateRegsToZero, MBB, InsertPt, TRI),
MBB, InsertPt, RS);
TFI.emitZeroCallUsedRegs(computeRegsToClearAtExit(Plan.CandidateRegsToZero,
MBB, InsertPt, TRI, TFI),
MBB, InsertPt, RS);
break;

case ClearingStep::ClearFlags:
Expand Down Expand Up @@ -1783,6 +1784,13 @@ PEIImpl::planClearRegisters(MachineFunction &MF,
for (MCRegister Reg : TRI.sub_and_superregs_inclusive(CSReg))
CandidateRegsToZero.reset(Reg.id());

// Some return instructions read the return address without an explicit
// operand, and conventions such as GHC leave it out of the callee-saved
// list. It must survive clearing regardless of either representation.
if (MCRegister RAReg = TRI.getRARegister())
for (MCRegister Reg : TRI.sub_and_superregs_inclusive(RAReg))
CandidateRegsToZero.reset(Reg.id());

return ClearingDisposition::Emit;
}

Expand Down
28 changes: 27 additions & 1 deletion llvm/lib/Target/ARM/ARMFrameLowering.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,20 @@ void ARMFrameLowering::emitEpilogue(MachineFunction &MF,
// register is only used when every part of it was asked for.
//===----------------------------------------------------------------------===//

bool ARMFrameLowering::supportsZeroCallUsedRegs(
const MachineFunction &MF) const {
// VFP registers may exist on a Thumb-1 target even though Thumb-1 has no
// instructions that can clear them. Keep core-only modes available, but do
// not advertise a request that may need floating-point clearing there.
if (!STI.isThumb1Only() || !STI.hasFPRegs())
return true;

StringRef Mode =
MF.getFunction().getFnAttribute("zero-call-used-regs").getValueAsString();
return Mode == "used-gpr-arg" || Mode == "used-gpr" ||
Mode == "all-gpr-arg" || Mode == "all-gpr";
}

/// Whether \p Reg is one of the registers this step sorts into the
/// floating-point half of the work.
///
Expand Down Expand Up @@ -1676,6 +1690,16 @@ void ARMFrameLowering::emitZeroCallUsedRegs(BitVector RegsToZero,
}
const bool ClearVPR = RegsToZero.test(ARM::VPR) && STI.hasMVEIntegerOps();

// PEI excludes subregisters and superregisters of live exit operands, but a
// partially overlapping tuple can survive that exclusion. Expanding it above
// can therefore reintroduce live leaves (for example, D0_D2 overlaps a Q0
// return value). Remove those leaves before choosing the clearing widths.
LiveRegUnits LiveUnits(TRI);
computeLiveUnitsAt(LiveUnits, MBB, MBBI);
for (MCRegister Reg : FPLeaves.set_bits())
if (!LiveUnits.available(Reg))
FPLeaves.reset(Reg);

// 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) {
Expand Down Expand Up @@ -1754,7 +1778,9 @@ void ARMFrameLowering::emitZeroCallUsedRegs(BitVector RegsToZero,
LiveRegUnits Used(TRI);
computeLiveUnitsAt(Used, MBB, MBBI);
for (MCRegister Reg : ZeroSrcRC)
if (!MRI.isReserved(Reg) && Used.available(Reg)) {
// A return can read LR without naming it as a machine operand, and
// not every calling convention lists it as callee-saved.
if (Reg != ARM::LR && !MRI.isReserved(Reg) && Used.available(Reg)) {
ZeroSrc = Reg;
break;
}
Expand Down
6 changes: 6 additions & 0 deletions llvm/lib/Target/ARM/ARMFrameLowering.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ class ARMFrameLowering : public TargetFrameLowering {
const SpillSlot *
getCalleeSavedSpillSlots(unsigned &NumEntries) const override;

bool supportsZeroCallUsedRegs(const MachineFunction &MF) const override;

bool zeroCallUsedRegsPreservesUnrequestedSiblings() const override {
return true;
}

protected:
bool hasFPImpl(const MachineFunction &MF) const override;

Expand Down
30 changes: 30 additions & 0 deletions llvm/test/CodeGen/ARM/zero-call-used-regs-capabilities.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
; RUN: llc -mtriple=armv7-unknown-linux-gnueabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error:
; RUN: llc -mtriple=armebv7-unknown-linux-gnueabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error:
; RUN: llc -mtriple=thumbv6m-none-eabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error:
; RUN: llc -mtriple=thumbv8m.base-none-eabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error:
; RUN: llc -mtriple=thumbv7m-none-eabi -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error:
; RUN: llc -mtriple=thumbv8m.main-none-eabi -mattr=+fp-armv8d16sp -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error:
; RUN: llc -mtriple=thumbv7-unknown-linux-gnueabihf -mattr=+neon -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error:
; RUN: llc -mtriple=thumbv8.1m.main-none-eabi -mattr=+mve -pei-print-clearing-sequence %s -o /dev/null 2>&1 | FileCheck %s --implicit-check-not=error:

; Register clearing is supported across instruction modes and FP/vector
; configurations. Stack clearing remains an independent, unsupported capability.

; CHECK-LABEL: clearing sequence for function 'used_gpr':
; CHECK: return: clear-stack=not-requested clear-registers=emitted clear-flags=unimplemented
define i32 @used_gpr(i32 %x) "zero-call-used-regs"="used-gpr" {
ret i32 %x
}

; CHECK-LABEL: clearing sequence for function 'all':
; CHECK: return: clear-stack=not-requested clear-registers=emitted clear-flags=unimplemented
define i32 @all(i32 %x) "zero-call-used-regs"="all" {
ret i32 %x
}

; "skip" asks for nothing, so there is nothing to report.
; CHECK-LABEL: clearing sequence for function 'skip':
; CHECK: return: clear-stack=not-requested clear-registers=not-requested clear-flags=unimplemented
define i32 @skip(i32 %x) "zero-call-used-regs"="skip" {
ret i32 %x
}
50 changes: 50 additions & 0 deletions llvm/test/CodeGen/ARM/zero-call-used-regs-cleanup.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
; RUN: llc -mtriple=armv7-none-eabi -verify-machineinstrs %s -o - | FileCheck %s
; RUN: llc -mtriple=thumbv7m-none-eabi -verify-machineinstrs %s -o - | FileCheck %s
; RUN: llc -mtriple=thumbv6m-none-eabi -verify-machineinstrs %s -o - | FileCheck %s

declare void @sink()
declare void @cleanup()
declare i32 @__gxx_personality_v0(...)
declare void @_Unwind_Resume(ptr) noreturn

; Both the ordinary return and ARM EHABI's cleanup exit need clearing. The
; latter must run before __cxa_end_cleanup rather than at the end of its block.
; CHECK-LABEL: cleanup_and_return:
; CHECK: mov{{.*}} r12,
; CHECK: {{(pop|ldm)}}
; CHECK: bl cleanup
; CHECK: mov{{.*}} r12,
; CHECK-NEXT: bl __cxa_end_cleanup
define i32 @cleanup_and_return(i32 %secret) "zero-call-used-regs"="all-gpr" personality ptr @__gxx_personality_v0 {
entry:
invoke void @sink() to label %cont unwind label %lpad
cont:
ret i32 %secret
lpad:
%l = landingpad { ptr, i32 } cleanup
call void @cleanup()
resume { ptr, i32 } %l
}

; _Unwind_Resume consumes the exception object in r0. This direct call also
; tests the other cleanup routine understood by the shared coordinator.
; CHECK-LABEL: resume_call:
; CHECK-NOT: mov{{.*}} r0,
; CHECK: mov{{.*}} r12,
; CHECK-NOT: mov{{.*}} r0,
; CHECK-NEXT: bl _Unwind_Resume
define void @resume_call(ptr %exception) "zero-call-used-regs"="all-gpr" {
call void @_Unwind_Resume(ptr %exception)
unreachable
}

; LR can be absent from the callee-saved list and from the return's explicit
; operands. It is still the return address and must never be cleared.
; CHECK-LABEL: ghc_return:
; CHECK-NOT: mov{{.*}} lr,
; CHECK: mov{{.*}} r12,
; CHECK-NOT: mov{{.*}} lr,
; CHECK: bx lr
define ghccc void @ghc_return() "zero-call-used-regs"="all-gpr" {
ret void
}
61 changes: 61 additions & 0 deletions llvm/test/CodeGen/ARM/zero-call-used-regs-exit-diagnostics.mir
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# RUN: split-file %s %t
# RUN: not llc -mtriple=thumbv6m-none-eabi -run-pass=prolog-epilog -verify-machineinstrs %t/flags.mir -o /dev/null 2>&1 | FileCheck %s --check-prefix=FLAGS
# RUN: llc -mtriple=thumbv7m-none-eabi -run-pass=prolog-epilog -verify-machineinstrs %t/flags.mir -o - | FileCheck %s --check-prefix=THUMB2
# RUN: llc -mtriple=thumbv8m.base-none-eabi -run-pass=prolog-epilog -verify-machineinstrs %t/flags.mir -o - | FileCheck %s --check-prefix=BASELINE
# RUN: not llc -mtriple=thumbv6m-none-eabi -run-pass=prolog-epilog -verify-machineinstrs %t/scratch.mir -o /dev/null 2>&1 | FileCheck %s --check-prefix=SCRATCH
# RUN: not llc -mtriple=armv7-none-eabihf -run-pass=prolog-epilog -verify-machineinstrs %t/return-address.mir -o /dev/null 2>&1 | FileCheck %s --check-prefix=SCRATCH

# Classic Thumb-1 needs a flag-setting instruction to materialize zero. A
# predicated exit must retain its flags. Thumb-2 and v8-M Baseline can use a
# flag-preserving move instead.
# FLAGS: error: {{.*}}clearing the call-used registers writes the condition flags on this subtarget, and they are live at this exit
# THUMB2-LABEL: name: live_flags
# THUMB2: $r12 = t2MOVi 0, 14{{.*}}$noreg, $noreg
# THUMB2: tBX_RET 0{{.*}}$cpsr, implicit $r0
# BASELINE-LABEL: name: live_flags
# BASELINE: $r12 = t2MOVi16 0, 14{{.*}}$noreg
# BASELINE: tBX_RET 0{{.*}}$cpsr, implicit $r0

# All caller-saved low registers are live. Clearing r12 must not borrow a
# callee-saved register or overwrite a live result to obtain a low zero source.
# SCRATCH: error: {{.*}}clearing the call-used registers needs a register to hold zero and none is free at this exit

#--- flags.mir
--- |
define void @live_flags() "zero-call-used-regs"="all-gpr" { ret void }
...
---
name: live_flags
tracksRegLiveness: true
body: |
bb.0:
liveins: $r0, $cpsr
tBX_RET 0, $cpsr, implicit $r0
...

#--- scratch.mir
--- |
define void @no_scratch() "zero-call-used-regs"="all-gpr" { ret void }
...
---
name: no_scratch
tracksRegLiveness: true
body: |
bb.0:
liveins: $r0, $r1, $r2, $r3
tBX_RET 14, $noreg, implicit $r0, implicit $r1, implicit $r2, implicit $r3
...

#--- return-address.mir
--- |
define ghccc void @no_scratch_lr() "zero-call-used-regs"="used" { ret void }
...
---
name: no_scratch_lr
tracksRegLiveness: true
body: |
bb.0:
liveins: $r0, $r1, $r2, $r3, $r4, $r5, $r6, $r7, $r8, $r9, $r10, $r11, $r12
$s1 = VMOVS undef $s2, 14, $noreg
BX_RET 14, $noreg, implicit $r0, implicit $r1, implicit $r2, implicit $r3, implicit $r4, implicit $r5, implicit $r6, implicit $r7, implicit $r8, implicit $r9, implicit $r10, implicit $r11, implicit $r12
...
84 changes: 84 additions & 0 deletions llvm/test/CodeGen/ARM/zero-call-used-regs-exits.mir
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# RUN: llc -mtriple=armv7-none-eabi -run-pass=prolog-epilog -verify-machineinstrs %s -o - | FileCheck %s

# The two returns deliberately have different physical live-outs. A clear at
# one must not inherit the other exit's exclusions.
# CHECK-LABEL: name: different_liveouts
# CHECK: bb.1:
# CHECK-NOT: $r0 = MOVi
# CHECK: $r1 = MOVi 0,
# CHECK: $r2 = MOVi 0,
# CHECK-NOT: $r0 = MOVi
# CHECK: BX_RET 14{{.*}}implicit $r0
# CHECK: bb.2:
# CHECK: $r0 = MOVi 0,
# CHECK-NOT: $r2 = MOVi
# CHECK: $r3 = MOVi 0,
# CHECK-NOT: $r2 = MOVi
# CHECK: BX_RET 14{{.*}}implicit $r2

# Clearing belongs before the non-returning cleanup call, while r0 still
# carries the exception object. Nothing emitted after the call can protect it.
# CHECK-LABEL: name: cleanup_exit
# CHECK-NOT: $r0 = MOVi
# CHECK: $r1 = MOVi 0,
# CHECK-NEXT: $r2 = MOVi 0,
# CHECK-NEXT: $r3 = MOVi 0,
# CHECK-NEXT: $r12 = MOVi 0,
# CHECK-NEXT: BL @_Unwind_Resume, {{.*}}implicit $r0
# CHECK-NOT: MOVi
# CHECK: ...

# r4 is modified, saved and restored. Register clearing must preserve the
# restored value as well as the return address and result.
# CHECK-LABEL: name: callee_saved
# CHECK: frame-setup
# CHECK: $r4 = MOVr $r0,
# CHECK: $r1 = MOVi 0,
# CHECK-NOT: $r4 = MOVi
# CHECK-NOT: $lr = MOVi
# CHECK: frame-destroy LDMIA_RET {{.*}}def $r4, def $pc, implicit $r0

--- |
declare void @_Unwind_Resume(ptr)
define void @different_liveouts() "zero-call-used-regs"="all-gpr" { ret void }
define void @cleanup_exit(ptr %exception) "zero-call-used-regs"="all-gpr" {
call void @_Unwind_Resume(ptr %exception)
unreachable
}
define void @callee_saved() "zero-call-used-regs"="all-gpr" { ret void }
...
---
name: different_liveouts
tracksRegLiveness: true
body: |
bb.0:
successors: %bb.1, %bb.2
liveins: $r0, $r2, $r3
CMPri $r3, 0, 14, $noreg, implicit-def $cpsr
Bcc %bb.2, 0, killed $cpsr
bb.1:
liveins: $r0
BX_RET 14, $noreg, implicit $r0
bb.2:
liveins: $r2
BX_RET 14, $noreg, implicit $r2
...
---
name: cleanup_exit
tracksRegLiveness: true
frameInfo:
hasCalls: true
body: |
bb.0:
liveins: $r0
BL @_Unwind_Resume, csr_aapcs, implicit-def $lr, implicit $sp, implicit $r0
...
---
name: callee_saved
tracksRegLiveness: true
body: |
bb.0:
liveins: $r0
$r4 = MOVr $r0, 14, $noreg, $noreg
BX_RET 14, $noreg, implicit $r0
...
Loading