Skip to main content

codehelion_artifact/
native.rs

1//! Shared native-object collection helpers.
2
3use std::collections::BTreeMap;
4
5use object::{Object, ObjectSection, ObjectSymbol, SymbolKind};
6
7use crate::symbols::demangle;
8use crate::x86::normalize_x86;
9use crate::{
10    ArtifactDataSegment, ArtifactFingerprint, ArtifactImport, ArtifactImportKind, ArtifactIr,
11    ArtifactRelocation, ArtifactSection, ArtifactSymbol, NormalizedInstructions,
12};
13
14/// Collect undefined native-object symbols as imports.
15///
16/// A text symbol names a function across ELF, Mach-O, and PE/COFF. Other
17/// undefined symbol kinds remain deliberately conservative. If a malformed
18/// object presents the same name with conflicting kinds, `function` wins: it
19/// retains the stronger parser evidence without inventing a signature.
20pub fn collect_undefined_imports<'data, S>(
21    symbols: impl IntoIterator<Item = S>,
22    ir: &mut ArtifactIr,
23) where
24    S: ObjectSymbol<'data>,
25{
26    let mut names = BTreeMap::new();
27    for symbol in symbols {
28        if !symbol.is_undefined() {
29            continue;
30        }
31        let Some(name) = symbol
32            .name()
33            .ok()
34            .filter(|name| !name.is_empty())
35            .map(demangle)
36        else {
37            continue;
38        };
39        // Mach-O and COFF do not retain a section-derived text kind for an
40        // undefined external in their object symbol table. `Unknown` is the
41        // corresponding native import spelling, while data remains explicit.
42        let kind = if matches!(symbol.kind(), SymbolKind::Text | SymbolKind::Unknown) {
43            ArtifactImportKind::Function
44        } else {
45            ArtifactImportKind::Other
46        };
47        names
48            .entry(name)
49            .and_modify(|existing| {
50                if kind == ArtifactImportKind::Function {
51                    *existing = kind;
52                }
53            })
54            .or_insert(kind);
55    }
56    ir.imports
57        .extend(names.into_iter().map(|(name, kind)| ArtifactImport {
58            module: None,
59            name: Some(name),
60            kind,
61        }));
62}
63
64/// Copy the section, read-only data, and relocation facts common to native
65/// object formats into format-neutral IR.
66///
67/// # Errors
68///
69/// Returns an object-reader error when a section's bytes cannot be read.
70pub fn collect_sections(file: &object::File<'_>, ir: &mut ArtifactIr) -> Result<(), object::Error> {
71    for section in file.sections() {
72        let (offset, size) = section.file_range().unwrap_or((0, 0));
73        ir.sections.push(ArtifactSection {
74            name: section.name().ok().map(str::to_owned),
75            offset,
76            size,
77            executable: section.kind() == object::SectionKind::Text,
78        });
79        if section.kind() == object::SectionKind::ReadOnlyData {
80            let data = section.data()?;
81            if !data.is_empty() {
82                ir.data_segments.push(ArtifactDataSegment {
83                    fingerprint: data_fingerprint(section.name().ok(), data),
84                    section: u32::try_from(section.index().0).ok(),
85                    offset,
86                    bytes: data.to_vec(),
87                });
88            }
89        }
90        for (relocation_offset, relocation) in section.relocations() {
91            ir.relocations.push(ArtifactRelocation {
92                section: u32::try_from(section.index().0).ok(),
93                offset: offset.saturating_add(relocation_offset),
94                kind: format!("{:?}", relocation.kind()),
95                target: relocation_target_name(file, relocation.target()),
96            });
97        }
98    }
99    Ok(())
100}
101
102/// Build the common native-symbol identity from unambiguous fields.
103///
104/// The representation includes an explicit payload kind, so a normalized
105/// instruction representation cannot collide with an equal-looking raw byte
106/// sequence. Each variable-length field is length-prefixed to preserve the
107/// boundary between adjacent fields.
108#[must_use]
109pub fn symbol_fingerprint(
110    name: Option<&str>,
111    section: Option<&str>,
112    normalized: Option<&NormalizedInstructions>,
113    code: &[u8],
114) -> ArtifactFingerprint {
115    let mut identity = Vec::new();
116    append_field(&mut identity, name.unwrap_or_default().as_bytes());
117    append_field(&mut identity, section.unwrap_or_default().as_bytes());
118    if let Some(normalized) = normalized {
119        identity.push(1);
120        append_field(&mut identity, normalized.version.as_bytes());
121        append_field(&mut identity, &normalized.bytes);
122    } else {
123        identity.push(0);
124        append_field(&mut identity, code);
125    }
126    ArtifactFingerprint::from_content("native-symbol", &identity)
127}
128
129/// Build the common native data-segment identity from its section and bytes.
130#[must_use]
131pub fn data_fingerprint(section: Option<&str>, data: &[u8]) -> ArtifactFingerprint {
132    let mut identity = Vec::new();
133    append_field(&mut identity, section.unwrap_or_default().as_bytes());
134    append_field(&mut identity, data);
135    ArtifactFingerprint::from_content("native-data", &identity)
136}
137
138/// Determine a native symbol's byte extent without attributing bytes to a
139/// zero-size alias.
140///
141/// A declared size always wins. For a boundary inferred from neighbouring
142/// symbols, only the next strictly greater address delimits the range. If an
143/// explicit definition shares the address, or an earlier zero-size symbol has
144/// already claimed that inferred range, this symbol is an alias and remains
145/// explicitly zero-sized.
146#[must_use]
147pub fn symbol_size(
148    address: u64,
149    declared_size: u64,
150    is_zero_size_alias: bool,
151    following_addresses: impl IntoIterator<Item = u64>,
152    section_end: u64,
153) -> u64 {
154    if declared_size != 0 {
155        return declared_size;
156    }
157    if is_zero_size_alias {
158        return 0;
159    }
160    following_addresses
161        .into_iter()
162        .find(|candidate| *candidate > address)
163        .unwrap_or(section_end)
164        .saturating_sub(address)
165}
166
167/// Remove only conventional trailing padding from an inferred native symbol.
168///
169/// Explicit symbol sizes are authoritative and must not use this helper.
170#[must_use]
171pub fn trim_inferred_symbol_padding(code: &[u8], architecture: object::Architecture) -> &[u8] {
172    crate::x86::trim_inferred_padding(code, architecture)
173}
174
175/// Transient native-symbol data used only by format-specific join code.
176#[derive(Debug, Clone, Copy)]
177pub struct NativeSymbolRange {
178    /// Stable identity assigned to the symbol.
179    pub fingerprint: ArtifactFingerprint,
180    /// Parser-local symbol index.
181    pub index: object::SymbolIndex,
182    /// Parser-local section index used to disambiguate relocatable addresses.
183    pub section: object::SectionIndex,
184    /// Parser address used only for local joins.
185    pub address: u64,
186    /// Retained code length.
187    pub size: u64,
188}
189
190/// Collect text symbols using the shared native identity and boundary rules.
191///
192/// # Errors
193///
194/// Returns an error when a text section cannot be read.
195pub fn collect_text_symbols(
196    file: &object::File<'_>,
197    ir: &mut ArtifactIr,
198) -> Result<Vec<NativeSymbolRange>, object::Error> {
199    let mut ranges = Vec::new();
200    for section in file
201        .sections()
202        .filter(|section| section.kind() == object::SectionKind::Text)
203    {
204        let section_index = section.index();
205        let data = section.data()?;
206        let (section_offset, _) = section.file_range().unwrap_or((0, 0));
207        let mut symbols: Vec<_> = file
208            .symbols()
209            .filter(|symbol| {
210                symbol.section_index() == Some(section_index)
211                    && symbol.kind() == SymbolKind::Text
212                    && !symbol.is_undefined()
213            })
214            .collect();
215        symbols.sort_by_key(ObjectSymbol::address);
216        for (position, symbol) in symbols.iter().enumerate() {
217            let Some(relative) = symbol.address().checked_sub(section.address()) else {
218                continue;
219            };
220            let alias = symbol.size() == 0
221                && (symbols[..position]
222                    .iter()
223                    .any(|prior| prior.address() == symbol.address())
224                    || symbols
225                        .iter()
226                        .any(|other| other.address() == symbol.address() && other.size() != 0));
227            let size = symbol_size(
228                symbol.address(),
229                symbol.size(),
230                alias,
231                symbols[position.saturating_add(1)..]
232                    .iter()
233                    .map(ObjectSymbol::address),
234                section
235                    .address()
236                    .saturating_add(u64::try_from(data.len()).unwrap_or(u64::MAX)),
237            );
238            let (Ok(start), Ok(size)) = (usize::try_from(relative), usize::try_from(size)) else {
239                continue;
240            };
241            let Some(raw) = data.get(start..start.saturating_add(size)) else {
242                continue;
243            };
244            let code = if symbol.size() == 0 {
245                trim_inferred_symbol_padding(raw, file.architecture())
246            } else {
247                raw
248            };
249            if code.is_empty() && symbol.size() != 0 {
250                continue;
251            }
252            let raw_name = symbol.name().ok().filter(|name| !name.is_empty());
253            let name = raw_name.map(demangle);
254            let fingerprint_name = raw_name.map(|name| {
255                let canonical = if file.format() == object::BinaryFormat::MachO {
256                    name.strip_prefix('_').unwrap_or(name)
257                } else {
258                    name
259                };
260                demangle(canonical)
261            });
262            let normalized = normalize_x86(code, file.architecture());
263            let fingerprint = symbol_fingerprint(
264                fingerprint_name.as_deref(),
265                Some("text"),
266                normalized.as_ref(),
267                code,
268            );
269            let size = u64::try_from(code.len()).unwrap_or(u64::MAX);
270            ir.symbols.push(ArtifactSymbol {
271                fingerprint,
272                name,
273                exported: symbol.is_global(),
274                section: u32::try_from(section_index.0).ok(),
275                offset: section_offset.saturating_add(relative),
276                size,
277                size_inferred: symbol.size() == 0,
278                code: code.to_vec(),
279                normalized,
280                inline_stack: Vec::new(),
281            });
282            ranges.push(NativeSymbolRange {
283                fingerprint,
284                index: symbol.index(),
285                section: section_index,
286                address: symbol.address(),
287                size,
288            });
289        }
290    }
291    Ok(ranges)
292}
293
294fn append_field(identity: &mut Vec<u8>, value: &[u8]) {
295    identity.extend(u64::try_from(value.len()).unwrap_or(u64::MAX).to_le_bytes());
296    identity.extend(value);
297}
298
299fn relocation_target_name(
300    file: &object::File<'_>,
301    target: object::RelocationTarget,
302) -> Option<String> {
303    let object::RelocationTarget::Symbol(index) = target else {
304        return None;
305    };
306    file.symbol_by_index(index)
307        .ok()
308        .and_then(|symbol| symbol.name().ok())
309        .filter(|name| !name.is_empty())
310        .map(demangle)
311}
312
313/// Add one explicitly inferred region for every non-empty text section.
314///
315/// The caller supplies its format-domain fingerprint recipe. Address ranges
316/// are returned as transient join evidence for backends that can attach debug
317/// frames; they are never used as identities.
318///
319/// # Errors
320///
321/// Returns an object-reader error when an executable section cannot be read.
322pub fn infer_text_regions<F>(
323    file: &object::File<'_>,
324    ir: &mut ArtifactIr,
325    mut fingerprint: F,
326) -> Result<Vec<(ArtifactFingerprint, u64, u64)>, object::Error>
327where
328    F: FnMut(Option<&str>, Option<&NormalizedInstructions>, &[u8]) -> ArtifactFingerprint,
329{
330    let mut ranges = Vec::new();
331    for section in file
332        .sections()
333        .filter(|section| section.kind() == object::SectionKind::Text)
334    {
335        let data = section.data()?;
336        if data.is_empty() {
337            continue;
338        }
339        let (offset, _) = section.file_range().unwrap_or((0, 0));
340        let normalized = normalize_x86(data, file.architecture());
341        let symbol_fingerprint = fingerprint(section.name().ok(), normalized.as_ref(), data);
342        let size = u64::try_from(data.len()).unwrap_or(u64::MAX);
343        ir.symbols.push(ArtifactSymbol {
344            fingerprint: symbol_fingerprint,
345            name: None,
346            exported: false,
347            section: u32::try_from(section.index().0).ok(),
348            offset,
349            size,
350            size_inferred: true,
351            code: data.to_vec(),
352            normalized,
353            inline_stack: Vec::new(),
354        });
355        ranges.push((symbol_fingerprint, section.address(), size));
356    }
357    Ok(ranges)
358}
359
360#[cfg(test)]
361mod tests {
362    use super::{symbol_fingerprint, symbol_size};
363    use crate::NormalizedInstructions;
364
365    #[test]
366    fn normalized_and_raw_payloads_with_the_same_bytes_have_distinct_identities() {
367        let normalized = NormalizedInstructions {
368            version: "x86-shape-v1".to_owned(),
369            bytes: vec![1, 2, 3],
370        };
371        let normalized_fingerprint =
372            symbol_fingerprint(Some("render"), Some(".text"), Some(&normalized), b"ignored");
373        let raw_fingerprint = symbol_fingerprint(
374            Some("render"),
375            Some(".text"),
376            None,
377            b"x86-shape-v1\x01\x02\x03",
378        );
379        assert_ne!(normalized_fingerprint, raw_fingerprint);
380    }
381
382    #[test]
383    fn field_boundaries_are_part_of_the_symbol_identity() {
384        let left = symbol_fingerprint(Some("ab"), Some("c"), None, b"payload");
385        let right = symbol_fingerprint(Some("a"), Some("bc"), None, b"payload");
386        assert_ne!(left, right);
387    }
388
389    #[test]
390    fn inferred_symbol_size_uses_the_next_strictly_greater_address() {
391        assert_eq!(symbol_size(10, 0, false, [10, 10, 14], 20), 4);
392    }
393
394    #[test]
395    fn zero_size_alias_remains_an_explicit_empty_region() {
396        assert_eq!(symbol_size(10, 0, true, [14], 20), 0);
397    }
398}