Skip to content

Commit d08e2ae

Browse files
authored
Merge pull request #73 from joschock/uefi_seh
Add SEH unwind for UEFI x86-64 targets
1 parent 95d06b6 commit d08e2ae

2 files changed

Lines changed: 129 additions & 28 deletions

File tree

src/arch/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ cfg_if::cfg_if! {
4141
""
4242
};
4343
}
44+
// This macro is not used on Windows targets (which use
45+
// x86_64_windows.rs) but is used on UEFI targets via x86_64.rs.
46+
#[allow(unused_macros)]
4447
macro_rules! cfi_signal_frame {
4548
() => { ".cfi_signal_frame" }
4649
}

src/arch/x86_64.rs

Lines changed: 126 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,36 @@
6969
//! And this is the layout of the parent stack when a coroutine is running:
7070
//!
7171
//! ```text
72-
//! | |
73-
//! ~ ... ~
74-
//! | |
75-
//! +-------------+
76-
//! | Saved RBX |
77-
//! +-------------+
78-
//! | Saved RIP | <- These 2 values form a valid entry in the frame pointer
79-
//! +-------------+ | chain. The parent link itself is another entry in the
80-
//! | Saved RBP | <- frame pointer chain since RBP points to it.
81-
//! +-------------+ <- Parent link points here.
72+
//! | |
73+
//! ~ ... ~
74+
//! | |
75+
//! +----------------+
76+
//! | Saved RBX |
77+
//! +----------------+
78+
//! | Saved RIP | <- These 2 values form a valid entry in the frame pointer
79+
//! +----------------+ | chain. The parent link itself is another entry in the
80+
//! | Saved RBP | <- frame pointer chain since RBP points to it.
81+
//! +----------------+ <- Parent link points here.
82+
//! ```
83+
//!
84+
//! On UEFI targets, a secondary copy of the saved RIP is added below the saved
85+
//! RBX. This is needed because SEH unwind codes are not as flexible as DWARF
86+
//! CFI and the unwinder always pops a return address after processing all
87+
//! unwind opcodes.
88+
//!
89+
//! ```text
90+
//! | |
91+
//! ~ ... ~
92+
//! | |
93+
//! +----------------+
94+
//! | Secondary RIP | <- Only on UEFI; used by the SEH unwinder.
95+
//! +----------------+
96+
//! | Saved RBX |
97+
//! +----------------+
98+
//! | Saved RIP |
99+
//! +----------------+
100+
//! | Saved RBP |
101+
//! +----------------+ <- Parent link points here.
82102
//! ```
83103
//!
84104
//! And finally, this is the stack layout of a coroutine that has just been
@@ -109,6 +129,32 @@ use crate::unwind::{
109129
};
110130
use crate::util::EncodedValue;
111131

132+
// On UEFI targets, we emit SEH unwind information so that PE/COFF debuggers
133+
// (WinDbg, LLDB) can reconstruct backtraces across coroutine stack boundaries.
134+
// Unlike Windows, UEFI does not have a Thread Environment Block (TEB), so the
135+
// SEH annotations are simpler with adjusted stack offsets.
136+
//
137+
// The cfi!() and seh!() macros ensure that DWARF CFI and SEH directives are
138+
// mutually exclusive: UEFI uses SEH (.pdata/.xdata), all other platforms use
139+
// DWARF CFI (.eh_frame).
140+
cfg_if::cfg_if! {
141+
if #[cfg(target_os = "uefi")] {
142+
macro_rules! seh {
143+
($asm:expr) => { $asm }
144+
}
145+
macro_rules! cfi {
146+
($asm:expr) => { "" }
147+
}
148+
} else {
149+
macro_rules! seh {
150+
($asm:expr) => { "" }
151+
}
152+
macro_rules! cfi {
153+
($asm:expr) => { $asm }
154+
}
155+
}
156+
}
157+
112158
pub const STACK_ALIGNMENT: usize = 16;
113159
pub const PARENT_STACK_OFFSET: usize = 0;
114160
pub const PARENT_LINK_OFFSET: usize = 16;
@@ -118,15 +164,19 @@ pub type StackWord = u64;
118164
// be the "base" function of all coroutines. This entrypoint is used in
119165
// init_stack() to bootstrap the execution of a new coroutine.
120166
//
121-
// We also use this function as a persistent frame on the stack to emit dwarf
167+
// We also use this function as a persistent frame on the stack to emit unwind
122168
// information to unwind into the caller. This allows us to unwind from the
123169
// coroutines's stack back to the main stack that the coroutine was called from.
124-
// We use special dwarf directives here to do so since this is a pretty
125-
// nonstandard function.
170+
// We use special directives here to do so since this is a pretty nonstandard
171+
// function.
172+
//
173+
// On non-UEFI platforms we use DWARF CFI directives. On UEFI we use SEH
174+
// directives instead (see the seh!() and cfi!() macros above).
126175
global_asm!(
127176
".balign 16",
128177
asm_function_begin!("stack_init_trampoline"),
129-
".cfi_startproc",
178+
cfi!(".cfi_startproc"),
179+
seh!(".seh_proc stack_init_trampoline"),
130180
// GDB has a hard-coded check that rejects backtraces where the frame
131181
// addresses do not monotonically increase. This can unfortunately trigger
132182
// when the stack of a coroutine is located at a higher address than its
@@ -142,7 +192,7 @@ global_asm!(
142192
// *after* the return address to search for unwind information. To avoid
143193
// issues, any asm! blocks containing a return address that may be unwound
144194
// into must not have that address at the end of the asm! block.
145-
cfi_signal_frame!(),
195+
cfi!(cfi_signal_frame!()),
146196
// This gets called by switch_and_link() the first time a coroutine is
147197
// resumed, due to the initial state set up by init_stack().
148198
//
@@ -171,9 +221,11 @@ global_asm!(
171221
// Set up the frame pointer to point at the parent link. This is needed for
172222
// the unwinding code below.
173223
"mov rbp, rsi",
174-
// This sequence of magic numbers deserves some explanation. We need to tell
175-
// the unwinder where to find the Canonical Frame Address (CFA) of the
176-
// parent context.
224+
//
225+
// DWARF CFI unwind directives (non-UEFI platforms)
226+
//
227+
// We need to tell the unwinder where to find the Canonical Frame Address
228+
// (CFA) of the parent context.
177229
//
178230
// The CFA is normally defined as the stack pointer value in the caller just
179231
// before executing the call instruction. In our case, this is the stack
@@ -192,13 +244,34 @@ global_asm!(
192244
// 0x76 0x00: DW_OP_breg6 (rbp + 0) -- GDB doesn't like DW_OP_reg6
193245
// 0x06: DW_OP_deref
194246
// 0x23, 0x18: DW_OP_plus_uconst 24
195-
".cfi_escape 0x0f, 5, 0x76, 0x00, 0x06, 0x23, 0x18",
247+
cfi!(".cfi_escape 0x0f, 5, 0x76, 0x00, 0x06, 0x23, 0x18"),
196248
// Now we can tell the unwinder how to restore the 3 registers that were
197249
// pushed on the parent stack. These are described as offsets from the CFA
198250
// that we just calculated.
199-
".cfi_offset rbx, -8",
200-
".cfi_offset rip, -16",
201-
".cfi_offset rbp, -24",
251+
cfi!(".cfi_offset rbx, -8"),
252+
cfi!(".cfi_offset rip, -16"),
253+
cfi!(".cfi_offset rbp, -24"),
254+
//
255+
// SEH unwind directives (UEFI only)
256+
//
257+
// These tell the SEH unwinder how to restore the register state to that of
258+
// the parent call frame. The SEH unwinder processes these in reverse order:
259+
// 1. .seh_setframe rbp, 0: Copy virtual RBP to virtual RSP.
260+
// 2. .seh_savereg rsp, 0: Read the parent link and place it in virtual RSP,
261+
// which now points to the top of the parent stack.
262+
// 3. .seh_pushreg rbp: Pop and restore RBP from the parent stack.
263+
// 4. .seh_stackalloc 8: Skip the saved RIP from the CALL instruction.
264+
// 5. .seh_pushreg rbx: Pop and restore RBX from the parent stack.
265+
//
266+
// After all these operations, the unwinder pops a return address off the
267+
// stack. This is the secondary copy of the return address created in
268+
// switch_and_link.
269+
seh!(".seh_pushreg rbx"),
270+
seh!(".seh_stackalloc 8"),
271+
seh!(".seh_pushreg rbp"),
272+
seh!(".seh_savereg rsp, 0"),
273+
seh!(".seh_setframe rbp, 0"),
274+
seh!(".seh_endprologue"),
202275
// Set up the 3rd argument to the initial function to point to the object
203276
// that init_stack() set up on the stack.
204277
"mov rdx, rsp",
@@ -234,7 +307,8 @@ global_asm!(
234307
// the bounds of the function. In any case, this instruction is never
235308
// executed since the function we are calling never returns.
236309
"int3",
237-
".cfi_endproc",
310+
cfi!(".cfi_endproc"),
311+
seh!(".seh_endproc"),
238312
asm_function_end!("stack_init_trampoline"),
239313
);
240314

@@ -247,8 +321,9 @@ global_asm!(
247321
// used here.
248322
".balign 16",
249323
asm_function_begin!("stack_call_trampoline"),
250-
".cfi_startproc",
251-
cfi_signal_frame!(),
324+
cfi!(".cfi_startproc"),
325+
seh!(".seh_proc stack_call_trampoline"),
326+
cfi!(cfi_signal_frame!()),
252327
// At this point our register state contains the following:
253328
// - RSP points to the top of the parent stack.
254329
// - RBP holds its value from the parent context.
@@ -259,8 +334,13 @@ global_asm!(
259334
// Create a stack frame and point the frame pointer at it.
260335
"push rbp",
261336
"mov rbp, rsp",
262-
".cfi_def_cfa rbp, 16",
263-
".cfi_offset rbp, -16",
337+
// DWARF CFI (non-UEFI)
338+
cfi!(".cfi_def_cfa rbp, 16"),
339+
cfi!(".cfi_offset rbp, -16"),
340+
// SEH (UEFI only)
341+
seh!(".seh_pushreg rbp"),
342+
seh!(".seh_setframe rbp, 0"),
343+
seh!(".seh_endprologue"),
264344
// Switch to the new stack.
265345
"mov rsp, rsi",
266346
// Call the function pointer. The argument is already in the correct
@@ -271,7 +351,8 @@ global_asm!(
271351
"mov rsp, rbp",
272352
"pop rbp",
273353
"ret",
274-
".cfi_endproc",
354+
cfi!(".cfi_endproc"),
355+
seh!(".seh_endproc"),
275356
asm_function_end!("stack_call_trampoline"),
276357
);
277358

@@ -331,6 +412,11 @@ pub unsafe fn switch_and_link(
331412
let (ret_val, ret_sp);
332413

333414
asm_may_unwind_root!(
415+
// Set up a secondary copy of the return address. This is only used by
416+
// the SEH unwinder on UEFI, not by actual returns.
417+
seh!("lea rax, [rip + 2f]"),
418+
seh!("push rax"),
419+
334420
// Save RBX. Ideally this would be done by specifying them as a clobber
335421
// but that is not possible since RBX is an LLVM reserved register.
336422
//
@@ -365,8 +451,12 @@ pub unsafe fn switch_and_link(
365451
// instruction. However this doesn't cause any issues in practice.
366452

367453
// Restore RBX.
454+
"2:",
368455
"pop rbx",
369456

457+
// Pop the secondary return address (UEFI only).
458+
seh!("add rsp, 8"),
459+
370460
// The RDI register is specifically chosen to hold the argument since
371461
// the ABI uses it for the first argument of a function call.
372462
//
@@ -554,6 +644,11 @@ pub unsafe fn switch_and_throw(
554644
let (ret_val, ret_sp);
555645

556646
asm_may_unwind_root!(
647+
// Set up a secondary copy of the return address for the SEH unwinder
648+
// (UEFI only), just like in switch_and_link().
649+
seh!("lea rax, [rip + 2f]"),
650+
seh!("push rax"),
651+
557652
// Save RBX just like the first half of switch_and_link().
558653
"push rbx",
559654

@@ -598,6 +693,9 @@ pub unsafe fn switch_and_throw(
598693
// Restore registers just like the second half of switch_and_link.
599694
"pop rbx",
600695

696+
// Pop the secondary return address (UEFI only).
697+
seh!("add rsp, 8"),
698+
601699
// Helper function to trigger stack unwinding.
602700
throw = sym throw,
603701

0 commit comments

Comments
 (0)