Skip to content

Commit 4bdcda3

Browse files
authored
PE: Implement UWOP_EPILOG parsing for unwind info v2 (#492)
* PE: Implement `UWOP_EPILOG` parsing for unwind info v2 * Raise error on invalid unwind info version * Add `non_exhaustive` for `UnwindOperation`
1 parent 92d223d commit 4bdcda3

File tree

1 file changed

+173
-7
lines changed

1 file changed

+173
-7
lines changed

src/pe/exception.rs

Lines changed: 173 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ impl fmt::Display for Register {
312312
/// Unwind operations can be used to reverse the effects of the function prolog and restore register
313313
/// values of parent stack frames that have been saved to the stack.
314314
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
315+
#[non_exhaustive]
315316
pub enum UnwindOperation {
316317
/// Push a nonvolatile integer register, decrementing `RSP` by 8.
317318
PushNonVolatile(Register),
@@ -333,10 +334,45 @@ pub enum UnwindOperation {
333334
/// Save the lower 64 bits of a nonvolatile XMM register on the stack.
334335
SaveXMM(Register, StackFrameOffset),
335336

336-
/// Describes the function epilog.
337+
/// Describes the function epilog location and size (Version 2).
337338
///
338-
/// This operation has been introduced with unwind info version 2 and is not implemented yet.
339-
Epilog,
339+
/// [UWOP_EPILOG] entries work together to describe where epilogues are located in the function,
340+
/// but not what operations they perform. The actual operations performed during epilogue
341+
/// execution are the reverse of the prolog operations.
342+
///
343+
/// These entries always appear at the beginning of the [UnwindCode] array, with a minimum of
344+
/// two entries required for alignment purposes, even when only one epilogue exists. The first
345+
/// [UWOP_EPILOG] entry is special as it contains the size of the epilogue in its `offset_low_or_size`
346+
/// field. If bit `0` of its `offset_high_or_flags` field is set, this first entry also describes
347+
/// an epilogue located at the function's end, with `offset_low_or_size` serving dual purpose as
348+
/// both size and offset. When bit `0` is not set, the first entry contains only the size, and
349+
/// subsequent [UWOP_EPILOG] entries provide the epilogue locations.
350+
///
351+
/// Each subsequent [UWOP_EPILOG] entry describes an additional epilogue location using a 12-bit
352+
/// offset from the function's end address. The offset is formed by combining `offset_low_or_size`
353+
/// (lower 8 bits) with the lower 4 bits of `offset_high_or_flags` (upper 4 bits). The epilogue's
354+
/// starting address is calculated by subtracting this offset from the function's `EndAddress`.
355+
Epilog {
356+
/// For the first [UWOP_EPILOG] entry:
357+
/// * Size of the epilogue in bytes
358+
/// * If `offset_high_or_flags` bit 0 is set, also serves as offset
359+
///
360+
/// For subsequent entries:
361+
/// * Lower 8 bits of the offset from function end
362+
offset_low_or_size: u8,
363+
364+
/// For the first [UWOP_EPILOG] entry:
365+
/// * Bit 0: If set, epilogue is at function end and `offset_low_or_size`
366+
/// is also the offset
367+
/// * Bits 1-3: Reserved/unused
368+
///
369+
/// For subsequent entries:
370+
/// * Upper 4 bits of the offset from function end (bits 0-3)
371+
///
372+
/// The complete offset is computed as:
373+
/// `EndAddress` - (`offset_high_or_flags` << 8 | `offset_low_or_size`)
374+
offset_high_or_flags: u8,
375+
},
340376

341377
/// Save all 128 bits of a nonvolatile XMM register on the stack.
342378
SaveXMM128(Register, StackFrameOffset),
@@ -452,22 +488,40 @@ impl<'a> TryFromCtx<'a, UnwindOpContext> for UnwindCode {
452488
UnwindOperation::SaveNonVolatile(register, StackFrameOffset::with_ctx(offset, ctx))
453489
}
454490
self::UWOP_EPILOG => {
455-
let data = u32::from(bytes.gread_with::<u16>(&mut read, scroll::LE)?) * 16;
456491
if ctx.version == 1 {
492+
// Version 1: This was UWOP_SAVE_XMM
493+
let data = u32::from(bytes.gread_with::<u16>(&mut read, scroll::LE)?) * 16;
457494
let register = Register::xmm(operation_info);
458495
UnwindOperation::SaveXMM(register, StackFrameOffset::with_ctx(data, ctx))
496+
} else if ctx.version == 2 {
497+
// Version 2: UWOP_EPILOG - describes epilogue locations
498+
// See https://github.com/BlancLoup/weekly-geekly.github.io/blob/1cbdf1c6127fcdaeda1f01bcbb006febd17d5a95/articles/322956/index.html
499+
// See https://github.com/Montura/cpp/blob/4393c678ee8e44dd98feb7a198c3983a6108cef8/src/exception_handling/msvc/eh_msvc_cxx_EH_x64.md?plain=1#L119
500+
UnwindOperation::Epilog {
501+
offset_low_or_size: code_offset,
502+
offset_high_or_flags: operation_info,
503+
}
459504
} else {
460-
// TODO: See https://weekly-geekly.github.io/articles/322956/index.html
461-
UnwindOperation::Epilog
505+
let msg = format!(
506+
"Unwind info version has to be either one of `1` or `2`: {}",
507+
ctx.version
508+
);
509+
return Err(error::Error::Malformed(msg));
462510
}
463511
}
464512
self::UWOP_SPARE_CODE => {
465513
let data = bytes.gread_with::<u32>(&mut read, scroll::LE)?;
466514
if ctx.version == 1 {
467515
let register = Register::xmm(operation_info);
468516
UnwindOperation::SaveXMM128(register, StackFrameOffset::with_ctx(data, ctx))
469-
} else {
517+
} else if ctx.version == 2 {
470518
UnwindOperation::Noop
519+
} else {
520+
let msg = format!(
521+
"Unwind info version has to be either one of `1` or `2`: {}",
522+
ctx.version
523+
);
524+
return Err(error::Error::Malformed(msg));
471525
}
472526
}
473527
self::UWOP_SAVE_XMM128 => {
@@ -1110,6 +1164,118 @@ mod tests {
11101164
// assert_eq!(unwind_info.code_bytes, expected);
11111165
// }
11121166

1167+
#[test]
1168+
fn test_unwind_codes_one_epilog() {
1169+
// ; Prologue
1170+
// .text:00000001400884E0 000 57 push rdi // Save RDI
1171+
//
1172+
// .text:00000001400884E1 008 8B C2 mov eax, edx
1173+
// .text:00000001400884E3 008 48 8B F9 mov rdi, rcx
1174+
// .text:00000001400884E6 008 49 8B C8 mov rcx, r8
1175+
// .text:00000001400884E9 008 F3 AA rep stosb
1176+
// .text:00000001400884EB 008 49 8B C1 mov rax, r9
1177+
//
1178+
// ; Epilogue
1179+
// .text:00000001400884EE 008 5F pop rdi // Restore RDI
1180+
// .text:00000001400884EF 000 C3 retn
1181+
1182+
const BYTES: &[u8] = &[
1183+
0x02, 0x01, 0x03, 0x00, // UNWIND_INFO_HDR <2, 0, 1, 3, 0, 0>
1184+
0x02, 0x16, // UNWIND_CODE <<2, 6, 1>> ; UWOP_EPILOG
1185+
0x00, 0x06, // UNWIND_CODE <<0, 6, 0>> ; UWOP_EPILOG
1186+
0x01, 0x70, // UNWIND_CODE <<1, 0, 7>> ; UWOP_PUSH_NONVOL
1187+
];
1188+
1189+
let unwind_info = UnwindInfo::parse(BYTES, 0).unwrap();
1190+
let unwind_codes: Vec<UnwindCode> = unwind_info
1191+
.unwind_codes()
1192+
.map(|result| result.expect("Unable to parse unwind code"))
1193+
.collect();
1194+
assert_eq!(unwind_codes.len(), 3);
1195+
assert_eq!(
1196+
unwind_codes,
1197+
[
1198+
UnwindCode {
1199+
code_offset: 2,
1200+
operation: UnwindOperation::Epilog {
1201+
offset_low_or_size: 2,
1202+
offset_high_or_flags: 1,
1203+
},
1204+
},
1205+
UnwindCode {
1206+
code_offset: 0,
1207+
operation: UnwindOperation::Epilog {
1208+
offset_low_or_size: 0,
1209+
offset_high_or_flags: 0,
1210+
},
1211+
},
1212+
UnwindCode {
1213+
code_offset: 1,
1214+
operation: UnwindOperation::PushNonVolatile(Register(7)), // RDI
1215+
},
1216+
]
1217+
);
1218+
}
1219+
1220+
#[test]
1221+
fn test_unwind_codes_two_epilog() {
1222+
// ; Prologue
1223+
// .text:00000001400889A0 000 57 push rdi // Save RDI
1224+
// .text:00000001400889A1 008 56 push rsi // Save RSI
1225+
//
1226+
// .text:00000001400889A2 010 48 8B F9 mov rdi, rcx
1227+
// .text:00000001400889A5 010 48 8B F2 mov rsi, rdx
1228+
// .text:00000001400889A8 010 49 8B C8 mov rcx, r8
1229+
// .text:00000001400889AB 010 F3 A4 rep movsb
1230+
//
1231+
// ; Epilogue
1232+
// .text:00000001400889AD 010 5E pop rsi // Restore RSI
1233+
// .text:00000001400889AE 008 5F pop rdi // Restore RDI
1234+
// .text:00000001400889AF 000 C3 retn
1235+
1236+
const BYTES: &[u8] = &[
1237+
0x02, 0x02, 0x04, 0x00, // $xdatasym_5 UNWIND_INFO_HDR <2, 0, 2, 4, 0, 0>
1238+
0x03, 0x16, // UNWIND_CODE <<3, 6, 1>> ; UWOP_EPILOG
1239+
0x00, 0x06, // UNWIND_CODE <<0, 6, 0>> ; UWOP_EPILOG
1240+
0x02, 0x60, // UNWIND_CODE <<2, 0, 6>> ; UWOP_PUSH_NONVOL
1241+
0x01, 0x70, // UNWIND_CODE <<1, 0, 7>> ; UWOP_PUSH_NONVOL
1242+
];
1243+
1244+
let unwind_info = UnwindInfo::parse(BYTES, 0).unwrap();
1245+
let unwind_codes: Vec<UnwindCode> = unwind_info
1246+
.unwind_codes()
1247+
.map(|result| result.expect("Unable to parse unwind code"))
1248+
.collect();
1249+
assert_eq!(unwind_codes.len(), 4);
1250+
assert_eq!(
1251+
unwind_codes,
1252+
[
1253+
UnwindCode {
1254+
code_offset: 3,
1255+
operation: UnwindOperation::Epilog {
1256+
offset_low_or_size: 3,
1257+
offset_high_or_flags: 1,
1258+
},
1259+
},
1260+
UnwindCode {
1261+
code_offset: 0,
1262+
operation: UnwindOperation::Epilog {
1263+
offset_low_or_size: 0,
1264+
offset_high_or_flags: 0,
1265+
},
1266+
},
1267+
UnwindCode {
1268+
code_offset: 2,
1269+
operation: UnwindOperation::PushNonVolatile(Register(6)), // RSI
1270+
},
1271+
UnwindCode {
1272+
code_offset: 1,
1273+
operation: UnwindOperation::PushNonVolatile(Register(7)), // RDI
1274+
},
1275+
]
1276+
);
1277+
}
1278+
11131279
#[test]
11141280
fn test_iter_unwind_codes() {
11151281
let unwind_info = UnwindInfo {

0 commit comments

Comments
 (0)