Skip to main content

gsym/reader/
lookup.rs

1use crate::endian::{Cursor, Endian};
2use crate::error::{Error, Result};
3use crate::format::function::{InfoType, check_inline_depth};
4use crate::format::leb::read_uleb;
5use crate::format::line;
6use crate::model::{FileIndex, LineEntry, Lookup, LookupFrame};
7use smallvec::SmallVec;
8
9use super::function::RawFunction;
10use super::{FrameLookupOptions, Gsym, LookupOptions, LookupScratch, RawInlineFrame};
11
12#[derive(Clone, Copy, Debug, Default)]
13struct ScannedRecords<'data> {
14    line: Option<LineEntry>,
15    inline: Option<&'data [u8]>,
16    call_sites: Option<&'data [u8]>,
17}
18
19#[derive(Clone, Copy, Debug)]
20struct FrameRequest<'data> {
21    address: u64,
22    options: LookupOptions,
23    function: RawFunction<'data>,
24}
25
26#[derive(Clone, Copy, Debug)]
27struct PreparedFrames<'data> {
28    function_name: &'data [u8],
29    directory: &'data [u8],
30    basename: &'data [u8],
31    line: u32,
32    count: usize,
33    call_sites: Option<&'data [u8]>,
34}
35
36impl<D: AsRef<[u8]>> Gsym<D> {
37    /// Resolves an unslid virtual address with the default lookup options.
38    ///
39    /// `address` must be an address of the image the file describes. An address
40    /// from a running PIE executable or shared object needs its load bias
41    /// removed first; see
42    /// [`docs::symbolication`](crate::docs::symbolication).
43    ///
44    /// Returns `Ok(None)` when no function covers the address, which is the
45    /// normal answer for padding between functions and for addresses belonging
46    /// to another module. The result borrows names and paths from this reader.
47    ///
48    /// Frames come back innermost first. Use
49    /// [`Self::lookup_with_options`] to skip record kinds you do not need, or
50    /// [`Self::for_each_frame`] to resolve an address without allocating.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the matched function's data is malformed.
55    ///
56    /// ```
57    /// use gsym::{AddressRange, FileEntry, Function, Gsym, GsymBuilder, LineEntry};
58    ///
59    /// let mut builder = GsymBuilder::new();
60    /// let file = builder.add_file(FileEntry::new(b"/src", b"main.rs"))?;
61    /// builder.add_function(Function {
62    ///     lines: vec![LineEntry::new(0x1000, file, 7)],
63    ///     ..Function::new(AddressRange::new(0x1000, 0x1010), b"main")
64    /// })?;
65    /// let bytes = builder.to_bytes()?;
66    /// let gsym = Gsym::parse(&bytes)?;
67    ///
68    /// let hit = gsym.lookup(0x1004)?.expect("covered address");
69    /// assert_eq!(hit.frames()[0].name, b"main");
70    /// assert_eq!(hit.frames()[0].basename, b"main.rs");
71    /// assert_eq!(hit.frames()[0].line, 7);
72    ///
73    /// assert!(gsym.lookup(0x2000)?.is_none());
74    /// # Ok::<(), gsym::Error>(())
75    /// ```
76    pub fn lookup(&self, address: u64) -> Result<Option<Lookup<'_>>> {
77        let mut scratch = LookupScratch::default();
78        self.lookup_with_options(address, LookupOptions::default(), &mut scratch)
79    }
80
81    /// Resolves an address while reusing caller-owned inline scratch storage.
82    ///
83    /// Same result as [`Self::lookup`], with control over which optional
84    /// records are read and with the scratch buffer supplied by the caller.
85    /// The returned [`Lookup`] still owns its frames; use
86    /// [`Self::for_each_frame`] to avoid that allocation as well.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if the matched function's data is malformed.
91    pub fn lookup_with_options<'data>(
92        &'data self,
93        address: u64,
94        options: LookupOptions,
95        scratch: &mut LookupScratch,
96    ) -> Result<Option<Lookup<'data>>> {
97        scratch.clear();
98        let Some(function) = self.matching_function(address)? else {
99            return Ok(None);
100        };
101        let request = FrameRequest {
102            address,
103            options,
104            function,
105        };
106        let prepared = self.prepare_frames(request, scratch)?;
107        let mut frames = Vec::with_capacity(prepared.count);
108        self.emit_frames(request, prepared, scratch, |frame| {
109            frames.push(frame);
110        })?;
111        self.finish_lookup(address, function, prepared.call_sites, frames)
112            .map(Some)
113    }
114
115    /// Visits source frames without allocating an output collection.
116    ///
117    /// Frames are yielded innermost first and borrow their names and paths from
118    /// the reader. Sizing the [`LookupScratch`] with
119    /// [`LookupScratch::with_capacity`] keeps repeated lookups allocation-free.
120    ///
121    /// Returns whether a function covered the address. The visitor is not
122    /// called when it returns `false`. Call-site patterns are not reported
123    /// here; use [`Self::lookup_with_options`] when they are needed.
124    ///
125    /// ```
126    /// use gsym::{
127    ///     AddressRange, FrameLookupOptions, Function, Gsym, GsymBuilder,
128    ///     LookupScratch,
129    /// };
130    ///
131    /// let mut builder = GsymBuilder::new();
132    /// builder.add_function(Function::new(
133    ///     AddressRange::new(0x3000, 0x3010),
134    ///     b"visited",
135    /// ))?;
136    /// let bytes = builder.to_bytes()?;
137    /// let gsym = Gsym::parse(bytes)?;
138    ///
139    /// let mut scratch = LookupScratch::with_capacity(8);
140    /// let mut names = Vec::new();
141    /// let found = gsym.for_each_frame(
142    ///     0x3004,
143    ///     FrameLookupOptions::default(),
144    ///     &mut scratch,
145    ///     |frame| names.push(frame.name.to_vec()),
146    /// )?;
147    /// assert!(found);
148    /// assert_eq!(names, [b"visited".to_vec()]);
149    /// # Ok::<(), gsym::Error>(())
150    /// ```
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if the matched function's data is malformed.
155    pub fn for_each_frame<'data>(
156        &'data self,
157        address: u64,
158        options: FrameLookupOptions,
159        scratch: &mut LookupScratch,
160        visitor: impl FnMut(LookupFrame<'data>),
161    ) -> Result<bool> {
162        scratch.clear();
163        let Some(function) = self.matching_function(address)? else {
164            return Ok(false);
165        };
166        self.visit_function_frames(
167            FrameRequest {
168                address,
169                options: options.into(),
170                function,
171            },
172            scratch,
173            visitor,
174        )?;
175        Ok(true)
176    }
177
178    fn matching_function(&self, address: u64) -> Result<Option<RawFunction<'_>>> {
179        let Some(mut index) = self.find_address_index(address)? else {
180            return Ok(None);
181        };
182        let first_start = self.address(index)?;
183        while index > 0 && self.address(index.saturating_sub(1))? == first_start {
184            index = index.saturating_sub(1);
185        }
186        let count = self.layout.address_count as usize;
187        while index < count {
188            let raw = self.raw_function_at(index, first_start)?;
189            if raw.range.is_empty() || raw.range.contains(address) {
190                return Ok(Some(raw));
191            }
192            index = index.saturating_add(1);
193            if index >= count || self.address(index)? != first_start {
194                break;
195            }
196        }
197        Ok(None)
198    }
199
200    fn visit_function_frames<'data>(
201        &'data self,
202        request: FrameRequest<'data>,
203        scratch: &mut LookupScratch,
204        visitor: impl FnMut(LookupFrame<'data>),
205    ) -> Result<Option<&'data [u8]>> {
206        let prepared = self.prepare_frames(request, scratch)?;
207        self.emit_frames(request, prepared, scratch, visitor)?;
208        Ok(prepared.call_sites)
209    }
210
211    fn prepare_frames<'data>(
212        &'data self,
213        request: FrameRequest<'data>,
214        scratch: &mut LookupScratch,
215    ) -> Result<PreparedFrames<'data>> {
216        let records = self.scan_records(request)?;
217        let function = request.function;
218        let function_name = self.string(function.name)?;
219        let (directory, basename, line_number) = if let Some(row) = records.line {
220            let (directory, basename) = self.file(row.file)?;
221            (directory, basename, row.line)
222        } else {
223            (&[][..], &[][..], 0)
224        };
225
226        if let Some(payload) = records.inline {
227            let mut cursor = Cursor::new(payload, self.layout.endian);
228            let mut scan = InlineScan {
229                string_offset_size: self.layout.string_offset_size,
230                address: request.address,
231                frames: &mut scratch.inline_frames,
232            };
233            let (present, _) =
234                scan_inline_node(&mut cursor, function.range.start, true, 0, &mut scan)?;
235            if !present || !cursor.is_empty() {
236                return Err(Error::InvalidFormat("malformed inline-info payload"));
237            }
238        }
239
240        Ok(PreparedFrames {
241            function_name,
242            directory,
243            basename,
244            line: line_number,
245            count: scratch.inline_frames.len().max(1),
246            call_sites: records.call_sites,
247        })
248    }
249
250    fn emit_frames<'data>(
251        &'data self,
252        request: FrameRequest<'data>,
253        prepared: PreparedFrames<'data>,
254        scratch: &LookupScratch,
255        mut visitor: impl FnMut(LookupFrame<'data>),
256    ) -> Result<()> {
257        let PreparedFrames {
258            function_name,
259            directory,
260            basename,
261            line: line_number,
262            ..
263        } = prepared;
264        let address = request.address;
265        let function = request.function;
266
267        if scratch.inline_frames.is_empty() {
268            visitor(LookupFrame {
269                name: function_name,
270                directory,
271                basename,
272                line: line_number,
273                offset: address.saturating_sub(function.range.start),
274                inlined: false,
275            });
276        } else {
277            let nodes = &scratch.inline_frames;
278            for (index, node) in nodes.iter().enumerate().rev() {
279                let (frame_directory, frame_basename, frame_line) =
280                    if let Some(callee) = nodes.get(index.saturating_add(1)) {
281                        let (directory, basename) = self.file(callee.call_file)?;
282                        (directory, basename, callee.call_line)
283                    } else {
284                        (directory, basename, line_number)
285                    };
286                visitor(LookupFrame {
287                    name: if node.name == 0 {
288                        function_name
289                    } else {
290                        self.string(node.name)?
291                    },
292                    directory: frame_directory,
293                    basename: frame_basename,
294                    line: frame_line,
295                    offset: address.saturating_sub(node.start),
296                    inlined: index != 0,
297                });
298            }
299        }
300        Ok(())
301    }
302
303    fn scan_records<'data>(&self, request: FrameRequest<'data>) -> Result<ScannedRecords<'data>> {
304        let FrameRequest {
305            address,
306            options,
307            function,
308        } = request;
309        if !options.line_information && !options.inline_frames && !options.call_sites {
310            return Ok(ScannedRecords::default());
311        }
312        let mut scanned = ScannedRecords::default();
313        for record in function.records(self.layout.endian) {
314            let record = record?;
315            match record.kind {
316                InfoType::LineTable => {
317                    if options.line_information {
318                        scanned.line = line::lookup(
319                            record.payload,
320                            self.layout.endian,
321                            function.range.start,
322                            address,
323                        )?;
324                    }
325                }
326                InfoType::Inline => {
327                    if options.inline_frames {
328                        scanned.inline = Some(record.payload);
329                    }
330                }
331                InfoType::Merged | InfoType::Unknown(_) => {}
332                InfoType::CallSite => {
333                    if options.call_sites {
334                        scanned.call_sites = Some(record.payload);
335                    }
336                }
337            }
338        }
339        Ok(scanned)
340    }
341
342    fn finish_lookup<'data>(
343        &'data self,
344        address: u64,
345        function: RawFunction<'data>,
346        call_site_payload: Option<&[u8]>,
347        frames: Vec<LookupFrame<'data>>,
348    ) -> Result<Lookup<'data>> {
349        let mut call_site_patterns = Vec::new();
350        if let Some(payload) = call_site_payload {
351            read_call_site_patterns(
352                payload,
353                self.layout.endian,
354                self.layout.string_offset_size,
355                address.saturating_sub(function.range.start),
356                self,
357                &mut call_site_patterns,
358            )?;
359        }
360        Ok(Lookup::new(
361            address,
362            function.range,
363            frames.into_boxed_slice(),
364            call_site_patterns.into_boxed_slice(),
365        ))
366    }
367}
368
369struct InlineScan<'scratch> {
370    string_offset_size: u8,
371    address: u64,
372    frames: &'scratch mut SmallVec<[RawInlineFrame; 4]>,
373}
374
375fn scan_inline_node(
376    cursor: &mut Cursor<'_>,
377    base: u64,
378    collect: bool,
379    depth: usize,
380    scan: &mut InlineScan<'_>,
381) -> Result<(bool, bool)> {
382    check_inline_depth(depth)?;
383    let count = read_uleb(cursor)?;
384    if count == 0 {
385        return Ok((false, false));
386    }
387    if count > cursor.remaining() as u64 / 2 {
388        return Err(Error::InvalidFormat(
389            "inline range count exceeds remaining payload",
390        ));
391    }
392    let mut contains = false;
393    let mut first_start = None;
394    for _ in 0..count {
395        let start = base
396            .checked_add(read_uleb(cursor)?)
397            .ok_or(Error::Overflow("inline range start"))?;
398        let end = start
399            .checked_add(read_uleb(cursor)?)
400            .ok_or(Error::Overflow("inline range end"))?;
401        first_start.get_or_insert(start);
402        if collect {
403            contains |= start <= scan.address && scan.address < end;
404        }
405    }
406    let first_start = first_start.ok_or(Error::InvalidFormat(
407        "inline node declares no address range",
408    ))?;
409    let has_children = cursor.read_u8()? != 0;
410    let name = cursor.read_uint(scan.string_offset_size)?;
411    let call_file = read_uleb(cursor)?;
412    let call_line = read_uleb(cursor)?;
413    let matched = collect && contains;
414    let original_len = scan.frames.len();
415    if matched && name != 0 {
416        scan.frames.push(RawInlineFrame {
417            name,
418            call_file: FileIndex::new(u32::try_from(call_file).map_err(|_| Error::OutOfRange {
419                field: "inline call-file index",
420                value: call_file,
421                max: u64::from(u32::MAX),
422            })?),
423            call_line: u32::try_from(call_line).map_err(|_| Error::OutOfRange {
424                field: "inline call-line",
425                value: call_line,
426                max: u64::from(u32::MAX),
427            })?,
428            start: first_start,
429        });
430    }
431    if has_children {
432        let child_base = first_start;
433        let mut found_child = false;
434        loop {
435            let (present, child_matched) = scan_inline_node(
436                cursor,
437                child_base,
438                matched && !found_child,
439                depth.saturating_add(1),
440                scan,
441            )?;
442            if !present {
443                break;
444            }
445            found_child |= child_matched;
446        }
447    }
448    if !matched {
449        scan.frames.truncate(original_len);
450    }
451    Ok((true, matched))
452}
453
454fn read_call_site_patterns<'data, D: AsRef<[u8]>>(
455    payload: &[u8],
456    endian: Endian,
457    string_offset_size: u8,
458    return_offset: u64,
459    gsym: &'data Gsym<D>,
460    output: &mut Vec<&'data [u8]>,
461) -> Result<()> {
462    let mut cursor = Cursor::new(payload, endian);
463    let count = cursor.read_u32()?;
464    for _ in 0..count {
465        let candidate = cursor.read_u64()?;
466        let _flags = cursor.read_u8()?;
467        let regex_count = cursor.read_u32()?;
468        for _ in 0..regex_count {
469            let offset = cursor.read_uint(string_offset_size)?;
470            if candidate == return_offset {
471                output.push(gsym.string(offset)?);
472            }
473        }
474    }
475    if !cursor.is_empty() {
476        return Err(Error::InvalidFormat("trailing call-site bytes"));
477    }
478    Ok(())
479}