LabWired The easy way to build hardware
Two small development boards sit side by side on a wooden desk.
The same binary has two questions. One question is what the linker placed. One question is what the run used.

One binary, two measurements: MemBrowse and LabWired

Category: Engineering

MemBrowse and LabWired look at the same firmware binary. They answer different questions.

MemBrowse reads the binary. MemBrowse reads the linker script. MemBrowse lists every byte that the linker placed. MemBrowse lists the symbol that owns those bytes. MemBrowse lists the memory region for those bytes.

LabWired loads the same binary onto a model of the chip. LabWired runs the binary. LabWired checks the UART text. LabWired checks the registers. On Cortex-M, LabWired also paints the free stack. On Cortex-M, LabWired also paints the free heap. On Cortex-M, LabWired then reads how far the firmware wrote.

The linker report shows only the static part. The stack grows while the firmware runs. The linker report does not include the stack. The sum must fit in the chip. The sum is the static RAM plus the measured stack plus the measured heap.

The example is examples/membrowse in labwired-core. I ran the example on 22 September 2026. I used MemBrowse 1.2.9. I used labwired-cli 0.25.0. The example includes two binaries. One binary is Cortex-M33. One binary is RISC-V.

The nRF54L15 smoke test

This is a small boot test for the nRF54L15 application core. The test copies .data. The test clears .bss. The test turns on LED0 (GPIO P2.09). The test sends four lines out of UARTE20 with EasyDMA.

The transmit buffer must be in RAM. EasyDMA reads the buffer over the bus. On the real chip, a buffer that stays in RRAM causes a fault. main.c declares the buffer.

static char tx_buf[128];

The buffer has no initial value. The linker puts the buffer in .bss.

The MemBrowse report is for the binary nrf54l15-smoke.elf. The sha256 prefix is 82ee70c659a9…. The compiler is GCC 16.1.0.

RegionPlacedLimit
RRAM428 B (64 B vector table + 364 B .text)1,560,576 B (1524 KB)
RAM128 B .bss, 0 B .data262,144 B (256 KB)

The only RAM symbol is tx_buf. tx_buf has 128 bytes. The address of tx_buf is 0x20000000. MemBrowse shows stdint-gcc.h line 0 as the source line for that symbol. The debug information is wrong for that source line. The debug information tags main as main.c. That tag is correct. The buffer is the declaration above. A size budget can use the symbol name. A size budget can use the size.

LabWired then ran examples/nrf54l15-dk/io-smoke.yaml on the same binary. The run passed all four checks. The UART printed nRF54L15 boot OK. The UART printed core=cortex-m33 rram=1524K ram=256K. The table shows the paint result.

ContributorBytesWhere it came from
.data0MemBrowse
.bss128MemBrowse
peak main stack24LabWired, paint
peak heap0LabWired, paint
worst case1520.06% of 262,144

Paint covers the free RAM. The paint starts at 0x20000080. That address is the start of RAM plus the 128 bytes of .bss. The paint ends at 0x20040000. That address is _estack. The linker script sets _estack to the top of the 256 KB SRAM. After the run, the paint pattern stayed in 261,992 bytes of that range. LabWired did not flag a stack overflow. This firmware does not allocate. The heap use is 0 for that reason. 261,992 equals 256 KB minus 152. The static bytes and the stack are in different parts of the same SRAM. Add the static bytes and the stack. The other bytes in the SRAM stay free.

1524 KB looks like an odd size. 1524 KB is the real RRAM size on this chip. Both tools report 1,560,576 bytes of code space. Both tools report 262,144 bytes of RAM. MemBrowse read the limits from nrf54l15.ld. LabWired read the limits from its chip description. The two sources match. A linker script copied from a similar chip makes this row disagree. A linker script that rounds 1524 KB up to 1.5 MB makes this row disagree.

The budgets in the example are wide on purpose. The code budget is 4 KB. The static RAM budget is 2 KB. The stack budget is 1 KB. The combined budget is 3 KB. This binary passes all four budgets. I changed only the combined budget to 140 bytes. I ran the merge again on the same two JSON reports.

GateActualBudget
static RAM1282,048pass
peak main stack241,024pass
combined RAM (static + peak)152140fail, over by 12

Static RAM still passes. The combined check fails. Those 12 bytes are the measured stack. A check that only reads the binary still passes.

The ESP32-C3 blinky

The second binary is the C3 blinky. The sha256 prefix is aea04c8527ef…. MemBrowse gives this report.

RegionPlacedLimit
FLASH268 B (232 B .text + 36 B .rodata)4,194,304 B
RAM0409,600 B (400 KB)

LabWired’s chip description matches both limits. The description also matches 0 bytes of static RAM. The description also matches 268 bytes of code. The run passed 5 of 5 checks. The checks include the boot text. The checks include LED ON. The checks include LED OFF. The checks include GPIO_ENABLE bit 8 set at 0x60004020.

LabWired does not implement stack paint on this core. LabWired reports unsupported. The reason is arch_not_implemented. The merged report leaves the stack as “not measured”. The merged report does not print a combined total. Static RAM of 0 is only what the linker placed. The C3 budgets in the example check the code size. The C3 budgets in the example check the static RAM. Both tools produced those numbers. A budget on the missing stack prints a warning. The run still passes the measured checks.

The blank stack cell is the point. The merged report does not invent a stack size. The merged report prints a combined total only after paint has a real number.

Where they meet

The two tools do not call each other. combined-report.py reads the MemBrowse JSON report. The script reads the LabWired file result.json. The script adds the static RAM, the peak stack, and the peak heap when both numbers exist. The script also compares four values. One value is the RAM size. One value is the code-region size. One value is the static RAM. One value is the code bytes. On these two binaries, all four values matched.

In CI, the first step builds the firmware one time. The next step runs MemBrowse. The next step runs labwired test. The last step merges the reports. If MEMBROWSE_API_KEY is set, MemBrowse also stores the symbol list for each commit. LabWired checks the UART text. LabWired checks the pin state. These checks show that the firmware did the right thing. The pull-request comment shows the sum. The pull-request comment shows whether the run passed.

These examples are small on purpose. 152 bytes out of 256 KB will not decide a product. You can reuse the method. The binary gives the static bytes. A run of that binary gives the stack value. A run of that binary gives the heap value. The budget is on the sum. The symbol name shows the part to shrink when the static part is too large. When .bss is already close to the RAM limit, the linker map does not show the 24 bytes of stack.

Run the example on your machine. Neither tool needs an account.

pip install membrowse pyyaml
curl -fsSL https://labwired.com/install.sh | sh
./examples/membrowse/run-demo.sh
Andrii Shylenko
Andrii Shylenko

Founder, LabWired.