framehop/x86_64/instruction_analysis/
epilogue.rs

1use super::super::unwind_rule::UnwindRuleX86_64;
2
3pub fn unwind_rule_from_detected_epilogue(
4    text_bytes: &[u8],
5    pc_offset: usize,
6) -> Option<UnwindRuleX86_64> {
7    let (slice_from_start, slice_to_end) = text_bytes.split_at(pc_offset);
8
9    let mut sp_offset_by_8 = 0;
10    let mut bp_offset_by_8 = None;
11    let mut bytes = slice_to_end;
12    loop {
13        if bytes.is_empty() {
14            return None;
15        }
16
17        // Detect ret
18        if bytes[0] == 0xc3 {
19            break;
20        }
21        // Detect jmp
22        if bytes[0] == 0xeb || bytes[0] == 0xe9 || bytes[0] == 0xff {
23            // This could be a tail call, or just a regular jump inside the current function.
24            // Ideally, we would check whether the jump target is inside this function.
25            // But this would require having an accurate idea of where the current function
26            // starts and ends.
27            // For now, we instead use the following heuristic: Any jmp that directly follows
28            // a `pop` instruction is treated as a tail call.
29            if sp_offset_by_8 != 0 {
30                // We have detected a pop in the previous loop iteration.
31                break;
32            }
33            // This must be the first iteration. Look backwards.
34            if let Some(potential_pop_byte) = slice_from_start.last() {
35                // Get the previous byte. We have no idea how long the previous instruction
36                // is, so we might be looking at a random last byte of a wider instruction.
37                // Let's just pray that this is not the case.
38                if potential_pop_byte & 0xf8 == 0x58 {
39                    // Assuming we haven't just misinterpreted the last byte of a wider
40                    // instruction, this is a `pop rXX`.
41                    break;
42                }
43            }
44            return None;
45        }
46        // Detect pop rbp
47        if bytes[0] == 0x5d {
48            bp_offset_by_8 = Some(sp_offset_by_8 as i16);
49            sp_offset_by_8 += 1;
50            bytes = &bytes[1..];
51            continue;
52        }
53        // Detect pop rXX
54        if (0x58..=0x5f).contains(&bytes[0]) {
55            sp_offset_by_8 += 1;
56            bytes = &bytes[1..];
57            continue;
58        }
59        // Detect pop rXX with prefix
60        if bytes.len() >= 2 && bytes[0] & 0xfe == 0x40 && bytes[1] & 0xf8 == 0x58 {
61            sp_offset_by_8 += 1;
62            bytes = &bytes[2..];
63            continue;
64        }
65        // Unexpected instruction.
66        // This probably means that we weren't in an epilogue after all.
67        return None;
68    }
69
70    // We've found the return or the tail call.
71    let rule = if sp_offset_by_8 == 0 {
72        UnwindRuleX86_64::JustReturn
73    } else {
74        sp_offset_by_8 += 1; // Add one for popping the return address.
75        if let Some(bp_storage_offset_from_sp_by_8) = bp_offset_by_8 {
76            UnwindRuleX86_64::OffsetSpAndRestoreBp {
77                sp_offset_by_8,
78                bp_storage_offset_from_sp_by_8,
79            }
80        } else {
81            UnwindRuleX86_64::OffsetSp { sp_offset_by_8 }
82        }
83    };
84    Some(rule)
85}