Skip to main content

rudy_dwarf/symbols/
mod.rs

1//! Symbol-based indexing for fast debug info lookups
2
3mod names;
4
5use std::{collections::BTreeMap, path::PathBuf};
6
7use anyhow::{Context, Result};
8use itertools::Itertools;
9pub use names::{RawSymbol, SymbolName, TypeName};
10use object::{Object, ObjectSymbol};
11
12use crate::{
13    file::{load, Binary, DebugFile, File},
14    function::FunctionIndex,
15    DwarfDb,
16};
17
18/// Information about a symbol from the symbol table
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub struct Symbol {
21    pub name: SymbolName,
22    pub address: u64,
23    pub debug_file: DebugFile,
24}
25pub type DebugFiles = BTreeMap<(PathBuf, Option<String>), DebugFile>;
26
27/// Reads the binary file for all the declared symbols
28/// and potentially external symbols. Turns it into
29/// a map of symbol names, as well as finds all
30/// external debug files.
31pub fn index_symbol_map(
32    db: &dyn DwarfDb,
33    binary: Binary,
34) -> anyhow::Result<(DebugFiles, SymbolIndex)> {
35    let mut debug_files = DebugFiles::new();
36
37    // load the binary file
38    let binary_file = binary.file(db);
39    let loaded_file = match load(db, binary_file) {
40        Ok(file) => file,
41        Err(e) => {
42            return Err(e.clone())
43                .with_context(|| format!("Failed to load binary file: {}", binary_file.name(db)));
44        }
45    };
46
47    // create debug file for teh binary
48    let debug_file = DebugFile::new(db, binary_file, false);
49    debug_files.insert((debug_file.file(db).path(db).clone(), None), debug_file);
50
51    // index the symbols in the binary (if it has debug info)
52    let mut symbol_index = SymbolIndex::default();
53    if loaded_file.object.has_debug_symbols() {
54        symbol_index.index_binary(&loaded_file.object, debug_file)?;
55    }
56
57    // next, if we have any mapped objects (via Mach-O)
58    // then we'll locate all the debug files, and index their symbols
59    let mut indexed_object_files = vec![None; loaded_file.object.object_map().objects().len()];
60    let object_map = loaded_file.object.object_map();
61    for (i, object_file) in object_map.objects().iter().enumerate() {
62        let object_path = object_file.path();
63        let Ok(object_path) = String::from_utf8(object_path.to_vec()) else {
64            tracing::debug!("Failed to parse object file path: {:?}", object_file.path());
65            continue;
66        };
67        let object_path = PathBuf::from(object_path);
68        let Ok(member) = object_file
69            .member()
70            .map(|m| String::from_utf8(m.to_vec()))
71            .transpose()
72        else {
73            tracing::debug!(
74                "Failed to parse object file member: {:?}",
75                object_file.member()
76            );
77            continue;
78        };
79
80        let file = match File::build(db, object_path.clone(), member.clone()) {
81            Ok(file) => file,
82            Err(e) => {
83                tracing::error!(
84                    "Failed to load debug file {} with member: {member:?}: {e}",
85                    object_path.display()
86                );
87                continue;
88            }
89        };
90        tracing::trace!("Found debug file: {}", file.name(db));
91        // Create a debug file for this object
92        let debug_file = DebugFile::new(db, file, true);
93        debug_files.insert((file.path(db).clone(), member), debug_file);
94        indexed_object_files[i] = Some(debug_file);
95    }
96
97    // split objects by index
98    let grouped_symbols = object_map
99        .symbols()
100        .iter()
101        .into_group_map_by(|s| s.object_index());
102
103    for (object_index, symbols) in grouped_symbols {
104        if let Some(debug_file) = indexed_object_files[object_index] {
105            tracing::trace!(
106                "Indexing mapped symbols for debug file: {}",
107                debug_file.name(db)
108            );
109            symbol_index.index_mapped_file(symbols.into_iter(), debug_file)?;
110        }
111    }
112
113    Ok((debug_files, symbol_index))
114}
115
116pub type DebugFileSymbols = BTreeMap<RawSymbol, Symbol>;
117
118/// Fast symbol-based index built from symbol tables
119#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
120pub struct SymbolIndex {
121    /// Function name -> [module1::func -> info, module2::func -> info, ...]
122    /// Grouped by lookup_name, then by full SymbolName
123    pub functions: BTreeMap<String, BTreeMap<SymbolName, Symbol>>,
124
125    /// Non-function symbols, grouped similarly
126    pub symbols: BTreeMap<String, BTreeMap<SymbolName, Symbol>>,
127
128    /// All symbols grouped by file
129    ///
130    /// This is useful for quickly finding all symbols in a specific file
131    pub symbols_by_file: BTreeMap<DebugFile, DebugFileSymbols>,
132
133    /// All functions sorted by address for binary search lookup
134    /// Used for address-to-function mapping
135    pub functions_by_address: BTreeMap<u64, Vec<Symbol>>,
136}
137
138impl SymbolIndex {
139    /// Find function by exact name match
140    pub fn get_function(&self, name: &SymbolName) -> Option<&Symbol> {
141        self.functions.get(&name.lookup_name)?.get(name)
142    }
143
144    /// Find all functions with the given lookup name
145    pub fn get_functions_by_lookup_name(
146        &self,
147        lookup_name: &str,
148    ) -> Option<&BTreeMap<SymbolName, Symbol>> {
149        self.functions.get(lookup_name)
150    }
151
152    /// Find function containing the given address using binary search
153    pub fn function_at_address(&self, address: u64) -> Option<(u64, &Vec<Symbol>)> {
154        // Find the first function(s) with an address less than or equal to the given address
155        self.functions_by_address
156            .range(..=address)
157            .next_back()
158            .map(|(base_addr, v)| (*base_addr, v))
159    }
160
161    pub fn function_index<'db>(
162        &'db self,
163        db: &'db dyn DwarfDb,
164        debug_file: DebugFile,
165    ) -> Option<&'db FunctionIndex<'db>> {
166        Some(crate::function::function_index(
167            db,
168            debug_file,
169            self.symbols_by_file.get(&debug_file)?,
170        ))
171    }
172
173    pub fn index_binary(&mut self, object: &object::File<'_>, debug_file: DebugFile) -> Result<()> {
174        let file_symbols = self.symbols_by_file.entry(debug_file).or_default();
175
176        for s in object.symbols() {
177            let Ok(name) = s.name_bytes() else {
178                tracing::debug!("Failed to parse symbol name at: {:#010x}", s.address());
179                continue;
180            };
181
182            let symbol = RawSymbol::new(name.to_vec());
183
184            let Ok(demangled) = symbol.demangle() else {
185                tracing::trace!(
186                    "Failed to demangle symbol at: {:#010x}: {}",
187                    s.address(),
188                    String::from_utf8_lossy(name)
189                );
190                continue;
191            };
192
193            // We'll assume that symbols from the .TEXT section are
194            // functions
195            let is_function = s.kind() == object::SymbolKind::Text;
196
197            let index_name = demangled.lookup_name.clone();
198            let entry = Symbol {
199                name: demangled.clone(),
200                address: s.address(),
201                debug_file,
202            };
203            file_symbols.insert(symbol.clone(), entry.clone());
204
205            let map = if is_function {
206                self.functions_by_address
207                    .entry(entry.address)
208                    .or_default()
209                    .push(entry.clone());
210                &mut self.functions
211            } else {
212                &mut self.symbols
213            };
214
215            // Insert the symbol into the appropriate map
216            map.entry(index_name)
217                .or_default()
218                .insert(demangled.clone(), entry);
219        }
220        Ok(())
221    }
222
223    pub fn index_mapped_file<'a>(
224        &mut self,
225        symbol_iter: impl Iterator<Item = &'a object::ObjectMapEntry<'a>>,
226        debug_file: DebugFile,
227    ) -> Result<()> {
228        let file_symbols = self.symbols_by_file.entry(debug_file).or_default();
229        for s in symbol_iter {
230            let symbol = RawSymbol::new(s.name().to_vec());
231
232            let Ok(demangled) = symbol.demangle() else {
233                tracing::trace!(
234                    "Failed to demangle symbol at: {:#010x} {}",
235                    s.address(),
236                    String::from_utf8_lossy(s.name())
237                );
238                continue;
239            };
240
241            // We'll assume that symbols all have 0 size
242            // and functions are non-zero.
243            let is_function = s.size() > 0;
244
245            tracing::trace!(
246                "Indexing symbol at: {:#010x} {} (is_function: {is_function})",
247                s.address(),
248                demangled.lookup_name
249            );
250
251            let index_name = demangled.lookup_name.clone();
252            let entry = Symbol {
253                name: demangled.clone(),
254                address: s.address(),
255                debug_file,
256            };
257            file_symbols.insert(symbol.clone(), entry.clone());
258            let map = if is_function {
259                self.functions_by_address
260                    .entry(entry.address)
261                    .or_default()
262                    .push(entry.clone());
263                &mut self.functions
264            } else {
265                &mut self.symbols
266            };
267
268            // Insert the symbol into the appropriate map
269            map.entry(index_name)
270                .or_default()
271                .insert(demangled.clone(), entry);
272        }
273        Ok(())
274    }
275}
276
277#[cfg(test)]
278mod test {
279    use std::env::current_exe;
280
281    use anyhow::Result;
282
283    use super::*;
284
285    #[test]
286    fn test_symbol_index_basic() -> Result<()> {
287        crate::test_utils::init_tracing();
288
289        // Initialize the debug database and load a binary with debug info
290        // test on macos file because it has the external symbol files
291        let artifact_dir = crate::test_utils::artifacts_dir(Some("aarch64-apple-darwin"));
292        let exe_path = artifact_dir.join("examples/small");
293
294        let db = crate::test_utils::test_db(Some("aarch64-apple-darwin"));
295        let db = &db;
296        let binary = crate::test_utils::load_binary(db, &exe_path);
297
298        // Build the symbol index
299        let (_debug_files, symbol_index) = index_symbol_map(db, binary).unwrap();
300
301        // Verify we have some functions indexed
302        assert!(
303            !symbol_index.functions.is_empty(),
304            "Should have indexed some functions"
305        );
306        assert!(
307            !symbol_index.functions_by_address.is_empty(),
308            "Should have functions grouped by address"
309        );
310
311        tracing::info!(
312            "Symbol index created successfully with {} function groups and {} total functions",
313            symbol_index.functions.len(),
314            symbol_index.functions_by_address.len()
315        );
316
317        // Test address lookup
318        if let Some((addr, first_funcs)) = symbol_index.functions_by_address.first_key_value() {
319            let (_, found_func) = symbol_index
320                .function_at_address(*addr)
321                .expect("Should find function at address");
322            assert_eq!(found_func, first_funcs);
323        }
324
325        Ok(())
326    }
327
328    #[test]
329    fn test_symbol_index_performance() -> Result<()> {
330        crate::test_utils::init_tracing();
331
332        let exe_path = current_exe().unwrap();
333
334        let db = crate::test_utils::test_db(Some("aarch64-apple-darwin"));
335        let db = &db;
336        let binary = crate::test_utils::load_binary(db, &exe_path);
337
338        let start = std::time::Instant::now();
339        let (_debug_files, symbol_index) = index_symbol_map(db, binary).unwrap();
340        let symbol_index_time = start.elapsed();
341
342        tracing::info!(
343            "Symbol index built in {:?}. Got: {} functions and {} symbols",
344            symbol_index_time,
345            symbol_index.functions.len(),
346            symbol_index.symbols.len()
347        );
348
349        // This should be much faster than full DWARF indexing
350        // We expect it to be under 100ms for most binaries
351        assert!(
352            symbol_index_time.as_millis() < 5000,
353            "Symbol index should be fast, took {symbol_index_time:?}"
354        );
355
356        Ok(())
357    }
358}