arm64: implement support for static call trampolines
Subsystems (2)
Ard Biesheuvel
reviewerPATCH v2 1/1 arm64: implement support for static call trampolines
Implement arm64 support for the 'unoptimized' static call variety, which routes all calls through a single trampoline that is patched to perform a tail call to the selected function.
Since static call targets may be located in modules loaded out of direct branching range, we need to be able to fall back to issuing a ADRP/ADD pair to load the branch target into R16 and use a BR instruction. As this involves patching more than a single B or NOP instruction (for which the architecture makes special provisions in terms of the synchronization needed), we may need to run the full blown instruction patching logic that uses stop_machine(). It also means that once we've patched in a ADRP/ADD pair once, we are quite restricted in the patching we can code subsequently, and we may end up using an indirect call after all (note that
---
v2:
This turned nasty really quickly when I realized that any sleeping task could have been interrupted right in the middle of the ADRP/ADD pair that we emit for static call targets that are out of immediate branching range.
arch/arm64/Kconfig | 1 + arch/arm64/include/asm/static_call.h | 32 +++++ arch/arm64/kernel/Makefile | 2 +- arch/arm64/kernel/static_call.c | 129 ++++++++++++++++++++ arch/arm64/kernel/vmlinux.lds.S | 1 + 5 files changed, 164 insertions(+), 1 deletion(-)
arch/arm64/Kconfig +1 -0
@@ -173,6 +173,7 @@ config ARM64 select HAVE_PERF_REGS select HAVE_PERF_USER_STACK_DUMP select HAVE_REGS_AND_STACK_ACCESS_API + select HAVE_STATIC_CALL select HAVE_FUNCTION_ARG_ACCESS_API select HAVE_FUTEX_CMPXCHG if FUTEX select MMU_GATHER_RCU_TABLE_FREE
arch/arm64/include/asm/static_call.h new file +32 -0
@@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _ASM_STATIC_CALL_H +#define _ASM_STATIC_CALL_H + +/* +* We have to account for the possibility that the static call site may +* be updated to refer to a target that is out of range for an ordinary +* 'B' branch instruction, and so we need to pre-allocate some space for +* a ADRP/ADD/BR sequence. +*/ +#define __ARCH_DEFINE_STATIC_CALL_TRAMP(name, insn) \ + asm(".pushsection .static_call.text, \"ax\" \n" \ + ".align 5 \n" \ + ".globl " STATIC_CALL_TRAMP_STR(name) " \n" \ + STATIC_CALL_TRAMP_STR(name) ": \n" \ + "hint 34 /* BTI C */ \n" \ + insn " \n" \ + "ret \n" \ + "nop \n" \ + "nop \n" \ + "adrp x16, " STATIC_CALL_KEY(name) " \n" \ + "ldr x16, [x16, :lo12:" STATIC_CALL_KEY(name) "] \n" \ + "br x16 \n" \ + ".popsection \n") + +#define ARCH_DEFINE_STATIC_CALL_TRAMP (name, func) \ + __ARCH_DEFINE_STATIC_CALL_TRAMP(name, "b " #func) + +#define ARCH_DEFINE_STATIC_CALL_NULL_TRAMP (name) \ + __ARCH_DEFINE_STATIC_CALL_TRAMP(name, "nop") + +#endif /* _ASM_STATIC_CALL_H */
arch/arm64/kernel/Makefile +1 -1
@@ -17,7 +17,7 @@ obj-y := debug-monitors.o entry.o irq.o fpsimd.o \ return_address.o cpuinfo.o cpu_errata.o \ cpufeature.o alternative.o cacheinfo.o \ smp.o smp_spin_table.o topology.o smccc-call.o \ - syscall.o proton-pack.o + syscall.o proton-pack.o static_call.o targets += efi-entry.o
arch/arm64/kernel/static_call.c new file +129 -0
129 lines not on this page · raw message
arch/arm64/kernel/vmlinux.lds.S +1 -0
@@ -135,6 +135,7 @@ SECTIONS IDMAP_TEXT HIBERNATE_TEXT TRAMP_TEXT + STATIC_CALL_TEXT *(.fixup) *(.gnu.warning) . = ALIGN(16);
Signature · 7 lines
2.17.1 _______________________________________________ linux-arm-kernel mailing list [email protected] http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Peter Zijlstra
maintainerQuoted 4 lines · Ard Biesheuvel
This turned nasty really quickly when I realized that any sleeping task could have been interrupted right in the middle of the ADRP/ADD pair that we emit for static call targets that are out of immediate branching range.
Ha! Yes, that's a 'problem' allright. On x86 we normally only patch a single instruction because of this, however we have some kprobe code that has this issue and there we use tasks_rcu when PREEMPT=y.
Now, let me try and read this patch properly..
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Peter Zijlstra
maintainerQuoted 41 lines · Ard Biesheuvel
+/*
+ * The static call trampoline consists of one of the following sequences:
+ *
+ * (A) (B) (C) (D) (E)+ * 00: BTI C BTI C BTI C BTI C BTI C + * 04: B fn NOP NOP NOP NOP + * 08: RET RET ADRP X16, fn ADRP X16, fn ADRP X16, fn + * 0c: NOP NOP ADD X16, fn ADD X16, fn ADD X16, fn + * 10: BR X16 RET NOP + * 14: ADRP X16, &fn + * 18: LDR X16, [X16, &fn] + * 1c: BR X16+ *
+ * The architecture permits us to patch B instructions into NOPs or vice versa
+ * directly, but patching any other instruction sequence requires careful
+ * synchronization. Since branch targets may be out of range for ordinary
+ * immediate branch instructions, we may have to fall back to ADRP/ADD/BR
+ * sequences in some cases, which complicates things considerably; since any
+ * sleeping tasks may have been preempted right in the middle of any of these
+ * sequences, we have to carefully transform one into the other, and ensure
+ * that it is safe to resume execution at any point in the sequence for tasks
+ * that have already executed part of it.
+ *
+ * So the rules are:
+ * - we start out with (A) or (B)
+ * - a branch within immediate range can always be patched in at offset0x4;
+ * - sequence (A) can be turned into (B) for NULL branch targets;
+ * - a branch outside of immediate range can be patched using (C), but only if
+ * . the sequence being updated is (A) or (B), or
+ * . the branch target address modulo 4k results in the same ADD opcode
+ * (which could occur when patching the same far target a second time)
+ * - once we have patched in (C) we cannot go back to (A) or (B), so patching
+ * in a NULL target now requires sequence (D);
+ * - if we cannot patch a far target using (C), we fall back to sequence (E),
+ * which loads the function pointer from memory.
+ *
+ * If we abide by these rules, then the following must hold for tasks that were
+ * interrupted halfway through execution of the trampoline:
+ * - when resuming at offset0x8, we can only encounter a RET if (B) or (D)
+ * was patched in at any point, and therefore a NULL target is valid;
+ * - when resuming at offset0xc, we are executing the ADD opcode that is only
Quoted 8 lines
+ * reachable via the preceding ADRP, and which is patched in only a single
+ * time, and is therefore guaranteed to be consistent with the ADRP target;
+ * - when resuming at offset0x10, X16 must refer to a valid target, since it
+ * is only reachable via a ADRP/ADD pair that is guaranteed to be consistent.
+ *
+ * Note that sequence (E) is only used when switching between multiple far
+ * targets, and that it is not a terminal degraded state.
+ */
Would it make things easier if your trampoline consisted of two complete slots, between which you can flip?
Something like:
0x00 B 0x24 / NOP 0x04 < slot 1 > .... 0x20 0x24 < slot 2 > .... 0x40
Then each (20 byte) slot can contain any of the variants above and you can write the unused slot without stop-machine. Then, when the unused slot is populated, flip the initial instruction (like a static-branch), issue synchronize_rcu_tasks() and flip to using the other slot for next time.
Alternatively, you can patch the call-sites to point to the alternative trampoline slot, but that might be pushing things a bit.
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Ard Biesheuvel
reviewerQuoted 65 lines · Peter Zijlstra
Quoted 41 lines · Ard Biesheuvel
+/*
+ * The static call trampoline consists of one of the following sequences:
+ *
+ * (A) (B) (C) (D) (E)+ * 00: BTI C BTI C BTI C BTI C BTI C + * 04: B fn NOP NOP NOP NOP + * 08: RET RET ADRP X16, fn ADRP X16, fn ADRP X16, fn + * 0c: NOP NOP ADD X16, fn ADD X16, fn ADD X16, fn + * 10: BR X16 RET NOP + * 14: ADRP X16, &fn + * 18: LDR X16, [X16, &fn] + * 1c: BR X16+ *
+ * The architecture permits us to patch B instructions into NOPs or vice versa
+ * directly, but patching any other instruction sequence requires careful
+ * synchronization. Since branch targets may be out of range for ordinary
+ * immediate branch instructions, we may have to fall back to ADRP/ADD/BR
+ * sequences in some cases, which complicates things considerably; since any
+ * sleeping tasks may have been preempted right in the middle of any of these
+ * sequences, we have to carefully transform one into the other, and ensure
+ * that it is safe to resume execution at any point in the sequence for tasks
+ * that have already executed part of it.
+ *
+ * So the rules are:
+ * - we start out with (A) or (B)
+ * - a branch within immediate range can always be patched in at offset0x4;
+ * - sequence (A) can be turned into (B) for NULL branch targets;
+ * - a branch outside of immediate range can be patched using (C), but only if
+ * . the sequence being updated is (A) or (B), or
+ * . the branch target address modulo 4k results in the same ADD opcode
+ * (which could occur when patching the same far target a second time)
+ * - once we have patched in (C) we cannot go back to (A) or (B), so patching
+ * in a NULL target now requires sequence (D);
+ * - if we cannot patch a far target using (C), we fall back to sequence (E),
+ * which loads the function pointer from memory.
+ *
+ * If we abide by these rules, then the following must hold for tasks that were
+ * interrupted halfway through execution of the trampoline:
+ * - when resuming at offset0x8, we can only encounter a RET if (B) or (D)
+ * was patched in at any point, and therefore a NULL target is valid;
+ * - when resuming at offset0xc, we are executing the ADD opcode that is onlyQuoted 8 lines
+ * reachable via the preceding ADRP, and which is patched in only a single
+ * time, and is therefore guaranteed to be consistent with the ADRP target;
+ * - when resuming at offset0x10, X16 must refer to a valid target, since it
+ * is only reachable via a ADRP/ADD pair that is guaranteed to be consistent.
+ *
+ * Note that sequence (E) is only used when switching between multiple far
+ * targets, and that it is not a terminal degraded state.
+ */Would it make things easier if your trampoline consisted of two complete slots, between which you can flip?
Something like:
0x00 B 0x24 / NOP 0x04 < slot 1 > .... 0x20 0x24 < slot 2 > .... 0x40
Quoted 6 lines
Then each (20 byte) slot can contain any of the variants above and you can write the unused slot without stop-machine. Then, when the unused slot is populated, flip the initial instruction (like a static-branch), issue
synchronize_rcu_tasks()and flip to using the other slot for next time.
Once we've populated a slot and activated it, we have to assume that it is live and we can no longer modify it freely.
Quoted 2 lines
Alternatively, you can patch the call-sites to point to the alternative trampoline slot, but that might be pushing things a bit.
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Peter Zijlstra
maintainerQuoted 22 lines · Ard Biesheuvel
Quoted 13 lines · Peter Zijlstra
Would it make things easier if your trampoline consisted of two complete slots, between which you can flip?
Something like:
0x00 B 0x24 / NOP 0x04 < slot 1 > .... 0x20 0x24 < slot 2 > .... 0x40Quoted 6 lines
Then each (20 byte) slot can contain any of the variants above and you can write the unused slot without stop-machine. Then, when the unused slot is populated, flip the initial instruction (like a static-branch), issue
synchronize_rcu_tasks()and flip to using the other slot for next time.
Quoted 2 lines
Once we've populated a slot and activated it, we have to assume that it is live and we can no longer modify it freely.
Urhm how so? Once you pass through synchronize_rcu_tasks() nobody should still be using the old one.
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Ard Biesheuvel
reviewerQuoted 24 lines · Peter Zijlstra
Quoted 22 lines · Ard Biesheuvel
Quoted 19 lines · Peter Zijlstra
Would it make things easier if your trampoline consisted of two complete slots, between which you can flip?
Something like:
0x00 B 0x24 / NOP 0x04 < slot 1 > .... 0x20 0x24 < slot 2 > .... 0x40Then each (20 byte) slot can contain any of the variants above and you can write the unused slot without stop-machine. Then, when the unused slot is populated, flip the initial instruction (like a static-branch), issue
synchronize_rcu_tasks()and flip to using the other slot for next time.
Quoted 5 lines
Quoted 2 lines
Once we've populated a slot and activated it, we have to assume that it is live and we can no longer modify it freely.
Urhm how so? Once you pass through
synchronize_rcu_tasks()nobody should still be using the old one.
But doesn't that require some RCU foo in the static call wrapper itself?
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Peter Zijlstra
maintainerQuoted 25 lines · Ard Biesheuvel
Quoted 24 lines · Peter Zijlstra
Quoted 22 lines · Ard Biesheuvel
Quoted 19 lines · Peter Zijlstra
Would it make things easier if your trampoline consisted of two complete slots, between which you can flip?
Something like:
0x00 B 0x24 / NOP 0x04 < slot 1 > .... 0x20 0x24 < slot 2 > .... 0x40Then each (20 byte) slot can contain any of the variants above and you can write the unused slot without stop-machine. Then, when the unused slot is populated, flip the initial instruction (like a static-branch), issue
synchronize_rcu_tasks()and flip to using the other slot for next time.
Quoted 7 lines
Quoted 5 lines
Quoted 2 lines
Once we've populated a slot and activated it, we have to assume that it is live and we can no longer modify it freely.
Urhm how so? Once you pass through
synchronize_rcu_tasks()nobody should still be using the old one.But doesn't that require some RCU foo in the static call wrapper itself?
Nope, synchronize_rcu_tasks() ensures that every task will pass a safe point (aka. voluntary schedule()) before returning. This then guarantees that, no matter where they were preempted, they'll have left (and will observe the new state).
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Ard Biesheuvel
reviewerQuoted 33 lines · Peter Zijlstra
Quoted 31 lines · Ard Biesheuvel
Quoted 29 lines · Peter Zijlstra
Quoted 22 lines · Ard Biesheuvel
Quoted 13 lines · Peter Zijlstra
Would it make things easier if your trampoline consisted of two complete slots, between which you can flip?
Something like:
0x00 B 0x24 / NOP 0x04 < slot 1 > .... 0x20 0x24 < slot 2 > .... 0x40Quoted 6 lines
Then each (20 byte) slot can contain any of the variants above and you can write the unused slot without stop-machine. Then, when the unused slot is populated, flip the initial instruction (like a static-branch), issue
synchronize_rcu_tasks()and flip to using the other slot for next time.Quoted 2 lines
Once we've populated a slot and activated it, we have to assume that it is live and we can no longer modify it freely.
Urhm how so? Once you pass through
synchronize_rcu_tasks()nobody should still be using the old one.
Quoted 7 lines
Quoted 1 line
But doesn't that require some RCU foo in the static call wrapper itself?
Nope,
synchronize_rcu_tasks()ensures that every task will pass a safe point (aka. voluntaryschedule()) before returning. This then guarantees that, no matter where they were preempted, they'll have left (and will observe the new state).
Ah, wonderful. Yes, that does simplify things substantially.
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Hi Ard,
Quoted 20 lines · Ard Biesheuvel
arch/arm64/include/asm/static_call.h new file +14
-0@@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _ASM_STATIC_CALL_H +#define _ASM_STATIC_CALL_H + +/* +* We have to account for the possibility that the static call site may +* be updated to refer to a target that is out of range for an ordinary +* 'B' branch instruction, and so we need to pre-allocate some space for +* a ADRP/ADD/BR sequence. +*/ +#define __ARCH_DEFINE_STATIC_CALL_TRAMP (name, insn) \ + asm(".pushsection .static_call.text, \"ax\" \n" \ + ".align 5 \n" \ + ".globl " STATIC_CALL_TRAMP_STR(name) " \n" \
arch/arm64/include/asm/static_call.h line 24
Quoted 10 lines
arch/arm64/include/asm/static_call.h new file +10
-0+ STATIC_CALL_TRAMP_STR(name) ": \n" \ +"hint 34 /* BTI C */ \n" \ + insn" \n" \ +"ret \n" \ +"nop \n" \ +"nop \n" \ + "adrp x16, " STATIC_CALL_KEY(name) " \n" \ + "ldr x16, [x16, :lo12:" STATIC_CALL_KEY(name) "] \n" \ +"br x16 \n" \ + ".popsection \n")
Still trying to understand all this in details, so bear with me, but is there any way this could be turned into the 'inline' static call variant?
That is, could we have a piece of inline assembly at all static_call locations that would do a branch-link, with basically x0-x18 and x29,x30 in the clobber list (+ some have some magic to place the parameters in the right register, like we do for SMCCC calls for instance). That'd save a branch to the trampoline.
Ideally we'd need a way to tell the compile 'this inline assembly code does a function call', but I'm not aware of any easy way to do that (other that marking register clobbered + probably other things that I'm missing).
In any case, I was looking forward to an arm64 static call port so thanks for putting that together.
Quentin
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Ard Biesheuvel
reviewerQuoted 45 lines · Quentin Perret
Hi Ard,
Quoted 20 lines · Ard Biesheuvel
arch/arm64/include/asm/static_call.h new file +14
-0@@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _ASM_STATIC_CALL_H +#define _ASM_STATIC_CALL_H + +/* +* We have to account for the possibility that the static call site may +* be updated to refer to a target that is out of range for an ordinary +* 'B' branch instruction, and so we need to pre-allocate some space for +* a ADRP/ADD/BR sequence. +*/ +#define __ARCH_DEFINE_STATIC_CALL_TRAMP (name, insn) \ + asm(".pushsection .static_call.text, \"ax\" \n" \ + ".align 5 \n" \ + ".globl " STATIC_CALL_TRAMP_STR(name) " \n" \arch/arm64/include/asm/static_call.h line 24
Quoted 10 lines
arch/arm64/include/asm/static_call.h new file +10
-0+ STATIC_CALL_TRAMP_STR(name) ": \n" \ +"hint 34 /* BTI C */ \n" \ + insn" \n" \ +"ret \n" \ +"nop \n" \ +"nop \n" \ + "adrp x16, " STATIC_CALL_KEY(name) " \n" \ + "ldr x16, [x16, :lo12:" STATIC_CALL_KEY(name) "] \n" \ +"br x16 \n" \ + ".popsection \n")Still trying to understand all this in details, so bear with me, but is there any way this could be turned into the 'inline' static call variant?
That is, could we have a piece of inline assembly at all
static_calllocations that would do a branch-link, with basically x0-x18 and x29,x30 in the clobber list (+ some have some magic to place the parameters in the right register, like we do for SMCCC calls for instance). That'd save a branch to the trampoline.
Quoted 8 lines
Ideally we'd need a way to tell the compile 'this inline assembly code does a function call', but I'm not aware of any easy way to do that (other that marking register clobbered + probably other things that I'm missing).
In any case, I was looking forward to an arm64 static call port so thanks for putting that together.
Hello Quentin,
We'll need tooling along the lines of the GCC plugin I wrote [0] to implement inline static calls - doing function calls from inline assembly is too messy, too fragile and too error prone to rely on.
However, as I discussed with Will offline yesterday as well, the question that got snowed under is whether we need any of this on arm64 in the first place. It seems highly unlikely that inline static calls are worth it, and even out-of-line static calls are probably not worth the hassle as we don't have the retpoline problem.
So this code should be considered an invitation for discussion, and perhaps someone can invent a use case where benchmarks can show a worthwhile improvement. But let's not get ahead of ourselves.
[0] https://git.kernel.org/pub/scm/linux/kernel/git/ardb/linux.git/log/?h=static-calls
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Peter Zijlstra
maintainerQuoted 9 lines · Ard Biesheuvel
However, as I discussed with Will offline yesterday as well, the question that got snowed under is whether we need any of this on arm64 in the first place. It seems highly unlikely that inline static calls are worth it, and even out-of-line static calls are probably not worth the hassle as we don't have the retpoline problem.
So this code should be considered an invitation for discussion, and perhaps someone can invent a use case where benchmarks can show a worthwhile improvement. But let's not get ahead of ourselves.
So the obvious benefit is not having to do the extra load. Any indirect call will have to do a load first, which can miss etc.. And yes, retpoline is a horrible mess as well.
Doing the direct vs indirect saves one I$ miss I suppose, which can be noticable.
IIRC Steve had benchmarks for the ftrace conversion, which is now upstream, so that should be simple enough to run.
Steve, remember how to get numbers out of that?
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Steven Rostedt
reviewerQuoted 4 lines · On Thu, 29 Oct 2020 12:44:57 +0100 Peter Zijlstra <[email protected]> wrote:
IIRC Steve had benchmarks for the ftrace conversion, which is now upstream, so that should be simple enough to run.
Steve, remember how to get numbers out of that?
IIRC, all I did was, before and after the patches run:
# trace-cmd start -e all
(which is equivalent to "echo 1 > /sys/kernel/tracing/events/enable")
and do:
# perf stat -r 10 -a ./hackbench 50
Basically running perf stat on hackbench with all events being traced. As the tracing does indirect jumps without Peter's patches and direct jumps with the static calls, it gave me a good idea on how much it changed, as hackbench triggers a lot of trace events when tracing is enabled.
Note, the number of events can change depending on the config. There's a a few config options that could really stress it (like enabling preempt enabled/disable tracepoints).
-- Steve
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Quoted 3 lines · Ard Biesheuvel
We'll need tooling along the lines of the GCC plugin I wrote [0] to implement inline static calls - doing function calls from inline assembly is too messy, too fragile and too error prone to rely on.
Right, and that is the gut feeling I had too, but I can't quite put my finger on what exactly can go wrong. Any pointers?
Quoted 9 lines
However, as I discussed with Will offline yesterday as well, the question that got snowed under is whether we need any of this on arm64 in the first place. It seems highly unlikely that inline static calls are worth it, and even out-of-line static calls are probably not worth the hassle as we don't have the retpoline problem.
So this code should be considered an invitation for discussion, and perhaps someone can invent a use case where benchmarks can show a worthwhile improvement. But let's not get ahead of ourselves.
The reason I'm interested in this is because Android makes heavy use of trace points/hooks, so any potential improvement in this area would be welcome. Now I agree we need numbers to show the benefit is real before this can be considered for inclusion in the kernel. I'll try and see if we can get something.
Thanks,
Quentin
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Ard Biesheuvel
reviewerQuoted 9 lines · Quentin Perret
Quoted 3 lines · Ard Biesheuvel
We'll need tooling along the lines of the GCC plugin I wrote [0] to implement inline static calls - doing function calls from inline assembly is too messy, too fragile and too error prone to rely on.
Right, and that is the gut feeling I had too, but I can't quite put my finger on what exactly can go wrong. Any pointers?
The main problem is that the compiler is not aware that a function call is being made. For instance, this code
int fred(int l)
{
asm("bl barney"
:"+r"(l)
::"x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9",
"x10", "x11", "x12", "x13", "x14", "x15", "x16", "x17", "x30", "cc");
return l + 1;
}
works with GCC, but only if I omit the x29 clobber, or it gives an error. With that omitted, Clang produces
.type fred,@function
fred: // @fred
// %bb.0:
str x30, [sp, #-16]! // 8-byte Folded Spill
//APP
bl barney
//NO_APP
add w0, w0, #1 // =1
ldr x30, [sp], #16 // 8-byte Folded Reload
ret
i.e., it does not set up the stack frame correctly, and so we lose the ability to unwind the stack through this call.
There may be some other corner cases, although I agree it makes sense to get to the bottom of this.
Quoted 10 lines
Quoted 9 lines
However, as I discussed with Will offline yesterday as well, the question that got snowed under is whether we need any of this on arm64 in the first place. It seems highly unlikely that inline static calls are worth it, and even out-of-line static calls are probably not worth the hassle as we don't have the retpoline problem.
So this code should be considered an invitation for discussion, and perhaps someone can invent a use case where benchmarks can show a worthwhile improvement. But let's not get ahead of ourselves.
Quoted 8 lines
The reason I'm interested in this is because Android makes heavy use of trace points/hooks, so any potential improvement in this area would be welcome. Now I agree we need numbers to show the benefit is real before this can be considered for inclusion in the kernel. I'll try and see if we can get something.
Thanks,
Quentin
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Quoted 5 lines · Quentin Perret
The reason I'm interested in this is because Android makes heavy use of trace points/hooks, so any potential improvement in this area would be welcome. Now I agree we need numbers to show the benefit is real before this can be considered for inclusion in the kernel. I'll try and see if we can get something.
Following up on this as we've just figured out what was causing performance issues in our use-case. Basically, we have a setup where some modules attach to trace hooks for a few things (e.g. the pelt scheduler hooks + other Android-specific hooks), and that appeared to cause up ~6% perf regression on the Androbench benchmark.
The bulk of the regression came from a feature that is currently Android-specific but should hopefully make it upstream (soon?): Control Flow Integrity (CFI) -- see [1] for more details. In essence CFI is a software-based cousin of BTI, which is basically about ensuring the target of an indirect function call has a compatible prototype. This can be relatively easily checked for potential targets that are known at compile-time, but is a little harder when the targets are dynamically loaded, hence causing extra overhead when the target is in a module.
Anyway, I don't think any of the above is particularly relevant to upstream just yet, but I figured this would interesting to share. The short-term fix for Android was to locally disable CFI checks around the trace hooks that cause the perf regression, but I think static-calls would be a preferable alternative to that (I'll try to confirm that experimentally). And when/if CFI makes it upstream, then that may become relevant to upstream as well, though the integration of CFI and static-calls is not very clear yet.
Thanks,
Quentin
[1] https://www.youtube.com/watch?v=0Bj6W7qrOOI
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Ard Biesheuvel
reviewerQuoted 23 lines · Quentin Perret
Quoted 5 lines · Quentin Perret
The reason I'm interested in this is because Android makes heavy use of trace points/hooks, so any potential improvement in this area would be welcome. Now I agree we need numbers to show the benefit is real before this can be considered for inclusion in the kernel. I'll try and see if we can get something.
Following up on this as we've just figured out what was causing performance issues in our use-case. Basically, we have a setup where some modules attach to trace hooks for a few things (e.g. the pelt scheduler hooks + other Android-specific hooks), and that appeared to cause up ~6% perf regression on the Androbench benchmark.
The bulk of the regression came from a feature that is currently Android-specific but should hopefully make it upstream (soon?): Control Flow Integrity (CFI) -- see [1] for more details. In essence CFI is a software-based cousin of BTI, which is basically about ensuring the target of an indirect function call has a compatible prototype. This can be relatively easily checked for potential targets that are known at compile-time, but is a little harder when the targets are dynamically loaded, hence causing extra overhead when the target is in a module.
Quoted 9 lines
Anyway, I don't think any of the above is particularly relevant to upstream just yet, but I figured this would interesting to share. The short-term fix for Android was to locally disable CFI checks around the trace hooks that cause the perf regression, but I think static-calls would be a preferable alternative to that (I'll try to confirm that experimentally). And when/if CFI makes it upstream, then that may become relevant to upstream as well, though the integration of CFI and static-calls is not very clear yet.
OK, so that would suggest that having at least the out-of-line trampoline would help with CFI, but only because the indirect call is decorated with CFI checks, not because the indirect call itself is any slower.
So that suggests that something like
bti c ldr x16, 0f br x16 0:.quad <target>
may well be sufficient in the arm64 case - it is hidden from the assembler, so we don't get the CFI overhead, and since it is emitted as .text (and therefore requires code patching to be updated), it does not need the same level of protection that CFI offers elsewhere when it comes to indirect calls.
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Quoted 4 lines · Ard Biesheuvel
OK, so that would suggest that having at least the out-of-line trampoline would help with CFI, but only because the indirect call is decorated with CFI checks, not because the indirect call itself is any slower.
Right. By disabling CFI checks in Android we get something that is more comparable to the inline static-call implementation as we get a 'raw' indirect call. But yes, it's very likely that even an out-of-line static call is going be much faster than a CFI-enabled indirect call, so definitely worth a try.
Quoted 7 lines
So that suggests that something like
bti c ldr x16, 0f br x16 0:.quad <target>
Quoted 5 lines
may well be sufficient in the arm64 case - it is hidden from the assembler, so we don't get the CFI overhead, and since it is emitted as .text (and therefore requires code patching to be updated), it does not need the same level of protection that CFI offers elsewhere when it comes to indirect calls.
Agreed. I'm thinking the static-call infrastructure itself could perhaps do the CFI target validation before actually patching the text. But I suppose we probably have bigger problems if we can't trust whoever initiated the static-call patching, so ...
Thanks,
Quentin
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Mark Rutland
reviewerHi Ard,
Quoted 3 lines · Ard Biesheuvel
Implement arm64 support for the 'unoptimized' static call variety, which routes all calls through a single trampoline that is patched to perform a tail call to the selected function.
Given the complexity and subtlety here, do we actually need this?
If there's some core feature that depends on this (or wants to), it'd be nice to call that out in the commit message.
Quoted 10 lines
Since static call targets may be located in modules loaded out of direct branching range, we need to be able to fall back to issuing a ADRP/ADD pair to load the branch target into R16 and use a BR instruction. As this involves patching more than a single B or NOP instruction (for which the architecture makes special provisions in terms of the synchronization needed), we may need to run the full blown instruction patching logic that uses
stop_machine(). It also means that once we've patched in a ADRP/ADD pair once, we are quite restricted in the patching we can code subsequently, and we may end up using an indirect call after all (note that
Noted. I guess we
[...]
Quoted 10 lines
v2:
This turned nasty really quickly when I realized that any sleeping task could have been interrupted right in the middle of the ADRP/ADD pair that we emit for static call targets that are out of immediate branching range.+/*
+ * The static call trampoline consists of one of the following sequences:
+ *
+ * (A) (B) (C) (D) (E)
Quoted 8 lines
+ * 00: BTI C BTI C BTI C BTI C BTI C + * 04: B fn NOP NOP NOP NOP + * 08: RET RET ADRP X16, fn ADRP X16, fn ADRP X16, fn + * 0c: NOP NOP ADD X16, fn ADD X16, fn ADD X16, fn + * 10: BR X16 RET NOP + * 14: ADRP X16, &fn + * 18: LDR X16, [X16, &fn] + * 1c: BR X16
Are these all padded with trailing NOPs or UDFs? I assume the same space is statically reserved.
Quoted 15 lines
+ *
+ * The architecture permits us to patch B instructions into NOPs or vice versa
+ * directly, but patching any other instruction sequence requires careful
+ * synchronization. Since branch targets may be out of range for ordinary
+ * immediate branch instructions, we may have to fall back to ADRP/ADD/BR
+ * sequences in some cases, which complicates things considerably; since any
+ * sleeping tasks may have been preempted right in the middle of any of these
+ * sequences, we have to carefully transform one into the other, and ensure
+ * that it is safe to resume execution at any point in the sequence for tasks
+ * that have already executed part of it.
+ *
+ * So the rules are:
+ * - we start out with (A) or (B)
+ * - a branch within immediate range can always be patched in at offset0x4;
+ * - sequence (A) can be turned into (B) for NULL branch targets;
Quoted 8 lines
+ * - a branch outside of immediate range can be patched using (C), but only if
+ * . the sequence being updated is (A) or (B), or
+ * . the branch target address modulo 4k results in the same ADD opcode
+ * (which could occur when patching the same far target a second time)
+ * - once we have patched in (C) we cannot go back to (A) or (B), so patching
+ * in a NULL target now requires sequence (D);
+ * - if we cannot patch a far target using (C), we fall back to sequence (E),
+ * which loads the function pointer from memory.
Cases C-E all use an indirect branch, which goes against one of the arguments for having static calls (the assumption that CPUs won't mis-predict direct branches). Similarly case E is a literal pool with more steps.
That means that for us, static calls would only be an opportunistic optimization rather than a hardening feature. Do they actually save us much, or could we get by with an inline literal pool in the trampoline?
It'd be much easier to reason consistently if the trampoline were always:
| BTI C | LDR X16, _literal // pc-relative load | BR X16 | _literal: | < patch a 64-bit value here atomically >
... and I would strongly prefer that to having multiple sequences that could all be live -- I'm really not keen on the complexity and subtlety that entails.
[...]
Quoted 2 lines
+ * Note that sequence (E) is only used when switching between multiple far
+ * targets, and that it is not a terminal degraded state.
I think what you're saying here is that you can go from (E) to (C) if switching to a new far branch that's in ADRP+ADD range, but it can be misread to mean (E) is a transient step while patching a branch rather than some branches only being possible to encode with (E).
Thanks,
Mark.
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Peter Zijlstra
maintainerQuoted 8 lines · Mark Rutland
Hi Ard,
Quoted 3 lines · Ard Biesheuvel
Implement arm64 support for the 'unoptimized' static call variety, which routes all calls through a single trampoline that is patched to perform a tail call to the selected function.
Given the complexity and subtlety here, do we actually need this?
Only if you can get a performance win. The obvious benefit is loosing the load that's inherent in indirect function calls. The down-side of the indirect static-call implementation is that it will incur an extra I$ miss.
So it might be a wash, loose a data load miss, gain an I$ miss.
The direct method (patching the call-site, where possible) would alleviate that (mostly) and be more of a win.
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Mark Rutland
reviewerQuoted 8 lines · Peter Zijlstra
Quoted 7 lines · Mark Rutland
Hi Ard,
Quoted 3 lines · Ard Biesheuvel
Implement arm64 support for the 'unoptimized' static call variety, which routes all calls through a single trampoline that is patched to perform a tail call to the selected function.
Quoted 8 lines
Quoted 1 line
Given the complexity and subtlety here, do we actually need this?
Only if you can get a performance win. The obvious benefit is loosing the load that's inherent in indirect function calls. The down-side of the indirect static-call implementation is that it will incur an extra I$ miss.
So it might be a wash, loose a data load miss, gain an I$ miss.
I reckon it'll be highly dependent on microarchitecture since it'll also depend on how indirect branches are handled (with prediction, forwarding, speculation, etc). I don't think we can easily reason about this in general.
Quoted 2 lines
The direct method (patching the call-site, where possible) would alleviate that (mostly) and be more of a win.
I think that where the original callsite can be patched with a direct branch, it's desireable that we do so. That's simple enough, and there are places where that'd be useful from a functional pov (e.g. if we want to patch branches in hyp text to other hyp text).
However, if the range of the branch requires a trampoline I'd rather the trampoline (and the procedure for updating it) be as simple as possible.
Thanks,
Mark.
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Ard Biesheuvel
reviewerHi Mark,
Quoted 10 lines · Mark Rutland
Hi Ard,
Quoted 3 lines · Ard Biesheuvel
Implement arm64 support for the 'unoptimized' static call variety, which routes all calls through a single trampoline that is patched to perform a tail call to the selected function.
Given the complexity and subtlety here, do we actually need this?
The more time I spend on this, the more I lean towards 'no' :-)
Quoted 3 lines
If there's some core feature that depends on this (or wants to), it'd be nice to call that out in the commit message.
Agreed. Perhaps I should have put RFC in the subject, because this is more intended as a call for discussion (and v1 had little success in that regard)
Quoted 11 lines
Quoted 10 lines
Since static call targets may be located in modules loaded out of direct branching range, we need to be able to fall back to issuing a ADRP/ADD pair to load the branch target into R16 and use a BR instruction. As this involves patching more than a single B or NOP instruction (for which the architecture makes special provisions in terms of the synchronization needed), we may need to run the full blown instruction patching logic that uses
stop_machine(). It also means that once we've patched in a ADRP/ADD pair once, we are quite restricted in the patching we can code subsequently, and we may end up using an indirect call after all (note that
Quoted 4 lines
Noted. I guess we
[...]
?
Quoted 19 lines
Quoted 10 lines
v2:
This turned nasty really quickly when I realized that any sleeping task could have been interrupted right in the middle of the ADRP/ADD pair that we emit for static call targets that are out of immediate branching range.+/*
+ * The static call trampoline consists of one of the following sequences:
+ *
+ * (A) (B) (C) (D) (E)Quoted 8 lines
+ * 00: BTI C BTI C BTI C BTI C BTI C + * 04: B fn NOP NOP NOP NOP + * 08: RET RET ADRP X16, fn ADRP X16, fn ADRP X16, fn + * 0c: NOP NOP ADD X16, fn ADD X16, fn ADD X16, fn + * 10: BR X16 RET NOP + * 14: ADRP X16, &fn + * 18: LDR X16, [X16, &fn] + * 1c: BR X16
Quoted 3 lines
Are these all padded with trailing NOPs or UDFs? I assume the same space is statically reserved.
Yes, it is a fixed size 32 byte area. I only included the relevant ones here, the empty slots are D/C in the context of each individual sequence.
Quoted 24 lines
Quoted 15 lines
+ *
+ * The architecture permits us to patch B instructions into NOPs or vice versa
+ * directly, but patching any other instruction sequence requires careful
+ * synchronization. Since branch targets may be out of range for ordinary
+ * immediate branch instructions, we may have to fall back to ADRP/ADD/BR
+ * sequences in some cases, which complicates things considerably; since any
+ * sleeping tasks may have been preempted right in the middle of any of these
+ * sequences, we have to carefully transform one into the other, and ensure
+ * that it is safe to resume execution at any point in the sequence for tasks
+ * that have already executed part of it.
+ *
+ * So the rules are:
+ * - we start out with (A) or (B)
+ * - a branch within immediate range can always be patched in at offset0x4;
+ * - sequence (A) can be turned into (B) for NULL branch targets;Quoted 8 lines
+ * - a branch outside of immediate range can be patched using (C), but only if
+ * . the sequence being updated is (A) or (B), or
+ * . the branch target address modulo 4k results in the same ADD opcode
+ * (which could occur when patching the same far target a second time)
+ * - once we have patched in (C) we cannot go back to (A) or (B), so patching
+ * in a NULL target now requires sequence (D);
+ * - if we cannot patch a far target using (C), we fall back to sequence (E),
+ * which loads the function pointer from memory.
Quoted 9 lines
Cases C-E all use an indirect branch, which goes against one of the arguments for having static calls (the assumption that CPUs won't mis-predict direct branches). Similarly case E is a literal pool with more steps.
That means that for us, static calls would only be an opportunistic optimization rather than a hardening feature. Do they actually save us much, or could we get by with an inline literal pool in the trampoline?
Another assumption this is based on is that a literal load is more costly than a ADRP/ADD.
Quoted 9 lines
It'd be much easier to reason consistently if the trampoline were always:
| BTI C | LDR X16, _literal // pc-relative load | BR X16 | _literal: | < patch a 64-bit value here atomically >
Quoted 4 lines
... and I would strongly prefer that to having multiple sequences that could all be live -- I'm really not keen on the complexity and subtlety that entails.
I don't see this having any benefit over a ADRP/LDR pair that accesses the static call key struct directly.
Quoted 10 lines
[...]
Quoted 2 lines
+ * Note that sequence (E) is only used when switching between multiple far
+ * targets, and that it is not a terminal degraded state.I think what you're saying here is that you can go from (E) to (C) if switching to a new far branch that's in ADRP+ADD range, but it can be misread to mean (E) is a transient step while patching a branch rather than some branches only being possible to encode with (E).
Indeed.
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel
Mark Rutland
reviewerQuoted 9 lines · Ard Biesheuvel
Quoted 6 lines · Mark Rutland
Quoted 3 lines · Ard Biesheuvel
Implement arm64 support for the 'unoptimized' static call variety, which routes all calls through a single trampoline that is patched to perform a tail call to the selected function.
Given the complexity and subtlety here, do we actually need this?
The more time I spend on this, the more I lean towards 'no' :-)
That would make this all simpler! :)
[...]
Quoted 11 lines
Quoted 11 lines
Quoted 10 lines
Since static call targets may be located in modules loaded out of direct branching range, we need to be able to fall back to issuing a ADRP/ADD pair to load the branch target into R16 and use a BR instruction. As this involves patching more than a single B or NOP instruction (for which the architecture makes special provisions in terms of the synchronization needed), we may need to run the full blown instruction patching logic that uses
stop_machine(). It also means that once we've patched in a ADRP/ADD pair once, we are quite restricted in the patching we can code subsequently, and we may end up using an indirect call after all (note that
Quoted 6 lines
Quoted 4 lines
Noted. I guess we
[...]
?
Sorry; I was playing on the commit message ending on "(note that", as I wasn't sure where that was going.
Quoted 29 lines
Quoted 29 lines
Quoted 15 lines
+ *
+ * The architecture permits us to patch B instructions into NOPs or vice versa
+ * directly, but patching any other instruction sequence requires careful
+ * synchronization. Since branch targets may be out of range for ordinary
+ * immediate branch instructions, we may have to fall back to ADRP/ADD/BR
+ * sequences in some cases, which complicates things considerably; since any
+ * sleeping tasks may have been preempted right in the middle of any of these
+ * sequences, we have to carefully transform one into the other, and ensure
+ * that it is safe to resume execution at any point in the sequence for tasks
+ * that have already executed part of it.
+ *
+ * So the rules are:
+ * - we start out with (A) or (B)
+ * - a branch within immediate range can always be patched in at offset0x4;
+ * - sequence (A) can be turned into (B) for NULL branch targets;Quoted 8 lines
+ * - a branch outside of immediate range can be patched using (C), but only if
+ * . the sequence being updated is (A) or (B), or
+ * . the branch target address modulo 4k results in the same ADD opcode
+ * (which could occur when patching the same far target a second time)
+ * - once we have patched in (C) we cannot go back to (A) or (B), so patching
+ * in a NULL target now requires sequence (D);
+ * - if we cannot patch a far target using (C), we fall back to sequence (E),
+ * which loads the function pointer from memory.Cases C-E all use an indirect branch, which goes against one of the arguments for having static calls (the assumption that CPUs won't mis-predict direct branches). Similarly case E is a literal pool with more steps.
Quoted 6 lines
Quoted 3 lines
That means that for us, static calls would only be an opportunistic optimization rather than a hardening feature. Do they actually save us much, or could we get by with an inline literal pool in the trampoline?
Another assumption this is based on is that a literal load is more costly than a ADRP/ADD.
Agreed. I think in practice it's going to depend on the surrounding context and microarchitecture. If the result is being fed into a BR, I'd expect no difference on a big OoO core, and even for a small in-order core it'll depend on how/when the core can forward the result relative to predicting the branch.
Quoted 9 lines
Quoted 9 lines
It'd be much easier to reason consistently if the trampoline were always:
| BTI C | LDR X16, _literal // pc-relative load | BR X16 | _literal: | < patch a 64-bit value here atomically >
Quoted 6 lines
Quoted 3 lines
... and I would strongly prefer that to having multiple sequences that could all be live -- I'm really not keen on the complexity and subtlety that entails.
I don't see this having any benefit over a ADRP/LDR pair that accesses the static call key struct directly.
Even better!
Thanks,
Mark.
_______________________________________________
linux-arm-kernel mailing list
[email protected]
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel