defmt_decoder/elf2table/
mod.rs

1//! Reads ELF metadata and builds a table containing [`defmt`](https://github.com/knurling-rs/defmt) format strings.
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
6mod symbol;
7
8use std::{
9    borrow::Cow,
10    collections::{BTreeMap, HashMap},
11    convert::TryInto,
12    fmt,
13    path::{Path, PathBuf},
14};
15
16use anyhow::{anyhow, bail, ensure};
17use object::{Object, ObjectSection, ObjectSymbol};
18
19use crate::{BitflagsKey, StringEntry, Table, TableEntry, Tag, DEFMT_VERSIONS};
20
21pub fn parse_impl(elf: &[u8], check_version: bool) -> Result<Option<Table>, anyhow::Error> {
22    let elf = object::File::parse(elf)?;
23    // first pass to extract the `_defmt_version`
24    let mut version = None;
25    let mut encoding = None;
26
27    // Note that we check for a quoted and unquoted version symbol, since LLD has a bug that
28    // makes it keep the quotes from the linker script.
29    let try_get_version = |name: &str| {
30        if name.starts_with("\"_defmt_version_ = ") || name.starts_with("_defmt_version_ = ") {
31            Some(
32                name.trim_start_matches("\"_defmt_version_ = ")
33                    .trim_start_matches("_defmt_version_ = ")
34                    .trim_end_matches('"')
35                    .to_string(),
36            )
37        } else {
38            None
39        }
40    };
41
42    // No need to remove quotes for `_defmt_encoding_`, since it's defined in Rust code
43    // using `#[export_name = "_defmt_encoding_ = x"]`, never in linker scripts.
44    let try_get_encoding = |name: &str| {
45        name.strip_prefix("_defmt_encoding_ = ")
46            .map(ToString::to_string)
47    };
48
49    for entry in elf.symbols() {
50        let name = match entry.name() {
51            Ok(name) => name,
52            Err(_) => continue,
53        };
54
55        // Not in the `.defmt` section because it's not tied to the address of any symbol
56        // in `.defmt`.
57        if let Some(new_version) = try_get_version(name) {
58            if let Some(version) = version {
59                return Err(anyhow!(
60                    "multiple defmt versions in use: {} and {} (only one is supported)",
61                    version,
62                    new_version
63                ));
64            }
65            version = Some(new_version);
66        }
67
68        if let Some(new_encoding) = try_get_encoding(name) {
69            if let Some(encoding) = encoding {
70                return Err(anyhow!(
71                    "multiple defmt encodings in use: {} and {} (only one is supported)",
72                    encoding,
73                    new_encoding
74                ));
75            }
76            encoding = Some(new_encoding);
77        }
78    }
79
80    // NOTE: We need to make sure to return `Ok(None)`, not `Err`, when defmt is not in use.
81    // Otherwise probe-run won't work with apps that don't use defmt.
82
83    let defmt_section = elf.section_by_name(".defmt");
84
85    let (defmt_section, version) = match (defmt_section, version) {
86        (None, None) => return Ok(None), // defmt is not used
87        (Some(defmt_section), Some(version)) => (defmt_section, version),
88        (None, Some(_)) => {
89            bail!("defmt version found, but no `.defmt` section - check your linker configuration");
90        }
91        (Some(_), None) => {
92            bail!(
93                "`.defmt` section found, but no version symbol - check your linker configuration"
94            );
95        }
96    };
97
98    if check_version {
99        self::check_version(&version).map_err(anyhow::Error::msg)?;
100    }
101
102    let encoding = match encoding {
103        Some(e) => e.parse()?,
104        None => bail!("No defmt encoding specified. This is a bug."),
105    };
106
107    // second pass to demangle symbols
108    let mut map = BTreeMap::new();
109    let mut bitflags_map = HashMap::new();
110    let mut timestamp = None;
111    for entry in elf.symbols() {
112        let Ok(name) = entry.name() else {
113            continue;
114        };
115
116        if name.is_empty() {
117            // Skipping symbols with empty string names, as they may be added by
118            // `objcopy`, and breaks JSON demangling
119            continue;
120        }
121
122        if name == "$d" || name.starts_with("$d.") {
123            // Skip AArch64 mapping symbols
124            continue;
125        }
126
127        if name.starts_with("_defmt") || name.starts_with("__DEFMT_MARKER") {
128            // `_defmt_version_` is not a JSON encoded `defmt` symbol / log-message; skip it
129            // LLD and GNU LD behave differently here. LLD doesn't include `_defmt_version_`
130            // (defined in a linker script) in the `.defmt` section but GNU LD does.
131            continue;
132        }
133
134        if entry.section_index() == Some(defmt_section.index()) {
135            let sym = symbol::Symbol::demangle(name)?;
136            match sym.tag() {
137                symbol::SymbolTag::Defmt(Tag::Timestamp) => {
138                    if timestamp.is_some() {
139                        bail!("multiple timestamp format specifications found");
140                    }
141
142                    timestamp = Some(TableEntry::new(
143                        StringEntry::new(Tag::Timestamp, sym.data().to_string()),
144                        name.to_string(),
145                    ));
146                }
147                symbol::SymbolTag::Defmt(Tag::BitflagsValue) => {
148                    // Bitflags values always occupy 128 bits / 16 bytes.
149                    const BITFLAGS_VALUE_SIZE: u64 = 16;
150
151                    if entry.size() != BITFLAGS_VALUE_SIZE {
152                        bail!(
153                            "bitflags value does not occupy 16 bytes (symbol `{}`)",
154                            name
155                        );
156                    }
157
158                    let defmt_data = defmt_section.data()?;
159                    let addr = entry.address() as usize;
160                    let value = match defmt_data.get(addr..addr + 16) {
161                        Some(bytes) => u128::from_le_bytes(bytes.try_into().unwrap()),
162                        None => bail!(
163                            "bitflags value at {:#x} outside of defmt section",
164                            entry.address()
165                        ),
166                    };
167                    log::debug!("bitflags value `{}` has value {:#x}", sym.data(), value);
168
169                    let segments = sym.data().split("::").collect::<Vec<_>>();
170                    let (bitflags_name, value_idx, value_name) = match &*segments {
171                        [bitflags_name, value_idx, value_name] => {
172                            (*bitflags_name, value_idx.parse::<u128>()?, *value_name)
173                        }
174                        _ => bail!("malformed bitflags value string '{}'", sym.data()),
175                    };
176
177                    let key = BitflagsKey {
178                        ident: bitflags_name.into(),
179                        package: sym.package().into(),
180                        disambig: sym.disambiguator().into(),
181                        crate_name: sym.crate_name().map(|s| s.into()),
182                    };
183
184                    bitflags_map.entry(key).or_insert_with(Vec::new).push((
185                        value_name.into(),
186                        value_idx,
187                        value,
188                    ));
189                }
190                symbol::SymbolTag::Defmt(tag) => {
191                    map.insert(
192                        entry.address() as usize,
193                        TableEntry::new(
194                            StringEntry::new(tag, sym.data().to_string()),
195                            name.to_string(),
196                        ),
197                    );
198                }
199                symbol::SymbolTag::Custom(_) => {}
200            }
201        }
202    }
203
204    // Sort bitflags values by the value's index in definition order. Since all values get their own
205    // symbol and section, their order in the final binary is unspecified and can't be relied on, so
206    // we put them back in the original order here.
207    let bitflags = bitflags_map
208        .into_iter()
209        .map(|(k, mut values)| {
210            values.sort_by_key(|(_, index, _)| *index);
211            let values = values
212                .into_iter()
213                .map(|(name, _index, value)| (name, value))
214                .collect();
215
216            (k, values)
217        })
218        .collect();
219
220    Ok(Some(Table {
221        entries: map,
222        timestamp,
223        bitflags,
224        encoding,
225    }))
226}
227
228/// Checks if the version encoded in the symbol table is compatible with this version of the `decoder` crate
229fn check_version(version: &str) -> Result<(), String> {
230    if !DEFMT_VERSIONS.contains(&version) {
231        let msg = format!(
232            "defmt wire format version mismatch: firmware is using {}, `probe-run` supports {}\nsuggestion: use a newer version of `defmt` or `cargo install` a different version of `probe-run` that supports defmt {}",
233            version, DEFMT_VERSIONS.join(", "), version
234        );
235
236        return Err(msg);
237    }
238
239    Ok(())
240}
241
242/// Location of a defmt log statement in the elf-file
243#[derive(Clone)]
244pub struct Location {
245    pub file: PathBuf,
246    pub line: u64,
247    pub module: String,
248}
249
250impl fmt::Debug for Location {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        write!(f, "{}:{}", self.file.display(), self.line)
253    }
254}
255
256/// Mapping of memory address to [`Location`]
257pub type Locations = BTreeMap<u64, Location>;
258
259pub fn get_locations(elf: &[u8], table: &Table) -> Result<Locations, anyhow::Error> {
260    let object = object::File::parse(elf)?;
261    let endian = if object.is_little_endian() {
262        gimli::RunTimeEndian::Little
263    } else {
264        gimli::RunTimeEndian::Big
265    };
266
267    let load_section = |id: gimli::SectionId| {
268        Ok(if let Some(s) = object.section_by_name(id.name()) {
269            s.uncompressed_data().unwrap_or(Cow::Borrowed(&[][..]))
270        } else {
271            Cow::Borrowed(&[][..])
272        })
273    };
274    let load_section_sup = |_| Ok(Cow::Borrowed(&[][..]));
275
276    let dwarf_sections =
277        gimli::DwarfSections::<Cow<[u8]>>::load::<_, anyhow::Error>(&load_section)?;
278    let dwarf_sup_sections = gimli::DwarfSections::load::<_, anyhow::Error>(&load_section_sup)?;
279
280    let borrow_section: &dyn for<'a> Fn(
281        &'a Cow<[u8]>,
282    ) -> gimli::EndianSlice<'a, gimli::RunTimeEndian> =
283        &|section| gimli::EndianSlice::new(section, endian);
284
285    let dwarf = dwarf_sections.borrow_with_sup(&dwarf_sup_sections, &borrow_section);
286
287    let mut units = dwarf.debug_info.units();
288
289    let mut map = BTreeMap::new();
290    while let Some(header) = units.next()? {
291        let unit = dwarf.unit(header)?;
292        let abbrev = header.abbreviations(&dwarf.debug_abbrev)?;
293
294        let mut cursor = header.entries(&abbrev);
295
296        ensure!(cursor.next_dfs()?.is_some(), "empty DWARF?");
297
298        let mut segments = vec![];
299        let mut depth = 0;
300        while let Some((delta_depth, entry)) = cursor.next_dfs()? {
301            depth += delta_depth;
302
303            // NOTE .. here start the custom logic
304            if entry.tag() == gimli::constants::DW_TAG_namespace {
305                let mut attrs = entry.attrs();
306
307                while let Some(attr) = attrs.next()? {
308                    if attr.name() == gimli::constants::DW_AT_name {
309                        if let gimli::AttributeValue::DebugStrRef(off) = attr.value() {
310                            let s = dwarf.string(off)?;
311                            for _ in (depth as usize)..segments.len() + 1 {
312                                segments.pop();
313                            }
314                            segments.push(core::str::from_utf8(&s)?.to_string());
315                        }
316                    }
317                }
318            } else if entry.tag() == gimli::constants::DW_TAG_variable {
319                // Iterate over the attributes in the DIE.
320                let mut attrs = entry.attrs();
321
322                // what we are after
323                let mut decl_file = None;
324                let mut decl_line = None; // line number
325                let mut name = None;
326                let mut linkage_name = None;
327                let mut location = None;
328
329                while let Some(attr) = attrs.next()? {
330                    match attr.name() {
331                        gimli::constants::DW_AT_name => {
332                            if let gimli::AttributeValue::DebugStrRef(off) = attr.value() {
333                                name = Some(off);
334                            }
335                        }
336                        gimli::constants::DW_AT_decl_file => {
337                            if let gimli::AttributeValue::FileIndex(idx) = attr.value() {
338                                decl_file = Some(idx);
339                            }
340                        }
341                        gimli::constants::DW_AT_decl_line => {
342                            if let gimli::AttributeValue::Udata(line) = attr.value() {
343                                decl_line = Some(line);
344                            }
345                        }
346                        gimli::constants::DW_AT_location => {
347                            if let gimli::AttributeValue::Exprloc(loc) = attr.value() {
348                                location = Some(loc);
349                            }
350                        }
351                        gimli::constants::DW_AT_linkage_name => {
352                            if let gimli::AttributeValue::DebugStrRef(off) = attr.value() {
353                                linkage_name = Some(off);
354                            }
355                        }
356                        _ => {}
357                    }
358                }
359
360                if let (
361                    Some(name_index),
362                    Some(linkage_name_index),
363                    Some(file_index),
364                    Some(line),
365                    Some(loc),
366                ) = (name, linkage_name, decl_file, decl_line, location)
367                {
368                    let name_slice = dwarf.string(name_index)?;
369                    let name = core::str::from_utf8(&name_slice)?;
370                    let linkage_name_slice = dwarf.string(linkage_name_index)?;
371                    let linkage_name = core::str::from_utf8(&linkage_name_slice)?;
372
373                    if name == "DEFMT_LOG_STATEMENT" {
374                        if table.raw_symbols().any(|i| i == linkage_name) {
375                            let addr = exprloc2address(unit.encoding(), &loc)?;
376                            let file = file_index_to_path(file_index, &unit, &dwarf)?;
377                            let module = segments.join("::");
378
379                            let loc = Location { file, line, module };
380
381                            if let Some(old) = map.insert(addr, loc.clone()) {
382                                bail!("BUG in DWARF variable filter: index collision for addr 0x{:08x} (old = {:?}, new = {:?})", addr, old, loc);
383                            }
384                        } else {
385                            // this symbol was GC-ed by the linker (but remains in the DWARF info)
386                            // so we discard it (its `addr` info is also wrong which causes collisions)
387                        }
388                    }
389                }
390            }
391        }
392    }
393
394    Ok(map)
395}
396
397fn file_index_to_path<R>(
398    index: u64,
399    unit: &gimli::Unit<R>,
400    dwarf: &gimli::Dwarf<R>,
401) -> Result<PathBuf, anyhow::Error>
402where
403    R: gimli::read::Reader,
404{
405    ensure!(index != 0, "`FileIndex` was zero");
406
407    let header = if let Some(program) = &unit.line_program {
408        program.header()
409    } else {
410        bail!("no `LineProgram`");
411    };
412
413    let file = if let Some(file) = header.file(index) {
414        file
415    } else {
416        bail!("no `FileEntry` for index {}", index)
417    };
418
419    let mut p = PathBuf::new();
420    if let Some(dir) = file.directory(header) {
421        let dir = dwarf.attr_string(unit, dir)?;
422        let dir_s = dir.to_string_lossy()?;
423        let dir = Path::new(&dir_s[..]);
424
425        if !dir.is_absolute() {
426            if let Some(ref comp_dir) = unit.comp_dir {
427                p.push(&comp_dir.to_string_lossy()?[..]);
428            }
429        }
430        p.push(dir);
431    }
432
433    p.push(
434        &dwarf
435            .attr_string(unit, file.path_name())?
436            .to_string_lossy()?[..],
437    );
438
439    Ok(p)
440}
441
442fn exprloc2address<R: gimli::read::Reader<Offset = usize>>(
443    encoding: gimli::Encoding,
444    data: &gimli::Expression<R>,
445) -> Result<u64, anyhow::Error> {
446    let mut pc = data.0.clone();
447    while pc.len() != 0 {
448        if let Ok(gimli::Operation::Address { address }) =
449            gimli::Operation::parse(&mut pc, encoding)
450        {
451            return Ok(address);
452        }
453    }
454
455    Err(anyhow!("`Operation::Address` not found"))
456}