Skip to main content

codehelion_artifact/
x86.rs

1//! Shared x86 instruction-shape normalization for native artifact backends.
2//!
3//! ELF, Mach-O, and PE/COFF all describe the same instruction stream once a
4//! symbol's code bytes have been isolated. Keeping normalization here gives a
5//! byte sequence one meaning across those container formats; backend-specific
6//! implementations must not silently reuse a version label for different
7//! encodings.
8
9use iced_x86::{Decoder, DecoderOptions, OpKind};
10use object::Architecture;
11
12use crate::NormalizedInstructions;
13
14/// Version of the x86 instruction-shape normalization representation.
15pub const X86_NORMALIZATION_VERSION: &str = "x86-operand-shape-v1";
16
17/// Whether this architecture has a supported normalized-instruction recipe.
18#[must_use]
19pub const fn supports_normalized_duplicates(architecture: Architecture) -> bool {
20    matches!(architecture, Architecture::I386 | Architecture::X86_64)
21}
22
23/// Normalize an x86 instruction stream without retaining immediate values or
24/// register choices.
25///
26/// `None` means either that the architecture is not x86 or the byte stream
27/// does not decode into complete instructions. It is a fact about the bytes,
28/// not a fallback to a lossy best-effort representation.
29#[must_use]
30pub fn normalize_x86(code: &[u8], architecture: Architecture) -> Option<NormalizedInstructions> {
31    let bitness = match architecture {
32        Architecture::I386 => 32,
33        Architecture::X86_64 => 64,
34        _ => return None,
35    };
36    let mut decoder = Decoder::with_ip(bitness, code, 0, DecoderOptions::NONE);
37    let mut normalized = Vec::new();
38    while decoder.can_decode() {
39        let instruction = decoder.decode();
40        if instruction.is_invalid() {
41            return None;
42        }
43        normalized.extend((instruction.code() as u32).to_le_bytes());
44        normalized.push(u8::try_from(instruction.op_count()).ok()?);
45        for operand in 0..instruction.op_count() {
46            let kind = instruction.op_kind(operand);
47            normalized.push(kind as u8);
48            if kind == OpKind::Memory {
49                // Register choices and immediate displacements are not kept;
50                // address width and scale preserve the operand's shape.
51                normalized.push(instruction.memory_size() as u8);
52                normalized.push(u8::try_from(instruction.memory_index_scale()).ok()?);
53                normalized.push(u8::try_from(instruction.memory_displ_size()).ok()?);
54            }
55        }
56    }
57    Some(NormalizedInstructions {
58        version: X86_NORMALIZATION_VERSION.to_owned(),
59        bytes: normalized,
60    })
61}
62
63/// Remove conventional trailing alignment bytes from an inferred x86 range.
64///
65/// Explicit symbol sizes are authoritative. This applies only when a native
66/// format supplied no size and the next symbol or section boundary was used.
67#[must_use]
68pub fn trim_inferred_padding(code: &[u8], architecture: Architecture) -> &[u8] {
69    if !matches!(architecture, Architecture::I386 | Architecture::X86_64) {
70        return code;
71    }
72    let end = code
73        .iter()
74        .rposition(|byte| !matches!(byte, 0x00 | 0x90 | 0xcc))
75        .map_or(0, |index| index + 1);
76    &code[..end]
77}
78
79#[cfg(test)]
80#[allow(clippy::unwrap_used)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn normalization_ignores_immediate_values_but_not_instruction_shape() {
86        let first = normalize_x86(&[0xb8, 1, 0, 0, 0, 0xc3], Architecture::X86_64).unwrap();
87        let second = normalize_x86(&[0xb8, 2, 0, 0, 0, 0xc3], Architecture::X86_64).unwrap();
88        let call = normalize_x86(&[0xe8, 1, 0, 0, 0, 0xc3], Architecture::X86_64).unwrap();
89
90        assert_eq!(first.version, X86_NORMALIZATION_VERSION);
91        assert_eq!(first, second);
92        assert_ne!(first, call);
93        assert!(normalize_x86(&[0x0f], Architecture::X86_64).is_none());
94        assert!(normalize_x86(&[0xc3], Architecture::Aarch64).is_none());
95    }
96
97    #[test]
98    fn inferred_x86_ranges_drop_only_conventional_trailing_padding() {
99        assert_eq!(
100            trim_inferred_padding(&[0x90, 0xc3, 0x00, 0x90, 0xcc], Architecture::X86_64),
101            &[0x90, 0xc3]
102        );
103        assert_eq!(
104            trim_inferred_padding(&[0xc3, 0x00], Architecture::Aarch64),
105            &[0xc3, 0x00]
106        );
107    }
108
109    #[test]
110    fn normalized_duplicate_capability_is_explicit_for_each_architecture() {
111        assert!(supports_normalized_duplicates(Architecture::X86_64));
112        assert!(!supports_normalized_duplicates(Architecture::Aarch64));
113    }
114}