blazesym/symbolize/mod.rs
1//! Functionality for symbolizing addresses.
2//!
3//! This module contains functionality for symbolizing addresses, i.e., finding
4//! symbol names and other information based on "raw" addresses.
5//!
6//! For example, here we symbolize the address of `libc`'s `fopen` and `fseek`
7//! functions, given their addresses in the current process:
8//! ```no_run
9//! use blazesym::symbolize::source::Process;
10//! use blazesym::symbolize::source::Source;
11//! use blazesym::symbolize::Input;
12//! use blazesym::symbolize::Symbolizer;
13//! use blazesym::Addr;
14//! use blazesym::Pid;
15//!
16//! let addrs = [libc::fopen as Addr, libc::fseek as Addr];
17//!
18//! // Symbolize the addresses for the current process, as that's what they
19//! // belong to. The library also supports other symbolization sources, such as
20//! // arbitrary ELF files.
21//! let src = Source::Process(Process::new(Pid::Slf));
22//! let symbolizer = Symbolizer::new();
23//! let syms = symbolizer.symbolize(&src, Input::AbsAddr(&addrs)).unwrap();
24//!
25//! assert_eq!(syms.len(), 2);
26//!
27//! let fopen = syms[0].as_sym().unwrap();
28//! assert_eq!(fopen.name, "fopen");
29//!
30//! let fseek = syms[1].as_sym().unwrap();
31//! assert_eq!(fseek.name, "fseek");
32//! ```
33//!
34//! The example is contrived, of course, because we already know the names
35//! corresponding to the addresses, but it gets the basic workings across. Also,
36//! the library not only reports the name but additional metadata such as the
37//! symbol's start address and size, and even inlined callees if asked for and
38//! available. See the [`Sym`] type for details.
39//!
40//! In more realistic setting you can envision a backtrace being captured and
41//! symbolized instead. Refer to the runnable
42//! [`backtrace`](https://github.com/libbpf/blazesym/blob/main/examples/backtrace.rs)
43//! example.
44//!
45//! # Advanced use cases
46//! In many cases symbolization is straight forward: the user provides a
47//! symbolization source -- typically a file of a certain data format -- and the
48//! library knows how to parse it and look up a symbol.
49//!
50//! ### Processes
51//! However, in the case of process symbolization as briefly shown above, a
52//! process is really a form of container. That is to say, it contains a set of
53//! entities (e.g., loaded shared objects, otherwise mapped files etc.) that
54//! addresses can fall into and that each may require different ways of
55//! symbolization. **blazesym** comes with a default way of dealing with the
56//! entities inside such a container, where it honors embedded symbols and debug
57//! information (implicitly used in the above example), but advanced users may
58//! desire more flexibility. For example, envision a case where, instead of
59//! using embedded symbols in an executable, all binaries are stripped and
60//! symbol information is co-located somewhere on the file system. Said symbol
61//! data could also be not in the ELF or DWARF formats, but in the Gsym, which
62//! is optimized for fast lookup and also typically requires much less disk
63//! space.
64//!
65//! In such a setup, a user can install a custom process "dispatcher". This
66//! dispatcher is a callback function that **blazesym** invokes and that it
67//! provides certain information about the "member" that an address falls into
68//! to (in the form of a [`ProcessMemberInfo`] object). It is then the
69//! dispatcher's responsibility to use this information to instantiate and
70//! return a "resolver" that the library will use as part of address
71//! symbolization for addresses mapping to a single entity in inside the process
72//! (e.g., a shared object).
73//!
74//! **blazesym** provides a set of resolvers, that can act as building blocks
75//! for implementing a custom dispatch function. These resolver are all
76//! available in the [`helper`][crate::helper] module.
77//!
78//! A complete example using a custom dispatch function to symbolize addresses
79//! in a process after fetching their debug symbols via a
80//! [`debuginfod`](https://sourceware.org/elfutils/Debuginfod.html) client is
81//! available in the
82//! [`sym-debuginfod`](https://github.com/libbpf/blazesym/blob/main/examples/sym-debuginfod)
83//! example.
84//!
85//! ### APKs
86//! APKs are another container format (common on Android systems) that can be
87//! customized with a dispatcher. Installation of a custom dispatcher works
88//! similar to the process symbolization case, the only difference is that
89//! different data is provided to the dispatch function (refer to
90//! [`ApkMemberInfo`]). Please refer to the
91//! [`gsym-in-apk`](https://github.com/libbpf/blazesym/blob/main/examples/gsym-in-apk)
92//! example, which illustrates the basic workflow.
93
94pub mod cache;
95pub mod source;
96mod symbolizer;
97
98use std::borrow::Cow;
99use std::ffi::OsStr;
100use std::fmt::Debug;
101use std::fmt::Display;
102use std::fmt::Formatter;
103use std::fmt::Result as FmtResult;
104use std::path::Path;
105use std::str;
106
107cfg_apk! {
108 pub use symbolizer::ApkDispatch;
109 pub use symbolizer::ApkMemberInfo;
110}
111pub use symbolizer::Builder;
112pub use symbolizer::ProcessDispatch;
113pub use symbolizer::ProcessMemberInfo;
114pub use symbolizer::Symbolizer;
115
116pub(crate) use symbolizer::symbolize_with_resolver;
117pub(crate) use symbolizer::Resolver;
118
119// Strictly speaking these types are applicable to the entire crate, but right
120// now they are only used as part of the symbolization APIs, so we re-export
121// them through this module only.
122pub use crate::maps::EntryPath as ProcessMemberPath;
123pub use crate::maps::PathName as ProcessMemberType;
124
125use crate::Addr;
126use crate::Result;
127
128
129/// Options determining what data about a symbol to look up.
130#[derive(Debug)]
131#[non_exhaustive]
132pub enum FindSymOpts {
133 /// Only look up the "basic" symbol data (name, address, size, ...), without
134 /// source code location and inlined function information.
135 Basic,
136 /// Look up symbol data and source code location information.
137 CodeInfo,
138 /// Look up symbol data, source code location information, and inlined
139 /// function information.
140 CodeInfoAndInlined,
141}
142
143impl FindSymOpts {
144 #[inline]
145 pub(crate) fn code_info(&self) -> bool {
146 match self {
147 Self::Basic => false,
148 Self::CodeInfo | Self::CodeInfoAndInlined => true,
149 }
150 }
151
152 #[inline]
153 pub(crate) fn inlined_fns(&self) -> bool {
154 match self {
155 Self::Basic | Self::CodeInfo => false,
156 Self::CodeInfoAndInlined => true,
157 }
158 }
159}
160
161
162/// A enumeration of the different input types the symbolization APIs
163/// support.
164#[derive(Clone, Copy, Debug, PartialEq)]
165pub enum Input<T> {
166 /// An absolute address.
167 ///
168 /// A absolute address is an address as a process would see it, for example.
169 /// It may include relocation or address space randomization artifacts.
170 AbsAddr(T),
171 /// A virtual offset.
172 ///
173 /// A virtual offset is an address as it would appear in a binary or debug
174 /// symbol file.
175 VirtOffset(T),
176 /// A file offset.
177 ///
178 /// A file offset is the linear offset of a symbol in a file.
179 FileOffset(T),
180}
181
182impl<T> Input<T> {
183 /// Apply a function to the inner address while preserving the
184 /// active variant.
185 #[inline]
186 pub fn map<F, U>(&self, f: F) -> Input<U>
187 where
188 T: Copy,
189 F: FnOnce(T) -> U,
190 {
191 match self {
192 Self::AbsAddr(x) => Input::AbsAddr(f(*x)),
193 Self::VirtOffset(x) => Input::VirtOffset(f(*x)),
194 Self::FileOffset(x) => Input::FileOffset(f(*x)),
195 }
196 }
197
198 /// Retrieve a reference to the inner payload.
199 #[inline]
200 pub fn as_inner_ref(&self) -> &T {
201 match self {
202 Self::AbsAddr(x) | Self::VirtOffset(x) | Self::FileOffset(x) => x,
203 }
204 }
205
206 /// Extract the inner payload.
207 ///
208 /// ```rust
209 /// # use blazesym::symbolize;
210 /// let addrs = [1, 2, 3, 4];
211 /// let input = symbolize::Input::FileOffset(addrs.as_slice());
212 /// assert_eq!(input.into_inner(), &[1, 2, 3, 4]);
213 /// ```
214 #[inline]
215 pub fn into_inner(self) -> T {
216 match self {
217 Self::AbsAddr(x) | Self::VirtOffset(x) | Self::FileOffset(x) => x,
218 }
219 }
220}
221
222#[cfg(test)]
223impl<T> Input<&[T]>
224where
225 T: Copy,
226{
227 fn try_to_single(&self) -> Option<Input<T>> {
228 match self {
229 Self::AbsAddr([addr]) => Some(Input::AbsAddr(*addr)),
230 Self::VirtOffset([addr]) => Some(Input::VirtOffset(*addr)),
231 Self::FileOffset([offset]) => Some(Input::FileOffset(*offset)),
232 _ => None,
233 }
234 }
235}
236
237
238/// Source code location information for a symbol or inlined function.
239#[derive(Clone, Debug, PartialEq)]
240pub struct CodeInfo<'src> {
241 /// The directory in which the source file resides.
242 pub dir: Option<Cow<'src, Path>>,
243 /// The file that defines the symbol.
244 pub file: Cow<'src, OsStr>,
245 /// The line number of the symbolized instruction in the source
246 /// code.
247 ///
248 /// This is the line number of the instruction of the address being
249 /// symbolized, not the line number that defines the symbol
250 /// (function).
251 pub line: Option<u32>,
252 /// The column number of the symbolized instruction in the source
253 /// code.
254 pub column: Option<u16>,
255 /// The struct is non-exhaustive and open to extension.
256 #[doc(hidden)]
257 pub _non_exhaustive: (),
258}
259
260impl CodeInfo<'_> {
261 /// Helper method to retrieve the path to the represented source file,
262 /// on a best-effort basis. It depends on the symbolization source data
263 /// whether this path is absolute or relative and, if its the latter, what
264 /// directory it is relative to. In general this path is mostly intended for
265 /// displaying purposes.
266 #[inline]
267 pub fn to_path(&self) -> Cow<'_, Path> {
268 self.dir.as_ref().map_or_else(
269 || Cow::Borrowed(Path::new(&self.file)),
270 |dir| Cow::Owned(dir.join(&self.file)),
271 )
272 }
273
274 /// Convert this object into one with all references converted into
275 /// guaranteed owned (i.e., heap allocated) members.
276 pub fn into_owned(self) -> CodeInfo<'static> {
277 let Self {
278 dir,
279 file,
280 line,
281 column,
282 _non_exhaustive: (),
283 } = self;
284
285 CodeInfo {
286 dir: dir.map(|dir| Cow::Owned(dir.into_owned())),
287 file: Cow::Owned(file.into_owned()),
288 line,
289 column,
290 _non_exhaustive: (),
291 }
292 }
293}
294
295
296/// A type representing an inlined function.
297#[derive(Clone, Debug, PartialEq)]
298pub struct InlinedFn<'src> {
299 /// The symbol name of the inlined function.
300 pub name: Cow<'src, str>,
301 /// Source code location information for the call to the function.
302 pub code_info: Option<CodeInfo<'src>>,
303 /// The struct is non-exhaustive and open to extension.
304 #[doc(hidden)]
305 pub _non_exhaustive: (),
306}
307
308impl InlinedFn<'_> {
309 /// Convert this object into one with all references converted into
310 /// guaranteed owned (i.e., heap allocated) members.
311 pub fn into_owned(self) -> InlinedFn<'static> {
312 let Self {
313 name,
314 code_info,
315 _non_exhaustive: (),
316 } = self;
317
318 InlinedFn {
319 name: Cow::Owned(name.into_owned()),
320 code_info: code_info.map(CodeInfo::into_owned),
321 _non_exhaustive: (),
322 }
323 }
324}
325
326
327/// The source code language from which a symbol originates.
328#[derive(Clone, Copy, Default, Debug, PartialEq)]
329#[non_exhaustive]
330pub enum SrcLang {
331 /// The language is unknown.
332 #[default]
333 Unknown,
334 /// The language is C++.
335 Cpp,
336 /// The language is Rust.
337 Rust,
338}
339
340
341/// A type representing a symbol as produced by a [`Resolve`] object.
342#[derive(Debug, PartialEq)]
343pub struct ResolvedSym<'src> {
344 /// The name of the symbol.
345 pub name: &'src str,
346 /// The path to or name of the module containing the symbol.
347 pub module: Option<&'src OsStr>,
348 /// The symbol's normalized address.
349 pub addr: Addr,
350 /// The symbol's size, if available.
351 pub size: Option<usize>,
352 /// The source code language from which the symbol originates.
353 pub lang: SrcLang,
354 /// Source code location information.
355 pub code_info: Option<Box<CodeInfo<'src>>>,
356 /// Inlined function information.
357 pub inlined: Box<[InlinedFn<'src>]>,
358 /// The struct is non-exhaustive and open to extension.
359 // TODO: In the future we may want to make this type exhaustive to
360 // allow users to construct it easily themselves, in order to
361 // enable usage of custom "resolvers".
362 #[doc(hidden)]
363 pub _non_exhaustive: (),
364}
365
366
367/// The result of address symbolization by [`Symbolizer`].
368#[derive(Clone, Debug, PartialEq)]
369pub struct Sym<'src> {
370 /// The symbol name that an address belongs to.
371 pub name: Cow<'src, str>,
372 /// The path to or name of the module containing the symbol.
373 ///
374 /// Typically this would be the path to a executable or shared
375 /// object. Depending on the symbol source this member may not be
376 /// present or it could also just be a file name without path or a
377 /// symbolic name such as `[vdso]` representing the vDSO or `[bpf]`
378 /// for symbols in BPF programs. In case of an ELF file contained
379 /// inside an APK, this will be an Android style path of the form
380 /// `<apk>!<elf-in-apk>`. E.g., `/root/test.apk!/lib/libc.so`.
381 pub module: Option<Cow<'src, OsStr>>,
382 /// The address at which the symbol is located (i.e., its "start").
383 ///
384 /// This is the "normalized" address of the symbol, as present in
385 /// the file (and reported by tools such as `readelf(1)`,
386 /// `llvm-gsymutil`, or similar).
387 pub addr: Addr,
388 /// The byte offset of the address that got symbolized from the
389 /// start of the symbol (i.e., from `addr`).
390 ///
391 /// E.g., when symbolizing address 0x1337 of a function that starts at
392 /// 0x1330, the offset will be set to 0x07 (and `addr` will be 0x1330). This
393 /// member is especially useful in contexts when input addresses are not
394 /// already normalized, such as when symbolizing an address in a process
395 /// context (which may have been relocated and/or have layout randomizations
396 /// applied).
397 pub offset: usize,
398 /// The symbol's size, if available.
399 pub size: Option<usize>,
400 /// Source code location information for the symbol.
401 pub code_info: Option<Box<CodeInfo<'src>>>,
402 /// Inlined function information, if requested and available.
403 ///
404 /// Availability depends on both the underlying symbolization source (e.g.,
405 /// ELF does not contain inline information, but DWARF does) as well as
406 /// whether a function was actually inlined at the address in question.
407 ///
408 /// Inlined functions are reported in the order in which their calls are
409 /// nested. For example, if the instruction at the address to symbolize
410 /// falls into a function `f` at an inlined call to `g`, which in turn
411 /// contains an inlined call to `h`, the symbols will be reported in the
412 /// order `f`, `g`, `h`.
413 pub inlined: Box<[InlinedFn<'src>]>,
414 /// The struct is non-exhaustive and open to extension.
415 #[doc(hidden)]
416 pub _non_exhaustive: (),
417}
418
419impl Sym<'_> {
420 /// Convert this object into one with all references converted into
421 /// guaranteed owned (i.e., heap allocated) members.
422 pub fn into_owned(self) -> Sym<'static> {
423 let Self {
424 name,
425 module,
426 addr,
427 offset,
428 size,
429 code_info,
430 inlined,
431 _non_exhaustive,
432 } = self;
433
434 Sym {
435 name: Cow::Owned(name.into_owned()),
436 module: module.map(|module| Cow::Owned(module.into_owned())),
437 addr,
438 offset,
439 size,
440 code_info: code_info.map(|info| Box::new(info.into_owned())),
441 inlined: Vec::from(inlined)
442 .into_iter()
443 .map(InlinedFn::into_owned)
444 .collect::<Box<[_]>>(),
445 _non_exhaustive: (),
446 }
447 }
448}
449
450
451/// The reason why symbolization failed.
452///
453/// The reason is generally only meant as a hint. Reasons reported may change
454/// over time and, hence, should not be relied upon for the correctness of the
455/// application.
456#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
457#[non_exhaustive]
458pub enum Reason {
459 /// The absolute address was not found in the corresponding process' virtual
460 /// memory map.
461 Unmapped,
462 /// The file offset does not map to a valid piece of code/data.
463 InvalidFileOffset,
464 /// The `/proc/<pid>/maps` entry corresponding to the address does not have
465 /// a component (file system path, object, ...) associated with it.
466 MissingComponent,
467 /// The symbolization source has no or no relevant symbols.
468 ///
469 /// This reason could for instance be used if a shared object only
470 /// has dynamic symbols, but appears to be stripped aside from that.
471 MissingSyms,
472 /// The address belonged to an entity that is currently unsupported.
473 Unsupported,
474 /// The address could not be found in the symbolization source.
475 UnknownAddr,
476 /// An error prevented the symbolization of the address from
477 /// succeeding.
478 ///
479 /// This variant is used in batch operations to indicate a failure
480 /// preventing a single address from being symbolized.
481 IgnoredError,
482}
483
484impl Reason {
485 #[doc(hidden)]
486 #[inline]
487 pub fn as_bytes(&self) -> &'static [u8] {
488 match self {
489 Self::Unmapped => b"absolute address not found in virtual memory map of process\0",
490 Self::InvalidFileOffset => b"file offset does not map to a valid piece of code/data\0",
491 Self::MissingComponent => b"proc maps entry has no component\0",
492 Self::MissingSyms => b"symbolization source has no or no relevant symbols\0",
493 Self::Unsupported => b"address belongs to unsupported entity\0",
494 Self::UnknownAddr => b"address not found in symbolization source\0",
495 Self::IgnoredError => b"an error occurred but was ignored in batch mode\0",
496 }
497 }
498}
499
500impl Display for Reason {
501 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
502 let cstr = self.as_bytes();
503 // SAFETY: `as_bytes` always returns a valid string.
504 let s = unsafe { str::from_utf8_unchecked(&cstr[..cstr.len() - 1]) };
505
506 f.write_str(s)
507 }
508}
509
510
511/// An enumeration used as reporting vehicle for address symbolization.
512// We keep this enum as exhaustive because additions to it, should they occur,
513// are expected to be backwards-compatibility breaking.
514#[derive(Clone, Debug, PartialEq)]
515pub enum Symbolized<'src> {
516 /// The input address was symbolized as the provided symbol.
517 Sym(Sym<'src>),
518 /// The input address was not found and could not be symbolized.
519 ///
520 /// The provided reason is a best guess, hinting at what ultimately
521 /// prevented the symbolization from being successful.
522 Unknown(Reason),
523}
524
525impl<'src> Symbolized<'src> {
526 /// Convert the object into a [`Sym`] reference, if the corresponding
527 /// variant is active.
528 #[inline]
529 pub fn as_sym(&self) -> Option<&Sym<'src>> {
530 match self {
531 Self::Sym(sym) => Some(sym),
532 Self::Unknown(..) => None,
533 }
534 }
535
536 /// Convert the object into a [`Sym`] object, if the corresponding variant
537 /// is active.
538 #[inline]
539 pub fn into_sym(self) -> Option<Sym<'src>> {
540 match self {
541 Self::Sym(sym) => Some(sym),
542 Self::Unknown(..) => None,
543 }
544 }
545}
546
547
548/// A trait helping with upcasting into a `dyn Symbolize`.
549// TODO: This trait is currently necessary because our MSRV is <1.86.
550// Once we upgrade to that, we can make use of "native" upcasting
551// support on stable.
552#[doc(hidden)]
553pub trait AsSymbolize {
554 fn as_symbolize(&self) -> &dyn Symbolize;
555}
556
557/// The trait for types providing address symbolization services.
558pub trait Symbolize
559where
560 Self: AsSymbolize + Debug,
561{
562 /// Find the symbol corresponding to the given address.
563 fn find_sym(&self, addr: Addr, opts: &FindSymOpts) -> Result<Result<ResolvedSym<'_>, Reason>>;
564}
565
566impl<S> AsSymbolize for S
567where
568 S: Symbolize,
569{
570 fn as_symbolize(&self) -> &dyn Symbolize {
571 self
572 }
573}
574
575
576/// A meta-trait encompassing functionality necessary for plugging into
577/// the container symbolization logic.
578///
579/// Refer to [`Builder::set_apk_dispatcher`] and
580/// [`Builder::set_process_dispatcher`] for additional details.
581pub trait Resolve: Symbolize + TranslateFileOffset {}
582
583impl<R> Resolve for R where R: Symbolize + TranslateFileOffset {}
584
585
586/// A trait representing the ability to convert file offsets into virtual
587/// offsets.
588///
589/// Please refer to the [`Input`] enum for an overview of the various offset
590/// types.
591pub trait TranslateFileOffset
592where
593 Self: Debug,
594{
595 /// Convert the provided file offset into a virtual offset.
596 fn file_offset_to_virt_offset(&self, file_offset: u64) -> Result<Option<Addr>>;
597}
598
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604
605 /// Exercise the `Debug` representation of various types.
606 #[test]
607 fn debug_repr() {
608 let lang = SrcLang::default();
609 assert_ne!(format!("{lang:?}"), "");
610
611 let input = Input::FileOffset(0x1337);
612 assert_ne!(format!("{input:?}"), "");
613
614 let code_info = CodeInfo {
615 dir: Some(Cow::Borrowed(Path::new("/tmp/some-dir"))),
616 file: Cow::Borrowed(OsStr::new("test.c")),
617 line: Some(1337),
618 column: None,
619 _non_exhaustive: (),
620 };
621
622 let sym = Sym {
623 name: Cow::Borrowed("test"),
624 module: Some(Cow::Borrowed(OsStr::new("module"))),
625 addr: 1337,
626 offset: 42,
627 size: None,
628 code_info: None,
629 inlined: Box::new([InlinedFn {
630 name: Cow::Borrowed("inlined_test"),
631 code_info: Some(code_info.clone()),
632 _non_exhaustive: (),
633 }]),
634 _non_exhaustive: (),
635 };
636 assert_ne!(format!("{sym:?}"), "");
637
638 let symbolized = Symbolized::Sym(sym);
639 assert_ne!(format!("{symbolized:?}"), "");
640 }
641
642 /// Exercise the `Display` representation of various types.
643 #[test]
644 fn display_repr() {
645 assert_eq!(
646 Reason::MissingSyms.to_string(),
647 "symbolization source has no or no relevant symbols"
648 );
649 }
650
651 /// Test forcing a double check of all `Sym` size changes.
652 #[cfg(target_pointer_width = "64")]
653 #[test]
654 fn sym_size() {
655 assert_eq!(size_of::<Sym>(), 104);
656 }
657
658 /// Check that [`Sym::into_owned`] works as expected.
659 #[test]
660 fn owned_conversion() {
661 let sym = Sym {
662 name: Cow::Borrowed("test"),
663 module: Some(Cow::Borrowed(OsStr::new("module"))),
664 addr: 1337,
665 offset: 42,
666 size: None,
667 code_info: None,
668 inlined: Box::new([InlinedFn {
669 name: Cow::Borrowed("inlined_test"),
670 code_info: Some(CodeInfo {
671 dir: Some(Cow::Borrowed(Path::new("/tmp/some-dir"))),
672 file: Cow::Borrowed(OsStr::new("test.c")),
673 line: Some(1337),
674 column: None,
675 _non_exhaustive: (),
676 }),
677 _non_exhaustive: (),
678 }]),
679 _non_exhaustive: (),
680 };
681
682 assert_eq!(sym, sym.clone().into_owned());
683 }
684
685 /// Check that the [`Input::map`] helper works as expected.
686 #[test]
687 fn input_mapping() {
688 fn test<F>(f: F)
689 where
690 F: Fn(usize) -> Input<usize>,
691 {
692 let input = f(0x1337);
693 let input = input.map(|x| 2 * x);
694 assert_eq!(input, f(2 * 0x1337));
695 assert_eq!(input.into_inner(), 2 * 0x1337);
696 }
697
698 for variant in [Input::AbsAddr, Input::VirtOffset, Input::FileOffset] {
699 let () = test(variant);
700 }
701 }
702
703 /// Test the `Symbolized::*_sym()` conversion methods for the `Unknown`
704 /// variant.
705 #[test]
706 fn symbolized_unknown_conversions() {
707 let symbolized = Symbolized::Unknown(Reason::UnknownAddr);
708 assert_eq!(symbolized.as_sym(), None);
709 assert_eq!(symbolized.into_sym(), None);
710 }
711}