Developer Guide

Purpose

avrmem is designed to inspect AVR ELF executables without maintaining an MCU-specific memory database in the source tree.

The implementation separates:

The central design principle is:

ELF semantics
     !=
device semantics
     !=
presentation

Source tree

The project is organized as:

.
├── Makefile
├── README.md
├── src/
│   ├── avr_device.c
│   ├── avr_device_probe.c
│   ├── avr_elf.c
│   ├── avr_sfr.c
│   └── avrmem.c
├── include/
│   ├── avr_device.h
│   ├── avr_device_probe.h
│   ├── avr_elf.h
│   └── avr_sfr.h
├── bin/
└── docs/
    ├── _config.yml
    ├── index.md
    ├── README.md
    ├── guide.md
    ├── api.md
    ├── testing.md
    └── architecture.md

The source code belongs in src/.

Public and internal headers belong in include/.

Generated executables belong in bin/.

The docs/ directory is also the source tree for the Jekyll documentation site.


Build system

The project is built with the top-level Makefile.

Normal build:

make

The executable is:

bin/avrmem

Debug builds

A debug configuration can be enabled with:

make debug

or:

make DEBUG=1
make DEBUG=yes
make DEBUG=true
make DEBUG=on

The debug configuration uses:

-O0
-g3
-DDEBUG

The optimized configuration uses:

-O2

with the project’s warning options.

Clean build

make clean
make

For a full rebuild:

make rebuild

Running tests

make test       # unit + integration + CLI black-box
make test-asan  # same suite with AddressSanitizer/UBSan

Unit tiers run without a toolchain; integration and CLI tiers build AVR fixtures with avr-gcc first and SKIP gracefully without it. See the Testing Guide (Automated test suite section) for tiers, fixtures, and conventions.


Host/toolchain requirements

The current implementation assumes a POSIX host.

The build itself is a host C application, while the AVR target information is discovered through the installed AVR-GCC toolchain.

Required tools include:

gcc or compatible C11 compiler
avr-gcc
avr-libc
AVR binutils selected by avr-gcc
make

Verify the AVR compiler:

which avr-gcc
avr-gcc --version

Verify a device is accepted:

avr-gcc -mmcu=atmega328p --version

Runtime device selection

The device can be selected explicitly:

bin/avrmem --device atmega328p firmware.elf

or inferred from the ELF:

bin/avrmem firmware.elf

When --device is omitted, avrmem obtains the device name from the ELF AVR device metadata.

The selected device is then passed into:

avr_device_init()

The device layer is therefore initialized before symbol/section address resolution.


Overall runtime flow

A normal invocation follows this sequence:

argv
 |
 v
getopt_long()
 |
 v
CommandLine
 |
 v
avr_elf_open()
 |
 +--> ELF validation
 +--> sections
 +--> symbols
 +--> program headers
 +--> AVR device metadata
 |
 v
device selection
 |
 v
avr_device_init()
 |
 +--> avr-gcc macro probe
 |      |
 |      +--> device name
 |      +--> physical memory
 |      +--> SFR definitions
 |
 +--> avr-gcc linker-map probe
        |
        +--> TEXT
        +--> DATA
        +--> EEPROM
 |
 v
AvrDevice
 |
 v
section/symbol resolution
 |
 v
presentation

avr_device_probe.c

The probe layer is responsible for discovering device information from the installed toolchain.

It intentionally does not contain a database such as:

if (strcmp(device_name, "atmega328p") == 0)
  ...

Instead, the toolchain itself is the source of device-specific information.


AVR-LibC macro probing

The first discovery path uses AVR-GCC preprocessing.

Conceptually:

avr-gcc -mmcu=<device> -dM -E ...

with:

#include <avr/io.h>

The resulting macro environment contains target-specific information.

The probe extracts:

device name
FLASH range
SRAM range
EEPROM range
SFR definitions

The canonical device name is taken from:

__AVR_DEVICE_NAME__

The physical memory information is obtained from avr-libc macros such as:

RAMSTART
RAMEND
FLASHSTART
FLASHEND
E2START
E2END

Not every avr-libc revision defines every optional macro, so the probe contains fallbacks where appropriate.


SFR discovery

SFR discovery uses the same preprocessor output.

Typical definitions include:

#define PORTB  _SFR_IO8(0x05)
#define UCSR0A _SFR_MEM8(0xC0)

The SFR parser converts both forms into canonical physical DATA-space addresses.

For:

_SFR_IO8(x)

the physical address is:

x + __SFR_OFFSET

For:

_SFR_MEM8(x)

the physical address is:

x

The resulting AvrSfrDatabase provides both symbolic-name and address-based lookup.


SFR probing is non-fatal

SFR parsing is intentionally an auxiliary capability.

The device must still be usable when:

physical memory discovery succeeds
linker memory discovery succeeds

but:

SFR parsing fails

This is important because avr-libc macro representations can vary between toolchain versions.

A future avr-libc change should not prevent basic ELF/device analysis from working simply because a new SFR representation is not yet recognized.


Linker-memory probing

The second discovery path is the AVR linker itself.

The probe performs a real device-specific link using:

-mmcu=<device>

and requests a linker map:

-Wl,-Map=<temporary-map>

The generated map is then parsed.

The important information is the linker’s resolved memory configuration:

TEXT
DATA
EEPROM

with their:

origin
length

Why the linker map is used

The default linker script may contain expressions and linker symbols instead of concrete numeric values.

For example, a script may ultimately resolve a region from another symbol or expression.

The generated map records the values after the linker has evaluated those expressions.

Therefore the project treats the generated linker map as the authoritative source for the selected linker address-space layout.

Do not replace this with a hardcoded MCU lookup table.


AvrDevice

avr_device.c converts the probe result into the public AvrDevice representation.

The device contains:

physical FLASH
physical SRAM
physical EEPROM

ELF TEXT linker space
ELF DATA linker space
ELF EEPROM linker space

physical register-file region
physical I/O region
physical extended-I/O region

SFR database

FLASH byte -> CPU word address shift

Physical memory versus linker memory

This distinction is fundamental.

For example:

physical SRAM
    |
    +--> physical AVR DATA-space address

ELF DATA linker space
    |
    +--> ELF/VMA address

The numerical values may differ.

Therefore this is wrong:

physical = symbol_value;

when symbol_value is an AVR-GCC DATA-space VMA.

Instead, the address must be interpreted by AvrDevice.


SRAM model

SRAM is one physical memory region.

The following runtime objects can all reside in the same physical SRAM:

.data
.bss
.noinit
heap
stack

They differ in ELF section semantics, not physical memory type.

This is why AvrDevice contains one:

AvrMemoryRegion sram;

rather than separate physical regions for .data and .bss.


DATA-space classification

The physical AVR DATA space is conceptually divided into:

register file
I/O
extended I/O
SRAM

The exact ranges are discovered or derived from the selected device rather than hardcoded in avrmem.c.

The classification flow is:

ELF DATA VMA
     |
     v
DATA linker-space translation
     |
     v
physical DATA address
     |
     +--> register file
     +--> I/O
     +--> extended I/O
     +--> SRAM

Unified address resolution

The preferred device API is:

avr_device_resolve_address()

It accepts an ELF/linker address and returns:

memory space
AVR/physical address
physical target address

Conceptually:

ELF address
    |
    v
AvrDevice
    |
    +--> FLASH
    +--> REGISTER
    +--> I/O
    +--> SRAM
    +--> EEPROM

The caller should not replicate this logic.


avr_elf.c

The ELF parser is deliberately independent of MCU-specific address constants.

It handles:

ELF identification
ELF32 parsing
little-endian decoding
section headers
program headers
symbol table
string tables
ELF AVR device metadata
LMA calculation
symbol resolution
section resolution

The parser stores raw ELF values exactly as represented in the ELF file.

Device translation is delegated to AvrDevice.


ELF sections

An AvrElfSection contains the raw section-header representation.

Important fields include:

name
addr
offset
size
flags
type

where:

addr
    =
ELF section VMA

offset
    =
file offset

The section header itself does not contain a direct LMA.


ELF program headers and LMA

LMA is calculated from PT_LOAD.

Given:

section offset
PT_LOAD.p_offset
PT_LOAD.p_paddr

the section’s load address is:

section LMA =
  PT_LOAD.p_paddr +
  (section.sh_offset - PT_LOAD.p_offset)

For a symbol at a section-relative offset:

symbol LMA =
  section LMA +
  (symbol.value - section.addr)

This is particularly important for .data.


.data handling

A .data section typically has:

VMA -> SRAM
LMA -> FLASH

Example conceptually:

          ELF
           |
      +----+----+
      |         |
      v         v
     VMA       LMA
      |         |
      v         v
     SRAM      FLASH

The runtime object lives in SRAM.

Its initialization image is stored in FLASH.

The symbol resolver keeps these addresses separate.


.bss handling

.bss is normally represented by:

SHT_NOBITS

There are no bytes for the .bss contents in the ELF image.

The runtime address still resolves to SRAM, but:

has_lma == false

because there is no file-backed initialization image.

The startup code normally zeroes the region.


.noinit handling

.noinit is a runtime SRAM section whose contents are intended to survive normal startup initialization.

The section has a VMA, but normally no FLASH initialization image.

Therefore the presentation layer should report the section as preserved rather than attempting to invent an LMA.


Symbol resolution

Symbol resolution starts with the raw ELF symbol value.

The indexed resolver:

avr_elf_resolve_symbol_at()

performs these steps:

raw ELF symbol
      |
      +--> section lookup
      |
      +--> AvrDevice address resolution
      |
      +--> SFR association
      |
      +--> LMA calculation
      |
      +--> FLASH word-address conversion
      |
      v
AvrResolvedSymbol

The name-based resolver delegates to the indexed resolver so there is only one implementation of symbol-resolution semantics.


SFR association

Once a symbol is resolved into AVR DATA space, the device layer can look up an SFR using its canonical physical address.

Conceptually:

ELF symbol value
       |
       v
AvrDevice address resolution
       |
       v
physical DATA address
       |
       v
avr_device_find_register_address()
       |
       v
AvrSfr

This allows a symbol representing or referring to an SFR address to be displayed with its symbolic register name.


FLASH byte and CPU word addresses

ELF program-space addresses are treated as FLASH byte addresses.

The AVR CPU instruction address may use words instead.

For a traditional AVR:

word address = byte address >> 1

The shift is a device property:

avr_device_flash_address_shift()

The frontend should never hardcode:

address / 2

or:

address >> 1

for device-independent output.


Command-line parsing

avrmem uses getopt_long().

Canonical syntax:

avrmem [options] <elf>

Examples:

bin/avrmem --sections firmware.elf
bin/avrmem --symbols firmware.elf
bin/avrmem --symbol led_app firmware.elf
bin/avrmem --device atmega328p firmware.elf
bin/avrmem --device=atmega328p --symbol=led_app firmware.elf

The option string begins with +.

This prevents GNU getopt_long() from permuting arguments and means option processing stops when the first non-option argument is encountered.

Therefore:

bin/avrmem firmware.elf --sections

is intentionally rejected.


Adding a new CLI option

New options should be added in three places in src/avrmem.c:

  1. struct option long_options[]
  2. the short-option string
  3. the switch handling getopt_long() results

Then add:

The option parser should remain independent of ELF/device implementation details.


Adding support for a new AVR device

Do not add:

if (strcmp(device_name, "..."))

to the core implementation.

Instead:

  1. Verify that the installed avr-gcc accepts the device.
  2. Verify <avr/io.h> is selected.
  3. Verify the physical-memory macros are available.
  4. Verify the generated linker map contains the required regions.
  5. Verify SFR macros can be parsed.
  6. Build a representative ELF fixture.
  7. Add a regression test.

The expected architecture is:

new MCU
   |
   v
existing toolchain probe
   |
   +--> device description

not:

new MCU
   |
   v
new source-code table entry

Debugging device-probe failures

The probe is intentionally staged.

Diagnostics should identify the stage:

macro probe
physical-memory extraction
SFR discovery
linker-map generation
linker-region extraction

When debugging an initialization failure, first determine the first failed stage.

For example:

probe: probing 'atmega328p'
probe: macro output = ...
probe: physical memory: ...
probe: SFR database = ...
probe: linker command:
  ...

If the failure occurs after the linker command, inspect the linker diagnostic before changing the parser.

The same rule applies to the macro stage.


Temporary files and POSIX behavior

The current probe implementation uses POSIX facilities such as:

mkstemp()
fdopen()
popen()
pclose()
unlink()

Temporary paths should use the host’s TMPDIR convention where appropriate, with a sensible fallback.

The feature-test macro:

#define _POSIX_C_SOURCE 200809L

must appear before system headers in source files that require the corresponding POSIX declarations.


Memory ownership

Ownership transfers should be explicit.

The probe result initially owns:

result->name
result->sfr_database

avr_device_init() takes ownership of those values.

The probe result must then be cleared so its destruction routine does not free transferred objects.

The device destructor releases them.

For ELF:

avr_elf_open()
    |
    v
AvrElf owns parsed data
    |
    v
avr_elf_close()
    |
    v
all ELF-owned data released

Do not retain borrowed pointers beyond the lifetime of their owning object.


Error-handling conventions

Public boolean APIs should return:

true

only when the requested operation succeeded.

A function should not silently fabricate a result when the mapping is unknown.

For example, an unknown ELF address should not be guessed as SRAM simply because its numeric value looks similar to another known address.

Where appropriate:

has_physical_address = false
has_lma = false
has_flash_word_address = false

should explicitly communicate unavailable information.


Coding conventions

Use two spaces for indentation.

Prefer clear, small helper functions.

Keep comments focused on:

Avoid comments that merely restate obvious C syntax.

Do not introduce MCU-specific constants into the frontend or ELF parser.


Modifying the architecture safely

Before modifying a central abstraction, check whether the behavior is already working in another translation unit.

In particular, preserve established mechanisms for:

toolchain probing
linker-map generation
ELF parsing
LMA calculation
SFR normalization

when adding new features.

A new feature should normally be layered onto a working path instead of replacing a proven mechanism with a different discovery strategy.

For example, SFR support should extend the existing AVR-GCC macro probe:

existing macro probe
       |
       +--> physical memory
       |
       +--> device identity
       |
       +--> SFR database

rather than creating a second independent compiler invocation.


Documentation workflow

When behavior changes, update the relevant documentation at the same time.

Typical mapping:

source/API change
    |
    +--> docs/api.md

architecture change
    |
    +--> docs/architecture.md

developer workflow change
    |
    +--> docs/guide.md

testing behavior change
    |
    +--> docs/testing.md

user-visible CLI change
    |
    +--> README.md

The top-level README should remain concise.

Detailed implementation information belongs in docs/.


Jekyll documentation

The docs/ directory is intended to be a Jekyll site.

The site configuration is in:

docs/_config.yml

Documentation pages contain front matter such as:

---
layout: default
title: Developer Guide
---

The main entry point is:

docs/index.md

The project README can be copied to:

docs/README.md

for direct browsing from the generated site.


Recommended development cycle

A typical change should follow:

1. Identify the owning abstraction.
2. Modify the smallest appropriate translation unit.
3. Compile with full warnings.
4. Run the focused smoke test.
5. Run the broader test suite.
6. Update documentation.

For device/discovery changes:

make clean
make

then run the target ELF:

bin/avrmem --symbol <name> <firmware.elf>

and verify:

device
memory classification
physical address
LMA
SFR association

Completion criteria for a feature

A feature is considered complete when: