Skip to main content

sys_rs/
debug.rs

1use gimli;
2use goblin::elf::header::{ELFDATA2LSB, ELFDATA2MSB};
3use nix::errno::Errno;
4use std::{
5    collections::HashMap,
6    fmt,
7    fs::File,
8    io::{BufRead, BufReader},
9    path::PathBuf,
10};
11
12use crate::{
13    diag::{Error, Result},
14    process,
15};
16
17const FIRST_UNSUPPORTED_DWARF_VERSION: u16 = 5;
18
19/// Information about a source line mapped from an address.
20///
21/// `LineInfo` contains the instruction address, the source file path, and
22/// the 1-based line number. It provides helpers for retrieving the line's
23/// text and formatting it for display.
24pub struct LineInfo {
25    addr: u64,
26    path: PathBuf,
27    line: usize,
28}
29
30impl LineInfo {
31    /// Create a new `LineInfo` from an address, path and 1-based line number.
32    ///
33    /// # Arguments
34    ///
35    /// * `addr` - The instruction address associated with the source line.
36    /// * `path` - The path to the source file.
37    /// * `line` - The 1-based line number in the file.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the provided `line` cannot be converted to a
42    /// `usize`.
43    ///
44    /// # Returns
45    ///
46    /// Returns `Ok(LineInfo)` on success with the provided address, path,
47    /// and converted line number. Returns `Err` if the `line` argument
48    /// cannot be converted to `usize`.
49    pub fn new(addr: u64, path: PathBuf, line: u64) -> Result<Self> {
50        Ok(Self {
51            addr,
52            path,
53            line: usize::try_from(line)?,
54        })
55    }
56
57    #[must_use]
58    /// Return the source path as a displayable `String`.
59    ///
60    /// # Returns
61    ///
62    /// A `String` containing the display representation of the stored path.
63    pub fn path(&self) -> String {
64        self.path.display().to_string()
65    }
66
67    #[must_use]
68    /// Return the 1-based source line number.
69    ///
70    /// # Returns
71    ///
72    /// The 1-based source line number stored in this `LineInfo`.
73    pub fn line(&self) -> usize {
74        self.line
75    }
76
77    fn read(&self) -> Result<String> {
78        let file = File::open(&self.path)?;
79        let reader = BufReader::new(file);
80
81        let mut lines = reader.lines();
82
83        let line = lines
84            .nth(self.line - 1)
85            .ok_or_else(|| Error::from(Errno::ENODATA))??;
86        Ok(line)
87    }
88}
89
90impl fmt::Display for LineInfo {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        let line = self.read().map_err(|_| fmt::Error)?;
93        write!(
94            f,
95            "{:#x}: {}:{} | {}",
96            self.addr,
97            self.path.display(),
98            self.line,
99            line
100        )
101    }
102}
103
104type AddressRange = Vec<(u64, u64)>;
105type DebugArangesMap = HashMap<gimli::DebugInfoOffset, AddressRange>;
106type SectionData<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>;
107
108/// DWARF debugging information parsed from an ELF image.
109///
110/// `Dwarf` encapsulates parsed DWARF sections and a mapping from compile
111/// unit offsets to address ranges. It provides helpers to build the DWARF
112/// representation from an executable and resolve addresses to source
113/// locations.
114pub struct Dwarf<'a> {
115    data: gimli::Dwarf<SectionData<'a>>,
116    aranges: DebugArangesMap,
117    offset: u64,
118}
119
120impl<'a> Dwarf<'a> {
121    /// Builds a `Dwarf` struct from the given process information.
122    ///
123    /// # Arguments
124    ///
125    /// * `process` - The process information.
126    ///
127    /// # Errors
128    ///
129    /// Returns an `Err` upon any failure to retrieve ELF sections or parse DWARF format.
130    ///
131    /// # Returns
132    ///
133    /// Returns a `Dwarf` struct on success, wrapped in an `Ok` variant. Returns an error on failure, wrapped in an `Err` variant.
134    pub fn build(process: &'a process::Info) -> Result<Self> {
135        let endianness = match process.endianness() {
136            ELFDATA2LSB => Ok(gimli::RunTimeEndian::Little),
137            ELFDATA2MSB => Ok(gimli::RunTimeEndian::Big),
138            _ => Err(Error::from(Errno::ENOEXEC)),
139        }?;
140
141        let debug_ranges = gimli::DebugRanges::new(
142            Self::get_section(".debug_ranges", process).unwrap_or(&[]),
143            endianness,
144        );
145        let debug_rnglists = gimli::DebugRngLists::new(
146            Self::get_section(".debug_rnglists", process).unwrap_or(&[]),
147            endianness,
148        );
149        let ranges = gimli::RangeLists::new(debug_ranges, debug_rnglists);
150        let data = gimli::Dwarf {
151            debug_abbrev: gimli::DebugAbbrev::new(
152                Self::get_section(".debug_abbrev", process)?,
153                endianness,
154            ),
155            debug_info: gimli::DebugInfo::new(
156                Self::get_section(".debug_info", process)?,
157                endianness,
158            ),
159            debug_line: gimli::DebugLine::new(
160                Self::get_section(".debug_line", process)?,
161                endianness,
162            ),
163            debug_str: gimli::DebugStr::new(
164                Self::get_section(".debug_str", process)?,
165                endianness,
166            ),
167            ranges,
168            ..Default::default()
169        };
170
171        let aranges = Self::build_aranges(&data)?;
172        let offset = process.offset();
173
174        Ok(Self {
175            data,
176            aranges,
177            offset,
178        })
179    }
180
181    fn get_section(
182        section_name: &'a str,
183        process: &'a process::Info,
184    ) -> Result<&'a [u8]> {
185        process
186            .get_section_data(section_name)?
187            .ok_or_else(|| Error::from(Errno::ENODATA))
188    }
189
190    fn build_aranges(
191        dwarf: &gimli::Dwarf<gimli::EndianSlice<gimli::RunTimeEndian>>,
192    ) -> Result<HashMap<gimli::DebugInfoOffset, Vec<(u64, u64)>>> {
193        let mut aranges = HashMap::new();
194        let mut iter = dwarf.units();
195        while let Some(unit_header) = iter.next()? {
196            if unit_header.version() >= FIRST_UNSUPPORTED_DWARF_VERSION {
197                Err(Errno::ENOEXEC)?;
198            }
199
200            let mut unit_ranges = Vec::new();
201            let unit = dwarf.unit(unit_header)?;
202            let mut entries = unit.entries();
203            while let Some((_, entry)) = entries.next_dfs()? {
204                let mut attrs = entry.attrs();
205                let mut low_pc = None;
206                let mut high_pc = None;
207                let mut high_pc_offset = None;
208                let mut ranges_offset = None;
209                while let Some(attr) = attrs.next()? {
210                    match attr.name() {
211                        gimli::DW_AT_low_pc => {
212                            if let gimli::AttributeValue::Addr(addr) = attr.value() {
213                                low_pc = Some(addr);
214                            }
215                        }
216                        gimli::DW_AT_high_pc => match attr.value() {
217                            gimli::AttributeValue::Addr(val) => high_pc = Some(val),
218                            gimli::AttributeValue::Udata(val) => {
219                                high_pc_offset = Some(val);
220                            }
221                            _ => Err(Error::from(Errno::ENODATA))?,
222                        },
223                        gimli::DW_AT_ranges => {
224                            if let gimli::AttributeValue::RangeListsRef(val) =
225                                attr.value()
226                            {
227                                ranges_offset = Some(val);
228                            }
229                        }
230                        _ => {}
231                    }
232                }
233
234                if let (Some(low_pc), Some(high_pc)) = (low_pc, high_pc) {
235                    unit_ranges.push((low_pc, high_pc));
236                } else if let (Some(low_pc), Some(high_pc_offset)) =
237                    (low_pc, high_pc_offset)
238                {
239                    unit_ranges.push((low_pc, (low_pc + high_pc_offset)));
240                } else if let Some(ranges_offset) = ranges_offset {
241                    let offset = dwarf.ranges_offset_from_raw(&unit, ranges_offset);
242                    let mut iter = dwarf.ranges(&unit, offset)?;
243                    while let Some(range) = iter.next()? {
244                        unit_ranges.push((range.begin, range.end));
245                    }
246                }
247            }
248
249            let offset = unit_header
250                .offset()
251                .as_debug_info_offset()
252                .ok_or_else(|| Error::from(Errno::ENODATA))?;
253            aranges.insert(offset, unit_ranges);
254        }
255
256        Ok(aranges)
257    }
258
259    fn is_addr_in_unit<R: gimli::Reader<Offset = usize>>(
260        &self,
261        addr: u64,
262        unit_header: &gimli::UnitHeader<R>,
263    ) -> Result<bool> {
264        let offset = unit_header
265            .offset()
266            .as_debug_info_offset()
267            .ok_or_else(|| Error::from(Errno::ENODATA))?;
268
269        self.aranges
270            .get(&offset)
271            .map(|ranges| {
272                ranges
273                    .iter()
274                    .any(|(start, end)| (*start..*end).contains(&addr))
275            })
276            .ok_or_else(|| Error::from(Errno::ENODATA))
277    }
278
279    fn path_from_row(
280        &self,
281        unit_header: &gimli::UnitHeader<SectionData<'_>>,
282        program_header: &gimli::LineProgramHeader<SectionData<'_>>,
283        row: &gimli::LineRow,
284    ) -> Result<PathBuf> {
285        let mut path = PathBuf::new();
286
287        let unit = self.data.unit(*unit_header)?;
288        if let Some(dir) = unit.comp_dir {
289            path.push(dir.to_string_lossy().into_owned());
290        }
291
292        let file = row
293            .file(program_header)
294            .ok_or_else(|| Error::from(Errno::ENODATA))?;
295        if file.directory_index() != 0 {
296            if let Some(dir) = file.directory(program_header) {
297                let dir_path = self
298                    .data
299                    .attr_string(&unit, dir)?
300                    .to_string_lossy()
301                    .into_owned();
302                path.push(dir_path);
303            }
304        }
305
306        let file_path = self
307            .data
308            .attr_string(&unit, file.path_name())?
309            .to_string_lossy()
310            .into_owned();
311        path.push(file_path);
312
313        Ok(path)
314    }
315
316    fn info_from_row(
317        &self,
318        unit_header: &gimli::UnitHeader<SectionData<'_>>,
319        program_header: &gimli::LineProgramHeader<SectionData<'_>>,
320        row: &gimli::LineRow,
321    ) -> Result<LineInfo> {
322        let line = row.line().ok_or_else(|| Error::from(Errno::ENODATA))?;
323
324        let line = line.get();
325        let path = self.path_from_row(unit_header, program_header, row)?;
326        LineInfo::new(row.address() + self.offset, path, line)
327    }
328
329    fn info_from_unit(
330        &self,
331        addr: u64,
332        unit_header: &gimli::UnitHeader<SectionData<'_>>,
333    ) -> Result<Option<LineInfo>> {
334        let mut info = None;
335
336        let unit = self.data.unit(*unit_header)?;
337        if let Some(program) = unit.line_program {
338            let mut rows = program.rows();
339            while let Some((program_header, row)) = rows.next_row()? {
340                if !row.is_stmt() {
341                    continue;
342                }
343
344                if addr != row.address() {
345                    continue;
346                }
347
348                info = Some(self.info_from_row(unit_header, program_header, row)?);
349                break;
350            }
351        }
352
353        Ok(info)
354    }
355
356    /// Resolves the source file name and line number for a given address in the binary.
357    ///
358    /// # Arguments
359    ///
360    /// * `addr`: The address in the binary's address space.
361    ///
362    /// # Errors
363    ///
364    /// Returns an error if there's any failure in reading or parsing the DWARF debug information.
365    ///
366    /// # Returns
367    ///
368    /// - `Ok(Some(LineInfo))`: The resolved source file name and line number.
369    /// - `Ok(None)`: The address does not correspond to any source line information.
370    /// - `Err`: Error reading or parsing the DWARF information.
371    pub fn addr2line(&self, addr: u64) -> Result<Option<LineInfo>> {
372        let addr = addr - self.offset;
373
374        let mut info: Option<LineInfo> = None;
375        let mut iter = self.data.units();
376        while let Some(unit_header) = iter.next()? {
377            if !self.is_addr_in_unit(addr, &unit_header)? {
378                continue;
379            }
380
381            info = self.info_from_unit(addr, &unit_header)?;
382            if info.is_some() {
383                break;
384            }
385        }
386
387        Ok(info)
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    use gimli::{
396        DebugAbbrev, DebugInfo, DebugLine, DebugRanges, DebugRngLists, DebugStr,
397        RangeLists, RunTimeEndian,
398    };
399    use std::{collections::HashMap, io::Write};
400
401    use crate::diag::Result;
402
403    #[test]
404    fn test_line_info_new() {
405        let path = PathBuf::from("/path/to/file.rs");
406        let line_info = LineInfo::new(0x1234, path.clone(), 42)
407            .expect("Failed to create LineInfo");
408        assert_eq!(line_info.addr, 0x1234);
409        assert_eq!(line_info.path, path);
410        assert_eq!(line_info.line, 42);
411    }
412
413    #[test]
414    fn test_line_info_path() {
415        let path = PathBuf::from("/path/to/file.rs");
416        let line_info = LineInfo::new(0x1234, path.clone(), 42)
417            .expect("Failed to create LineInfo");
418        assert_eq!(line_info.path(), "/path/to/file.rs");
419    }
420
421    #[test]
422    fn test_line_info_line() {
423        let path = PathBuf::from("/path/to/file.rs");
424        let line_info =
425            LineInfo::new(0x1234, path, 42).expect("Failed to create LineInfo");
426        assert_eq!(line_info.line(), 42);
427    }
428
429    #[test]
430    fn test_line_info_display() {
431        let mut tmpfile =
432            tempfile::NamedTempFile::new().expect("Failed to create temp file");
433        for i in 1..100 {
434            writeln!(tmpfile, "line {}", i).expect("Failed to write to temp file");
435        }
436        let path = tmpfile.path().to_path_buf();
437        let line_info = LineInfo::new(0x1234, path.clone(), 42)
438            .expect("Failed to create LineInfo");
439        let display = format!("{}", line_info);
440        assert!(display.contains("0x1234"));
441        assert!(
442            display.contains(path.to_str().expect("Failed to convert path to str"))
443        );
444        assert!(display.contains("42"));
445    }
446
447    #[test]
448    fn test_build_aranges_empty() -> Result<()> {
449        let endian = RunTimeEndian::Little;
450        let data = gimli::Dwarf {
451            debug_abbrev: DebugAbbrev::new(&[], endian),
452            debug_info: DebugInfo::new(&[], endian),
453            debug_line: DebugLine::new(&[], endian),
454            debug_str: DebugStr::new(&[], endian),
455            ranges: RangeLists::new(
456                DebugRanges::new(&[], endian),
457                DebugRngLists::new(&[], endian),
458            ),
459            ..Default::default()
460        };
461
462        let map = Dwarf::build_aranges(&data)?;
463        assert!(map.is_empty());
464        Ok(())
465    }
466
467    #[test]
468    fn test_addr2line_empty_returns_none() -> Result<()> {
469        let endian = RunTimeEndian::Little;
470        let data = gimli::Dwarf {
471            debug_abbrev: DebugAbbrev::new(&[], endian),
472            debug_info: DebugInfo::new(&[], endian),
473            debug_line: DebugLine::new(&[], endian),
474            debug_str: DebugStr::new(&[], endian),
475            ranges: RangeLists::new(
476                DebugRanges::new(&[], endian),
477                DebugRngLists::new(&[], endian),
478            ),
479            ..Default::default()
480        };
481
482        let dwarf = Dwarf {
483            data,
484            aranges: HashMap::new(),
485            offset: 0,
486        };
487        let res = dwarf.addr2line(0x1000)?;
488        assert!(res.is_none());
489        Ok(())
490    }
491}