Skip to content

Hex-encode defmt symbols to avoid compatibility issues. #878

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]

- [#880]: Merge function calls emitted by the macro to save space.
- [#878]: Hex-encode defmt symbols to avoid compatibility issues.
- [#874]: `defmt`: Fix doc test
- [#872]: `defmt`: Add `expect!` as alias for `unwrap!` for discoverability
- [#871]: Set MSRV to Rust 1.76
Expand Down
16 changes: 16 additions & 0 deletions book/src/interning.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,19 @@ static SYM: u8 = 0;

The next thing to note is that each interned string symbol is one byte in size (because `static SYM` has type `u8`).
Thanks to this the addresses of the symbols are consecutive: 0, 1, 2, etc.

## Encoding

Storing strings as-is in symbol names can cause compatibility problems, since it can contain any arbitrary character. For example:

- The `'@'` character can't be used in symbol names because it's reserved for denoting symbol versions.
- The double-quote character `'"'` causes issues with escaping if you use it with `sym` inside an `asm!()` call.

To deal with this, as of Wire Format Version 5, strings are encoded to bytes as UTF-8, and then the bytes are hex-encoded.
The symbol is prefixed with `__defmt_hex_` to denote it's is hex-encoded, and to allow for future expansion.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The symbol is prefixed with `__defmt_hex_` to denote it's is hex-encoded, and to allow for future expansion.
The symbol is prefixed with `__defmt_hex_` to denote that it's hex-encoded, and to allow for future expansion.



``` rust
#[export_name = "__defmt_hex_55534220636f6e74726f6c6c6572206973207265616479"]
static SYM: u8 = 0;
```
2 changes: 1 addition & 1 deletion decoder/src/elf2table/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ pub fn parse_impl(elf: &[u8], check_version: bool) -> Result<Option<Table>, anyh
fn check_version(version: &str) -> Result<(), String> {
if !DEFMT_VERSIONS.contains(&version) {
let msg = format!(
"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 {}",
"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 {}",
version, DEFMT_VERSIONS.join(", "), version
);

Expand Down
29 changes: 28 additions & 1 deletion decoder/src/elf2table/symbol.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use std::borrow::Cow;

use anyhow::bail;
use serde::Deserialize;

use crate::Tag;
Expand Down Expand Up @@ -38,7 +41,11 @@ pub(super) enum SymbolTag {

impl Symbol {
pub fn demangle(raw: &str) -> anyhow::Result<Self> {
serde_json::from_str(raw)
let mut raw = Cow::from(raw);
if let Some(s) = raw.strip_prefix("__defmt_hex_") {
raw = Cow::from(hex_decode(s)?);
}
serde_json::from_str(&raw)
.map_err(|j| anyhow::anyhow!("failed to demangle defmt symbol `{}`: {}", raw, j))
}

Expand Down Expand Up @@ -77,3 +84,23 @@ impl Symbol {
self.crate_name.as_deref()
}
}

fn hex_decode_digit(c: u8) -> anyhow::Result<u8> {
match c {
b'0'..=b'9' => Ok(c - b'0'),
b'a'..=b'f' => Ok(c - b'a' + 0xa),
_ => bail!("invalid hex char '{c}'"),
}
}

fn hex_decode(s: &str) -> anyhow::Result<String> {
let s = s.as_bytes();
if s.len() % 2 != 0 {
bail!("invalid hex: length must be even")
}
let mut res = vec![0u8; s.len() / 2];
for i in 0..(s.len() / 2) {
res[i] = (hex_decode_digit(s[i * 2])? << 4) | hex_decode_digit(s[i * 2 + 1])?;
}
Ok(String::from_utf8(res)?)
}
4 changes: 2 additions & 2 deletions decoder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
#![cfg_attr(docsrs, doc(cfg(unstable)))]
#![doc(html_logo_url = "https://knurling.ferrous-systems.com/knurling_logo_light_text.svg")]

pub const DEFMT_VERSIONS: &[&str] = &["3", "4"];
pub const DEFMT_VERSIONS: &[&str] = &["3", "4", "5"];
// To avoid a breaking change, still provide `DEFMT_VERSION`.
#[deprecated = "Please use DEFMT_VERSIONS instead"]
pub const DEFMT_VERSION: &str = DEFMT_VERSIONS[1];
pub const DEFMT_VERSION: &str = DEFMT_VERSIONS[DEFMT_VERSIONS.len() - 1];

mod decoder;
mod elf2table;
Expand Down
2 changes: 1 addition & 1 deletion defmt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extern crate alloc;
#[used]
#[cfg_attr(target_os = "macos", link_section = ".defmt,end.VERSION")]
#[cfg_attr(not(target_os = "macos"), link_section = ".defmt.end")]
#[export_name = "_defmt_version_ = 4"]
#[export_name = "_defmt_version_ = 5"]
static DEFMT_VERSION: u8 = 0;

#[used]
Expand Down
13 changes: 11 additions & 2 deletions macros/src/construct/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,26 @@ impl<'a> Symbol<'a> {
}

fn mangle(&self) -> String {
format!(
let json = format!(
r#"{{"package":"{}","tag":"{}","data":"{}","disambiguator":"{}","crate_name":"{}"}}"#,
json_escape(&self.package),
json_escape(&self.tag),
json_escape(self.data),
self.disambiguator,
json_escape(&self.crate_name),
)
);
format!("__defmt_hex_{}", hex_encode(&json))
}
}

fn hex_encode(string: &str) -> String {
let mut res = String::new();
for &b in string.as_bytes() {
write!(&mut res, "{b:02x}").unwrap();
}
res
}

fn json_escape(string: &str) -> String {
let mut escaped = String::new();
for c in string.chars() {
Expand Down
4 changes: 2 additions & 2 deletions xtask/src/backcompat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ use crate::{
const DISABLED: bool = false;

// use this format: PR <number> - <what feature / change broke compatibility>
// PR #747 - Bump wire format
const REVISION_UNDER_TEST: &str = "0e92d3a88aa472377b964979f522829d961d8986";
// PR #878 - Bump wire format
const REVISION_UNDER_TEST: &str = "a36db0b100ea86f306225e02a222aede21f9adf5";

// the target name is in `firmware/qemu/.cargo/config.toml` but it'd be hard to extract it from that file
const RUNNER_ENV_VAR: &str = "CARGO_TARGET_THUMBV7M_NONE_EABI_RUNNER";
Expand Down
Loading