defmt_elf2table/
lib.rs

1//! Reads ELF metadata and builds a [`defmt`](https://github.com/knurling-rs/defmt) interner table.
2//!
3//! This is an implementation detail of [`probe-run`](https://github.com/knurling-rs/probe-run) and
4//! not meant to be consumed by other tools at the moment so all the API is unstable.
5
6#![cfg(feature = "unstable")]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8#![cfg_attr(docsrs, doc(cfg(unstable)))]
9
10mod symbol;
11
12use std::{
13    borrow::Cow,
14    collections::BTreeMap,
15    fmt,
16    path::{Path, PathBuf},
17};
18
19use anyhow::{anyhow, bail, ensure};
20pub use defmt_decoder::Table;
21use defmt_decoder::{StringEntry, TableEntry};
22use object::{Object, ObjectSection};
23
24/// Parses an ELF file and returns the decoded `defmt` table
25///
26/// This function returns `None` if the ELF file contains no `.defmt` section
27pub fn parse(elf: &[u8]) -> Result<Option<Table>, anyhow::Error> {
28    let elf = object::File::parse(elf)?;
29    // first pass to extract the `_defmt_version`
30    let mut version = None;
31    let is_defmt_version = |name: &str| {
32        name.starts_with("\"_defmt_version_ = ") || name.starts_with("_defmt_version_ = ")
33    };
34    for (_, entry) in elf.symbols() {
35        let name = match entry.name() {
36            Some(name) => name,
37            None => continue,
38        };
39
40        // Not in the `.defmt` section because it's not tied to the address of any symbol
41        // in `.defmt`.
42        // Note that we check for a quoted and unquoted version symbol, since LLD has a bug that
43        // makes it keep the quotes from the linker script.
44        if is_defmt_version(name) {
45            let new_version = name
46                .trim_start_matches("\"_defmt_version_ = ")
47                .trim_start_matches("_defmt_version_ = ")
48                .trim_end_matches('"');
49            if let Some(version) = version {
50                return Err(anyhow!(
51                    "multiple defmt versions in use: {} and {} (only one is supported)",
52                    version,
53                    new_version
54                ));
55            }
56            version = Some(new_version);
57        }
58    }
59
60    // NOTE: We need to make sure to return `Ok(None)`, not `Err`, when defmt is not in use.
61    // Otherwise probe-run won't work with apps that don't use defmt.
62
63    let defmt_shndx = elf.section_by_name(".defmt").map(|s| s.index());
64
65    let (defmt_shndx, version) = match (defmt_shndx, version) {
66        (None, None) => return Ok(None), // defmt is not used
67        (Some(defmt_shndx), Some(version)) => (defmt_shndx, version),
68        (None, Some(_)) => {
69            bail!("defmt version found, but no `.defmt` section - check your linker configuration");
70        }
71        (Some(_), None) => {
72            bail!(
73                "`.defmt` section found, but no version symbol - check your linker configuration"
74            );
75        }
76    };
77
78    defmt_decoder::check_version(version).map_err(anyhow::Error::msg)?;
79
80    // second pass to demangle symbols
81    let mut map = BTreeMap::new();
82    for (_, entry) in elf.symbols() {
83        // Skipping symbols with empty string names, as they may be added by
84        // `objcopy`, and breaks JSON demangling
85        let name = match entry.name() {
86            Some(name) if !name.is_empty() => name,
87            _ => continue,
88        };
89
90        if is_defmt_version(name) {
91            // `_defmt_version_` is not a JSON encoded `defmt` symbol / log-message; skip it
92            // LLD and GNU LD behave differently here. LLD doesn't include `_defmt_version_`
93            // (defined in a linker script) in the `.defmt` section but GNU LD does.
94            continue;
95        }
96
97        if entry.section_index() == Some(defmt_shndx) {
98            let sym = symbol::Symbol::demangle(name)?;
99            if let symbol::SymbolTag::Defmt(tag) = sym.tag() {
100                map.insert(
101                    entry.address() as usize,
102                    TableEntry::new(
103                        StringEntry::new(tag, sym.data().to_string()),
104                        name.to_string(),
105                    ),
106                );
107            }
108        }
109    }
110
111    Ok(Some(Table::new(map)))
112}
113
114#[derive(Clone)]
115pub struct Location {
116    pub file: PathBuf,
117    pub line: u64,
118    pub module: String,
119}
120
121impl fmt::Debug for Location {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "{}:{}", self.file.display(), self.line)
124    }
125}
126
127pub type Locations = BTreeMap<u64, Location>;
128
129pub fn get_locations(elf: &[u8], table: &Table) -> Result<Locations, anyhow::Error> {
130    let live_syms = table.raw_symbols().collect::<Vec<_>>();
131    let object = object::File::parse(elf)?;
132    let endian = if object.is_little_endian() {
133        gimli::RunTimeEndian::Little
134    } else {
135        gimli::RunTimeEndian::Big
136    };
137
138    let load_section = |id: gimli::SectionId| {
139        Ok(if let Some(s) = object.section_by_name(id.name()) {
140            s.uncompressed_data().unwrap_or(Cow::Borrowed(&[][..]))
141        } else {
142            Cow::Borrowed(&[][..])
143        })
144    };
145    let load_section_sup = |_| Ok(Cow::Borrowed(&[][..]));
146
147    let dwarf_cow =
148        gimli::Dwarf::<Cow<[u8]>>::load::<_, _, anyhow::Error>(&load_section, &load_section_sup)?;
149
150    let borrow_section: &dyn for<'a> Fn(
151        &'a Cow<[u8]>,
152    ) -> gimli::EndianSlice<'a, gimli::RunTimeEndian> =
153        &|section| gimli::EndianSlice::new(&*section, endian);
154
155    let dwarf = dwarf_cow.borrow(&borrow_section);
156
157    let mut units = dwarf.debug_info.units();
158
159    let mut map = BTreeMap::new();
160    while let Some(header) = units.next()? {
161        let unit = dwarf.unit(header)?;
162        let abbrev = header.abbreviations(&dwarf.debug_abbrev)?;
163
164        let mut cursor = header.entries(&abbrev);
165
166        ensure!(cursor.next_dfs()?.is_some(), "empty DWARF?");
167
168        let mut segments = vec![];
169        let mut depth = 0;
170        while let Some((delta_depth, entry)) = cursor.next_dfs()? {
171            depth += delta_depth;
172
173            // NOTE .. here start the custom logic
174            if entry.tag() == gimli::constants::DW_TAG_namespace {
175                let mut attrs = entry.attrs();
176
177                while let Some(attr) = attrs.next()? {
178                    match attr.name() {
179                        gimli::constants::DW_AT_name => {
180                            if let gimli::AttributeValue::DebugStrRef(off) = attr.value() {
181                                let s = dwarf.string(off)?;
182                                for _ in (depth as usize)..segments.len() + 1 {
183                                    segments.pop();
184                                }
185                                segments.push(core::str::from_utf8(&s)?.to_string());
186                            }
187                        }
188                        _ => {}
189                    }
190                }
191            } else if entry.tag() == gimli::constants::DW_TAG_variable {
192                // Iterate over the attributes in the DIE.
193                let mut attrs = entry.attrs();
194
195                // what we are after
196                let mut decl_file = None;
197                let mut decl_line = None; // line number
198                let mut name = None;
199                let mut linkage_name = None;
200                let mut location = None;
201
202                while let Some(attr) = attrs.next()? {
203                    match attr.name() {
204                        gimli::constants::DW_AT_name => {
205                            if let gimli::AttributeValue::DebugStrRef(off) = attr.value() {
206                                name = Some(off);
207                            }
208                        }
209
210                        gimli::constants::DW_AT_decl_file => {
211                            if let gimli::AttributeValue::FileIndex(idx) = attr.value() {
212                                decl_file = Some(idx);
213                            }
214                        }
215
216                        gimli::constants::DW_AT_decl_line => {
217                            if let gimli::AttributeValue::Udata(line) = attr.value() {
218                                decl_line = Some(line);
219                            }
220                        }
221
222                        gimli::constants::DW_AT_location => {
223                            if let gimli::AttributeValue::Exprloc(loc) = attr.value() {
224                                location = Some(loc);
225                            }
226                        }
227
228                        gimli::constants::DW_AT_linkage_name => {
229                            if let gimli::AttributeValue::DebugStrRef(off) = attr.value() {
230                                linkage_name = Some(off);
231                            }
232                        }
233
234                        _ => {}
235                    }
236                }
237
238                if let (
239                    Some(name_index),
240                    Some(linkage_name_index),
241                    Some(file_index),
242                    Some(line),
243                    Some(loc),
244                ) = (name, linkage_name, decl_file, decl_line, location)
245                {
246                    let name_slice = dwarf.string(name_index)?;
247                    let name = core::str::from_utf8(&name_slice)?;
248                    let linkage_name_slice = dwarf.string(linkage_name_index)?;
249                    let linkage_name = core::str::from_utf8(&linkage_name_slice)?;
250
251                    if name == "DEFMT_LOG_STATEMENT" {
252                        if live_syms.contains(&linkage_name) {
253                            let addr = exprloc2address(unit.encoding(), &loc)?;
254                            let file = file_index_to_path(file_index, &unit, &dwarf)?;
255                            let module = segments.join("::");
256
257                            let loc = Location { file, line, module };
258
259                            if let Some(old) = map.insert(addr, loc.clone()) {
260                                bail!("BUG in DWARF variable filter: index collision for addr 0x{:08x} (old = {:?}, new = {:?})", addr, old, loc);
261                            }
262                        } else {
263                            // this symbol was GC-ed by the linker (but remains in the DWARF info)
264                            // so we discard it (its `addr` info is also wrong which causes collisions)
265                        }
266                    }
267                }
268            }
269        }
270    }
271
272    Ok(map)
273}
274
275fn file_index_to_path<R>(
276    index: u64,
277    unit: &gimli::Unit<R>,
278    dwarf: &gimli::Dwarf<R>,
279) -> Result<PathBuf, anyhow::Error>
280where
281    R: gimli::read::Reader,
282{
283    ensure!(index != 0, "`FileIndex` was zero");
284
285    let header = if let Some(program) = &unit.line_program {
286        program.header()
287    } else {
288        bail!("no `LineProgram`");
289    };
290
291    let file = if let Some(file) = header.file(index) {
292        file
293    } else {
294        bail!("no `FileEntry` for index {}", index)
295    };
296
297    let mut p = PathBuf::new();
298    if let Some(dir) = file.directory(header) {
299        let dir = dwarf.attr_string(unit, dir)?;
300        let dir_s = dir.to_string_lossy()?;
301        let dir = Path::new(&dir_s[..]);
302
303        if !dir.is_absolute() {
304            if let Some(ref comp_dir) = unit.comp_dir {
305                p.push(&comp_dir.to_string_lossy()?[..]);
306            }
307        }
308        p.push(&dir);
309    }
310
311    p.push(
312        &dwarf
313            .attr_string(unit, file.path_name())?
314            .to_string_lossy()?[..],
315    );
316
317    Ok(p)
318}
319
320fn exprloc2address<R: gimli::read::Reader<Offset = usize>>(
321    encoding: gimli::Encoding,
322    data: &gimli::Expression<R>,
323) -> Result<u64, anyhow::Error> {
324    let mut pc = data.0.clone();
325    while pc.len() != 0 {
326        if let Ok(gimli::Operation::Address { address }) =
327            gimli::Operation::parse(&mut pc, encoding)
328        {
329            return Ok(address);
330        }
331    }
332
333    Err(anyhow!("`Operation::Address` not found"))
334}