Skip to main content

vmi_os_windows/unwind/
amd64.rs

1//! x64 (AMD64) stack unwinding using .pdata / UNWIND_INFO.
2//!
3//! Implements the Windows x64 stack unwinding algorithm by reading
4//! RUNTIME_FUNCTION entries from the PE exception directory and
5//! processing UNWIND_INFO structures to recover caller register state.
6
7use vmi_arch_amd64::Registers;
8use vmi_core::{Va, VmiError, VmiState, driver::VmiRead};
9use zerocopy::{FromBytes, IntoBytes};
10
11use super::{Frame, Unwinder, Unwound};
12use crate::{
13    ArchAdapter, WindowsOs,
14    pe::{ImageRuntimeFunctionEntry, PeImage},
15};
16
17// Reference:
18// https://github.com/dotnet/coreclr/blob/a9f3fc16483eecfc47fb79c362811d870be02249/src/unwinder/amd64/unwinder_amd64.cpp
19// TODO: epilog unwind
20
21/// Process UNWIND_INFO entries, following chains.
22/// Limit chain depth to guard against corrupted/circular data.
23pub const UNWIND_CHAIN_LIMIT: u32 = 32;
24
25// Unwind operation codes.
26
27/// Push a nonvolatile integer register, decrementing RSP by 8.
28///
29/// The operation info is the number of the register. Because of the constraints
30/// on epilogs, `UWOP_PUSH_NONVOL` unwind codes must appear first in the prolog
31/// and correspondingly, last in the unwind code array. This relative ordering
32/// applies to all other unwind codes except `UWOP_PUSH_MACHFRAME`.
33pub const UWOP_PUSH_NONVOL: u8 = 0;
34
35/// Allocate a large-sized area on the stack.
36///
37/// There are two forms. If the operation info equals 0, then the size of the
38/// allocation divided by 8 is recorded in the next slot, allowing an allocation
39/// up to 512K - 8. If the operation info equals 1, then the unscaled size of
40/// the allocation is recorded in the next two slots in little-endian format,
41/// allowing allocations up to 4GB - 8.
42pub const UWOP_ALLOC_LARGE: u8 = 1;
43
44/// Allocate a small-sized area on the stack.
45///
46/// The size of the allocation is the operation info field * 8 + 8, allowing
47/// allocations from 8 to 128 bytes.
48///
49/// The unwind code for a stack allocation should always use the shortest
50/// possible encoding:
51///
52/// | Allocation Size     | Unwind Code                            |
53/// | ------------------- | -------------------------------------- |
54/// | 8 to 128 bytes      | `UWOP_ALLOC_SMALL`                     |
55/// | 136 to 512K-8 bytes | `UWOP_ALLOC_LARGE`, operation info = 0 |
56/// | 512K to 4G-8 bytes  | `UWOP_ALLOC_LARGE`, operation info = 1 |
57pub const UWOP_ALLOC_SMALL: u8 = 2;
58
59/// Establish the frame pointer register by setting the register to some offset of the current RSP.
60///
61/// The offset is equal to the Frame Register offset (scaled) field in the
62/// `UNWIND_INFO * 16`, allowing offsets from 0 to 240. The use of an offset
63/// permits establishing a frame pointer that points to the middle of the fixed
64/// stack allocation, helping code density by allowing more accesses to use
65/// short instruction forms. The operation info field is reserved and shouldn't
66/// be used.
67pub const UWOP_SET_FPREG: u8 = 3;
68
69/// Save a nonvolatile integer register on the stack using a MOV instead of a PUSH.
70///
71/// This code is primarily used for *shrink-wrapping*, where a nonvolatile
72/// register is saved to the stack in a position that was previously allocated.
73/// The operation info is the number of the register. The scaled-by-8 stack
74/// offset is recorded in the next unwind operation code slot.
75pub const UWOP_SAVE_NONVOL: u8 = 4;
76
77/// Save a nonvolatile integer register on the stack with a long offset, using a MOV instead of a PUSH.
78///
79/// This code is primarily used for *shrink-wrapping*, where a nonvolatile
80/// register is saved to the stack in a position that was previously allocated.
81/// The operation info is the number of the register. The unscaled stack offset
82/// is recorded in the next two unwind operation code slots.
83pub const UWOP_SAVE_NONVOL_FAR: u8 = 5;
84
85/// Undocumented.
86pub const UWOP_EPILOG: u8 = 6;
87
88/// Undocumented.
89///
90/// Previously 64-bit `UWOP_SAVE_XMM_FAR`.
91pub const UWOP_SPARE_CODE: u8 = 7;
92
93/// Save all 128 bits of a nonvolatile XMM register on the stack.
94///
95/// The operation info is the number of the register. The scaled-by-16 stack
96/// offset is recorded in the next slot.
97pub const UWOP_SAVE_XMM128: u8 = 8;
98
99/// Save all 128 bits of a nonvolatile XMM register on the stack with a long offset.
100///
101/// The operation info is the number of the register. The unscaled stack offset
102/// is recorded in the next two slots.
103pub const UWOP_SAVE_XMM128_FAR: u8 = 9;
104
105/// Push a machine frame.
106///
107/// This unwind code is used to record the effect of a hardware interrupt or
108/// exception. There are two forms. If the operation info equals 0, one of these
109/// frames has been pushed on the stack:
110///
111/// | Location | Value   |
112/// | -------- | ------- |
113/// | RSP+32   | SS      |
114/// | RSP+24   | Old RSP |
115/// | RSP+16   | EFLAGS  |
116/// | RSP+8    | CS      |
117/// | RSP      | RIP     |
118///
119/// If the operation info equals 1, then one of these frames has been pushed:
120///
121/// | Location | Value      |
122/// | -------- | ---------- |
123/// | RSP+40   | SS         |
124/// | RSP+32   | Old RSP    |
125/// | RSP+24   | EFLAGS     |
126/// | RSP+16   | CS         |
127/// | RSP+8    | RIP        |
128/// | RSP      | Error code |
129///
130/// This unwind code always appears in a dummy prolog, which is never actually
131/// executed, but instead appears before the real entry point of an interrupt
132/// routine, and exists only to provide a place to simulate the push of a
133/// machine frame. `UWOP_PUSH_MACHFRAME` records that simulation, which
134/// indicates the machine has conceptually done this operation:
135///
136/// 1. Pop RIP return address from top of stack into *Temp*
137/// 2. Push SS
138/// 3. Push old RSP
139/// 4. Push EFLAGS
140/// 5. Push CS
141/// 6. Push *Temp*
142/// 7. Push Error Code (if op info equals 1)
143///
144/// The simulated `UWOP_PUSH_MACHFRAME` operation decrements RSP by 40 (op info
145/// equals 0) or 48 (op info equals 1).
146pub const UWOP_PUSH_MACHFRAME: u8 = 10;
147
148// UNW_FLAG values.
149
150/// The function has no handler.
151pub const UNW_FLAG_NHANDLER: u8 = 0x0;
152
153/// The function has an exception handler that should be called.
154pub const UNW_FLAG_EHANDLER: u8 = 0x1;
155
156/// The function has a termination handler that should be called when unwinding an exception.
157pub const UNW_FLAG_UHANDLER: u8 = 0x2;
158
159/// The FunctionEntry member is the contents of a previous function table entry.
160pub const UNW_FLAG_CHAININFO: u8 = 0x4;
161
162/// Corresponds to the first 4 bytes of `UNWIND_INFO`.
163#[allow(non_camel_case_types, non_snake_case)]
164#[repr(C)]
165#[derive(Debug, Clone, Copy, FromBytes, IntoBytes)]
166pub struct UNWIND_INFO {
167    /// UCHAR Version : 3;
168    /// UCHAR Flags : 5;
169    pub VersionAndFlags: u8,
170
171    /// UCHAR SizeOfProlog;
172    pub SizeOfProlog: u8,
173
174    /// UCHAR CountOfCodes;
175    pub CountOfCodes: u8,
176
177    /// UCHAR FrameRegister : 4;
178    /// UCHAR FrameOffset : 4;
179    pub FrameRegisterAndOffset: u8,
180}
181
182impl UNWIND_INFO {
183    /// Returns the version field (bits 0-2).
184    pub fn version(&self) -> u8 {
185        self.VersionAndFlags & 0x07
186    }
187
188    /// Returns the flags field (bits 3-7).
189    pub fn flags(&self) -> u8 {
190        (self.VersionAndFlags >> 3) & 0x1f
191    }
192
193    /// Returns the size of the prolog in bytes.
194    pub fn size_of_prolog(&self) -> u8 {
195        self.SizeOfProlog
196    }
197
198    /// Returns the number of unwind codes.
199    pub fn count_of_codes(&self) -> u8 {
200        self.CountOfCodes
201    }
202
203    /// Returns the frame register index (bits 0-3), or 0 if none.
204    pub fn frame_register(&self) -> u8 {
205        self.FrameRegisterAndOffset & 0x0f
206    }
207
208    /// Returns the scaled frame offset (bits 4-7).
209    pub fn frame_offset(&self) -> u8 {
210        (self.FrameRegisterAndOffset >> 4) & 0x0f
211    }
212
213    /// Returns the byte offset past the unwind codes where a chained
214    /// `RUNTIME_FUNCTION` or handler data starts. The codes are
215    /// 2 bytes each, aligned to a 4-byte boundary.
216    pub fn codes_end_offset(&self) -> u32 {
217        let aligned = self.CountOfCodes as u32 + (self.CountOfCodes as u32 & 1);
218        4 + aligned * 2
219    }
220}
221
222// x64 register encoding indices, matching the CPU instruction encoding
223// (REX.B + reg field) and the UNWIND_CODE.OpInfo numbering. These are
224// used to index into the unwind context when processing unwind codes.
225
226/// Index of `AMD64_CONTEXT::Rax`.
227pub const REG_RAX: u8 = 0;
228
229/// Index of `AMD64_CONTEXT::Rcx`.
230pub const REG_RCX: u8 = 1;
231
232/// Index of `AMD64_CONTEXT::Rdx`.
233pub const REG_RDX: u8 = 2;
234
235/// Index of `AMD64_CONTEXT::Rbx`.
236pub const REG_RBX: u8 = 3;
237
238/// Index of `AMD64_CONTEXT::Rsp`.
239pub const REG_RSP: u8 = 4;
240
241/// Index of `AMD64_CONTEXT::Rbp`.
242pub const REG_RBP: u8 = 5;
243
244/// Index of `AMD64_CONTEXT::Rsi`.
245pub const REG_RSI: u8 = 6;
246
247/// Index of `AMD64_CONTEXT::Rdi`.
248pub const REG_RDI: u8 = 7;
249
250/// Index of `AMD64_CONTEXT::R8`.
251pub const REG_R8: u8 = 8;
252
253/// Index of `AMD64_CONTEXT::R9`.
254pub const REG_R9: u8 = 9;
255
256/// Index of `AMD64_CONTEXT::R10`.
257pub const REG_R10: u8 = 10;
258
259/// Index of `AMD64_CONTEXT::R11`.
260pub const REG_R11: u8 = 11;
261
262/// Index of `AMD64_CONTEXT::R12`.
263pub const REG_R12: u8 = 12;
264
265/// Index of `AMD64_CONTEXT::R13`.
266pub const REG_R13: u8 = 13;
267
268/// Index of `AMD64_CONTEXT::R14`.
269pub const REG_R14: u8 = 14;
270
271/// Index of `AMD64_CONTEXT::R15`.
272pub const REG_R15: u8 = 15;
273
274/// Unwind context for x64 (AMD64).
275///
276/// Holds the register state needed for stack unwinding, including
277/// the instruction pointer, stack pointer, and all callee-saved
278/// (non-volatile) general-purpose registers.
279#[derive(Debug, Clone)]
280pub struct UnwindContextAmd64 {
281    /// Instruction pointer (RIP).
282    pub rip: u64,
283    /// Stack pointer (RSP).
284    pub rsp: u64,
285    /// Base pointer (RBP) - callee-saved.
286    pub rbp: u64,
287    /// RBX - callee-saved.
288    pub rbx: u64,
289    /// RSI - callee-saved.
290    pub rsi: u64,
291    /// RDI - callee-saved.
292    pub rdi: u64,
293    /// R12 - callee-saved.
294    pub r12: u64,
295    /// R13 - callee-saved.
296    pub r13: u64,
297    /// R14 - callee-saved.
298    pub r14: u64,
299    /// R15 - callee-saved.
300    pub r15: u64,
301}
302
303impl From<&Registers> for UnwindContextAmd64 {
304    fn from(value: &Registers) -> Self {
305        Self {
306            rip: value.rip,
307            rsp: value.rsp,
308            rbp: value.rbp,
309            rbx: value.rbx,
310            rsi: value.rsi,
311            rdi: value.rdi,
312            r12: value.r12,
313            r13: value.r13,
314            r14: value.r14,
315            r15: value.r15,
316        }
317    }
318}
319
320impl UnwindContextAmd64 {
321    /// Returns the value of a register by its x64 encoding index.
322    pub fn get_register(&self, reg: u8) -> u64 {
323        match reg {
324            REG_RBX => self.rbx,
325            REG_RBP => self.rbp,
326            REG_RSI => self.rsi,
327            REG_RDI => self.rdi,
328            REG_R12 => self.r12,
329            REG_R13 => self.r13,
330            REG_R14 => self.r14,
331            REG_R15 => self.r15,
332            _ => 0,
333        }
334    }
335
336    /// Sets the value of a register by its x64 encoding index.
337    ///
338    /// Only updates non-volatile registers; volatile registers are ignored.
339    pub fn set_register(&mut self, reg: u8, value: u64) {
340        match reg {
341            REG_RBX => self.rbx = value,
342            REG_RBP => self.rbp = value,
343            REG_RSI => self.rsi = value,
344            REG_RDI => self.rdi = value,
345            REG_R12 => self.r12 = value,
346            REG_R13 => self.r13 = value,
347            REG_R14 => self.r14 = value,
348            REG_R15 => self.r15 = value,
349            _ => {} // volatile registers - ignore
350        }
351    }
352}
353
354/// x64 stack unwinder.
355///
356/// Implements stack unwinding for the Windows x64 ABI by reading
357/// .pdata RUNTIME_FUNCTION entries and processing UNWIND_INFO
358/// structures to recover the caller's register state.
359pub struct UnwinderAmd64;
360
361impl<Driver> Unwinder<Driver> for UnwinderAmd64
362where
363    Driver: VmiRead,
364    Driver::Architecture: ArchAdapter<Driver>,
365{
366    type Context = UnwindContextAmd64;
367
368    fn unwind(
369        &self,
370        vmi: &VmiState<WindowsOs<Driver>>,
371        image_base: Va,
372        image: &impl PeImage,
373        context: &mut UnwindContextAmd64,
374    ) -> Result<Unwound, VmiError> {
375        // Compute RVA of the current instruction pointer.
376        let rva = context.rip.saturating_sub(image_base.0) as u32;
377
378        // Look up RUNTIME_FUNCTION for this RVA.
379        let exception_dir = image.exception_directory()?;
380        let runtime_function = exception_dir.as_ref().and_then(|dir| dir.find(rva));
381
382        let entry = match runtime_function {
383            Some(entry) => entry,
384            None => {
385                // Leaf function: return address is at [RSP].
386                return unwind_leaf(vmi, context);
387            }
388        };
389
390        let begin_address = entry.begin_address;
391        let unwind_data_rva = entry.unwind_info_address_or_data;
392        let mut unwind_rva = unwind_data_rva;
393
394        // RIP offset within the function, for prolog detection.
395        // Only meaningful for the first (non-chained) UNWIND_INFO.
396        let rip_offset = rva.saturating_sub(begin_address);
397        let mut is_first = true;
398        let mut machine_frame = false;
399        let mut chain_count = 0u32;
400
401        loop {
402            let header = image.read_struct_at_rva::<UNWIND_INFO>(unwind_rva)?;
403
404            let flags = header.flags();
405            let size_of_prolog = header.size_of_prolog();
406            let count_of_codes = header.count_of_codes() as usize;
407            let frame_register = header.frame_register();
408            let frame_offset = header.frame_offset();
409
410            // Read unwind codes.
411            let codes_size = count_of_codes * 2;
412            let mut codes_data = vec![0u8; codes_size];
413            if codes_size > 0 {
414                image.read_at_rva(unwind_rva + 4, &mut codes_data)?;
415            }
416
417            // Compute FrameBase (per Microsoft spec, computed ONCE before
418            // processing codes). SAVE_NONVOL offsets are relative to this.
419            //
420            // `wrapping_sub`: when the prolog has not yet executed the LEA
421            // that establishes the frame register, the register holds an
422            // unrelated value that may be smaller than `frame_offset * 16`.
423            // Frame base is unread in that case (the gate at line below
424            // and the `code_offset > rip_offset` skip in the loop both
425            // exclude it), so an underflowing computation is harmless.
426            let frame_base = if frame_register != 0 {
427                context
428                    .get_register(frame_register)
429                    .wrapping_sub((frame_offset as u64) * 16)
430            }
431            else {
432                context.rsp
433            };
434
435            // If frame_register is set and we are past the prolog (or in
436            // a chained entry), restore RSP from the frame register.
437            if frame_register != 0 && (!is_first || rip_offset >= size_of_prolog as u32) {
438                context.rsp = frame_base;
439            }
440
441            // Process unwind codes.
442            let mut slot = 0;
443            while slot < count_of_codes {
444                let code_offset = codes_data[slot * 2];
445                let op_info = codes_data[slot * 2 + 1];
446                let op = op_info & 0x0f;
447                let info = (op_info >> 4) & 0x0f;
448
449                // In the first (non-chained) entry, if we are in the prolog,
450                // skip codes whose instructions have not yet executed.
451                if is_first
452                    && (rip_offset < size_of_prolog as u32)
453                    && (code_offset as u32 > rip_offset)
454                {
455                    slot += slots_for_op(op, info);
456                    continue;
457                }
458
459                match op {
460                    UWOP_PUSH_NONVOL => {
461                        let value = vmi.read_u64(Va(context.rsp))?;
462                        context.set_register(info, value);
463                        context.rsp += 8;
464                        slot += 1;
465                    }
466                    UWOP_ALLOC_LARGE => {
467                        if info == 0 {
468                            let alloc = read_u16_from_codes(&codes_data, slot + 1) as u64 * 8;
469                            context.rsp += alloc;
470                            slot += 2;
471                        }
472                        else {
473                            let alloc = read_u32_from_codes(&codes_data, slot + 1) as u64;
474                            context.rsp += alloc;
475                            slot += 3;
476                        }
477                    }
478                    UWOP_ALLOC_SMALL => {
479                        context.rsp += info as u64 * 8 + 8;
480                        slot += 1;
481                    }
482                    UWOP_SET_FPREG => {
483                        // Restore RSP from the frame register.
484                        // frame_base was already computed with the same formula.
485                        context.rsp = frame_base;
486                        slot += 1;
487                    }
488                    UWOP_SAVE_NONVOL => {
489                        // Offset is relative to FrameBase, NOT current RSP.
490                        // `wrapping_add`: if frame_base underflowed above,
491                        // the resulting address is bogus and the read will
492                        // fail with VmiError instead of panicking.
493                        let offset = read_u16_from_codes(&codes_data, slot + 1) as u64 * 8;
494                        let value = vmi.read_u64(Va(frame_base.wrapping_add(offset)))?;
495                        context.set_register(info, value);
496                        slot += 2;
497                    }
498                    UWOP_SAVE_NONVOL_FAR => {
499                        // Offset is relative to FrameBase, NOT current RSP.
500                        // `wrapping_add`: see UWOP_SAVE_NONVOL above.
501                        let offset = read_u32_from_codes(&codes_data, slot + 1) as u64;
502                        let value = vmi.read_u64(Va(frame_base.wrapping_add(offset)))?;
503                        context.set_register(info, value);
504                        slot += 3;
505                    }
506                    UWOP_EPILOG => {
507                        // v2 epilog descriptor - skip.
508                        slot += 2;
509                    }
510                    UWOP_SPARE_CODE => {
511                        // Reserved/undocumented opcode, consumes 3 slots.
512                        slot += 3;
513                    }
514                    UWOP_SAVE_XMM128 => {
515                        slot += 2;
516                    }
517                    UWOP_SAVE_XMM128_FAR => {
518                        slot += 3;
519                    }
520                    UWOP_PUSH_MACHFRAME => {
521                        if info == 1 {
522                            context.rsp += 8; // skip error code
523                        }
524                        // Machine frame: RIP at [RSP], RSP at [RSP+24].
525                        let new_rip = vmi.read_u64(Va(context.rsp))?;
526                        let new_rsp = vmi.read_u64(Va(context.rsp + 24))?;
527                        context.rip = new_rip;
528                        context.rsp = new_rsp;
529                        machine_frame = true;
530                        slot += 1;
531                    }
532                    _ => {
533                        tracing::warn!(op, info, "unknown unwind opcode");
534                        slot += 1;
535                    }
536                }
537            }
538
539            // If no chained info, we are done processing codes.
540            if flags & UNW_FLAG_CHAININFO == 0 {
541                break;
542            }
543
544            // Follow the chain: RUNTIME_FUNCTION is after the unwind codes.
545            let chain_rva = unwind_rva + header.codes_end_offset();
546
547            let mut chain_buf = [0u8; 12];
548            image.read_at_rva(chain_rva, &mut chain_buf)?;
549
550            unwind_rva = u32::from_le_bytes(chain_buf[8..12].try_into().unwrap());
551            is_first = false;
552            chain_count += 1;
553
554            if chain_count > UNWIND_CHAIN_LIMIT {
555                tracing::warn!("unwind chain limit exceeded");
556                break;
557            }
558        }
559
560        // If a machine frame was encountered, RIP/RSP are already set.
561        if machine_frame {
562            if context.rip == 0 {
563                return Ok(Unwound::MachineEnd);
564            }
565
566            // Read home space from the caller's RSP.
567            let params = read_params(vmi, Va(context.rsp));
568
569            return Ok(Unwound::Frame(Frame {
570                instruction_pointer: Va(context.rip),
571                stack_pointer: Va(context.rsp),
572                params,
573                machine_frame: true,
574            }));
575        }
576
577        // Pop return address from [RSP].
578        let return_addr = vmi.read_u64(Va(context.rsp))?;
579        context.rsp += 8;
580
581        if return_addr == 0 {
582            return Ok(Unwound::End);
583        }
584
585        context.rip = return_addr;
586
587        // Read home space from the caller's RSP (after popping the
588        // return address, RSP points to the start of the home space).
589        let params = read_params(vmi, Va(context.rsp));
590
591        Ok(Unwound::Frame(Frame {
592            instruction_pointer: Va(return_addr),
593            stack_pointer: Va(context.rsp),
594            params,
595            machine_frame: false,
596        }))
597    }
598}
599
600/// Reads the four parameter home-space values from the caller's stack.
601///
602/// Called after unwinding and popping the return address, so RSP
603/// points to the start of the home space (P1Home at RSP+0).
604/// Returns zeros for any unreadable slots.
605pub fn read_params<Driver>(vmi: &VmiState<WindowsOs<Driver>>, rsp: Va) -> [u64; 4]
606where
607    Driver: VmiRead,
608    Driver::Architecture: ArchAdapter<Driver>,
609{
610    // Params are informational only - don't abort the unwind on failure.
611    [
612        vmi.read_u64(rsp).unwrap_or(0),
613        vmi.read_u64(rsp + 8).unwrap_or(0),
614        vmi.read_u64(rsp + 16).unwrap_or(0),
615        vmi.read_u64(rsp + 24).unwrap_or(0),
616    ]
617}
618
619/// Unwinds a leaf function (no RUNTIME_FUNCTION entry).
620///
621/// For leaf functions, the return address is at `[RSP]` and the
622/// caller's RSP is `RSP + 8`. This is also useful as a fallback
623/// when .pdata is unavailable (e.g., file-backed pages missing
624/// from a crash dump).
625pub fn unwind_leaf<Driver>(
626    vmi: &VmiState<WindowsOs<Driver>>,
627    context: &mut UnwindContextAmd64,
628) -> Result<Unwound, VmiError>
629where
630    Driver: VmiRead,
631    Driver::Architecture: ArchAdapter<Driver>,
632{
633    let return_addr = vmi.read_u64(Va(context.rsp))?;
634    context.rsp += 8;
635
636    if return_addr == 0 {
637        return Ok(Unwound::End);
638    }
639
640    context.rip = return_addr;
641
642    // Read home space from the caller's RSP.
643    let params = read_params(vmi, Va(context.rsp));
644
645    Ok(Unwound::Frame(Frame {
646        instruction_pointer: Va(return_addr),
647        stack_pointer: Va(context.rsp),
648        params,
649        machine_frame: false,
650    }))
651}
652
653/// Resolves an RVA to the primary function's BeginAddress by following
654/// `UNW_FLAG_CHAININFO` chains in the exception directory.
655///
656/// MSVC can split a single function into non-contiguous chunks scattered
657/// across the binary (common with SEH and large functions). Each chunk
658/// has its own `RUNTIME_FUNCTION` entry in `.pdata`, with the chunk's
659/// `UNWIND_INFO` carrying the `UNW_FLAG_CHAININFO` flag and a pointer
660/// to the parent entry. This function follows that chain back to the
661/// primary (non-chained) entry whose `BeginAddress` is the real
662/// function start - the address that maps to the symbol in the PDB.
663///
664/// Returns `None` if the RVA is not covered by any `RUNTIME_FUNCTION`.
665pub fn resolve_primary_function(image: &impl PeImage, rva: u32) -> Result<Option<u32>, VmiError> {
666    let exception_dir = match image.exception_directory()? {
667        Some(exception_dir) => exception_dir,
668        None => return Ok(None),
669    };
670
671    let entry = match exception_dir.find(rva) {
672        Some(entry) => entry,
673        None => return Ok(None),
674    };
675
676    let mut begin_address = entry.begin_address;
677    let mut unwind_rva = entry.unwind_info_address_or_data;
678
679    for _ in 0..UNWIND_CHAIN_LIMIT {
680        let header = image.read_struct_at_rva::<UNWIND_INFO>(unwind_rva)?;
681
682        if header.flags() & UNW_FLAG_CHAININFO == 0 {
683            break;
684        }
685
686        let entry = image.read_struct_at_rva::<ImageRuntimeFunctionEntry>(
687            unwind_rva + header.codes_end_offset(),
688        )?;
689
690        begin_address = entry.begin_address;
691        unwind_rva = entry.unwind_info_address_or_data;
692    }
693
694    Ok(Some(begin_address))
695}
696
697/// Returns the number of code slots consumed by an unwind operation.
698fn slots_for_op(op: u8, info: u8) -> usize {
699    match op {
700        UWOP_PUSH_NONVOL => 1,
701        UWOP_ALLOC_LARGE => {
702            if info == 0 {
703                2
704            }
705            else {
706                3
707            }
708        }
709        UWOP_ALLOC_SMALL => 1,
710        UWOP_SET_FPREG => 1,
711        UWOP_SAVE_NONVOL => 2,
712        UWOP_SAVE_NONVOL_FAR => 3,
713        UWOP_SPARE_CODE => 3,
714        UWOP_SAVE_XMM128 => 2,
715        UWOP_SAVE_XMM128_FAR => 3,
716        UWOP_PUSH_MACHFRAME => 1,
717        _ => 1,
718    }
719}
720
721/// Reads a u16 from the unwind code data at the given slot index.
722///
723/// Each slot is 2 bytes. The u16 is stored in little-endian format.
724fn read_u16_from_codes(data: &[u8], slot: usize) -> u16 {
725    let offset = slot * 2;
726    if offset + 2 <= data.len() {
727        u16::from_le_bytes([data[offset], data[offset + 1]])
728    }
729    else {
730        0
731    }
732}
733
734/// Reads a u32 from the unwind code data at the given slot index.
735///
736/// The u32 spans two consecutive slots (4 bytes) in little-endian format.
737fn read_u32_from_codes(data: &[u8], slot: usize) -> u32 {
738    let offset = slot * 2;
739    if offset + 4 <= data.len() {
740        u32::from_le_bytes([
741            data[offset],
742            data[offset + 1],
743            data[offset + 2],
744            data[offset + 3],
745        ])
746    }
747    else {
748        0
749    }
750}