Compare commits
9 Commits
14694fcbc4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22da85de66 | ||
|
|
e790736868 | ||
|
|
ddccae54a0 | ||
|
|
70561276fe | ||
|
|
91172e86d8 | ||
|
|
276817fc48 | ||
|
|
2a8a952f22 | ||
|
|
26f01a5bdb | ||
| 82958cfe74 |
10
README.md
10
README.md
@@ -24,8 +24,8 @@ engineering ADSP-2191 firmware.
|
||||
|
||||
## Plugin Status
|
||||
|
||||
The decoder now covers most ADSP-219x instruction types and has
|
||||
assembler verification for 34 of 37 documented opcode families.
|
||||
The decoder implements all documented ADSP-219x instruction types and
|
||||
has assembler verification for 34 of 37 documented opcode families.
|
||||
|
||||
Assembler-verified coverage includes:
|
||||
|
||||
@@ -49,6 +49,12 @@ an open-source assembler/linker for ADSP-218x/219x. Build
|
||||
instructions are in the open21xx README. The resulting ELF is
|
||||
converted to raw binary with `dd` (extract the `int_pm` section).
|
||||
|
||||
## Analysis Guides
|
||||
|
||||
- `docs/GETTING_STARTED.md` - setup, loading ROMs, and basic radare2 use
|
||||
- `docs/LARGE_ROM_ANALYSIS_WORKFLOW.md` - step-by-step workflow for
|
||||
analyzing large raw ADSP-219x ROM dumps
|
||||
|
||||
## License
|
||||
|
||||
Plugin code: LGPL-3.0-only. Example code from Analog Devices
|
||||
|
||||
@@ -57,3 +57,11 @@ instruction set reference chapters (PDF and text extracts):
|
||||
- `9x_flowops.*` - Flow control (jumps, loops, returns)
|
||||
- `9x_moveops.*` - Data move operations
|
||||
- `9x_multiops.*` - Multifunction operations
|
||||
|
||||
## Large ROM Workflow
|
||||
|
||||
For a practical step-by-step workflow for analyzing a large raw
|
||||
ADSP-219x ROM image, including how to separate likely code from likely
|
||||
data and when to use graph analysis, see:
|
||||
|
||||
- `LARGE_ROM_ANALYSIS_WORKFLOW.md`
|
||||
|
||||
524
docs/LARGE_ROM_ANALYSIS_WORKFLOW.md
Normal file
524
docs/LARGE_ROM_ANALYSIS_WORKFLOW.md
Normal file
@@ -0,0 +1,524 @@
|
||||
# Large ADSP-219x ROM Analysis Workflow
|
||||
|
||||
This guide describes a practical workflow for reverse engineering a
|
||||
large raw ADSP-219x ROM image in radare2 using the `adsp219x`
|
||||
architecture plugin from this repository.
|
||||
|
||||
It is written for the common case where you have a large blob, for
|
||||
example a 1 MB ROM dump, and you do not yet know:
|
||||
|
||||
- where code starts and where data starts
|
||||
- how many independent code regions exist
|
||||
- where the main loops and dispatchers are
|
||||
- which PM words are instructions and which are tables
|
||||
|
||||
The workflow below is intentionally conservative. For raw DSP ROMs,
|
||||
manual validation beats aggressive auto-analysis.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The ROM is for an ADSP-219x family DSP.
|
||||
- Instructions are 24-bit words.
|
||||
- The dump is raw program memory, not a richly annotated executable.
|
||||
- You already have a working radare2 installation and the
|
||||
`adsp219x` plugin is installed or loadable with `-L`.
|
||||
|
||||
## Load The ROM
|
||||
|
||||
For an installed plugin:
|
||||
|
||||
```bash
|
||||
r2 -a adsp219x -b 24 firmware.bin
|
||||
```
|
||||
|
||||
For a local plugin build that is not installed:
|
||||
|
||||
```bash
|
||||
r2 -a adsp219x -L ./r2plugin/asm_adsp219x.so -b 24 firmware.bin
|
||||
```
|
||||
|
||||
Why the flags matter:
|
||||
|
||||
- `-a adsp219x`: forces the custom architecture plugin
|
||||
- `-b 24`: tells radare2 to treat the code as 24-bit ADSP-219x words
|
||||
|
||||
## ROM Format Sanity Check
|
||||
|
||||
Before analyzing flow, check whether the byte layout looks correct.
|
||||
|
||||
ADSP-219x code is usually stored either as:
|
||||
|
||||
- packed 24-bit words: `aa bb cc dd ee ff ...`
|
||||
- padded 32-bit words with a leading zero byte
|
||||
|
||||
If your disassembly looks completely implausible, confirm the ROM
|
||||
format first.
|
||||
|
||||
Helpful first commands:
|
||||
|
||||
```text
|
||||
px 64
|
||||
s 0
|
||||
pd 16
|
||||
```
|
||||
|
||||
If the first few decoded instructions look impossible but the hex dump
|
||||
shows a repeating `00 xx xx xx` structure, you may be looking at a
|
||||
32-bit padded dump and need to strip padding before analysis.
|
||||
|
||||
## High-Level Strategy
|
||||
|
||||
For a large ROM, do not start with `aaa`.
|
||||
|
||||
A better sequence is:
|
||||
|
||||
1. validate the entry region manually
|
||||
2. mark only plausible functions
|
||||
3. follow explicit control flow
|
||||
4. distinguish code from tables
|
||||
5. expand analysis gradually
|
||||
6. use graph views only after local validation
|
||||
|
||||
`aaa` on a large raw DSP ROM often creates a false sense of structure
|
||||
by treating valid-looking data words as code.
|
||||
|
||||
## Workflow Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Load ROM in r2] --> B[Check raw bytes and first instructions]
|
||||
B --> C{Plausible code at reset area?}
|
||||
C -- no --> D[Verify ROM packing, offset, endianness, dump source]
|
||||
D --> B
|
||||
C -- yes --> E[Create first function manually]
|
||||
E --> F[Inspect linear disassembly and graph]
|
||||
F --> G{Looks like real control flow?}
|
||||
G -- no --> H[Do not mark as function, treat as possible data]
|
||||
G -- yes --> I[Follow jumps, calls, DO UNTIL loops]
|
||||
I --> J[Name regions and comment findings]
|
||||
J --> K[Search for more entry points and dispatchers]
|
||||
K --> L[Only then expand with broader analysis]
|
||||
L --> M[Separate code islands from PM data tables]
|
||||
```
|
||||
|
||||
## Step 1: Inspect The Reset Region Manually
|
||||
|
||||
Start at address zero unless you have evidence of a different boot
|
||||
mapping.
|
||||
|
||||
```text
|
||||
s 0
|
||||
pd 20
|
||||
pd 64
|
||||
```
|
||||
|
||||
Look for signs of real code:
|
||||
|
||||
- immediate register loads
|
||||
- `JUMP` or `CALL`
|
||||
- `DO ... UNTIL`
|
||||
- initialization of `I`, `M`, `L`, `CNTR`, status registers
|
||||
- branches to other regions
|
||||
|
||||
Red flags that suggest you are not in code:
|
||||
|
||||
- long stretches of decodeable but nonsensical instructions
|
||||
- no branches, no calls, no returns
|
||||
- bizarre register moves with no purpose
|
||||
- disassembly that looks random but hex bytes look regular
|
||||
|
||||
## Step 2: Mark The First Function Manually
|
||||
|
||||
Once the entry region looks plausible:
|
||||
|
||||
```text
|
||||
s 0
|
||||
af
|
||||
pdf
|
||||
agf
|
||||
```
|
||||
|
||||
Meaning:
|
||||
|
||||
- `af`: define a function at the current address
|
||||
- `pdf`: print the current function linearly
|
||||
- `agf`: show the control-flow graph of the current function
|
||||
|
||||
This is a better first move than `aaa`, because it forces you to
|
||||
validate one region before scaling out.
|
||||
|
||||
## Step 3: Use Graphs Locally, Not Globally
|
||||
|
||||
For a large ROM, graphs are most useful once you already believe the
|
||||
current region is code.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```text
|
||||
agf
|
||||
VV
|
||||
afb
|
||||
```
|
||||
|
||||
- `agf`: static graph of current function
|
||||
- `VV`: visual graph mode
|
||||
- `afb`: list/show basic blocks
|
||||
|
||||
If `agf` produces a clean graph with obvious branches and loop edges,
|
||||
the region is probably code.
|
||||
|
||||
If `agf` is trivial, chaotic, or makes no semantic sense, you may be
|
||||
looking at data or a bad function boundary.
|
||||
|
||||
## Step 4: Expand By Following Explicit Flow
|
||||
|
||||
After validating the first function, expand manually through explicit
|
||||
targets:
|
||||
|
||||
```text
|
||||
pdf
|
||||
axt
|
||||
afl
|
||||
```
|
||||
|
||||
Then jump to interesting destinations:
|
||||
|
||||
```text
|
||||
s <target>
|
||||
af
|
||||
pdf
|
||||
agf
|
||||
```
|
||||
|
||||
Prioritize:
|
||||
|
||||
- call targets
|
||||
- jump targets
|
||||
- loop bodies
|
||||
- indirect branch setup code
|
||||
|
||||
For ADSP-219x specifically, also watch for:
|
||||
|
||||
- `DO ... UNTIL`
|
||||
- tight MAC loops
|
||||
- register setup for DAGs
|
||||
- memory access kernels
|
||||
|
||||
## Step 5: Search For ADSP-219x-Specific Patterns
|
||||
|
||||
Search directly for common control-flow patterns:
|
||||
|
||||
```text
|
||||
/a JUMP
|
||||
/a CALL
|
||||
/a DO
|
||||
/a RTS
|
||||
/a RTI
|
||||
```
|
||||
|
||||
Also inspect likely DSP kernel patterns:
|
||||
|
||||
```text
|
||||
/a MR
|
||||
/a MX0
|
||||
/a MY0
|
||||
/a IO(
|
||||
```
|
||||
|
||||
These searches are not perfect, but they help locate:
|
||||
|
||||
- processing loops
|
||||
- dispatch logic
|
||||
- hardware initialization
|
||||
- coefficient loads
|
||||
|
||||
## Step 6: Distinguish Code From Data
|
||||
|
||||
In a large ADSP-219x ROM, many PM words are data, not instructions.
|
||||
You must actively separate them.
|
||||
|
||||
### Heuristic: likely code
|
||||
|
||||
A region is likely code if it has:
|
||||
|
||||
- incoming xrefs from jumps or calls
|
||||
- meaningful block structure
|
||||
- function-like boundaries
|
||||
- setup followed by control flow
|
||||
- loops, branches, and exits
|
||||
|
||||
Check with:
|
||||
|
||||
```text
|
||||
axt @ <addr>
|
||||
pdf
|
||||
agf
|
||||
```
|
||||
|
||||
### Heuristic: likely data
|
||||
|
||||
A region is likely data if it has:
|
||||
|
||||
- regular numeric patterns in hex
|
||||
- no meaningful control flow
|
||||
- no incoming code xrefs
|
||||
- no returns or loop structure
|
||||
- many decodeable instructions that make no programmatic sense
|
||||
|
||||
Check with both disassembly and raw bytes:
|
||||
|
||||
```text
|
||||
pd 32
|
||||
px 64
|
||||
```
|
||||
|
||||
If the hex dump looks more plausible than the disassembly, it is often
|
||||
a table.
|
||||
|
||||
### ADSP-219x-specific data patterns
|
||||
|
||||
Common PM data in DSP firmware:
|
||||
|
||||
- FIR coefficients
|
||||
- IIR coefficients
|
||||
- FFT sine/cosine tables
|
||||
- lookup tables
|
||||
- packed constants
|
||||
- boot configuration words
|
||||
|
||||
These often decode into valid-looking instructions by accident.
|
||||
|
||||
## Step 7: Delay Global Auto-Analysis
|
||||
|
||||
Only after you have mapped a few real code islands should you broaden
|
||||
analysis:
|
||||
|
||||
```text
|
||||
aa
|
||||
afl
|
||||
```
|
||||
|
||||
Prefer `aa` first.
|
||||
|
||||
Use `aaa` only when:
|
||||
|
||||
- the ROM format is confirmed
|
||||
- the entry region is valid
|
||||
- you already understand where major code regions are
|
||||
- the plugin behaves consistently on this image
|
||||
|
||||
Why this matters:
|
||||
|
||||
- `aa` is less aggressive
|
||||
- `aaa` can create junk functions in large raw ROMs
|
||||
- false positives are expensive to clean up mentally
|
||||
|
||||
## Step 8: Name What You Understand
|
||||
|
||||
As soon as a region is understood, name and annotate it.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```text
|
||||
afn entry_init
|
||||
afn main_loop
|
||||
afn io_dispatch
|
||||
CCu probable coefficient table
|
||||
CCu hardware init and watchdog setup
|
||||
```
|
||||
|
||||
Naming reduces rework and makes graph navigation much easier.
|
||||
|
||||
## Step 9: Build A Region Map
|
||||
|
||||
For a 1 MB ROM, keep a rough map as you go:
|
||||
|
||||
- boot/reset code
|
||||
- hardware init
|
||||
- interrupt vector area
|
||||
- main loop
|
||||
- DSP kernels
|
||||
- dispatch tables
|
||||
- PM data tables
|
||||
- obvious unused or padding regions
|
||||
|
||||
This can live in:
|
||||
|
||||
- r2 comments
|
||||
- a notebook
|
||||
- a separate markdown file
|
||||
|
||||
The important thing is to stop treating the ROM as one continuous
|
||||
thing. Large firmware becomes manageable once you divide it into
|
||||
regions.
|
||||
|
||||
## Step 10: Revisit Ambiguous Areas Later
|
||||
|
||||
Do not force a conclusion too early.
|
||||
|
||||
When a region is ambiguous:
|
||||
|
||||
- leave it unnamed or mark it as tentative
|
||||
- inspect surrounding xrefs first
|
||||
- compare with neighboring validated code
|
||||
- revisit it after understanding more of the firmware
|
||||
|
||||
Good reverse engineering on large DSP ROMs is iterative.
|
||||
|
||||
## Recommended First 15 Minutes
|
||||
|
||||
If you want a concrete first-pass routine for a 1 MB ROM:
|
||||
|
||||
### 1. Open the ROM
|
||||
|
||||
```bash
|
||||
r2 -a adsp219x -b 24 firmware.bin
|
||||
```
|
||||
|
||||
### 2. Check the first bytes and first instructions
|
||||
|
||||
```text
|
||||
s 0
|
||||
px 64
|
||||
pd 32
|
||||
```
|
||||
|
||||
### 3. If plausible, define the first function
|
||||
|
||||
```text
|
||||
af
|
||||
pdf
|
||||
agf
|
||||
```
|
||||
|
||||
### 4. Enter visual graph mode
|
||||
|
||||
```text
|
||||
VV
|
||||
```
|
||||
|
||||
### 5. Follow obvious branch targets manually
|
||||
|
||||
```text
|
||||
s <target>
|
||||
af
|
||||
pdf
|
||||
agf
|
||||
```
|
||||
|
||||
### 6. Search for loops and calls
|
||||
|
||||
```text
|
||||
/a DO
|
||||
/a CALL
|
||||
/a JUMP
|
||||
```
|
||||
|
||||
### 7. Compare suspicious regions with hex
|
||||
|
||||
```text
|
||||
pd 32
|
||||
px 64
|
||||
```
|
||||
|
||||
### 8. Only then widen analysis
|
||||
|
||||
```text
|
||||
aa
|
||||
afl
|
||||
```
|
||||
|
||||
## When To Suspect A Bad Decode
|
||||
|
||||
Pause and reassess if you see:
|
||||
|
||||
- no meaningful flow anywhere near the entry region
|
||||
- every region looks equally nonsensical
|
||||
- branch targets never lead to reasonable code
|
||||
- graph mode shows nonsense everywhere
|
||||
- the ROM appears to decode but nothing behaves like firmware
|
||||
|
||||
Then check:
|
||||
|
||||
- ROM packing
|
||||
- dump alignment
|
||||
- whether the image is compressed or encrypted
|
||||
- whether the base offset is wrong
|
||||
- whether the file contains headers before the actual code
|
||||
|
||||
## Practical Command Cheat Sheet
|
||||
|
||||
Open:
|
||||
|
||||
```bash
|
||||
r2 -a adsp219x -b 24 firmware.bin
|
||||
```
|
||||
|
||||
Start region:
|
||||
|
||||
```text
|
||||
s 0
|
||||
pd 20
|
||||
pd 64
|
||||
px 64
|
||||
```
|
||||
|
||||
Define and inspect function:
|
||||
|
||||
```text
|
||||
af
|
||||
pdf
|
||||
agf
|
||||
```
|
||||
|
||||
Visual graph:
|
||||
|
||||
```text
|
||||
VV
|
||||
```
|
||||
|
||||
Search:
|
||||
|
||||
```text
|
||||
/a JUMP
|
||||
/a CALL
|
||||
/a DO
|
||||
/a RTS
|
||||
```
|
||||
|
||||
Cross-references:
|
||||
|
||||
```text
|
||||
axt
|
||||
axt @ <addr>
|
||||
```
|
||||
|
||||
Broader analysis:
|
||||
|
||||
```text
|
||||
aa
|
||||
afl
|
||||
```
|
||||
|
||||
Naming:
|
||||
|
||||
```text
|
||||
afn main_loop
|
||||
CCu probable PM coefficient table
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
For a large ADSP-219x ROM:
|
||||
|
||||
- do not trust auto-analysis first
|
||||
- validate the entry region manually
|
||||
- graph only locally at first
|
||||
- follow explicit flow edges
|
||||
- use both disassembly and hex dumps
|
||||
- treat PM as mixed code and data
|
||||
- expand analysis gradually
|
||||
|
||||
The core rule is simple:
|
||||
|
||||
**Local confidence first, global analysis later.**
|
||||
@@ -1,48 +1,134 @@
|
||||
# ROM Analysis Walkthrough
|
||||
|
||||
## 1. Determine the ROM Format
|
||||
## 1. Identify Your Files
|
||||
|
||||
ADSP-2191 instructions are 24 bits (3 bytes). A raw dump can be:
|
||||
A firmware dump from an ADSP-2191 typically comes as separate
|
||||
files for each memory space:
|
||||
|
||||
- **Packed (3 bytes/word)**: Most common for SPI flash dumps.
|
||||
Load directly: `r2 -a adsp219x -b 24 dump.bin`
|
||||
- **Padded (4 bytes/word)**: 32-bit aligned with a leading 0x00.
|
||||
Strip padding or use `r2 -s 1` to skip the first pad byte.
|
||||
- **Boot stream**: Contains block headers (target address, byte
|
||||
count, flags) followed by data. Requires parsing the header
|
||||
format first.
|
||||
- **PM files** (Program Memory): 24-bit words — code and PM data
|
||||
- **DM files** (Data Memory): 16-bit words — variables, buffers
|
||||
|
||||
### Quick Format Check
|
||||
Only PM files contain executable code. DM files are pure data
|
||||
and cannot be disassembled as instructions.
|
||||
|
||||
Look at the first few bytes. If you see `00 00 00` at offset 0
|
||||
(a NOP), you likely have packed 3-byte format. If you see
|
||||
`00 00 00 00` followed by meaningful data at offset 4, it is
|
||||
probably 4-byte padded.
|
||||
If you have multiple sets (e.g. three directories), these may be:
|
||||
- Different firmware versions
|
||||
- Different memory banks (Block 0, 1, 2)
|
||||
- Boot loader vs application code
|
||||
|
||||
## 2. Find the Entry Point
|
||||
## 2. Determine the PM Format
|
||||
|
||||
ADSP-2191 instructions are 24 bits (3 bytes). A raw dump can be
|
||||
packed (3 bytes/word) or padded (4 bytes/word, 32-bit aligned).
|
||||
|
||||
### Check with od
|
||||
|
||||
Dump the first 24 bytes and look at the pattern:
|
||||
|
||||
od -A x -t x1 -N 24 firmware_pm.bin
|
||||
|
||||
**Packed (3 bytes/word)** — most common:
|
||||
|
||||
000000 1c 00 30 00 00 00 50 10 00 41 23 40 ...
|
||||
|
||||
Every group of 3 bytes is one instruction. The first byte of
|
||||
real code is typically 0x1C (JUMP), 0x00 (NOP), or 0x50/0x30
|
||||
(register load).
|
||||
|
||||
**Padded (4 bytes/word)** — some EPROM/JTAG tools:
|
||||
|
||||
000000 00 1c 00 30 00 00 00 00 00 50 10 00 ...
|
||||
|
||||
There is a leading 0x00 before each 3-byte instruction.
|
||||
To strip padding: `dd if=input.bin of=output.bin bs=3 count=N`
|
||||
after removing every 4th byte.
|
||||
|
||||
**Boot stream** — ADSP boot loader format:
|
||||
|
||||
000000 xx xx xx xx yy yy ...
|
||||
|
||||
Has block headers (target address, byte count, flags) before
|
||||
the actual code data. Look for a repeating structure of
|
||||
header + data blocks. The data payload inside is packed 24-bit.
|
||||
|
||||
### Check file size
|
||||
|
||||
ls -la firmware_pm.bin
|
||||
|
||||
- Packed: file size is divisible by 3
|
||||
- Padded: file size is divisible by 4
|
||||
- Boot stream: neither cleanly divisible
|
||||
|
||||
python3 -c "import os; s=os.path.getsize('firmware_pm.bin'); print(f'Size: {s} bytes, /3={s/3:.1f}, /4={s/4:.1f}')"
|
||||
|
||||
### Verify with a test disassembly
|
||||
|
||||
r2 -a adsp219x -b 24 -e asm.parser=null -q -c "pd 20" firmware_pm.bin
|
||||
|
||||
If you see coherent instructions (register loads, JUMPs, NOPs),
|
||||
the format is correct. If the output is mostly `unk 0x...` or
|
||||
nonsensical, try the other format or adjust the start offset.
|
||||
|
||||
## 3. Load in radare2
|
||||
|
||||
### Packed 3-byte format (direct)
|
||||
|
||||
r2 -a adsp219x -b 24 -e asm.parser=null firmware_pm.bin
|
||||
|
||||
### Padded 4-byte format
|
||||
|
||||
Strip padding first, then load:
|
||||
|
||||
python3 -c "
|
||||
d = open('firmware_pm.bin','rb').read()
|
||||
o = b''.join(d[i+1:i+4] for i in range(0, len(d), 4))
|
||||
open('firmware_pm_packed.bin','wb').write(o)
|
||||
"
|
||||
r2 -a adsp219x -b 24 -e asm.parser=null firmware_pm_packed.bin
|
||||
|
||||
### Suppress the parser warning
|
||||
|
||||
Add to ~/.radare2rc (one-time):
|
||||
|
||||
echo "e asm.parser=null" >> ~/.radare2rc
|
||||
|
||||
## 4. Initial Analysis
|
||||
|
||||
[0x00000000]> aaa # Full auto-analysis
|
||||
[0x00000000]> afl # List detected functions
|
||||
[0x00000000]> afb @ main # Show basic blocks
|
||||
[0x00000000]> VV # Visual control flow graph
|
||||
|
||||
## 5. Find the Entry Point
|
||||
|
||||
The reset vector is at PM address 0x0000. Typical patterns:
|
||||
|
||||
0x0000: JUMP main (Type 10a, opcode starts with 0x1C)
|
||||
0x0000: JUMP main (Type 10a, opcode byte 0x1C)
|
||||
0x0000: NOP (entry at next instruction)
|
||||
|
||||
The interrupt vector table occupies the first ~128 PM words,
|
||||
with 4-word spacing per vector. Most vectors contain RTI (return
|
||||
from interrupt) or JUMP to a handler.
|
||||
The interrupt vector table occupies the first ~128 PM words
|
||||
(0x000-0x17F), with 4-word spacing per vector. Most vectors
|
||||
contain RTI or JUMP to a handler.
|
||||
|
||||
## 3. Identify Code vs Data Regions
|
||||
## 6. Identify Code vs Data Regions
|
||||
|
||||
**Code regions** produce coherent disassembly: register loads,
|
||||
compute instructions, jumps, and loops in logical sequence.
|
||||
|
||||
**Data regions** (coefficient tables, lookup tables) produce
|
||||
nonsensical disassembly: random-looking mnemonics, impossible
|
||||
register combinations, jumps to invalid addresses. Mark these
|
||||
as data in r2:
|
||||
nonsensical output: random-looking mnemonics, jumps to invalid
|
||||
addresses, many `unk` opcodes. Mark as data in r2:
|
||||
|
||||
Cd 300 @ 0x1000 # Mark 300 bytes as data at offset 0x1000
|
||||
Cd 300 @ 0x1000 # 300 bytes as data at offset 0x1000
|
||||
|
||||
## 4. Recognize DSP Patterns
|
||||
**Null regions** (0x000000 repeated) are uninitialized memory:
|
||||
|
||||
# Find next non-null byte
|
||||
/x 01
|
||||
# Skip to it
|
||||
s hit0_0
|
||||
|
||||
## 7. Recognize DSP Patterns
|
||||
|
||||
### FIR Filter
|
||||
|
||||
@@ -51,26 +137,47 @@ as data in r2:
|
||||
MR = MR + MX0*MY0 (SS), MX0 = DM(I0,M0), MY0 = PM(I4,M4);
|
||||
loop_end: ...
|
||||
|
||||
Look for: Type 11 (DO UNTIL CE) followed by Type 1 multifunction
|
||||
instructions with MAC operations.
|
||||
Look for: DO UNTIL CE + multifunction MAC instructions.
|
||||
|
||||
### IIR Filter (Biquad)
|
||||
|
||||
Nested loops: outer loop over samples, inner loop over biquad
|
||||
sections. Contains ASHIFT for scaling between stages.
|
||||
Nested loops: outer over samples, inner over biquad sections.
|
||||
Contains ASHIFT for inter-stage scaling.
|
||||
|
||||
### Initialization Sequence
|
||||
|
||||
Sequences of Type 6/7 instructions loading I/M/L registers.
|
||||
This sets up circular buffers for the signal processing kernel.
|
||||
Sequences of Type 6/7 loads (I/M/L register setup).
|
||||
Circular buffer initialization before entering a processing loop.
|
||||
|
||||
## 5. Useful r2 Commands
|
||||
### I/O Configuration
|
||||
|
||||
IO(addr) = Dreg / Dreg = IO(addr) instructions configure
|
||||
peripherals: Serial Ports, Timers, DMA, etc.
|
||||
|
||||
## 8. DM File Analysis
|
||||
|
||||
DM files contain 16-bit data words. These are not code.
|
||||
You can inspect them for patterns:
|
||||
|
||||
od -A x -t x2 -N 200 firmware_dm.bin # 16-bit hex words
|
||||
od -A x -t d2 -N 200 firmware_dm.bin # Signed 16-bit decimal
|
||||
|
||||
Common contents:
|
||||
- Filter coefficients (Q15 fixed-point: values near 0x0000-0x7FFF)
|
||||
- Lookup tables (sine, cosine, window functions)
|
||||
- Configuration data (peripheral registers)
|
||||
|
||||
## 9. Useful r2 Commands Reference
|
||||
|
||||
e asm.arch = adsp219x
|
||||
e asm.bits = 24
|
||||
pd 200 # Disassemble 200 instructions
|
||||
pD 600 # Disassemble 600 bytes (= 200 instructions)
|
||||
/x 1c00 # Find unconditional JUMPs
|
||||
pD 600 # Disassemble 600 bytes (= 200 words)
|
||||
/x 1c # Find unconditional JUMPs
|
||||
/x 16 # Find DO UNTIL loops
|
||||
/x 0a # Find RTS/RTI instructions
|
||||
V # Enter visual mode
|
||||
/x 0b # Find indirect JUMP/CALL
|
||||
axt @ addr # Who references this address?
|
||||
axf @ addr # What does this address reference?
|
||||
VV # Visual graph mode
|
||||
V # Visual hex/disasm mode
|
||||
pdf # Print current function disassembly
|
||||
agf # ASCII control flow graph
|
||||
|
||||
11
examples/build/regmap_test.dsp
Normal file
11
examples/build/regmap_test.dsp
Normal file
@@ -0,0 +1,11 @@
|
||||
.section/PM program0;
|
||||
.global _start;
|
||||
_start:
|
||||
cntr = 10;
|
||||
stacka = 0x1000;
|
||||
imask = 0xFF;
|
||||
irptl = 0;
|
||||
icntl = 0;
|
||||
nop;
|
||||
_halt:
|
||||
jump _halt;
|
||||
Binary file not shown.
@@ -1,8 +1,6 @@
|
||||
NAME ?= asm_adsp219x
|
||||
CC ?= gcc
|
||||
CC ?= cc
|
||||
CFLAGS ?= -Wall -Wextra -O2 -fPIC
|
||||
LDFLAGS ?=
|
||||
LDLIBS ?= -lr_arch -lr_util
|
||||
|
||||
R2_PLUGINS := $(shell r2 -H R2_USER_PLUGINS)
|
||||
R2_INCDIR := $(shell r2 -H R2_INCDIR)
|
||||
@@ -10,24 +8,24 @@ R2_LIBDIR := $(shell r2 -H R2_LIBDIR)
|
||||
|
||||
CPPFLAGS += -I$(R2_INCDIR) -I$(R2_INCDIR)/sdb
|
||||
|
||||
all: $(NAME).so
|
||||
all: asm_adsp219x.so
|
||||
|
||||
$(NAME).so: $(NAME).c
|
||||
asm_adsp219x.so: asm_adsp219x.c
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) -shared $(LDFLAGS) \
|
||||
-L$(R2_LIBDIR) \
|
||||
$< -o $@ $(LDLIBS)
|
||||
$< -o $@ -lr_arch -lr_util
|
||||
|
||||
test: $(NAME).so
|
||||
r2 -q -a adsp219x -L ./$(NAME).so -b 24 -c "pd 48" ../examples/isa_test.bin >/dev/null
|
||||
test: asm_adsp219x.so
|
||||
r2 -q -a adsp219x -b 24 -c "pd 48" ../examples/isa_test.bin >/dev/null
|
||||
|
||||
install: $(NAME).so
|
||||
install: all
|
||||
mkdir -p $(R2_PLUGINS)
|
||||
cp -f $(NAME).so $(R2_PLUGINS)/
|
||||
cp -f asm_adsp219x.so $(R2_PLUGINS)/
|
||||
|
||||
uninstall:
|
||||
rm -f $(R2_PLUGINS)/$(NAME).so
|
||||
rm -f $(R2_PLUGINS)/asm_adsp219x.so
|
||||
|
||||
clean:
|
||||
rm -f $(NAME).so
|
||||
rm -f asm_adsp219x.so
|
||||
|
||||
.PHONY: all test install uninstall clean
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/* asm_adsp219x.c -- Radare2 arch plugin for Analog Devices ADSP-219x
|
||||
/* asm_adsp219x.c -- Radare2 arch+anal plugin for Analog Devices ADSP-219x
|
||||
Author: Dr. Christian Giessen
|
||||
Copyright (C) 2026 Dr. Christian Giessen
|
||||
|
||||
Decodes most of the ADSP-219x instruction set (Types 1-37).
|
||||
Verified against open21xx assembler output for Types 1, 3, 4,
|
||||
6, 7, 8, 9, 9a, 10, 10a, 11, 15, 17, 18, 20, 25, 33.
|
||||
Structurally implemented but not yet assembler-verified:
|
||||
Types 12, 14, 16, 19, 21, 21a, 22, 22a, 23, 24, 26, 29,
|
||||
30, 31, 32, 32a, 34, 35, 36, 37. */
|
||||
Decodes all documented ADSP-219x instruction types (Types 1-37)
|
||||
with op->type annotations for analysis, function detection, and
|
||||
control flow graphing. Most types verified against open21xx
|
||||
assembler output; see TESTING.md for the verification matrix. */
|
||||
|
||||
#include <r_arch.h>
|
||||
|
||||
@@ -75,7 +74,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
ins2 = ((ut32) b[3] << 16) | ((ut32) b[4] << 8) | (ut32) b[5];
|
||||
op->size = 3;
|
||||
op->type = R_ANAL_OP_TYPE_UNK;
|
||||
if (!(mask & R_ARCH_OP_MASK_DISASM)) return true;
|
||||
(void) mask; /* decode always runs full; r2 frees mnemonic */
|
||||
|
||||
/* Priority check: High bits */
|
||||
ut32 b23_22 = (ins >> 22) & 0x3;
|
||||
@@ -102,6 +101,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->mnemonic = r_str_newf ("%s, %s=DM(I%d+=M%d), %s=PM(I%d+=M%d)",
|
||||
f, reg0[dd], dmi, dmm,
|
||||
reg0[pd + 4], pmi + 4, pmm + 4);
|
||||
op->type = (amf < 16) ? R_ANAL_OP_TYPE_MUL : R_ANAL_OP_TYPE_ADD;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -116,11 +116,17 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
const char *rname = ((ins >> 21) & 1)
|
||||
? reg1[reg] : reg0[reg];
|
||||
if (d)
|
||||
{
|
||||
op->mnemonic = r_str_newf ("DM(0x%04X) = %s",
|
||||
addr, rname);
|
||||
op->type = R_ANAL_OP_TYPE_STORE;
|
||||
}
|
||||
else
|
||||
{
|
||||
op->mnemonic = r_str_newf ("%s = DM(0x%04X)",
|
||||
rname, addr);
|
||||
op->type = R_ANAL_OP_TYPE_LOAD;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -139,13 +145,19 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
ut32 mreg = (ins & 3) | (g ? 4 : 0);
|
||||
char mem = g ? 'P' : 'D';
|
||||
if (d)
|
||||
{
|
||||
op->mnemonic = r_str_newf ("%s, %cM(I%d += M%d) = %s",
|
||||
f, mem, ireg, mreg,
|
||||
reg0[dreg]);
|
||||
op->type = R_ANAL_OP_TYPE_STORE;
|
||||
}
|
||||
else
|
||||
{
|
||||
op->mnemonic = r_str_newf ("%s, %s = %cM(I%d += M%d)",
|
||||
f, reg0[dreg], mem,
|
||||
ireg, mreg);
|
||||
op->type = R_ANAL_OP_TYPE_LOAD;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -189,6 +201,8 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->mnemonic = r_str_newf ("%s = %s(%s, %s)",
|
||||
dst, f, reg0[xreg],
|
||||
reg0[yreg]);
|
||||
op->type = (amf < 16) ? R_ANAL_OP_TYPE_MUL
|
||||
: R_ANAL_OP_TYPE_ADD;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -203,6 +217,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
{
|
||||
op->mnemonic = r_str_newf ("%s%s%s%s = %s(%s^2)",
|
||||
cp, cs, sp, dst, f, x);
|
||||
op->type = R_ANAL_OP_TYPE_MUL;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -211,6 +226,8 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
{
|
||||
op->mnemonic = r_str_newf ("%s%s%s%s = %s(%s, 0)",
|
||||
cp, cs, sp, dst, f, x);
|
||||
op->type = (amf < 16) ? R_ANAL_OP_TYPE_MUL
|
||||
: R_ANAL_OP_TYPE_ADD;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -221,6 +238,8 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
: yop_alu[yop_i];
|
||||
op->mnemonic = r_str_newf ("%s%s%s%s = %s(%s, %s)",
|
||||
cp, cs, sp, dst, f, x, y);
|
||||
op->type = (amf < 16) ? R_ANAL_OP_TYPE_MUL
|
||||
: R_ANAL_OP_TYPE_ADD;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -289,21 +308,23 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->mnemonic = r_str_newf ("%s(%s, %s), %s = %s",
|
||||
f, x, y,
|
||||
reg0[ddreg], reg0[sdreg]);
|
||||
op->type = (amf < 16) ? R_ANAL_OP_TYPE_MUL
|
||||
: R_ANAL_OP_TYPE_ADD;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Type 6/7/IO/System (010xxx / 011xxx) */
|
||||
if (b23_22 == 1)
|
||||
{
|
||||
if (b21_20 == 0) { op->mnemonic = r_str_newf ("%s = 0x%04X", reg0[ins&0xF], (ins>>4)&0xFFFF); return true; } /* Type 6 */
|
||||
if (b21_20 == 1) { op->mnemonic = r_str_newf ("%s = 0x%04X", reg1[ins&0xF], (ins>>4)&0xFFFF); return true; } /* Type 7 */
|
||||
if (b21_20 == 0) { op->mnemonic = r_str_newf ("%s = 0x%04X", reg0[ins&0xF], (ins>>4)&0xFFFF); op->type = R_ANAL_OP_TYPE_MOV; return true; } /* Type 6 */
|
||||
if (b21_20 == 1) { op->mnemonic = r_str_newf ("%s = 0x%04X", reg1[ins&0xF], (ins>>4)&0xFFFF); op->type = R_ANAL_OP_TYPE_MOV; return true; } /* Type 7 */
|
||||
/* Type 34 and 35 are in the b23_22==0 block below */
|
||||
}
|
||||
|
||||
/* Type 8/9/10/11/17... (00xxxx) */
|
||||
if (b23_22 == 0)
|
||||
{
|
||||
if (b21_20 == 3) { op->mnemonic = r_str_newf ("%s = 0x%04X", reg2[ins&0xF], (ins>>4)&0xFFFF); return true; } /* Type 7 (Reg2) */
|
||||
if (b21_20 == 3) { op->mnemonic = r_str_newf ("%s = 0x%04X", reg2[ins&0xF], (ins>>4)&0xFFFF); op->type = R_ANAL_OP_TYPE_MOV; return true; } /* Type 7 (Reg2) */
|
||||
|
||||
/* Type 36: Long Jump/Call (2-word, bits 23-16 = 00000101) */
|
||||
if ((ins >> 16) == 0x05)
|
||||
@@ -331,6 +352,8 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->type = s_bit ? R_ANAL_OP_TYPE_CALL
|
||||
: R_ANAL_OP_TYPE_JMP;
|
||||
op->jump = addr;
|
||||
if (!s_bit)
|
||||
op->eob = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -395,6 +418,8 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->type = s_bit ? R_ANAL_OP_TYPE_CALL
|
||||
: R_ANAL_OP_TYPE_JMP;
|
||||
op->jump = target;
|
||||
if (!s_bit)
|
||||
op->eob = true;
|
||||
return true;
|
||||
}
|
||||
/* Type 10: bits 23-19 = 00011, bit18 = 0 (conditional 13-bit rel) */
|
||||
@@ -417,6 +442,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
b_bit ? " (DB)" : "");
|
||||
op->type = R_ANAL_OP_TYPE_CJMP;
|
||||
op->jump = target;
|
||||
op->fail = op->addr + 3;
|
||||
return true;
|
||||
}
|
||||
/* Type 17: Reg = Reg (bits 23-16 = 00001101) */
|
||||
@@ -425,6 +451,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->mnemonic = r_str_newf ("%s = %s",
|
||||
get_reg ((ins >> 10) & 3, (ins >> 4) & 0xF),
|
||||
get_reg ((ins >> 8) & 3, ins & 0xF));
|
||||
op->type = R_ANAL_OP_TYPE_MOV;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -443,6 +470,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
cond_str[cond], ret,
|
||||
b_bit ? " (DB)" : "");
|
||||
op->type = R_ANAL_OP_TYPE_RET;
|
||||
op->eob = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -470,6 +498,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
sf_names[sf], xop_shift[xop_i],
|
||||
reg0[dreg],
|
||||
g ? 'P' : 'D', ireg + base, mreg + base);
|
||||
op->type = d ? R_ANAL_OP_TYPE_STORE : R_ANAL_OP_TYPE_LOAD;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -485,6 +514,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->mnemonic = r_str_newf ("%s %s, %s = %s",
|
||||
sf_names[sf], reg0[xreg],
|
||||
reg0[ddreg], reg0[sdreg]);
|
||||
op->type = R_ANAL_OP_TYPE_SHR;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -503,6 +533,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->mnemonic = r_str_newf ("IF %s SR = %s %s",
|
||||
cond_str[cond], sf_names[sf],
|
||||
reg0[xreg]);
|
||||
op->type = R_ANAL_OP_TYPE_SHR;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -517,6 +548,8 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->mnemonic = r_str_newf (
|
||||
"DO 0x%06" PFMT64x " UNTIL %s",
|
||||
target, cond_str[term]);
|
||||
op->type = R_ANAL_OP_TYPE_REP;
|
||||
op->jump = target;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -529,6 +562,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->mnemonic = r_str_newf ("SR = %s %s BY %d",
|
||||
sf_names[sf], reg0[xreg],
|
||||
exp);
|
||||
op->type = R_ANAL_OP_TYPE_SHR;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -553,6 +587,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
}
|
||||
op->mnemonic = pos ? strdup (buf)
|
||||
: strdup ("MODE NOP");
|
||||
op->type = R_ANAL_OP_TYPE_MOV;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -562,6 +597,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
ut32 r = (ins >> 13) & 1;
|
||||
op->mnemonic = r_str_newf ("SAT %s",
|
||||
r ? "SR" : "MR");
|
||||
op->type = R_ANAL_OP_TYPE_MOV;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -572,6 +608,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
ut32 reg = ins & 0xF;
|
||||
op->mnemonic = r_str_newf ("%s = 0x%03X",
|
||||
reg3[reg], data);
|
||||
op->type = R_ANAL_OP_TYPE_MOV;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -597,6 +634,10 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
b_bit ? " (DB)" : "");
|
||||
op->type = s_bit ? R_ANAL_OP_TYPE_CALL
|
||||
: R_ANAL_OP_TYPE_JMP;
|
||||
if (!s_bit && cond == 15)
|
||||
op->eob = true;
|
||||
if (cond != 15)
|
||||
op->fail = op->addr + 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -609,6 +650,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
int base = g ? 4 : 0;
|
||||
op->mnemonic = r_str_newf ("MODIFY(I%d += M%d)",
|
||||
ireg + base, mreg + base);
|
||||
op->type = R_ANAL_OP_TYPE_ADD;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -618,6 +660,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
ut32 xop_i = (ins >> 8) & 0x7;
|
||||
op->mnemonic = r_str_newf ("DIVQ %s",
|
||||
xop_alu[xop_i]);
|
||||
op->type = R_ANAL_OP_TYPE_DIV;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -629,6 +672,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
op->mnemonic = r_str_newf ("DIVS %s, %s",
|
||||
yop_alu[yop_i],
|
||||
xop_alu[xop_i]);
|
||||
op->type = R_ANAL_OP_TYPE_DIV;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -670,6 +714,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
pos ? ", " : "");
|
||||
op->mnemonic = pos ? strdup (buf)
|
||||
: strdup ("STACK NOP");
|
||||
op->type = R_ANAL_OP_TYPE_PUSH;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -692,13 +737,19 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
int base = g ? 4 : 0;
|
||||
const char *op_str = u ? "+=" : "+";
|
||||
if (d)
|
||||
{
|
||||
op->mnemonic = r_str_newf ("DM(I%d %s %d) = %s",
|
||||
ireg + base, op_str, smod,
|
||||
reg0[dreg]);
|
||||
op->type = R_ANAL_OP_TYPE_STORE;
|
||||
}
|
||||
else
|
||||
{
|
||||
op->mnemonic = r_str_newf ("%s = DM(I%d %s %d)",
|
||||
reg0[dreg], ireg + base, op_str,
|
||||
smod);
|
||||
op->type = R_ANAL_OP_TYPE_LOAD;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -720,13 +771,19 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
const char *mod = u_bit ? "+=" : "+";
|
||||
const char *rname = get_reg (rgp, reg);
|
||||
if (d)
|
||||
{
|
||||
op->mnemonic = r_str_newf ("%s(I%d %s M%d) = %s",
|
||||
mem, ireg + base, mod, mreg + base,
|
||||
rname);
|
||||
op->type = R_ANAL_OP_TYPE_STORE;
|
||||
}
|
||||
else
|
||||
{
|
||||
op->mnemonic = r_str_newf ("%s = %s(I%d %s M%d)",
|
||||
rname, mem, ireg + base, mod,
|
||||
mreg + base);
|
||||
op->type = R_ANAL_OP_TYPE_LOAD;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -753,6 +810,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
"DM(I%d %s M%d) = %s, %s = I%d",
|
||||
ireg + base, mod, mreg + base,
|
||||
rname, rname, ireg + base);
|
||||
op->type = R_ANAL_OP_TYPE_STORE;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -765,6 +823,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
int base = g ? 4 : 0;
|
||||
op->mnemonic = r_str_newf ("MODIFY(I%d += %d)",
|
||||
ireg + base, mod);
|
||||
op->type = R_ANAL_OP_TYPE_ADD;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -776,11 +835,17 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
ut32 addr = (addr_hi << 8) | ((ins >> 4) & 0xFF);
|
||||
ut32 dreg = ins & 0xF;
|
||||
if (d)
|
||||
{
|
||||
op->mnemonic = r_str_newf ("IO(0x%03X) = %s",
|
||||
addr, reg0[dreg]);
|
||||
op->type = R_ANAL_OP_TYPE_IO;
|
||||
}
|
||||
else
|
||||
{
|
||||
op->mnemonic = r_str_newf ("%s = IO(0x%03X)",
|
||||
reg0[dreg], addr);
|
||||
op->type = R_ANAL_OP_TYPE_IO;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/* Type 35: Sys ctrl reg (bits 23-16 = 00000110, bit15=0) */
|
||||
@@ -790,11 +855,17 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
ut32 addr = (ins >> 4) & 0xFF;
|
||||
ut32 dreg = ins & 0xF;
|
||||
if (d)
|
||||
{
|
||||
op->mnemonic = r_str_newf ("REG(0x%02X) = %s",
|
||||
addr, reg0[dreg]);
|
||||
op->type = R_ANAL_OP_TYPE_IO;
|
||||
}
|
||||
else
|
||||
{
|
||||
op->mnemonic = r_str_newf ("%s = REG(0x%02X)",
|
||||
reg0[dreg], addr);
|
||||
op->type = R_ANAL_OP_TYPE_IO;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -805,6 +876,7 @@ decode (RArchSession *as, RAnalOp *op, RAnalOpMask mask)
|
||||
ut32 bit = ins & 0xF;
|
||||
op->mnemonic = r_str_newf ("%s %d",
|
||||
c ? "CLRINT" : "SETINT", bit);
|
||||
op->type = R_ANAL_OP_TYPE_SWI;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
251
tools/analyze_dm.py
Executable file
251
tools/analyze_dm.py
Executable file
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
"""analyze_dm.py -- Analyze ADSP-219x DM (Data Memory) dumps.
|
||||
|
||||
Scans a 16-bit DM binary for:
|
||||
- Null regions (uninitialized / BSS)
|
||||
- Coefficient tables (Q15 fixed-point patterns)
|
||||
- Lookup tables (periodic waveforms)
|
||||
- ASCII strings
|
||||
- Non-zero data regions
|
||||
|
||||
Usage:
|
||||
python3 analyze_dm.py firmware_dm.bin
|
||||
python3 analyze_dm.py firmware_dm.bin --endian little
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def read_words(path, endian="big"):
|
||||
"""Read 16-bit words from a binary file."""
|
||||
fmt = ">H" if endian == "big" else "<H"
|
||||
data = open(path, "rb").read()
|
||||
if len(data) % 2:
|
||||
data += b"\x00"
|
||||
return [struct.unpack_from(fmt, data, i)[0]
|
||||
for i in range(0, len(data), 2)]
|
||||
|
||||
|
||||
def signed16(v):
|
||||
"""Convert unsigned 16-bit to signed."""
|
||||
return v - 0x10000 if v >= 0x8000 else v
|
||||
|
||||
|
||||
def find_null_regions(words, min_run=8):
|
||||
"""Find contiguous runs of zero words."""
|
||||
regions = []
|
||||
start = None
|
||||
for i, w in enumerate(words):
|
||||
if w == 0:
|
||||
if start is None:
|
||||
start = i
|
||||
else:
|
||||
if start is not None and (i - start) >= min_run:
|
||||
regions.append((start, i - start))
|
||||
start = None
|
||||
if start is not None and (len(words) - start) >= min_run:
|
||||
regions.append((start, len(words) - start))
|
||||
return regions
|
||||
|
||||
|
||||
def find_coeff_tables(words, min_len=8, max_magnitude=0x7FFF):
|
||||
"""Find potential Q15 coefficient tables.
|
||||
|
||||
Looks for runs of non-zero values where all values are
|
||||
within the Q15 range and show some variance (not constant).
|
||||
"""
|
||||
tables = []
|
||||
start = None
|
||||
run = []
|
||||
|
||||
for i, w in enumerate(words):
|
||||
sw = signed16(w)
|
||||
if w != 0 and abs(sw) <= max_magnitude:
|
||||
if start is None:
|
||||
start = i
|
||||
run = []
|
||||
run.append(sw)
|
||||
else:
|
||||
if start is not None and len(run) >= min_len:
|
||||
mn = min(run)
|
||||
mx = max(run)
|
||||
if mx != mn: # not constant
|
||||
tables.append((start, len(run), mn, mx,
|
||||
sum(run) / len(run)))
|
||||
start = None
|
||||
run = []
|
||||
|
||||
if start is not None and len(run) >= min_len:
|
||||
mn = min(run)
|
||||
mx = max(run)
|
||||
if mx != mn:
|
||||
tables.append((start, len(run), mn, mx,
|
||||
sum(run) / len(run)))
|
||||
return tables
|
||||
|
||||
|
||||
def detect_periodicity(words, start, length):
|
||||
"""Check if a region has periodic content (sine/cosine table)."""
|
||||
if length < 16:
|
||||
return None
|
||||
vals = [signed16(words[start + i]) for i in range(length)]
|
||||
|
||||
# Try periods from 16 to length/2
|
||||
best_period = None
|
||||
best_err = float("inf")
|
||||
for period in range(16, min(length // 2 + 1, 1025)):
|
||||
if length < period * 2:
|
||||
continue
|
||||
err = 0.0
|
||||
count = 0
|
||||
for i in range(period, min(length, period * 4)):
|
||||
diff = vals[i] - vals[i % period]
|
||||
err += diff * diff
|
||||
count += 1
|
||||
if count > 0:
|
||||
rms = math.sqrt(err / count)
|
||||
if rms < best_err:
|
||||
best_err = rms
|
||||
best_period = period
|
||||
|
||||
if best_period and best_err < 500:
|
||||
return best_period
|
||||
return None
|
||||
|
||||
|
||||
def find_strings(data, min_len=4):
|
||||
"""Find ASCII strings in raw bytes."""
|
||||
strings = []
|
||||
current = []
|
||||
start = None
|
||||
for i, byte in enumerate(data):
|
||||
if 0x20 <= byte < 0x7F:
|
||||
if start is None:
|
||||
start = i
|
||||
current.append(chr(byte))
|
||||
else:
|
||||
if current and len(current) >= min_len:
|
||||
strings.append((start, "".join(current)))
|
||||
current = []
|
||||
start = None
|
||||
if current and len(current) >= min_len:
|
||||
strings.append((start, "".join(current)))
|
||||
return strings
|
||||
|
||||
|
||||
def analyze(path, endian="big"):
|
||||
"""Run full analysis on a DM dump."""
|
||||
data = open(path, "rb").read()
|
||||
size = len(data)
|
||||
words = read_words(path, endian)
|
||||
n_words = len(words)
|
||||
|
||||
print(f"File: {path}")
|
||||
print(f"Size: {size} bytes ({n_words} words, 16-bit {endian}-endian)")
|
||||
print()
|
||||
|
||||
# Null regions
|
||||
nulls = find_null_regions(words)
|
||||
if nulls:
|
||||
total_null = sum(n for _, n in nulls)
|
||||
print(f"=== Null Regions ({len(nulls)} found, "
|
||||
f"{total_null} words = {total_null*100//n_words}%) ===")
|
||||
for addr, length in nulls:
|
||||
print(f" DM 0x{addr:04X} - 0x{addr+length-1:04X}"
|
||||
f" ({length} words, {length*2} bytes)")
|
||||
print()
|
||||
|
||||
# Coefficient tables
|
||||
coeffs = find_coeff_tables(words)
|
||||
if coeffs:
|
||||
print(f"=== Potential Coefficient Tables ({len(coeffs)} found) ===")
|
||||
for addr, length, mn, mx, avg in coeffs:
|
||||
q15_min = mn / 32768.0
|
||||
q15_max = mx / 32768.0
|
||||
period = detect_periodicity(words, addr, length)
|
||||
pstr = f", period~{period}" if period else ""
|
||||
print(f" DM 0x{addr:04X} - 0x{addr+length-1:04X}"
|
||||
f" ({length} words)")
|
||||
print(f" Range: {mn}..{mx} (Q15: {q15_min:.4f}"
|
||||
f"..{q15_max:.4f}), avg={avg:.1f}{pstr}")
|
||||
print()
|
||||
|
||||
# Strings
|
||||
strings = find_strings(data)
|
||||
if strings:
|
||||
print(f"=== ASCII Strings ({len(strings)} found) ===")
|
||||
for offset, s in strings:
|
||||
dm_addr = offset // 2
|
||||
print(f" Byte 0x{offset:04X} (DM ~0x{dm_addr:04X}):"
|
||||
f" \"{s}\"")
|
||||
print()
|
||||
|
||||
# Non-null, non-coefficient data regions
|
||||
covered = set()
|
||||
for addr, length in nulls:
|
||||
for i in range(addr, addr + length):
|
||||
covered.add(i)
|
||||
for addr, length, _, _, _ in coeffs:
|
||||
for i in range(addr, addr + length):
|
||||
covered.add(i)
|
||||
|
||||
data_regions = []
|
||||
start = None
|
||||
for i in range(n_words):
|
||||
if i not in covered and words[i] != 0:
|
||||
if start is None:
|
||||
start = i
|
||||
else:
|
||||
if start is not None:
|
||||
length = i - start
|
||||
if length >= 4:
|
||||
data_regions.append((start, length))
|
||||
start = None
|
||||
if start is not None and (n_words - start) >= 4:
|
||||
data_regions.append((start, n_words - start))
|
||||
|
||||
if data_regions:
|
||||
print(f"=== Other Data Regions ({len(data_regions)} found) ===")
|
||||
for addr, length in data_regions:
|
||||
sample = [f"0x{words[addr+j]:04X}"
|
||||
for j in range(min(length, 8))]
|
||||
dots = " ..." if length > 8 else ""
|
||||
print(f" DM 0x{addr:04X} - 0x{addr+length-1:04X}"
|
||||
f" ({length} words)")
|
||||
print(f" [{', '.join(sample)}{dots}]")
|
||||
print()
|
||||
|
||||
# Summary
|
||||
total_nonzero = sum(1 for w in words if w != 0)
|
||||
print(f"=== Summary ===")
|
||||
print(f" Total words: {n_words}")
|
||||
print(f" Non-zero words: {total_nonzero}"
|
||||
f" ({total_nonzero*100//max(n_words,1)}%)")
|
||||
print(f" Null regions: {len(nulls)}")
|
||||
print(f" Coeff tables: {len(coeffs)}")
|
||||
print(f" Strings: {len(strings)}")
|
||||
print(f" Other data: {len(data_regions)}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyze ADSP-219x DM (Data Memory) dumps.")
|
||||
parser.add_argument("file", help="DM binary file")
|
||||
parser.add_argument("--endian", choices=["big", "little"],
|
||||
default="big",
|
||||
help="Byte order (default: big)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.isfile(args.file):
|
||||
print(f"Error: {args.file} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
analyze(args.file, args.endian)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
270
tools/dm_xref.py
Executable file
270
tools/dm_xref.py
Executable file
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
"""dm_xref.py -- Extract all DM address references from PM code
|
||||
and show what's stored at those addresses in a DM dump.
|
||||
|
||||
Finds:
|
||||
- I-register loads (I0=0x100 → DM base address)
|
||||
- Direct DM access (DM(0x1234) = reg / reg = DM(0x1234))
|
||||
- DM immediate modify (reg = DM(I0 += offset))
|
||||
- L-register loads (circular buffer lengths)
|
||||
- CNTR loads (loop counts)
|
||||
|
||||
Usage:
|
||||
python3 dm_xref.py firmware_pm.bin firmware_dm.bin
|
||||
python3 dm_xref.py firmware_pm.bin firmware_dm.bin --context 16
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
REG0 = ["AX0", "AX1", "MX0", "MX1", "AY0", "AY1", "MY0", "MY1",
|
||||
"MR2", "SR2", "AR", "SI", "MR1", "SR1", "MR0", "SR0"]
|
||||
REG1 = ["I0", "I1", "I2", "I3", "M0", "M1", "M2", "M3",
|
||||
"L0", "L1", "L2", "L3", "IMASK", "IRPTL", "ICNTL", "STACKA"]
|
||||
REG2 = ["I4", "I5", "I6", "I7", "M4", "M5", "M6", "M7",
|
||||
"L4", "L5", "L6", "L7", "RES", "RES", "CNTR", "LPSTACKA"]
|
||||
|
||||
|
||||
def read_pm(path):
|
||||
data = open(path, "rb").read()
|
||||
words = []
|
||||
for i in range(0, len(data) - 2, 3):
|
||||
w = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2]
|
||||
words.append(w)
|
||||
return words
|
||||
|
||||
|
||||
def read_dm(path, endian="big"):
|
||||
fmt = ">H" if endian == "big" else "<H"
|
||||
data = open(path, "rb").read()
|
||||
if len(data) % 2:
|
||||
data += b"\x00"
|
||||
return [struct.unpack_from(fmt, data, i)[0]
|
||||
for i in range(0, len(data), 2)]
|
||||
|
||||
|
||||
def signed16(v):
|
||||
return v - 0x10000 if v >= 0x8000 else v
|
||||
|
||||
|
||||
def extract_dm_refs(pm_words):
|
||||
"""Extract all DM address references from PM code."""
|
||||
refs = []
|
||||
|
||||
for i, w in enumerate(pm_words):
|
||||
pm_addr = i * 3
|
||||
b23_22 = (w >> 22) & 3
|
||||
b21_20 = (w >> 20) & 3
|
||||
|
||||
# Type 6: Dreg = Imm16 (data constants, not DM refs)
|
||||
# Skip — these load values, not addresses
|
||||
|
||||
# Type 7 REG1: I/M/L register loads
|
||||
if b23_22 == 1 and b21_20 == 1:
|
||||
val = (w >> 4) & 0xFFFF
|
||||
reg = REG1[w & 0xF]
|
||||
if reg.startswith("I"):
|
||||
refs.append({
|
||||
"pm_addr": pm_addr,
|
||||
"type": "I-reg load",
|
||||
"reg": reg,
|
||||
"dm_addr": val,
|
||||
"desc": f"{reg} = 0x{val:04X}"
|
||||
})
|
||||
elif reg.startswith("L"):
|
||||
refs.append({
|
||||
"pm_addr": pm_addr,
|
||||
"type": "L-reg (buf len)",
|
||||
"reg": reg,
|
||||
"dm_addr": None,
|
||||
"value": val,
|
||||
"desc": f"{reg} = {val} (buffer length)"
|
||||
})
|
||||
|
||||
# Type 7 REG2: I4-I7/M4-M7/L4-L7/CNTR loads
|
||||
if b23_22 == 0 and b21_20 == 3:
|
||||
val = (w >> 4) & 0xFFFF
|
||||
reg = REG2[w & 0xF]
|
||||
if reg.startswith("I"):
|
||||
refs.append({
|
||||
"pm_addr": pm_addr,
|
||||
"type": "I-reg load",
|
||||
"reg": reg,
|
||||
"dm_addr": val,
|
||||
"desc": f"{reg} = 0x{val:04X}"
|
||||
})
|
||||
elif reg.startswith("L"):
|
||||
refs.append({
|
||||
"pm_addr": pm_addr,
|
||||
"type": "L-reg (buf len)",
|
||||
"reg": reg,
|
||||
"dm_addr": None,
|
||||
"value": val,
|
||||
"desc": f"{reg} = {val} (buffer length)"
|
||||
})
|
||||
elif reg == "CNTR":
|
||||
refs.append({
|
||||
"pm_addr": pm_addr,
|
||||
"type": "CNTR",
|
||||
"reg": reg,
|
||||
"dm_addr": None,
|
||||
"value": val,
|
||||
"desc": f"CNTR = {val} (loop count)"
|
||||
})
|
||||
|
||||
# Type 3: Direct DM access — DM(addr16) = reg / reg = DM(addr16)
|
||||
if b23_22 == 2:
|
||||
d = (w >> 20) & 1
|
||||
addr = (w >> 4) & 0xFFFF
|
||||
reg_idx = w & 0xF
|
||||
is_ireg = (w >> 21) & 1
|
||||
rname = REG1[reg_idx] if is_ireg else REG0[reg_idx]
|
||||
direction = "write" if d else "read"
|
||||
refs.append({
|
||||
"pm_addr": pm_addr,
|
||||
"type": f"DM direct {direction}",
|
||||
"reg": rname,
|
||||
"dm_addr": addr,
|
||||
"desc": (f"DM(0x{addr:04X}) = {rname}" if d
|
||||
else f"{rname} = DM(0x{addr:04X})")
|
||||
})
|
||||
|
||||
return refs
|
||||
|
||||
|
||||
def show_dm_content(dm_words, dm_addr, context=8):
|
||||
"""Format DM content at a given address."""
|
||||
if dm_addr is None or dm_addr >= len(dm_words):
|
||||
return " (outside DM range)"
|
||||
|
||||
end = min(dm_addr + context, len(dm_words))
|
||||
hex_vals = []
|
||||
dec_vals = []
|
||||
for j in range(dm_addr, end):
|
||||
v = dm_words[j]
|
||||
hex_vals.append(f"0x{v:04X}")
|
||||
dec_vals.append(f"{signed16(v):6d}")
|
||||
|
||||
lines = []
|
||||
lines.append(f" Hex: [{', '.join(hex_vals)}]")
|
||||
lines.append(f" Dec: [{', '.join(dec_vals)}]")
|
||||
|
||||
# Check if it looks like Q15 coefficients
|
||||
vals = [signed16(dm_words[j]) for j in range(dm_addr, end)]
|
||||
if all(abs(v) <= 32767 for v in vals):
|
||||
q15 = [f"{v/32768:.4f}" for v in vals]
|
||||
lines.append(f" Q15: [{', '.join(q15)}]")
|
||||
|
||||
# Check for all zeros
|
||||
if all(v == 0 for v in vals):
|
||||
lines.append(f" (all zeros — uninitialized)")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract DM references from PM code and"
|
||||
" show DM contents.")
|
||||
parser.add_argument("pm_file", help="PM binary (packed 3-byte)")
|
||||
parser.add_argument("dm_file", help="DM binary (16-bit words)")
|
||||
parser.add_argument("--endian", choices=["big", "little"],
|
||||
default="big")
|
||||
parser.add_argument("--context", type=int, default=8,
|
||||
help="Words of DM context to show"
|
||||
" (default: 8)")
|
||||
args = parser.parse_args()
|
||||
|
||||
for f in (args.pm_file, args.dm_file):
|
||||
if not os.path.isfile(f):
|
||||
print(f"Error: {f} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
pm_words = read_pm(args.pm_file)
|
||||
dm_words = read_dm(args.dm_file, args.endian)
|
||||
|
||||
refs = extract_dm_refs(pm_words)
|
||||
|
||||
print(f"PM: {args.pm_file} ({len(pm_words)} instructions)")
|
||||
print(f"DM: {args.dm_file} ({len(dm_words)} words)")
|
||||
print()
|
||||
|
||||
# Group by type
|
||||
ireg_loads = [r for r in refs if r["type"] == "I-reg load"]
|
||||
direct_access = [r for r in refs
|
||||
if r["type"].startswith("DM direct")]
|
||||
buf_lengths = [r for r in refs
|
||||
if r["type"] == "L-reg (buf len)"]
|
||||
counters = [r for r in refs if r["type"] == "CNTR"]
|
||||
|
||||
# I-register loads → buffer base addresses
|
||||
if ireg_loads:
|
||||
print(f"=== Buffer Base Addresses"
|
||||
f" ({len(ireg_loads)} I-reg loads) ===")
|
||||
seen = {}
|
||||
for r in ireg_loads:
|
||||
key = (r["reg"], r["dm_addr"])
|
||||
if key not in seen:
|
||||
seen[key] = r
|
||||
for (reg, dm_addr), r in sorted(seen.items()):
|
||||
print(f"\n{reg} = 0x{dm_addr:04X}"
|
||||
f" (PM @ 0x{r['pm_addr']:06X})")
|
||||
# Find matching L-register for buffer length
|
||||
lreg = reg.replace("I", "L")
|
||||
for lr in buf_lengths:
|
||||
if lr["reg"] == lreg:
|
||||
print(f" {lreg} = {lr['value']}"
|
||||
f" (circular buffer, {lr['value']} words)")
|
||||
break
|
||||
print(show_dm_content(dm_words, dm_addr, args.context))
|
||||
print()
|
||||
|
||||
# Direct DM accesses
|
||||
if direct_access:
|
||||
print(f"=== Direct DM Accesses"
|
||||
f" ({len(direct_access)} found) ===")
|
||||
seen_addrs = {}
|
||||
for r in direct_access:
|
||||
a = r["dm_addr"]
|
||||
if a not in seen_addrs:
|
||||
seen_addrs[a] = []
|
||||
seen_addrs[a].append(r)
|
||||
|
||||
for dm_addr in sorted(seen_addrs.keys()):
|
||||
ops = seen_addrs[dm_addr]
|
||||
descs = [r["desc"] for r in ops[:3]]
|
||||
more = f" +{len(ops)-3} more" if len(ops) > 3 else ""
|
||||
print(f"\nDM 0x{dm_addr:04X}:")
|
||||
for d in descs:
|
||||
print(f" {d}")
|
||||
if more:
|
||||
print(f" {more}")
|
||||
print(show_dm_content(dm_words, dm_addr, args.context))
|
||||
print()
|
||||
|
||||
# Loop counters
|
||||
if counters:
|
||||
print(f"=== Loop Counters ({len(counters)} found) ===")
|
||||
for r in counters:
|
||||
print(f" PM 0x{r['pm_addr']:06X}: {r['desc']}")
|
||||
print()
|
||||
|
||||
# Summary
|
||||
all_dm_addrs = set()
|
||||
for r in refs:
|
||||
if r.get("dm_addr") is not None:
|
||||
all_dm_addrs.add(r["dm_addr"])
|
||||
|
||||
print(f"=== Summary ===")
|
||||
print(f" Unique DM addresses referenced: {len(all_dm_addrs)}")
|
||||
print(f" Buffer bases (I-reg): {len(ireg_loads)}")
|
||||
print(f" Direct DM access: {len(direct_access)}")
|
||||
print(f" Buffer lengths (L-reg): {len(buf_lengths)}")
|
||||
print(f" Loop counters (CNTR): {len(counters)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
334
tools/firmware_overview.py
Executable file
334
tools/firmware_overview.py
Executable file
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
"""firmware_overview.py -- Quick structural overview of ADSP-219x firmware.
|
||||
|
||||
Analyzes a PM binary and produces a human-readable report:
|
||||
- Memory map (code vs data vs empty regions)
|
||||
- Function-like blocks (code between RTS/JUMP boundaries)
|
||||
- DSP kernels (DO UNTIL loops with MAC operations)
|
||||
- I/O configuration (peripheral register writes)
|
||||
- All immediate constants loaded into registers
|
||||
- Cross-reference: which addresses are referenced
|
||||
|
||||
Usage:
|
||||
python3 firmware_overview.py firmware_pm.bin
|
||||
python3 firmware_overview.py firmware_pm.bin --dm firmware_dm.bin
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
# ADSP-2191 I/O register names (partial, most common)
|
||||
IO_NAMES = {
|
||||
0x00: "SYSCTRL", 0x01: "SYSTAT", 0x02: "IOPG",
|
||||
0x04: "BWCTRL", 0x05: "BWSTAT",
|
||||
0x10: "PICR0", 0x11: "PICR1", 0x12: "PICR2", 0x13: "PICR3",
|
||||
0x14: "IMASK0", 0x15: "IMASK1",
|
||||
0x20: "TCTL", 0x21: "TCOUNT", 0x22: "TPERIOD", 0x23: "TSCALE",
|
||||
0x40: "SPORT0_CTL", 0x41: "SPORT0_DIV",
|
||||
0x42: "SPORT0_MCTL", 0x43: "SPORT0_CS0",
|
||||
0x44: "SPORT0_TX", 0x45: "SPORT0_RX",
|
||||
0x46: "SPORT0_STAT",
|
||||
0x60: "SPORT1_CTL", 0x61: "SPORT1_DIV",
|
||||
0x62: "SPORT1_MCTL", 0x63: "SPORT1_CS0",
|
||||
0x64: "SPORT1_TX", 0x65: "SPORT1_RX",
|
||||
0x66: "SPORT1_STAT",
|
||||
0x80: "SPICTL", 0x81: "SPISTAT", 0x82: "SPIFLG",
|
||||
0x83: "SPIBAUD", 0x84: "SPIRX", 0x85: "SPITX",
|
||||
0xA0: "UART_TX", 0xA1: "UART_RX", 0xA2: "UART_CTL",
|
||||
0xA3: "UART_STAT", 0xA4: "UART_DIV",
|
||||
0xC0: "FLAGD", 0xC1: "FLAGP", 0xC2: "FLAGS",
|
||||
0xC3: "FLAGC",
|
||||
}
|
||||
|
||||
REG0 = ["AX0", "AX1", "MX0", "MX1", "AY0", "AY1", "MY0", "MY1",
|
||||
"MR2", "SR2", "AR", "SI", "MR1", "SR1", "MR0", "SR0"]
|
||||
REG1 = ["I0", "I1", "I2", "I3", "M0", "M1", "M2", "M3",
|
||||
"L0", "L1", "L2", "L3", "IMASK", "IRPTL", "ICNTL", "STACKA"]
|
||||
REG2 = ["I4", "I5", "I6", "I7", "M4", "M5", "M6", "M7",
|
||||
"L4", "L5", "L6", "L7", "RES", "RES", "CNTR", "LPSTACKA"]
|
||||
|
||||
|
||||
def read_pm(path):
|
||||
"""Read 24-bit PM words from packed binary."""
|
||||
data = open(path, "rb").read()
|
||||
words = []
|
||||
for i in range(0, len(data) - 2, 3):
|
||||
w = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2]
|
||||
words.append(w)
|
||||
return words
|
||||
|
||||
|
||||
def classify_word(w):
|
||||
"""Return a rough classification of a PM word."""
|
||||
if w == 0:
|
||||
return "null"
|
||||
b23_22 = (w >> 22) & 3
|
||||
if b23_22 == 3:
|
||||
return "compute_multi" # Type 1
|
||||
b23_16 = (w >> 16) & 0xFF
|
||||
if b23_16 == 0x0A:
|
||||
return "rts"
|
||||
if (w >> 18) == 0x07:
|
||||
return "jump"
|
||||
if (w >> 19) == 0x03:
|
||||
return "jump_cond"
|
||||
if b23_16 == 0x16:
|
||||
return "do_until"
|
||||
if b23_16 == 0x0B:
|
||||
return "jump_indirect"
|
||||
if b23_16 == 0x05:
|
||||
return "ljump"
|
||||
return "code"
|
||||
|
||||
|
||||
def find_constants(words):
|
||||
"""Extract all immediate constant loads."""
|
||||
constants = []
|
||||
for i, w in enumerate(words):
|
||||
addr = i * 3
|
||||
b23_22 = (w >> 22) & 3
|
||||
b21_20 = (w >> 20) & 3
|
||||
|
||||
# Type 6: Dreg = Imm16
|
||||
if b23_22 == 1 and b21_20 == 0:
|
||||
val = (w >> 4) & 0xFFFF
|
||||
reg = REG0[w & 0xF]
|
||||
constants.append((addr, reg, val, "dreg"))
|
||||
|
||||
# Type 7 REG1: Reg1 = Imm16
|
||||
elif b23_22 == 1 and b21_20 == 1:
|
||||
val = (w >> 4) & 0xFFFF
|
||||
reg = REG1[w & 0xF]
|
||||
constants.append((addr, reg, val, "reg1"))
|
||||
|
||||
# Type 7 REG2: Reg2 = Imm16
|
||||
elif b23_22 == 0 and b21_20 == 3:
|
||||
val = (w >> 4) & 0xFFFF
|
||||
reg = REG2[w & 0xF]
|
||||
constants.append((addr, reg, val, "reg2"))
|
||||
|
||||
# Type 33: Reg3 = Data12
|
||||
elif (w >> 16) == 0x10:
|
||||
val = (w >> 4) & 0xFFF
|
||||
constants.append((addr, "REG3", val, "short"))
|
||||
|
||||
# Type 34: IO write
|
||||
if (w >> 16) == 0x06 and ((w >> 15) & 1):
|
||||
d = (w >> 12) & 1
|
||||
io_addr = (((w >> 13) & 3) << 8) | ((w >> 4) & 0xFF)
|
||||
dreg = REG0[w & 0xF]
|
||||
io_name = IO_NAMES.get(io_addr, f"IO_0x{io_addr:03X}")
|
||||
if d:
|
||||
constants.append((addr, io_name, None,
|
||||
f"io_write({dreg})"))
|
||||
else:
|
||||
constants.append((addr, dreg, None,
|
||||
f"io_read({io_name})"))
|
||||
|
||||
return constants
|
||||
|
||||
|
||||
def find_do_loops(words):
|
||||
"""Find DO UNTIL loops and their bodies."""
|
||||
loops = []
|
||||
for i, w in enumerate(words):
|
||||
if (w >> 16) == 0x16:
|
||||
rel = (w >> 4) & 0xFFF
|
||||
term = w & 0xF
|
||||
cond = ["EQ", "NE", "GT", "LE", "LT", "GE",
|
||||
"AV", "NOT AV", "AC", "NOT AC",
|
||||
"SWCOND", "NOT SWCOND", "MV", "NOT MV",
|
||||
"NOT CE", "TRUE"][term]
|
||||
srel = rel if rel < 0x800 else rel - 0x1000
|
||||
target = i * 3 + srel * 3
|
||||
# Count MAC ops in loop body
|
||||
mac_count = 0
|
||||
for j in range(i + 1, min(i + srel + 1, len(words))):
|
||||
if (words[j] >> 22) & 3 == 3: # Type 1
|
||||
amf = (words[j] >> 13) & 0x1F
|
||||
if amf < 16: # MAC operation
|
||||
mac_count += 1
|
||||
loops.append((i * 3, target, cond, srel, mac_count))
|
||||
return loops
|
||||
|
||||
|
||||
def memory_map(words, chunk_size=64):
|
||||
"""Build a coarse memory map."""
|
||||
regions = []
|
||||
i = 0
|
||||
while i < len(words):
|
||||
end = min(i + chunk_size, len(words))
|
||||
chunk = words[i:end]
|
||||
n_null = sum(1 for w in chunk if w == 0)
|
||||
n_multi = sum(1 for w in chunk
|
||||
if (w >> 22) & 3 == 3)
|
||||
n_jump = sum(1 for w in chunk
|
||||
if classify_word(w) in
|
||||
("jump", "jump_cond", "rts", "do_until"))
|
||||
|
||||
if n_null > len(chunk) * 0.9:
|
||||
kind = "empty"
|
||||
elif n_multi > len(chunk) * 0.3:
|
||||
kind = "dsp_kernel"
|
||||
elif n_jump > len(chunk) * 0.1:
|
||||
kind = "control_flow"
|
||||
else:
|
||||
kind = "code/data"
|
||||
|
||||
if regions and regions[-1][2] == kind:
|
||||
regions[-1] = (regions[-1][0], end * 3, kind)
|
||||
else:
|
||||
regions.append((i * 3, end * 3, kind))
|
||||
i = end
|
||||
return regions
|
||||
|
||||
|
||||
def analyze_pm(path, dm_path=None):
|
||||
"""Full PM analysis."""
|
||||
words = read_pm(path)
|
||||
size = len(words) * 3
|
||||
|
||||
print(f"=== PM Firmware Overview ===")
|
||||
print(f"File: {path}")
|
||||
print(f"Size: {size} bytes ({len(words)} instructions)")
|
||||
print()
|
||||
|
||||
# Memory map
|
||||
regions = memory_map(words)
|
||||
print(f"--- Memory Map ---")
|
||||
for start, end, kind in regions:
|
||||
label = {"empty": "EMPTY (null)",
|
||||
"dsp_kernel": "DSP KERNEL (MAC-heavy)",
|
||||
"control_flow": "CONTROL FLOW (jumps)",
|
||||
"code/data": "CODE / DATA"}[kind]
|
||||
print(f" 0x{start:06X}-0x{end:06X}"
|
||||
f" ({(end-start)//3:4d} words) {label}")
|
||||
print()
|
||||
|
||||
# Entry point
|
||||
if words:
|
||||
w0 = words[0]
|
||||
if (w0 >> 18) == 0x07:
|
||||
rel = ((w0 >> 4) & 0x3FFF) | ((w0 & 3) << 14)
|
||||
srel = rel if rel < 0x8000 else rel - 0x10000
|
||||
target = srel * 3
|
||||
print(f"--- Entry Point ---")
|
||||
print(f" Reset vector: JUMP 0x{target:06X}")
|
||||
print()
|
||||
elif w0 == 0:
|
||||
print(f"--- Entry Point ---")
|
||||
print(f" Reset vector: NOP (code starts at 0x000003)")
|
||||
print()
|
||||
|
||||
# DO UNTIL loops (DSP kernels)
|
||||
loops = find_do_loops(words)
|
||||
if loops:
|
||||
print(f"--- DSP Loops ({len(loops)} found) ---")
|
||||
for addr, end, cond, length, macs in loops:
|
||||
kind = ""
|
||||
if macs > 0:
|
||||
kind = f" [MAC kernel, {macs} multiply-accumulate ops]"
|
||||
elif cond == "NOT CE":
|
||||
kind = " [counter loop]"
|
||||
print(f" 0x{addr:06X}: DO 0x{end:06X} UNTIL {cond}"
|
||||
f" ({length} words){kind}")
|
||||
print()
|
||||
|
||||
# Constants
|
||||
constants = find_constants(words)
|
||||
if constants:
|
||||
# Group by type
|
||||
reg_loads = [(a, r, v, t) for a, r, v, t in constants
|
||||
if t in ("dreg", "reg1", "reg2", "short")]
|
||||
io_ops = [(a, r, v, t) for a, r, v, t in constants
|
||||
if t.startswith("io_")]
|
||||
|
||||
if reg_loads:
|
||||
print(f"--- Constants ({len(reg_loads)} register loads) ---")
|
||||
# Show unique values
|
||||
seen = {}
|
||||
for addr, reg, val, _ in reg_loads:
|
||||
key = (reg, val)
|
||||
if key not in seen:
|
||||
seen[key] = []
|
||||
seen[key].append(addr)
|
||||
|
||||
for (reg, val), addrs in sorted(seen.items(),
|
||||
key=lambda x: x[0][1]):
|
||||
sval = val - 0x10000 if val >= 0x8000 else val
|
||||
locs = ", ".join(f"0x{a:06X}" for a in addrs[:3])
|
||||
more = f" +{len(addrs)-3} more" if len(addrs) > 3 else ""
|
||||
print(f" {reg:8s} = 0x{val:04X}"
|
||||
f" ({sval:6d}) @ {locs}{more}")
|
||||
print()
|
||||
|
||||
if io_ops:
|
||||
print(f"--- I/O Operations ({len(io_ops)} found) ---")
|
||||
for addr, reg, _, typ in io_ops:
|
||||
print(f" 0x{addr:06X}: {typ}")
|
||||
print()
|
||||
|
||||
# DM analysis if provided
|
||||
if dm_path and os.path.isfile(dm_path):
|
||||
print(f"--- DM Cross-Reference ---")
|
||||
dm_data = open(dm_path, "rb").read()
|
||||
dm_size = len(dm_data)
|
||||
print(f" DM file: {dm_path} ({dm_size} bytes,"
|
||||
f" {dm_size//2} words)")
|
||||
|
||||
# Find all I-register loads and show what's at those
|
||||
# DM addresses
|
||||
for addr, reg, val, typ in constants:
|
||||
if reg.startswith("I") and reg[1:].isdigit() \
|
||||
and typ in ("reg1", "reg2"):
|
||||
dm_byte = val * 2
|
||||
if dm_byte + 20 <= dm_size:
|
||||
sample = []
|
||||
for j in range(10):
|
||||
w = struct.unpack_from(
|
||||
">H", dm_data, dm_byte + j * 2)[0]
|
||||
sample.append(f"0x{w:04X}")
|
||||
print(f" {reg} = 0x{val:04X} -> DM content:"
|
||||
f" [{', '.join(sample[:5])} ...]")
|
||||
print()
|
||||
|
||||
# Summary
|
||||
n_code = sum(1 for w in words if w != 0)
|
||||
n_mac = sum(1 for w in words if (w >> 22) & 3 == 3)
|
||||
n_jump = sum(1 for w in words
|
||||
if classify_word(w) in
|
||||
("jump", "jump_cond", "jump_indirect"))
|
||||
n_rts = sum(1 for w in words
|
||||
if classify_word(w) == "rts")
|
||||
|
||||
print(f"--- Summary ---")
|
||||
print(f" Total words: {len(words)}")
|
||||
print(f" Non-zero: {n_code}"
|
||||
f" ({n_code*100//max(len(words),1)}%)")
|
||||
print(f" Multifunction: {n_mac} (MAC/ALU + memory)")
|
||||
print(f" Jumps/Calls: {n_jump}")
|
||||
print(f" Returns: {n_rts}")
|
||||
print(f" DO loops: {len(loops)}")
|
||||
print(f" I/O accesses: {len(io_ops) if constants else 0}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Quick overview of ADSP-219x PM firmware.")
|
||||
parser.add_argument("file", help="PM binary file (packed 3-byte)")
|
||||
parser.add_argument("--dm", help="Optional DM binary for"
|
||||
" cross-reference")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.isfile(args.file):
|
||||
print(f"Error: {args.file} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
analyze_pm(args.file, args.dm)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user