Files
Ricardo J. Rodríguez 5ab2c6aa7d msl: follow spec 1.1.1 register names and thread states
Thread Context (0x0011) changed in Memory Slice spec 1.1.0/1.1.1:
register names are now normative (Table 19g) and the block gained
Suspended/Partial flags, thread states 0x05-0x07 and a per-thread Arch
byte at +0x13.

debug.msl: map the ARM64 aliases fp/lr (and cpsr) that 1.1.0 producers
could write to x29/x30/pstate when seeding (consumer rule 29), using the
thread's Arch byte with the file header as fallback. `dpt` derives the
status letter from ThreadState (r/S/s/z) instead of only the Current flag.

core.msl (`dgm`): never write the fp/lr aliases on ARM64, tag the frame
pointer, status and link registers via r2's BP/SR/LR aliases, and set
Suspended alongside Current since the registers come from a halted
debuggee.
2026-10-26 18:38:36 +01:00
..
2026-06-27 09:51:49 +02:00
2026-06-27 09:51:49 +02:00
2026-06-27 09:51:49 +02:00
2026-06-27 09:51:49 +02:00
2026-06-27 09:51:49 +02:00

Memory Slice (.msl) plugins for radare2

radare2 can open and emulate Memory Slice (.msl) process memory dumps produced by memslicer. A slice is a static snapshot of a single process: its virtual address space (with per-page acquisition state) plus, when captured, the per-thread CPU registers. Because a slice is static, "execution" is driven by emulation (ESIL).

This directory ships the support as loadable radare2 plugins.

Plugin File Role
io.msl io_msl.c Exposes the virtual address space (msl://), and acts as a debug IO so a slice can be opened in debug mode without spawning a process. Verifies the SHA-256 integrity chain on open.
bin.msl bin_msl.c Parses the slice as a CORE object: architecture/bits/OS, entrypoint (the captured PC), one memory map per contiguous run of captured pages, and symbols/libraries from the captured modules (incl. PE export tables, so call targets are named).
debug.msl debug_msl.c Emulated debug backend: seeds the register file from the captured Thread Context and steps via ESIL, including reverse execution (dsb/aesb). Lists and selects captured threads (dpt / dpt=<tid>).
core.msl core_msl.c Producer: adds dgm/dgma to write a .msl from a live debug session.

Compression: lz4-compressed regions are decoded transparently by the io.msl plugin (open the slice as msl://…). zstd-compressed regions cannot be decoded — radare2 has no zstd — and read back as the fill byte with a warning; recapture with memslicer … -c lz4 (or -c none), or use the Python memslicer-emu tool, which handles both codecs. The bin.msl map path (r2 dump.msl) cannot express compressed regions as file maps either, so for compressed slices use the msl:// URI. Encrypted slices are not supported. Failed and unmapped pages (and unbacked addresses) read back as the fill byte.

Build & install

Requires a radare2 development install (pkg-config --exists r_core).

make
make install        # installs the 4 plugins into R2_USER_PLUGINS

make install copies the .so/.dylib into $(R2PM_PLUGDIR) (the per-user plugin directory reported by r2 -H R2_USER_PLUGINS). Verify they loaded:

$ r2 -qc 'Lo~msl; Lb~msl; Ld~msl; Lc~msl' --

Inspecting a slice (analysis)

Open the file normally — bin.msl provides the maps, architecture and entrypoint:

$ r2 dump.msl
[0x00401000]> i~type,arch,bits     # CORE, x86, 64
[0x00401000]> iS                   # memory maps (one per captured run)
[0x00401000]> il                   # captured modules (libraries)
[0x00401000]> is                   # symbols: one per module + PE exports
[0x00401000]> s entry0; pd 8       # disassemble from the captured PC
[0x00401000]> px @ 0x7ffff000      # read captured memory

Modules captured in the slice (ModuleEntry blocks) become libraries (il) and symbols (is): one symbol at each module base plus, for PE images whose headers are captured, one FUNC symbol per exported function at its address. radare2 then names call targets — e.g. call sym.kernel32.dll_CreateFileW — instead of bare addresses. (ELF .dynsym export resolution is handled by the Python memslicer tools for now, not yet by this plugin.)

For a raw virtual-address view without bin metadata, use the msl:// URI, which goes straight through io.msl:

$ r2 msl://dump.msl
[0x00000000]> px @ 0x401000

On open, io.msl recomputes the per-block SHA-256 chain and the End-of-Capture FileHash; a mismatch (truncated or tampered slice) prints a warning but the slice is still indexed and readable.

Emulated debugging (advancing execution)

Open the slice as an emulated debug target. io.msl is a debug IO, so this does not spawn a process:

$ r2 -D msl -d msl://dump.msl

On attach the backend sets the architecture from the slice header, initializes the ESIL VM, enables io.cache (so emulated writes don't touch the dump) and seeds every register from the Current thread's Thread Context, then seeks to the captured program counter.

Single-step and inspect with the usual debugger commands:

[0x00401000]> dr rip rax rbx        # registers as captured
[0x00401000]> ds                    # step one instruction
[0x00401000]> dr rax                # observe the change
[0x00401000]> dso                   # step over
[0x00401000]> dsb                   # step BACK (reverse / time-travel)
[0x00401000]> db 0x401040 ; dc      # run to a breakpoint
[0x00401000]> dr rcx=0x10           # modify a register
[0x00401000]> pd 4 @ rip            # disassemble at the current PC

Reverse execution. The backend records ESIL step history (esil.maxbacksteps, default 256), so dsb (or aesb) steps execution backwards, reverting both the program counter and register/memory changes — useful for walking back from a fault or an interesting state in the snapshot.

Multiple threads. A slice can capture more than one thread (one Thread Context block each). The backend seeds the Current thread by default, but every captured thread is listed and selectable:

[0x00401000]> dpt                   # list captured threads (PC, '*' = active)
 * 7 r msl (0x401000)
 - 8 s msl (0x401100)
[0x00401000]> dpt=8                 # re-seed from thread 8 and seek to its PC
[0x00401100]> dr rip rax            # now thread 8's captured registers

dpt=<tid> re-seeds the register file from that thread's Thread Context and seeks to its captured PC; stepping then continues from there. Reverse history resets at the switch (you cannot step back across into another thread). The status letter comes from the block's ThreadState (r running, S sleeping/waiting, s stopped/halted, z zombie); when the producer left it Unknown, the Current thread shows r and the rest s.

Register names follow spec 1.1.1 (Table 19g). Slices from 1.1.0 producers may still name the ARM64 frame/link registers fp/lr; the backend maps them to x29/x30 (and cpsr to pstate) when seeding, and dgm never writes the aliases.

The backend clears cfg.debug after seeding so that ds/dso dispatch to the ESIL stepper instead of radare2's r_debug_step machinery (software breakpoints, arena swap, trace), which assumes a live ptrace process. The raw ESIL commands (aes, aesu 0x401020, aer rax=...) work as well.

dc runs to a breakpoint via ESIL. A slice has no program exit and unmapped fetches read as the fill byte (which decodes as instructions), so dc without a breakpoint is bounded by esil.maxsteps (defaulted to 1,000,000 by this backend) rather than running forever — set e esil.maxsteps=0 for unlimited. The seek follows the PC after each step (dbg.follow=1), so the disassembly and visual modes track execution.

Note: routing dc to ESIL when cfg.debug is unset is a small radare2 core change (it benefits any emulated backend, not just msl). It is proposed separately upstream; without it, use aec instead of dc on older r2.

Producing a slice from radare2

The core.msl plugin adds dgm (debug-generate memory-slice). While debugging a process, write the current state as a slice:

[0xphysical]> dgm dump.msl        # or just `dgm` -> <pid>.msl
[0xphysical]> dgma dump.msl       # include all maps (no large-map skip)

It writes the file header, a Process Identity block, a Thread Context with the current thread's registers, and one Memory Region per debug map (three-state page map: pages that read back become Captured, the rest Failed). The integrity chain uses SHA-256 (radare2 has no BLAKE3; the format allows SHA-256 via HashAlgo 0x01). The resulting .msl is readable by these plugins and by memslicer / memslicer-emu.

Maps are read in 1 MiB chunks; unreadable maps are skipped. By default dgm skips very large maps (over 512 MiB — e.g. the macOS dyld shared cache) so the dump stays small and fast; use dgma to include everything.

Testing

test/genmsl.py writes a small but fully-formed .msl (file header, Process Identity, Thread Context, one Memory Region, End-of-Capture) with a valid SHA-256 chain — handy for exercising the read path without a live target:

$ python3 test/genmsl.py /tmp/sample.msl
$ r2 -qc 'iS' /tmp/sample.msl                   # bin: region.0 @ 0x1000
$ r2 -qc 's 0x1000; p8 8' msl:///tmp/sample.msl # io: captured bytes
$ r2 -D msl -d msl:///tmp/sample.msl            # emulated debug; dr, ds, dsb

Add --mt to emit a second (non-Current) thread, to exercise thread listing and selection:

$ python3 test/genmsl.py --mt /tmp/mt.msl
$ r2 -D msl -q -c 'dpt' -d msl:///tmp/mt.msl        # lists tid 7 (*) and 8
$ r2 -D msl -q -c 'dpt=8; dr rip' -d msl:///tmp/mt.msl  # 0x1100 (thread 8's PC)

Add --pe to emit a module with a minimal in-memory PE export table, to exercise symbol/export resolution:

$ python3 test/genmsl.py --pe /tmp/pe.msl
$ r2 -q -c 'is~MyExport' /tmp/pe.msl     # FUNC test.dll MyExport @ 0x10300
$ r2 -q -c 'il' /tmp/pe.msl              # C:\Windows\System32\test.dll

Format reference

The .msl binary format is specified in the Memory Slice specification. The plugins read: the 64/128-byte file header (MEMSLICE magic, OS/arch/PID), Memory Region blocks (0x0001, with the two-bit page-state map), and Thread Context blocks (0x0011, per-thread register file, incl. the 1.1 Arch/ThreadState bytes).