blazesym_c/
inspect.rs

1use std::alloc::alloc;
2use std::alloc::dealloc;
3use std::alloc::Layout;
4use std::ffi::CStr;
5use std::ffi::CString;
6use std::ffi::OsStr;
7use std::ffi::OsString;
8use std::fmt::Debug;
9use std::mem;
10use std::mem::ManuallyDrop;
11use std::ops::Deref as _;
12use std::os::raw::c_char;
13use std::os::unix::ffi::OsStrExt as _;
14use std::os::unix::ffi::OsStringExt as _;
15use std::path::PathBuf;
16use std::ptr;
17
18#[cfg(doc)]
19use blazesym::inspect;
20use blazesym::inspect::source::Elf;
21use blazesym::inspect::source::Source;
22use blazesym::inspect::Inspector;
23use blazesym::inspect::SymInfo;
24use blazesym::Addr;
25use blazesym::SymType;
26
27use crate::blaze_err;
28#[cfg(doc)]
29use crate::blaze_err_last;
30use crate::from_cstr;
31use crate::set_last_err;
32use crate::util::slice_from_user_array;
33
34
35/// C ABI compatible version of [`blazesym::inspect::Inspector`].
36pub type blaze_inspector = Inspector;
37
38
39/// An object representing an ELF inspection source.
40///
41/// C ABI compatible version of [`inspect::source::Elf`].
42#[repr(C)]
43#[derive(Debug)]
44pub struct blaze_inspect_elf_src {
45    /// The size of this object's type.
46    ///
47    /// Make sure to initialize it to `sizeof(<type>)`. This member is used to
48    /// ensure compatibility in the presence of member additions.
49    pub type_size: usize,
50    /// The path to the ELF file. This member is always present.
51    pub path: *const c_char,
52    /// Whether or not to consult debug symbols to satisfy the request
53    /// (if present).
54    pub debug_syms: bool,
55    /// Unused member available for future expansion. Must be initialized
56    /// to zero.
57    pub reserved: [u8; 23],
58}
59
60impl Default for blaze_inspect_elf_src {
61    fn default() -> Self {
62        Self {
63            type_size: mem::size_of::<Self>(),
64            path: ptr::null(),
65            debug_syms: false,
66            reserved: [0; 23],
67        }
68    }
69}
70
71#[cfg_attr(not(test), allow(unused))]
72impl blaze_inspect_elf_src {
73    fn from(other: Elf) -> ManuallyDrop<Self> {
74        let Elf {
75            path,
76            debug_syms,
77            _non_exhaustive: (),
78        } = other;
79
80        let slf = Self {
81            path: CString::new(path.into_os_string().into_vec())
82                .expect("encountered path with NUL bytes")
83                .into_raw(),
84            debug_syms,
85            ..Default::default()
86        };
87        ManuallyDrop::new(slf)
88    }
89
90    unsafe fn free(self) {
91        let Self {
92            type_size: _,
93            path,
94            debug_syms,
95            reserved: _,
96        } = self;
97
98        let _elf = Elf {
99            path: PathBuf::from(OsString::from_vec(
100                unsafe { CString::from_raw(path as *mut _) }.into_bytes(),
101            )),
102            debug_syms,
103            _non_exhaustive: (),
104        };
105    }
106}
107
108impl From<blaze_inspect_elf_src> for Elf {
109    fn from(other: blaze_inspect_elf_src) -> Self {
110        let blaze_inspect_elf_src {
111            type_size: _,
112            path,
113            debug_syms,
114            reserved: _,
115        } = other;
116
117        Self {
118            path: unsafe { from_cstr(path) },
119            debug_syms,
120            _non_exhaustive: (),
121        }
122    }
123}
124
125
126/// The type of a symbol.
127#[repr(transparent)]
128#[derive(Copy, Clone, Debug, PartialEq)]
129pub struct blaze_sym_type(u8);
130
131impl blaze_sym_type {
132    /// The symbol type is unspecified or unknown.
133    ///
134    /// In input contexts this variant can be used to encompass all
135    /// other variants (functions and variables), whereas in output
136    /// contexts it means that the type is not known.
137    pub const UNDEF: blaze_sym_type = blaze_sym_type(0);
138    /// The symbol is a function.
139    pub const FUNC: blaze_sym_type = blaze_sym_type(1);
140    /// The symbol is a variable.
141    pub const VAR: blaze_sym_type = blaze_sym_type(2);
142}
143
144impl From<SymType> for blaze_sym_type {
145    fn from(other: SymType) -> Self {
146        match other {
147            SymType::Undefined => blaze_sym_type::UNDEF,
148            SymType::Function => blaze_sym_type::FUNC,
149            SymType::Variable => blaze_sym_type::VAR,
150            _ => unreachable!(),
151        }
152    }
153}
154
155
156/// Information about a looked up symbol.
157#[repr(C)]
158#[derive(Debug)]
159pub struct blaze_sym_info {
160    /// See [`inspect::SymInfo::name`].
161    pub name: *const c_char,
162    /// See [`inspect::SymInfo::addr`].
163    pub addr: Addr,
164    /// See [`inspect::SymInfo::size`].
165    ///
166    /// If the symbol's size is not available, this member will be `-1`.
167    /// Note that some symbol sources may not distinguish between
168    /// "unknown" size and `0`. In that case the size will be reported
169    /// as `0` here as well.
170    pub size: isize,
171    /// See [`inspect::SymInfo::file_offset`].
172    pub file_offset: u64,
173    /// See [`inspect::SymInfo::module`].
174    pub module: *const c_char,
175    /// See [`inspect::SymInfo::sym_type`].
176    pub sym_type: blaze_sym_type,
177    /// Unused member available for future expansion.
178    pub reserved: [u8; 23],
179}
180
181
182/// Convert [`SymInfo`] objects as returned by
183/// [`Symbolizer::find_addrs`] to a C array.
184fn convert_syms_list_to_c(syms_list: Vec<Vec<SymInfo>>) -> *const *const blaze_sym_info {
185    let mut sym_cnt = 0;
186    let mut str_buf_sz = 0;
187
188    for syms in &syms_list {
189        sym_cnt += syms.len() + 1;
190        for sym in syms {
191            str_buf_sz += sym.name.len() + 1;
192            if let Some(fname) = sym.module.as_ref() {
193                str_buf_sz += AsRef::<OsStr>::as_ref(fname.deref()).as_bytes().len() + 1;
194            }
195        }
196    }
197
198    let array_sz = (mem::size_of::<*const u64>() * syms_list.len() + mem::size_of::<u64>() - 1)
199        / mem::size_of::<u64>()
200        * mem::size_of::<u64>();
201    let sym_buf_sz = mem::size_of::<blaze_sym_info>() * sym_cnt;
202    let buf_size = mem::size_of::<u64>() + array_sz + sym_buf_sz + str_buf_sz;
203    let raw_buf_with_sz = unsafe { alloc(Layout::from_size_align(buf_size, 8).unwrap()) };
204    if raw_buf_with_sz.is_null() {
205        return ptr::null()
206    }
207
208    unsafe { *(raw_buf_with_sz as *mut u64) = buf_size as u64 };
209
210    let raw_buf = unsafe { raw_buf_with_sz.add(mem::size_of::<u64>()) };
211    let mut syms_ptr = raw_buf as *mut *mut blaze_sym_info;
212    let mut sym_ptr = unsafe { raw_buf.add(array_sz) } as *mut blaze_sym_info;
213    let mut str_ptr = unsafe { raw_buf.add(array_sz + sym_buf_sz) } as *mut c_char;
214
215    for syms in syms_list {
216        unsafe { *syms_ptr = sym_ptr };
217        for SymInfo {
218            name,
219            addr,
220            size,
221            sym_type,
222            file_offset,
223            module,
224        } in syms
225        {
226            let name_ptr = str_ptr.cast();
227            unsafe { ptr::copy_nonoverlapping(name.as_ptr().cast(), str_ptr, name.len()) };
228            str_ptr = unsafe { str_ptr.add(name.len()) };
229            unsafe { *str_ptr = 0 };
230            str_ptr = unsafe { str_ptr.add(1) };
231            let module = if let Some(fname) = module.as_ref() {
232                let fname = AsRef::<OsStr>::as_ref(fname.deref()).as_bytes();
233                let obj_fname_ptr = str_ptr;
234                unsafe { ptr::copy_nonoverlapping(fname.as_ptr().cast(), str_ptr, fname.len()) };
235                str_ptr = unsafe { str_ptr.add(fname.len()) };
236                unsafe { *str_ptr = 0 };
237                str_ptr = unsafe { str_ptr.add(1) };
238                obj_fname_ptr
239            } else {
240                ptr::null()
241            };
242
243            unsafe {
244                (*sym_ptr) = blaze_sym_info {
245                    name: name_ptr,
246                    addr,
247                    size: size
248                        .map(|size| isize::try_from(size).unwrap_or(isize::MAX))
249                        .unwrap_or(-1),
250                    sym_type: sym_type.into(),
251                    file_offset: file_offset.unwrap_or(0),
252                    module,
253                    reserved: [0; 23],
254                }
255            };
256            sym_ptr = unsafe { sym_ptr.add(1) };
257        }
258        unsafe {
259            (*sym_ptr) = blaze_sym_info {
260                name: ptr::null(),
261                addr: 0,
262                size: 0,
263                sym_type: blaze_sym_type::UNDEF,
264                file_offset: 0,
265                module: ptr::null(),
266                reserved: [0; 23],
267            }
268        };
269        sym_ptr = unsafe { sym_ptr.add(1) };
270
271        syms_ptr = unsafe { syms_ptr.add(1) };
272    }
273
274    raw_buf as *const *const blaze_sym_info
275}
276
277
278/// Lookup symbol information in an ELF file.
279///
280/// On success, returns an array with `name_cnt` elements. Each such element, in
281/// turn, is NULL terminated array comprised of each symbol found. The returned
282/// object should be released using [`blaze_inspect_syms_free`] once it is no
283/// longer needed.
284///
285/// On error, the function returns `NULL` and sets the thread's last error to
286/// indicate the problem encountered. Use [`blaze_err_last`] to retrieve this
287/// error.
288///
289/// # Safety
290/// - `inspector` needs to point to an initialized [`blaze_inspector`] object
291/// - `src` needs to point to an initialized [`blaze_inspect_syms_elf`] object
292/// - `names` needs to be a valid pointer to `name_cnt` NUL terminated strings
293#[no_mangle]
294pub unsafe extern "C" fn blaze_inspect_syms_elf(
295    inspector: *const blaze_inspector,
296    src: *const blaze_inspect_elf_src,
297    names: *const *const c_char,
298    name_cnt: usize,
299) -> *const *const blaze_sym_info {
300    if !input_zeroed!(src, blaze_inspect_elf_src) {
301        let () = set_last_err(blaze_err::INVALID_INPUT);
302        return ptr::null()
303    }
304    let src = input_sanitize!(src, blaze_inspect_elf_src);
305
306    // SAFETY: The caller ensures that the pointer is valid.
307    let inspector = unsafe { &*inspector };
308    // SAFETY: The caller ensures that the pointer is valid.
309    let src = Source::Elf(Elf::from(src));
310    // SAFETY: The caller ensures that the pointer is valid and the count
311    //         matches.
312    let names = unsafe { slice_from_user_array(names, name_cnt) };
313    let names = names
314        .iter()
315        .map(|&p| {
316            // SAFETY: The caller ensures that the pointer is valid.
317            unsafe { CStr::from_ptr(p) }.to_str().unwrap()
318        })
319        .collect::<Vec<_>>();
320    let result = inspector.lookup(&src, &names);
321    match result {
322        Ok(syms) => {
323            let result = convert_syms_list_to_c(syms);
324            if result.is_null() {
325                let () = set_last_err(blaze_err::OUT_OF_MEMORY);
326            } else {
327                let () = set_last_err(blaze_err::OK);
328            }
329            result
330        }
331        Err(err) => {
332            let () = set_last_err(err.kind().into());
333            ptr::null()
334        }
335    }
336}
337
338
339/// Free an array returned by [`blaze_inspect_syms_elf`].
340///
341/// # Safety
342///
343/// The pointer must be returned by [`blaze_inspect_syms_elf`].
344#[no_mangle]
345pub unsafe extern "C" fn blaze_inspect_syms_free(syms: *const *const blaze_sym_info) {
346    if syms.is_null() {
347        return
348    }
349
350    let raw_buf_with_sz = unsafe { (syms as *mut u8).offset(-(mem::size_of::<u64>() as isize)) };
351    let sz = unsafe { *(raw_buf_with_sz as *mut u64) } as usize;
352    unsafe { dealloc(raw_buf_with_sz, Layout::from_size_align(sz, 8).unwrap()) };
353}
354
355
356/// Create an instance of a blazesym inspector.
357///
358/// C ABI compatible version of [`blazesym::inspect::Inspector::new()`].
359/// Please refer to its documentation for the default configuration in
360/// use.
361///
362/// On success, the function creates a new [`blaze_inspector`] object
363/// and returns it. The resulting object should be released using
364/// [`blaze_inspector_free`] once it is no longer needed.
365///
366/// On error, the function returns `NULL` and sets the thread's last error to
367/// indicate the problem encountered. Use [`blaze_err_last`] to retrieve this
368/// error.
369#[no_mangle]
370pub extern "C" fn blaze_inspector_new() -> *mut blaze_inspector {
371    let inspector = Inspector::new();
372    let inspector_box = Box::new(inspector);
373    let () = set_last_err(blaze_err::OK);
374    Box::into_raw(inspector_box)
375}
376
377
378/// Free a blazesym inspector.
379///
380/// Release resources associated with a inspector as created by
381/// [`blaze_inspector_new`], for example.
382///
383/// # Safety
384/// The provided inspector should have been created by
385/// [`blaze_inspector_new`].
386#[no_mangle]
387pub unsafe extern "C" fn blaze_inspector_free(inspector: *mut blaze_inspector) {
388    if !inspector.is_null() {
389        // SAFETY: The caller needs to ensure that `inspector` is a
390        //         valid pointer.
391        drop(unsafe { Box::from_raw(inspector) });
392    }
393}
394
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    use std::mem::MaybeUninit;
401    use std::path::Path;
402    use std::ptr::addr_of;
403    use std::slice;
404
405    use test_log::test;
406    use test_tag::tag;
407
408    use crate::blaze_err_last;
409
410
411    /// Check that various types have expected sizes.
412    #[tag(miri)]
413    #[test]
414    #[cfg(target_pointer_width = "64")]
415    fn type_sizes() {
416        assert_eq!(mem::size_of::<blaze_inspect_elf_src>(), 40);
417        assert_eq!(mem::size_of::<blaze_sym_info>(), 64);
418    }
419
420    /// Exercise the `Debug` representation of various types.
421    #[tag(miri)]
422    #[test]
423    fn debug_repr() {
424        let elf = blaze_inspect_elf_src {
425            type_size: 24,
426            path: ptr::null(),
427            debug_syms: true,
428            reserved: [0; 23],
429        };
430        assert_eq!(
431            format!("{elf:?}"),
432            "blaze_inspect_elf_src { type_size: 24, path: 0x0, debug_syms: true, reserved: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }"
433        );
434
435        let info = blaze_sym_info {
436            name: ptr::null(),
437            addr: 42,
438            size: 1337,
439            file_offset: 31,
440            module: ptr::null(),
441            sym_type: blaze_sym_type::VAR,
442            reserved: [0; 23],
443        };
444        assert_eq!(
445            format!("{info:?}"),
446            "blaze_sym_info { name: 0x0, addr: 42, size: 1337, file_offset: 31, module: 0x0, sym_type: blaze_sym_type(2), reserved: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }"
447        );
448    }
449
450    /// Test that we can correctly validate zeroed "extensions" of a
451    /// struct.
452    #[tag(miri)]
453    #[test]
454    fn elf_src_validity() {
455        #[repr(C)]
456        struct elf_src_with_ext {
457            type_size: usize,
458            _path: *const c_char,
459            debug_syms: bool,
460            reserved: [u8; 23],
461            foobar: bool,
462            reserved2: [u8; 7],
463        }
464
465        assert!(mem::size_of::<blaze_inspect_elf_src>() < mem::size_of::<elf_src_with_ext>());
466
467        let mut src = MaybeUninit::<elf_src_with_ext>::uninit();
468        let () = unsafe {
469            ptr::write_bytes(
470                src.as_mut_ptr().cast::<u8>(),
471                0,
472                mem::size_of::<elf_src_with_ext>(),
473            )
474        };
475
476        let mut src = unsafe { src.assume_init() };
477        src.type_size = mem::size_of::<elf_src_with_ext>();
478        src.debug_syms = true;
479
480        let src_ptr = addr_of!(src).cast::<blaze_inspect_elf_src>();
481        assert!(input_zeroed!(src_ptr, blaze_inspect_elf_src));
482
483        src.reserved[0] = 1;
484        let src_ptr = addr_of!(src).cast::<blaze_inspect_elf_src>();
485        assert!(!input_zeroed!(src_ptr, blaze_inspect_elf_src));
486        src.reserved[0] = 0;
487
488        src.type_size = mem::size_of::<usize>() - 1;
489        let src_ptr = addr_of!(src).cast::<blaze_inspect_elf_src>();
490        assert!(!input_zeroed!(src_ptr, blaze_inspect_elf_src));
491        src.type_size = mem::size_of::<elf_src_with_ext>();
492
493        src.foobar = true;
494        let src_ptr = addr_of!(src).cast::<blaze_inspect_elf_src>();
495        assert!(!input_zeroed!(src_ptr, blaze_inspect_elf_src));
496    }
497
498    /// Check that we can properly convert a "syms list" into the corresponding
499    /// C representation.
500    #[tag(miri)]
501    #[test]
502    fn syms_list_conversion() {
503        fn test(syms: Vec<Vec<SymInfo>>) {
504            let copy = syms.clone();
505            let ptr = convert_syms_list_to_c(syms);
506            assert!(!ptr.is_null());
507
508            for (i, list) in copy.into_iter().enumerate() {
509                for (j, sym) in list.into_iter().enumerate() {
510                    let c_sym = unsafe { &(*(*ptr.add(i)).add(j)) };
511                    assert_eq!(
512                        unsafe { CStr::from_ptr(c_sym.name) }.to_bytes(),
513                        CString::new(sym.name.deref()).unwrap().to_bytes()
514                    );
515                    assert_eq!(c_sym.addr, sym.addr);
516                    assert_eq!(
517                        c_sym.size,
518                        sym.size
519                            .map(|size| isize::try_from(size).unwrap_or(isize::MAX))
520                            .unwrap_or(-1)
521                    );
522                    assert_eq!(c_sym.sym_type, blaze_sym_type::from(sym.sym_type));
523                    assert_eq!(Some(c_sym.file_offset), sym.file_offset);
524                    assert_eq!(
525                        unsafe { CStr::from_ptr(c_sym.module) }.to_bytes(),
526                        CString::new(sym.module.as_deref().unwrap().to_os_string().into_vec())
527                            .unwrap()
528                            .to_bytes()
529                    );
530                }
531            }
532
533            let () = unsafe { blaze_inspect_syms_free(ptr) };
534        }
535
536        // Test conversion of no symbols.
537        let syms = vec![];
538        test(syms);
539
540        // Test conversion with a single symbol.
541        let syms = vec![vec![SymInfo {
542            name: "sym1".into(),
543            addr: 0xdeadbeef,
544            size: Some(42),
545            sym_type: SymType::Function,
546            file_offset: Some(1337),
547            module: Some(OsStr::new("/tmp/foobar.so").into()),
548        }]];
549        test(syms);
550
551        // Test conversion of two symbols in one result.
552        let syms = vec![vec![
553            SymInfo {
554                name: "sym1".into(),
555                addr: 0xdeadbeef,
556                size: Some(42),
557                sym_type: SymType::Function,
558                file_offset: Some(1337),
559                module: Some(OsStr::new("/tmp/foobar.so").into()),
560            },
561            SymInfo {
562                name: "sym2".into(),
563                addr: 0xdeadbeef + 52,
564                size: Some(45),
565                sym_type: SymType::Undefined,
566                file_offset: Some(1338),
567                module: Some(OsStr::new("other.so").into()),
568            },
569        ]];
570        test(syms);
571
572        // Test conversion of two symbols spread over two results.
573        let syms = vec![
574            vec![SymInfo {
575                name: "sym1".into(),
576                addr: 0xdeadbeef,
577                size: Some(42),
578                sym_type: SymType::Function,
579                file_offset: Some(1337),
580                module: Some(OsStr::new("/tmp/foobar.so").into()),
581            }],
582            vec![SymInfo {
583                name: "sym2".into(),
584                addr: 0xdeadbeef + 52,
585                size: Some(45),
586                sym_type: SymType::Undefined,
587                file_offset: Some(1338),
588                module: Some(OsStr::new("other.so").into()),
589            }],
590        ];
591        test(syms);
592
593        // Test conversion of a `SymInfo` vector with many elements.
594        let sym = SymInfo {
595            name: "sym1".into(),
596            addr: 0xdeadbeef,
597            size: Some(42),
598            sym_type: SymType::Function,
599            file_offset: Some(1337),
600            module: Some(OsStr::new("/tmp/foobar.so").into()),
601        };
602        let syms = vec![(0..200).map(|_| sym.clone()).collect()];
603        test(syms);
604
605        // Test conversion of many `SymInfo` vectors.
606        let syms = (0..200).map(|_| vec![sym.clone()]).collect();
607        test(syms);
608    }
609
610    /// Make sure that we can create and free an inspector instance.
611    #[tag(miri)]
612    #[test]
613    fn inspector_creation() {
614        let inspector = blaze_inspector_new();
615        let () = unsafe { blaze_inspector_free(inspector) };
616    }
617
618    /// Check that `blaze_inspect_syms_elf` fails if the input source
619    /// does not have reserved fields set to zero.
620    #[test]
621    fn non_zero_reserved() {
622        let path = Path::new(&env!("CARGO_MANIFEST_DIR"))
623            .join("..")
624            .join("data")
625            .join("test-stable-addrs.bin");
626
627        let mut src = blaze_inspect_elf_src::from(Elf::new(path));
628        src.reserved[1] = 1;
629
630        let factorial = CString::new("factorial").unwrap();
631        let names = [factorial.as_ptr()];
632        let inspector = blaze_inspector_new();
633
634        let result =
635            unsafe { blaze_inspect_syms_elf(inspector, &*src, names.as_ptr(), names.len()) };
636        let () = unsafe { ManuallyDrop::into_inner(src).free() };
637        assert_eq!(result, ptr::null());
638        assert_eq!(blaze_err_last(), blaze_err::INVALID_INPUT);
639
640        let () = unsafe { blaze_inspector_free(inspector) };
641    }
642
643    /// Check that we see the expected error being reported when a source file
644    /// does not exist.
645    #[test]
646    fn non_present_file() {
647        let path = Path::new(&env!("CARGO_MANIFEST_DIR"))
648            .join("..")
649            .join("data")
650            .join("does-not-exist");
651
652        let src = blaze_inspect_elf_src::from(Elf::new(path));
653        let factorial = CString::new("factorial").unwrap();
654        let names = [factorial.as_ptr()];
655        let inspector = blaze_inspector_new();
656
657        let result =
658            unsafe { blaze_inspect_syms_elf(inspector, &*src, names.as_ptr(), names.len()) };
659        let () = unsafe { ManuallyDrop::into_inner(src).free() };
660        assert_eq!(result, ptr::null());
661        assert_eq!(blaze_err_last(), blaze_err::NOT_FOUND);
662
663        let () = unsafe { blaze_inspector_free(inspector) };
664    }
665
666    /// Make sure that we can lookup a function's address using DWARF
667    /// information.
668    #[test]
669    fn lookup_dwarf() {
670        let test_dwarf = Path::new(&env!("CARGO_MANIFEST_DIR"))
671            .join("..")
672            .join("data")
673            .join("test-stable-addrs-stripped-elf-with-dwarf.bin");
674
675        let src = blaze_inspect_elf_src::from(Elf::new(test_dwarf));
676        let factorial = CString::new("factorial").unwrap();
677        let names = [factorial.as_ptr()];
678
679        let inspector = blaze_inspector_new();
680        let result =
681            unsafe { blaze_inspect_syms_elf(inspector, &*src, names.as_ptr(), names.len()) };
682        let () = unsafe { ManuallyDrop::into_inner(src).free() };
683        assert!(!result.is_null());
684
685        let sym_infos = unsafe { slice::from_raw_parts(result, names.len()) };
686        let sym_info = unsafe { &*sym_infos[0] };
687        assert_eq!(
688            unsafe { CStr::from_ptr(sym_info.name) },
689            CStr::from_bytes_with_nul(b"factorial\0").unwrap()
690        );
691        assert_eq!(sym_info.addr, 0x2000200);
692
693        let () = unsafe { blaze_inspect_syms_free(result) };
694        let () = unsafe { blaze_inspector_free(inspector) };
695    }
696}