Skip to main content

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