[CodeGen] Order the clearing sequence at every in-scope exit - #9
[CodeGen] Order the clearing sequence at every in-scope exit#9claude[bot] wants to merge 1 commit into
Conversation
|
|
|
Hello @claude[bot] 👋 Thank you for submitting a Pull Request (PR) to the LLVM Project. Since this is your first PR, here are a few useful links covering our main contribution policies and review practices.
Please reply to this message to confirm that you have read these policies, especially the LLVM AI Tool Use Policy, and that any AI tool usage has been noted in the PR description. Frequently asked questionsHow do I add reviewers? This PR will be automatically labeled, and the relevant teams will be notified. For some parts of the project, reviewers may also be added automatically. You can also add reviewers manually using the Reviewers section on this page. If you cannot use that section, it is probably because you do not have write permissions for the repository. In that case, you can request a review by tagging reviewers in a comment using What if there are no comments? If you have not received any comments on your PR after a week, you can request a review by pinging the PR with a comment such as “Ping”. The common courtesy ping rate is once a week. Please remember that you are asking for volunteer time from other developers. Are any special GitHub settings required to contribute to LLVM? We only require contributors to have a public email address associated with their GitHub commits, see this section of LLVM Developer Policy for details. If you have questions, feel free to leave a comment on this PR, or ask on LLVM Discord or LLVM Discourse. Thank you, |
Prologue and epilogue insertion emits register clearing at every block whose last instruction is a return. It is one step with no defined relationship to anything else that will be emitted there, which has cost nothing so far because it is the only step there is, and stops being free with the second one. Clearing the frame cannot be done without registers to hold the address it stores through and the value it stores, so it ends leaving in registers what it has just taken out of memory, and a register clear in front of it is undone by it. Clearing the flags has to be behind everything, because a register is cleared on x86 with an exclusive-or, which writes them, and a frame clear that loops writes them too. Those are constraints between steps, and a single emission has nowhere to record them. Make the emission point a coordinator. ClearingSequence is the order: ClearStack, then ClearRegisters, then ClearFlags. The emission walks that array at each exit and dispatches each step, instead of calling the one step there is. Two of the three emit nothing yet, and they are here rather than arriving with their implementations because the order is what is being settled: a step that arrives without one goes where it is convenient, and the reason it belonged somewhere else then has to be found again. The comment above the enumeration is where the reason for each position is written down, because that, rather than the order itself, is what an implementer of the remaining steps needs. It also records what the order is over. It is over the emitted code, not over one insertion point: the steps that exist today all emit in front of the instruction control leaves through, which is after the epilogue, while clearing the frame has to happen before the epilogue moves the stack pointer and the frame stops being addressable as the frame. What each step does is decided once per function and before anything is emitted, since a sequence whose steps differed between exits would not be one sequence. A step is not requested, unsupported and reported, unimplemented, or emitting. Planning is where the two capability queries are asked and where the two fail-closed diagnostics are raised, so a function is told what will not happen for it whether or not it has an exit to emit at. Nothing in the plan or in the walk consults a value the function computes, so the sequence a protected function runs is the same on every input. Where the sequence runs is the exit classification, not a fresh walk over the blocks that end in a return. The two answers differ in one direction here. A tail call and a funclet return are both marked as returns in the instruction description, so blocks ending in TCRETURN, CLEANUPRET or CATCHRET were already reached by the walk and are reached by the classification too; nothing changes at them. A landing pad that resumes unwinding ends in a call, which is not marked as a return, so it was not reached, and a function unwinding out of a cleanup left every register it had used to the unwinder. That exit is in scope, and the sequence now runs at it. Reaching it needs two things the block-scoped emission could not express. The target used to choose where to emit, the first terminator of the block, and a landing pad that ends in a call has no terminator, so the clearing would have been appended after the call that leaves the function. Steps that choose their own positions cannot be ordered against one another either, so the position is the coordinator's and is passed to emitZeroCallUsedRegs. At an exit that leaves through a terminator it is the first terminator, which is where the three targets implementing the hook already emitted, so nothing they generate moves. The other is the register set: it is computed once for the function with the registers its returns read removed, and it does not know that the resume call reads the exception object in an argument register. Clearing that register would leave the unwinder nothing to resume with, so the registers an exit names are dropped at that exit, which at a return exit removes nothing the function-wide computation had not already removed. Computing the set per exit instead of narrowing one computed for the function is trailofbits/vspells-ct-internal-notes#21. No existing test changes, anywhere in CodeGen/X86 or CodeGen/ARM. Nothing in the tree combines "zero-call-used-regs" with an unwind path, which is the same reason the gap lasted this long, so the new site is pinned by a new test rather than by an old one starting to expect more. The order is pinned separately from what is emitted, through -pei-print-clearing-sequence, which lists the steps in sequence order with what each of them does at each in-scope exit; it is a flag rather than a debug print for the reason -pei-print-exits is, that a sequence deciding what gets destroyed has to be checkable in the configuration a shipped compiler is built in. Each new test was confirmed load-bearing by breaking the implementation once and restoring it: reordering the array, going back to the walk over return blocks, dropping the out-of-scope filter, letting the target pick the position again, dropping the per-exit narrowing, and skipping the funclet exits each failed at least one of them. The scope is the order and the sites. What the register clear emits is unchanged, the scratch registers a frame clear will need are trailofbits/vspells-ct-internal-notes#20, clearing the frame is trailofbits/vspells-ct-internal-notes#26, and what a tail call needs beyond being in scope is trailofbits/vspells-ct-internal-notes#22. This is trailofbits/vspells-ct-internal-notes#19, under the umbrella trailofbits/vspells-ct-internal-notes#17.
864df60 to
3ba9ac7
Compare
7b680fc to
5631a9c
Compare
Requested by Francesco Bertolaccini · Slack thread
Prologue and epilogue insertion emits register clearing at every block whose
last instruction is a return. It is one step with no defined relationship to
anything else that will be emitted there, which has cost nothing so far because
it is the only step there is, and stops being free with the second one.
Clearing the frame cannot be done without registers to hold the address it
stores through and the value it stores, so it ends leaving in registers what it
has just taken out of memory, and a register clear in front of it is undone by
it. Clearing the flags has to be behind everything, because a register is
cleared on x86 with an exclusive-or, which writes them, and a frame clear that
loops writes them too. Those are constraints between steps, and a single
emission has nowhere to record them.
Make the emission point a coordinator. ClearingSequence is the order:
ClearStack, then ClearRegisters, then ClearFlags. The emission walks that array
at each exit and dispatches each step, instead of calling the one step there
is. Two of the three emit nothing yet, and they are here rather than arriving
with their implementations because the order is what is being settled: a step
that arrives without one goes where it is convenient, and the reason it
belonged somewhere else then has to be found again. The comment above the
enumeration is where the reason for each position is written down, because
that, rather than the order itself, is what an implementer of the remaining
steps needs. It also records what the order is over. It is over the emitted
code, not over one insertion point: the steps that exist today all emit in
front of the instruction control leaves through, which is after the epilogue,
while clearing the frame has to happen before the epilogue moves the stack
pointer and the frame stops being addressable as the frame.
What each step does is decided once per function and before anything is
emitted, since a sequence whose steps differed between exits would not be one
sequence. A step is not requested, unsupported and reported, unimplemented, or
emitting. Planning is where the two capability queries are asked and where the
two fail-closed diagnostics are raised, so a function is told what will not
happen for it whether or not it has an exit to emit at. Nothing in the plan or
in the walk consults a value the function computes, so the sequence a protected
function runs is the same on every input.
Where the sequence runs is the exit classification, not a fresh walk over the
blocks that end in a return. The two answers differ in one direction here. A
tail call and a funclet return are both marked as returns in the instruction
description, so blocks ending in TCRETURN, CLEANUPRET or CATCHRET were already
reached by the walk and are reached by the classification too; nothing changes
at them. A landing pad that resumes unwinding ends in a call, which is not
marked as a return, so it was not reached, and a function unwinding out of a
cleanup left every register it had used to the unwinder. That exit is in scope,
and the sequence now runs at it.
Reaching it needs two things the block-scoped emission could not express. The
target used to choose where to emit, the first terminator of the block, and a
landing pad that ends in a call has no terminator, so the clearing would have
been appended after the call that leaves the function. Steps that choose their
own positions cannot be ordered against one another either, so the position is
the coordinator's and is passed to emitZeroCallUsedRegs. At an exit that leaves
through a terminator it is the first terminator, which is where the three
targets implementing the hook already emitted, so nothing they generate moves.
The other is the register set: it is computed once for the function with the
registers its returns read removed, and it does not know that the resume call
reads the exception object in an argument register. Clearing that register
would leave the unwinder nothing to resume with, so the registers an exit names
are dropped at that exit, which at a return exit removes nothing the
function-wide computation had not already removed. Computing the set per exit
instead of narrowing one computed for the function is
trailofbits/vspells-ct-internal-notes#21.
No existing test changes, anywhere in CodeGen/X86 or CodeGen/ARM. Nothing in
the tree combines "zero-call-used-regs" with an unwind path, which is the same
reason the gap lasted this long, so the new site is pinned by a new test rather
than by an old one starting to expect more. The order is pinned separately from
what is emitted, through -pei-print-clearing-sequence, which lists the steps in
sequence order with what each of them does at each in-scope exit; it is a flag
rather than a debug print for the reason -pei-print-exits is, that a sequence
deciding what gets destroyed has to be checkable in the configuration a shipped
compiler is built in. Each new test was confirmed load-bearing by breaking the
implementation once and restoring it: reordering the array, going back to the
walk over return blocks, dropping the out-of-scope filter, letting the target
pick the position again, dropping the per-exit narrowing, and skipping the
funclet exits each failed at least one of them.
The scope is the order and the sites. What the register clear emits is
unchanged, the scratch registers a frame clear will need are
trailofbits/vspells-ct-internal-notes#20, clearing the frame is
trailofbits/vspells-ct-internal-notes#26, and what a tail call needs beyond
being in scope is trailofbits/vspells-ct-internal-notes#22.
This is trailofbits/vspells-ct-internal-notes#19, under the umbrella
trailofbits/vspells-ct-internal-notes#17.
AI tool use
This pull request contains AI-generated content. It was prepared with the assistance of Claude Code; the contributor has reviewed the generated code and text, is the author of the contribution, and is accountable for it, per the LLVM AI Tool Use Policy.
Generated by Claude Code