Skip to content
Open
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5da272f
Mac compatibility, part 1: macho support in object
aimir Oct 20, 2025
5733047
Mac compatibility, part 2: add explicit #[used] tags, as the trick a…
aimir Oct 20, 2025
4aed089
Mac compatibility, part 3: mac symbols are exported with a leading -…
aimir Oct 20, 2025
c0415da
Mac compatibility, part 4: get sections from the .defmt segment
aimir Oct 20, 2025
37e22ca
Mac compatibility, part 5: calculate the base address for the section…
aimir Oct 20, 2025
44692fc
Return to no-std, use linker symbols to calculate the base address
aimir Dec 7, 2025
60c0faf
Merge pull request #1 from gen-bc/feature/remove-std-from-base-addres…
aimir Dec 7, 2025
fd4e0f3
remove two deps from defmt/Cargo.toml
udigen Dec 8, 2025
e74bdc4
Merge pull request #2 from gen-bc/linux-and-mac-compatibility-cleanup
udigen Dec 8, 2025
87b4745
fix(macos): avoid 255 section-per-segment limit
Shai-Koffman Dec 31, 2025
0138020
Update macros/src/construct.rs
aimir Dec 31, 2025
06da809
Merge pull request #3 from gen-bc/fix/macos-255-section-limit
aimir Dec 31, 2025
dca26e6
add a fallback for base address calculation logic
aimir Jan 13, 2026
93eebd5
determine if we need to strip the leading _ based on the actual binar…
aimir Jan 13, 2026
62aa71c
leftover from some debugging session
aimir Jan 13, 2026
fc26e8b
make this comment more straightforward
aimir Jan 13, 2026
dcb8988
Merge pull request #6 from gen-bc/linux-and-mac-compatibility-fixes
aimir Jan 13, 2026
0383da7
Update decoder/src/elf2table/symbol.rs
aimir Jan 13, 2026
d24c7c3
section finding logic should be toggled based on the actual binary, r…
aimir Jan 13, 2026
267737b
Merge pull request #7 from gen-bc/linux-and-mac-compatibility-fixes2
aimir Jan 13, 2026
492fea1
move this comment to the right place
aimir Jan 13, 2026
3b4131b
avoid getting the section twice
aimir Jan 13, 2026
57acf1b
can I revert to this now?
aimir Jan 13, 2026
2dd95bd
this is fix is more lines, but I think it's less dependent on obscure…
aimir Jan 13, 2026
067b94c
Update the parsing logic accordingly
aimir Jan 13, 2026
ba1f9ae
debug prints
aimir Jan 13, 2026
311a12d
Revert "can I revert to this now?"
aimir Jan 13, 2026
8eb25bf
Revert "debug prints"
aimir Jan 13, 2026
34b395a
Merge pull request #8 from gen-bc/linux-and-mac-compatibility-fixes3
aimir Jan 13, 2026
6b12bee
Lint fix: this is unused
aimir Jan 21, 2026
1bbe735
Formatting
aimir Jan 21, 2026
714df9e
Taking addr_of external static is safe since rust 1.82
aimir Jan 21, 2026
88da8b9
Merge branch 'main' into linux-and-mac-compatibility-branched
aimir Jan 21, 2026
81410f3
Merge branch 'main' into linux-and-mac-compatibility-branched
aimir Jan 27, 2026
4238ec7
Fix tests - anything touching absolute addresses should now subtract …
aimir Jan 27, 2026
9601766
The "used" attribute causes Windows linker to try and resolve the sym…
aimir Jan 27, 2026
85ac935
Merge branch 'main' into linux-and-mac-compatibility-branched
aimir Jan 29, 2026
d378fde
move print to use object crate, so it can parse general object files
aimir Feb 26, 2026
77d6e3c
Merge branch 'main' into linux-and-mac-compatibility-branched
aimir Feb 26, 2026
f1dd0fc
Merge branch 'main' into linux-and-mac-compatibility-branched
aimir Apr 5, 2026
e9b82a6
resolve macOS dSYM location mapping via hash lookup
lakechurch Apr 19, 2026
785a6fb
Merge pull request #11 from lakechurch/fix-macos-dsym-mapping
aimir May 6, 2026
7b92af8
Fix lint and Linux/Windows compile errors from the macOS dSYM patch
aimir May 6, 2026
4bbe27f
Merge branch 'main' into linux-and-mac-compatibility-branched
aimir May 6, 2026
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 decoder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ gimli = { version = "0.29", default-features = false, features = [
] }
object = { version = "0.36", default-features = false, features = [
"read_core",
"macho",
"elf",
"std",
] }
Expand Down
68 changes: 47 additions & 21 deletions decoder/src/elf2table/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,28 @@ use std::{
};

use anyhow::{anyhow, bail, ensure};
use object::{Object, ObjectSection, ObjectSymbol};
use object::{Object, ObjectSection, ObjectSegment, ObjectSymbol};

use crate::{BitflagsKey, StringEntry, Table, TableEntry, Tag, DEFMT_VERSIONS};

pub fn parse_impl(elf: &[u8], check_version: bool) -> Result<Option<Table>, anyhow::Error> {
let elf = object::File::parse(elf)?;
let is_mac = elf.format() == object::BinaryFormat::MachO;
// first pass to extract the `_defmt_version`
let mut version = None;
let mut encoding = None;

// Note that we check for a quoted and unquoted version symbol, since LLD has a bug that
// makes it keep the quotes from the linker script.
let try_get_version = |name: &str| {
if name.starts_with("\"_defmt_version_ = ") || name.starts_with("_defmt_version_ = ") {
if name.starts_with("\"_defmt_version_ = ")
|| name.starts_with("_defmt_version_ = ")
|| name.starts_with("__defmt_version_ = ")
{
Some(
name.trim_start_matches("\"_defmt_version_ = ")
.trim_start_matches("_defmt_version_ = ")
.trim_start_matches("__defmt_version_ = ")
.trim_end_matches('"')
.to_string(),
)
Expand All @@ -43,6 +48,7 @@ pub fn parse_impl(elf: &[u8], check_version: bool) -> Result<Option<Table>, anyh
// using `#[export_name = "_defmt_encoding_ = x"]`, never in linker scripts.
let try_get_encoding = |name: &str| {
name.strip_prefix("_defmt_encoding_ = ")
.or_else(|| name.strip_prefix("__defmt_encoding_ = "))
.map(ToString::to_string)
};

Expand Down Expand Up @@ -77,12 +83,33 @@ pub fn parse_impl(elf: &[u8], check_version: bool) -> Result<Option<Table>, anyh
}
}

let mut defmt_sections = HashMap::new();
if is_mac {
let defmt_segment = elf.segments().find(|segment| {
object::ObjectSegment::name(segment).is_ok_and(|name| name == Some(".defmt"))
});
if let Some(defmt_segment) = defmt_segment {
for section in elf.sections() {
// check if the section is in the segment by comparing the section's address
// with the segment's address and size
if section.address() >= defmt_segment.address()
&& section.address() < defmt_segment.address() + defmt_segment.size()
{
defmt_sections.insert(section.index(), section);
}
}
}
} else {
let defmt_section = elf.section_by_name(".defmt");
if let Some(defmt_section) = defmt_section {
defmt_sections.insert(defmt_section.index(), defmt_section);
}
}

// NOTE: We need to make sure to return `Ok(None)`, not `Err`, when defmt is not in use.
// Otherwise probe-run won't work with apps that don't use defmt.

let defmt_section = elf.section_by_name(".defmt");

let (defmt_section, version) = match (defmt_section, version) {
let (_defmt_section, version) = match (defmt_sections.values().next(), version) {
(None, None) => return Ok(None), // defmt is not used
(Some(defmt_section), Some(version)) => (defmt_section, version),
(None, Some(_)) => {
Expand Down Expand Up @@ -124,15 +151,23 @@ pub fn parse_impl(elf: &[u8], check_version: bool) -> Result<Option<Table>, anyh
continue;
}

if name.starts_with("_defmt") || name.starts_with("__DEFMT_MARKER") {
if name.starts_with("_defmt")
|| name.starts_with("__DEFMT_MARKER")
|| name.starts_with("__defmt")
{
// `_defmt_version_` is not a JSON encoded `defmt` symbol / log-message; skip it
// LLD and GNU LD behave differently here. LLD doesn't include `_defmt_version_`
// (defined in a linker script) in the `.defmt` section but GNU LD does.
continue;
}

if entry.section_index() == Some(defmt_section.index()) {
let sym = symbol::Symbol::demangle(name)?;
// find the relevant section using the section index
if let Some(section_index) = entry.section_index() {
if !defmt_sections.contains_key(&section_index) {
continue;
}

let sym = symbol::Symbol::demangle(name, is_mac)?;
match sym.tag() {
symbol::SymbolTag::Defmt(Tag::Timestamp) => {
if timestamp.is_some() {
Expand All @@ -145,18 +180,9 @@ pub fn parse_impl(elf: &[u8], check_version: bool) -> Result<Option<Table>, anyh
));
}
symbol::SymbolTag::Defmt(Tag::BitflagsValue) => {
// Bitflags values always occupy 128 bits / 16 bytes.
const BITFLAGS_VALUE_SIZE: u64 = 16;

if entry.size() != BITFLAGS_VALUE_SIZE {
bail!(
"bitflags value does not occupy 16 bytes (symbol `{}`)",
name
);
}

let defmt_data = defmt_section.data()?;
let addr = entry.address() as usize;
let section = defmt_sections.get(&section_index).unwrap();
let defmt_data = section.data()?;
let addr = (entry.address() - section.address()) as usize;
let value = match defmt_data.get(addr..addr + 16) {
Some(bytes) => u128::from_le_bytes(bytes.try_into().unwrap()),
None => bail!(
Expand Down Expand Up @@ -189,7 +215,7 @@ pub fn parse_impl(elf: &[u8], check_version: bool) -> Result<Option<Table>, anyh
}
symbol::SymbolTag::Defmt(tag) => {
map.insert(
entry.address() as usize,
(entry.address() as u16) as usize,
TableEntry::new(
StringEntry::new(tag, sym.data().to_string()),
name.to_string(),
Expand Down
9 changes: 8 additions & 1 deletion decoder/src/elf2table/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ pub(super) enum SymbolTag {
}

impl Symbol {
pub fn demangle(raw: &str) -> anyhow::Result<Self> {
pub fn demangle(raw: &str, is_mac: bool) -> anyhow::Result<Self> {
let raw = if is_mac {
// remove the prefix from the raw string
raw.strip_prefix("_")
.ok_or_else(|| anyhow::anyhow!("macos symbol `{}` should start with `_`", raw))?
} else {
raw
};
serde_json::from_str(raw)
.map_err(|j| anyhow::anyhow!("failed to demangle defmt symbol `{}`: {}", raw, j))
}
Expand Down
55 changes: 54 additions & 1 deletion defmt/src/export/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ thread_local! {
#[cfg(feature = "unstable-test")]
pub fn fetch_string_index() -> u16 {
I.with(|i| i.load(core::sync::atomic::Ordering::Relaxed))
.wrapping_sub(binary_base())
}

/// For testing purposes
Expand Down Expand Up @@ -98,9 +99,61 @@ pub fn timestamp(fmt: crate::Formatter<'_>) {
unsafe { _defmt_timestamp(fmt) }
}

/// For bare-metal targets there is no ASLR, so the base address is always 0.
#[cfg(target_os = "none")]
fn binary_base() -> u16 {
0
}

/// For Linux (ELF), use the linker-provided `__executable_start` symbol.
#[cfg(target_os = "linux")]
#[allow(unused_unsafe)]
fn binary_base() -> u16 {
extern "C" {
static __executable_start: u8;
}
// SAFETY: `__executable_start` is a linker-provided symbol marking the start of the executable.
(unsafe { core::ptr::addr_of!(__executable_start) as usize }) as u16
Comment thread
aimir marked this conversation as resolved.
}

/// For macOS (Mach-O), use the linker-provided `_mh_execute_header` symbol.
#[cfg(target_os = "macos")]
#[allow(unused_unsafe)]
fn binary_base() -> u16 {
extern "C" {
static _mh_execute_header: u8;
}
// SAFETY: `_mh_execute_header` is a linker-provided symbol marking the Mach-O header.
(unsafe { core::ptr::addr_of!(_mh_execute_header) as usize }) as u16
}

/// For Windows, use the DOS header address from the PE format.
#[cfg(target_os = "windows")]
#[allow(unused_unsafe)]
fn binary_base() -> u16 {
extern "C" {
static __ImageBase: u8;
}
// SAFETY: `__ImageBase` is a linker-provided symbol marking the base of the PE image.
(unsafe { core::ptr::addr_of!(__ImageBase) as usize }) as u16
}

/// Fallback for other platforms - assume no ASLR, so the base address is always 0.
#[cfg(not(any(
target_os = "none",
target_os = "linux",
target_os = "macos",
target_os = "windows"
)))]
fn binary_base() -> u16 {
0
}

/// Returns the interned string at `address`.
pub fn make_istr(address: u16) -> Str {
Str { address }
Str {
address: address.wrapping_sub(binary_base()),
}
}

/// Create a Formatter.
Expand Down
10 changes: 8 additions & 2 deletions macros/src/construct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ pub(crate) fn interned_string(

/// work around restrictions on length and allowed characters imposed by macos linker
/// returns (note the comma character for macos):
/// under macos: ".defmt," + 16 character hex digest of symbol's hash
/// under macos: ".defmt," + prefix
/// otherwise: ".defmt." + prefix + symbol
pub(crate) fn linker_section(for_macos: bool, prefix: Option<&str>, symbol: &str) -> String {
let mut sub_section = if let Some(prefix) = prefix {
Expand All @@ -69,7 +69,13 @@ pub(crate) fn linker_section(for_macos: bool, prefix: Option<&str>, symbol: &str
};

if for_macos {
sub_section = format!(",{:x}", hash(&sub_section));
// Use a single section per severity level instead of unique section per log.
Comment thread
jonathanpallant marked this conversation as resolved.
// This avoids hitting macOS's 255 section-per-segment limit.
// The symbol's export_name is still unique, so address-based lookup works.
sub_section = match prefix {
Some(p) => format!(",{p}"),
None => ",data".to_string(),
};
}

format!(".defmt{sub_section}")
Expand Down
1 change: 1 addition & 0 deletions macros/src/items/bitflags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ fn codegen_flag_statics(input: &Input) -> Vec<TokenStream2> {

quote! {
#(#cfg_attrs)*
#[cfg_attr(target_os = "macos", used)]
#[cfg_attr(target_os = "macos", link_section = ".defmt,end")]
#[cfg_attr(not(target_os = "macos"), link_section = ".defmt.end")]
#[export_name = #sym_name]
Expand Down
1 change: 1 addition & 0 deletions macros/src/items/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub(crate) fn expand(args: TokenStream) -> TokenStream {
}
}

#[cfg_attr(target_os = "macos", used)]
#var_item;

// Unique symbol name to prevent multiple `timestamp!` invocations in the crate graph.
Expand Down
Loading