pyGdbToolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyGdbToolkit/__init__.py +55 -0
- pyGdbToolkit/arch/__init__.py +79 -0
- pyGdbToolkit/arch/arm/__init__.py +17 -0
- pyGdbToolkit/arch/arm/coresight.py +427 -0
- pyGdbToolkit/arch/arm/cortex_m.py +504 -0
- pyGdbToolkit/arch/arm/fault.py +629 -0
- pyGdbToolkit/arch/arm/models.py +60 -0
- pyGdbToolkit/arch/arm/mpu.py +556 -0
- pyGdbToolkit/arch/arm/probe.py +10 -0
- pyGdbToolkit/arch/arm/providers/__init__.py +16 -0
- pyGdbToolkit/arch/arm/providers/base.py +77 -0
- pyGdbToolkit/arch/arm/providers/manufacturers/__init__.py +4 -0
- pyGdbToolkit/arch/arm/providers/manufacturers/stm32.py +680 -0
- pyGdbToolkit/arch/arm/sau.py +253 -0
- pyGdbToolkit/arch/arm/security.py +816 -0
- pyGdbToolkit/arch/arm/session_state.py +96 -0
- pyGdbToolkit/arch/arm/target.py +32 -0
- pyGdbToolkit/arch/base.py +143 -0
- pyGdbToolkit/arch/diagnostics.py +300 -0
- pyGdbToolkit/arch/registry.py +43 -0
- pyGdbToolkit/cmd_faultinfo.py +99 -0
- pyGdbToolkit/cmd_lscpu.py +116 -0
- pyGdbToolkit/cmd_profile.py +1402 -0
- pyGdbToolkit/cmd_rtos.py +298 -0
- pyGdbToolkit/cmd_secscan.py +466 -0
- pyGdbToolkit/cmd_svd.py +1208 -0
- pyGdbToolkit/diagnostic_runtime.py +52 -0
- pyGdbToolkit/rtos/__init__.py +8 -0
- pyGdbToolkit/rtos/camelot.py +513 -0
- pyGdbToolkit/session.py +233 -0
- pyGdbToolkit/svd.py +1194 -0
- pyGdbToolkit/target_memory.py +190 -0
- pygdbtoolkit-0.1.0.dist-info/METADATA +586 -0
- pygdbtoolkit-0.1.0.dist-info/RECORD +36 -0
- pygdbtoolkit-0.1.0.dist-info/WHEEL +5 -0
- pygdbtoolkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Arm Cortex-M fault-analysis diagnostic service."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import struct
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
from ...target_memory import TargetMemory, TargetReadError
|
|
13
|
+
from ..base import Architecture, SystemRegisterSet, TargetDescription
|
|
14
|
+
from ..diagnostics import (
|
|
15
|
+
DiagnosticPanel,
|
|
16
|
+
DiagnosticReport,
|
|
17
|
+
DiagnosticRuntimeAccess,
|
|
18
|
+
DiagnosticServiceName,
|
|
19
|
+
DiagnosticTable,
|
|
20
|
+
DiagnosticTableRow,
|
|
21
|
+
)
|
|
22
|
+
from .cortex_m import CortexMTargetDescription, read_scb
|
|
23
|
+
|
|
24
|
+
_EXC_RETURN_MASK = 0xFFFFFF00
|
|
25
|
+
_EXCEPTION_NAMES: dict[int, str] = {
|
|
26
|
+
1: "Reset",
|
|
27
|
+
2: "NMI",
|
|
28
|
+
3: "HardFault",
|
|
29
|
+
4: "MemManage",
|
|
30
|
+
5: "BusFault",
|
|
31
|
+
6: "UsageFault",
|
|
32
|
+
7: "SecureFault",
|
|
33
|
+
11: "SVCall",
|
|
34
|
+
12: "DebugMonitor",
|
|
35
|
+
14: "PendSV",
|
|
36
|
+
15: "SysTick",
|
|
37
|
+
}
|
|
38
|
+
_UFSR_BITS: dict[int, tuple[str, str]] = {
|
|
39
|
+
0: ("UNDEFINSTR", "Undefined instruction executed"),
|
|
40
|
+
1: ("INVSTATE", "Invalid Thumb state (bit T=0 or branch to even address)"),
|
|
41
|
+
2: ("INVPC", "Invalid EXC_RETURN value on exception return"),
|
|
42
|
+
3: ("NOCP", "Access to disabled or non-existent coprocessor/FPU"),
|
|
43
|
+
4: ("STKOF", "Stack overflow detected"),
|
|
44
|
+
8: ("UNALIGNED", "Unaligned access trapped (CCR.UNALIGN_TRP=1)"),
|
|
45
|
+
9: ("DIVBYZERO", "Integer division by zero (CCR.DIV_0_TRP=1)"),
|
|
46
|
+
}
|
|
47
|
+
_BFSR_BITS: dict[int, tuple[str, str]] = {
|
|
48
|
+
0: ("IBUSERR", "Bus error on instruction fetch"),
|
|
49
|
+
1: ("PRECISERR", "Precise bus error on data access (BFAR valid)"),
|
|
50
|
+
2: ("IMPRECISERR", "Imprecise bus error (asynchronous write buffer)"),
|
|
51
|
+
3: ("UNSTKERR", "Bus error on exception return unstacking"),
|
|
52
|
+
4: ("STKERR", "Bus error on exception entry stacking"),
|
|
53
|
+
5: ("LSPERR", "Bus error during FPU lazy preservation"),
|
|
54
|
+
7: ("BFARVALID", "BFAR contains valid fault address"),
|
|
55
|
+
}
|
|
56
|
+
_MMFSR_BITS: dict[int, tuple[str, str]] = {
|
|
57
|
+
0: ("IACCVIOL", "MPU/security violation on instruction fetch (XN)"),
|
|
58
|
+
1: ("DACCVIOL", "MPU/security violation on data access"),
|
|
59
|
+
3: ("MUNSTKERR", "MPU violation on exception return unstacking"),
|
|
60
|
+
4: ("MSTKERR", "MPU violation on exception entry stacking"),
|
|
61
|
+
5: ("MLSPERR", "MPU violation during FPU lazy preservation"),
|
|
62
|
+
7: ("MMARVALID", "MMFAR contains valid fault address"),
|
|
63
|
+
}
|
|
64
|
+
_HFSR_BITS: dict[int, tuple[str, str]] = {
|
|
65
|
+
1: ("VECTTBL", "Vector table read error"),
|
|
66
|
+
30: ("FORCED", "Fault escalated to HardFault (source handler disabled/masked)"),
|
|
67
|
+
31: ("DEBUGEVT", "Debug event / breakpoint"),
|
|
68
|
+
}
|
|
69
|
+
_DFSR_BITS: dict[int, tuple[str, str]] = {
|
|
70
|
+
0: ("HALTED", "Core halted by debug request / DAP"),
|
|
71
|
+
1: ("BKPT", "BKPT instruction executed"),
|
|
72
|
+
2: ("DWTTRAP", "DWT trap / watchpoint"),
|
|
73
|
+
3: ("VCATCH", "Vector catch triggered"),
|
|
74
|
+
4: ("EXTERNAL", "External debug request"),
|
|
75
|
+
}
|
|
76
|
+
_SFSR_BITS: dict[int, tuple[str, str]] = {
|
|
77
|
+
0: ("INVEP", "Invalid entry point (missing SG instruction)"),
|
|
78
|
+
1: ("INVIS", "Invalid integrity signature"),
|
|
79
|
+
2: ("INVER", "Invalid Secure exception return"),
|
|
80
|
+
3: ("AUVIOL", "Attribution unit violation (SAU/IDAU)"),
|
|
81
|
+
4: ("INVTRAN", "Illegal security domain transition"),
|
|
82
|
+
5: ("LSPERR", "SAU/IDAU error during lazy preservation"),
|
|
83
|
+
6: ("SFARVALID", "SFAR contains valid fault address"),
|
|
84
|
+
7: ("LSERR", "Lazy state enable/disable error"),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class StackedFrame:
|
|
90
|
+
"""Decoded hardware exception frame restored from the active stack."""
|
|
91
|
+
|
|
92
|
+
lr_exc_return: int
|
|
93
|
+
sp_name: str
|
|
94
|
+
sp_address: int
|
|
95
|
+
return_mode: str
|
|
96
|
+
target_domain: str
|
|
97
|
+
has_fpu: bool
|
|
98
|
+
secure_stacking: bool
|
|
99
|
+
r0: int
|
|
100
|
+
r1: int
|
|
101
|
+
r2: int
|
|
102
|
+
r3: int
|
|
103
|
+
r12: int
|
|
104
|
+
lr: int
|
|
105
|
+
pc: int
|
|
106
|
+
xpsr: int
|
|
107
|
+
fp_registers: tuple[int, ...] | None = None
|
|
108
|
+
fpscr: int | None = None
|
|
109
|
+
s0_float: float | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _is_exc_return(value: int) -> bool:
|
|
113
|
+
"""Return whether a value has the Cortex-M EXC_RETURN high-byte pattern."""
|
|
114
|
+
return value & _EXC_RETURN_MASK == _EXC_RETURN_MASK
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _stack_register_names(exc_return: int, sp_name: str) -> tuple[str, str]:
|
|
118
|
+
"""Return the banked stack register selected by EXC_RETURN before its alias."""
|
|
119
|
+
stack_register = sp_name.lower()
|
|
120
|
+
security_suffix = "s" if exc_return & (1 << 6) else "ns"
|
|
121
|
+
return (f"{stack_register}_{security_suffix}", stack_register)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _decode_flags(value: int | None, table: dict[int, tuple[str, str]]) -> list[tuple[str, str]]:
|
|
125
|
+
"""Decode active named bit flags from one system-register value."""
|
|
126
|
+
if value is None:
|
|
127
|
+
return []
|
|
128
|
+
return [
|
|
129
|
+
(name, description)
|
|
130
|
+
for bit, (name, description) in sorted(table.items())
|
|
131
|
+
if value & (1 << bit)
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _memory_region(address: int | None) -> str:
|
|
136
|
+
"""Classify an address using the standard Cortex-M memory map."""
|
|
137
|
+
if address is None:
|
|
138
|
+
return "?"
|
|
139
|
+
if address < 0x1000:
|
|
140
|
+
return "NULL-pointer / Vector Table"
|
|
141
|
+
if address < 0x20000000:
|
|
142
|
+
return "CODE (Flash / ROM)"
|
|
143
|
+
if address < 0x40000000:
|
|
144
|
+
return "SRAM"
|
|
145
|
+
if address < 0x60000000:
|
|
146
|
+
return "Peripherals (APB/AHB)"
|
|
147
|
+
if address < 0x80000000:
|
|
148
|
+
return "External RAM"
|
|
149
|
+
if address < 0xA0000000:
|
|
150
|
+
return "External Device"
|
|
151
|
+
if address < 0xE0000000:
|
|
152
|
+
return "System / Reserved"
|
|
153
|
+
if address < 0xE0100000:
|
|
154
|
+
return "PPB (SCB / NVIC / Core)"
|
|
155
|
+
return "Vendor / Reserved"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read_stacked_frame(
|
|
159
|
+
reader: TargetMemory,
|
|
160
|
+
lr_value: int,
|
|
161
|
+
sp_value: int,
|
|
162
|
+
sp_name: str,
|
|
163
|
+
) -> StackedFrame | str:
|
|
164
|
+
"""Decode an EXC_RETURN-selected basic or extended Cortex-M stack frame."""
|
|
165
|
+
has_fpu = not bool(lr_value & (1 << 4))
|
|
166
|
+
word_count = 26 if has_fpu else 8
|
|
167
|
+
words: list[int] = []
|
|
168
|
+
for index in range(word_count):
|
|
169
|
+
address = sp_value + 4 * index
|
|
170
|
+
try:
|
|
171
|
+
words.append(reader.read_uint32(address))
|
|
172
|
+
except (TargetReadError, ValueError) as error:
|
|
173
|
+
return f"Cannot read stacked frame at 0x{address:08X}: {error}"
|
|
174
|
+
|
|
175
|
+
core_offset = 18 if has_fpu else 0
|
|
176
|
+
fp_registers = tuple(words[:16]) if has_fpu else None
|
|
177
|
+
fpscr = words[16] if has_fpu else None
|
|
178
|
+
s0_float = None
|
|
179
|
+
if fp_registers is not None:
|
|
180
|
+
s0_float = struct.unpack("<f", struct.pack("<I", fp_registers[0]))[0]
|
|
181
|
+
return StackedFrame(
|
|
182
|
+
lr_exc_return=lr_value,
|
|
183
|
+
sp_name=sp_name,
|
|
184
|
+
sp_address=sp_value,
|
|
185
|
+
return_mode="Handler" if not lr_value & (1 << 3) else "Thread",
|
|
186
|
+
target_domain="Secure" if not lr_value & 1 else "Non-Secure",
|
|
187
|
+
has_fpu=has_fpu,
|
|
188
|
+
secure_stacking=bool(lr_value & (1 << 6)),
|
|
189
|
+
r0=words[core_offset],
|
|
190
|
+
r1=words[core_offset + 1],
|
|
191
|
+
r2=words[core_offset + 2],
|
|
192
|
+
r3=words[core_offset + 3],
|
|
193
|
+
r12=words[core_offset + 4],
|
|
194
|
+
lr=words[core_offset + 5],
|
|
195
|
+
pc=words[core_offset + 6],
|
|
196
|
+
xpsr=words[core_offset + 7],
|
|
197
|
+
fp_registers=fp_registers,
|
|
198
|
+
fpscr=fpscr,
|
|
199
|
+
s0_float=s0_float,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class CortexMFaultCollector:
|
|
204
|
+
"""Collect the Cortex-M implementation of portable fault analysis."""
|
|
205
|
+
|
|
206
|
+
architecture = Architecture.ARM
|
|
207
|
+
service = DiagnosticServiceName.FAULT_ANALYSIS
|
|
208
|
+
|
|
209
|
+
def supports(self, target: TargetDescription) -> bool:
|
|
210
|
+
"""Return whether this service recognizes a typed Cortex-M target."""
|
|
211
|
+
return isinstance(target, CortexMTargetDescription) and target.core is not None
|
|
212
|
+
|
|
213
|
+
def collect(
|
|
214
|
+
self,
|
|
215
|
+
reader: TargetMemory,
|
|
216
|
+
target: TargetDescription,
|
|
217
|
+
access: DiagnosticRuntimeAccess | None = None,
|
|
218
|
+
) -> DiagnosticReport:
|
|
219
|
+
"""Collect typed SCB fault state and the selected exception frame."""
|
|
220
|
+
if not isinstance(target, CortexMTargetDescription) or target.core is None:
|
|
221
|
+
raise ValueError("Cortex-M fault analysis requires a known core description")
|
|
222
|
+
|
|
223
|
+
registers = access.registers if access is not None else None
|
|
224
|
+
symbols = access.symbols if access is not None else None
|
|
225
|
+
read_register = registers.read_first if registers is not None else lambda names: None
|
|
226
|
+
pc = read_register(("pc", "r15"))
|
|
227
|
+
lr = read_register(("lr", "r14"))
|
|
228
|
+
xpsr = read_register(("xpsr",))
|
|
229
|
+
ipsr_value = read_register(("ipsr",))
|
|
230
|
+
ipsr = ipsr_value & 0x1FF if ipsr_value is not None else (xpsr or 0) & 0x1FF
|
|
231
|
+
scb = read_scb(reader, target, cpuid_value=target.raw_cpuid)
|
|
232
|
+
stacked_frame: StackedFrame | str | None = None
|
|
233
|
+
if lr is not None and _is_exc_return(lr):
|
|
234
|
+
uses_psp = bool(lr & (1 << 2))
|
|
235
|
+
sp_name = "PSP" if uses_psp else "MSP"
|
|
236
|
+
sp = read_register(_stack_register_names(lr, sp_name))
|
|
237
|
+
if sp is None:
|
|
238
|
+
sp = read_register(("sp", "r13"))
|
|
239
|
+
if sp is None:
|
|
240
|
+
stacked_frame = f"Cannot read stacked frame: {sp_name} is unavailable."
|
|
241
|
+
else:
|
|
242
|
+
stacked_frame = read_stacked_frame(reader, lr, sp, sp_name)
|
|
243
|
+
|
|
244
|
+
resolve = symbols.resolve if symbols is not None else lambda address: "?"
|
|
245
|
+
overview = self._overview_table(target, ipsr, pc, lr, xpsr, resolve)
|
|
246
|
+
stack_table = self._stacked_frame_table(stacked_frame, resolve)
|
|
247
|
+
scb_table = self._scb_table(scb)
|
|
248
|
+
panels = self._diagnostic_panels(scb, stacked_frame, resolve)
|
|
249
|
+
blocks: list[DiagnosticTable | DiagnosticPanel] = [overview]
|
|
250
|
+
if stack_table is not None:
|
|
251
|
+
blocks.append(stack_table)
|
|
252
|
+
elif isinstance(stacked_frame, str):
|
|
253
|
+
blocks.append(DiagnosticPanel("Stack Frame", (stacked_frame,)))
|
|
254
|
+
blocks.extend((scb_table, *panels))
|
|
255
|
+
return DiagnosticReport(
|
|
256
|
+
self.service,
|
|
257
|
+
target,
|
|
258
|
+
tables=tuple(table for table in blocks if isinstance(table, DiagnosticTable)),
|
|
259
|
+
panels=tuple(panel for panel in blocks if isinstance(panel, DiagnosticPanel)),
|
|
260
|
+
blocks=tuple(blocks),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def _overview_table(
|
|
265
|
+
target: CortexMTargetDescription,
|
|
266
|
+
ipsr: int,
|
|
267
|
+
pc: int | None,
|
|
268
|
+
lr: int | None,
|
|
269
|
+
xpsr: int | None,
|
|
270
|
+
resolve: Callable[[int], str],
|
|
271
|
+
) -> DiagnosticTable:
|
|
272
|
+
"""Build the established overview as generic table data."""
|
|
273
|
+
exception = _EXCEPTION_NAMES.get(ipsr, f"Interrupt #{ipsr}")
|
|
274
|
+
exception_display = (
|
|
275
|
+
f"{exception} (Exception #{ipsr})"
|
|
276
|
+
if ipsr in (2, 3, 4, 5, 6, 7)
|
|
277
|
+
else "None (Thread Mode)" if ipsr == 0 else f"{exception} (#{ipsr})"
|
|
278
|
+
)
|
|
279
|
+
rows: list[DiagnosticTableRow] = [
|
|
280
|
+
DiagnosticTableRow(
|
|
281
|
+
(
|
|
282
|
+
"Target Core",
|
|
283
|
+
f"{target.core_name} ({target.rnp_revision}) - {target.implementer_name}",
|
|
284
|
+
)
|
|
285
|
+
),
|
|
286
|
+
DiagnosticTableRow(("Active Exception", exception_display)),
|
|
287
|
+
DiagnosticTableRow(
|
|
288
|
+
(
|
|
289
|
+
"Execution Mode",
|
|
290
|
+
"HANDLER (in exception)" if ipsr else "THREAD (normal execution)",
|
|
291
|
+
)
|
|
292
|
+
),
|
|
293
|
+
DiagnosticTableRow(
|
|
294
|
+
("Current PC", f"0x{pc:08X} [{resolve(pc)}]" if pc is not None else "Unknown")
|
|
295
|
+
),
|
|
296
|
+
]
|
|
297
|
+
if lr is None:
|
|
298
|
+
rows.append(DiagnosticTableRow(("Current LR", "Unknown")))
|
|
299
|
+
elif _is_exc_return(lr):
|
|
300
|
+
rows.append(DiagnosticTableRow(("Current LR", f"0x{lr:08X} (valid EXC_RETURN)")))
|
|
301
|
+
else:
|
|
302
|
+
rows.append(DiagnosticTableRow(("Current LR", f"0x{lr:08X} [{resolve(lr)}]")))
|
|
303
|
+
if xpsr is not None:
|
|
304
|
+
rows.append(
|
|
305
|
+
DiagnosticTableRow(
|
|
306
|
+
("Current xPSR", f"0x{xpsr:08X} (IPSR={xpsr & 0x1FF}, T={(xpsr >> 24) & 1})")
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
return DiagnosticTable("ARM Cortex-M Fault Overview", ("Property", "Value"), tuple(rows))
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def _stacked_frame_table(
|
|
313
|
+
frame: StackedFrame | str | None,
|
|
314
|
+
resolve: Callable[[int], str],
|
|
315
|
+
) -> DiagnosticTable | None:
|
|
316
|
+
"""Build stacked-frame table data only when hardware frame recovery succeeded."""
|
|
317
|
+
if not isinstance(frame, StackedFrame):
|
|
318
|
+
return None
|
|
319
|
+
summary = (
|
|
320
|
+
f"Stack: {frame.sp_name} @ 0x{frame.sp_address:08X} | "
|
|
321
|
+
f"Return to: {frame.return_mode} ({frame.target_domain}) | "
|
|
322
|
+
f"Frame: {'Extended (standard FPU)' if frame.has_fpu else 'Basic (8 registers)'}"
|
|
323
|
+
)
|
|
324
|
+
rows = [
|
|
325
|
+
DiagnosticTableRow(("r0", f"0x{frame.r0:08X}", "")),
|
|
326
|
+
DiagnosticTableRow(("r1", f"0x{frame.r1:08X}", "")),
|
|
327
|
+
DiagnosticTableRow(("r2", f"0x{frame.r2:08X}", "")),
|
|
328
|
+
DiagnosticTableRow(("r3", f"0x{frame.r3:08X}", "")),
|
|
329
|
+
DiagnosticTableRow(("r12", f"0x{frame.r12:08X}", "")),
|
|
330
|
+
DiagnosticTableRow(("lr", f"0x{frame.lr:08X}", f"Caller: {resolve(frame.lr)}")),
|
|
331
|
+
DiagnosticTableRow(
|
|
332
|
+
("pc", f"0x{frame.pc:08X}", f"<-- Faulting instruction: {resolve(frame.pc)}")
|
|
333
|
+
),
|
|
334
|
+
DiagnosticTableRow(
|
|
335
|
+
(
|
|
336
|
+
"xpsr",
|
|
337
|
+
f"0x{frame.xpsr:08X}",
|
|
338
|
+
f"IPSR={frame.xpsr & 0x1FF}, T={(frame.xpsr >> 24) & 1} "
|
|
339
|
+
f"({'Thumb' if frame.xpsr & (1 << 24) else 'ARM (invalid on Cortex-M)'})",
|
|
340
|
+
)
|
|
341
|
+
),
|
|
342
|
+
]
|
|
343
|
+
if frame.fp_registers is not None:
|
|
344
|
+
rows.extend(
|
|
345
|
+
DiagnosticTableRow(
|
|
346
|
+
(
|
|
347
|
+
f"s{index}",
|
|
348
|
+
f"0x{value:08X}",
|
|
349
|
+
(
|
|
350
|
+
f"s0 (float32) = {frame.s0_float:.7g}"
|
|
351
|
+
if index == 0 and frame.s0_float is not None
|
|
352
|
+
else ""
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
for index, value in enumerate(frame.fp_registers)
|
|
357
|
+
)
|
|
358
|
+
if frame.fpscr is not None:
|
|
359
|
+
rows.append(
|
|
360
|
+
DiagnosticTableRow(("fpscr", f"0x{frame.fpscr:08X}", "FPU Status & Control"))
|
|
361
|
+
)
|
|
362
|
+
return DiagnosticTable(
|
|
363
|
+
f"Stacked frame at crash time ({summary})",
|
|
364
|
+
("Register", "Stacked Value", "Details / Symbol"),
|
|
365
|
+
tuple(rows),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
@staticmethod
|
|
369
|
+
def _scb_table(scb: SystemRegisterSet) -> DiagnosticTable:
|
|
370
|
+
"""Build the SCB status table from typed architecture register results."""
|
|
371
|
+
get: Callable[[str], int | None] = lambda name: CortexMFaultCollector._scb_value(scb, name)
|
|
372
|
+
cfsr = get("CFSR")
|
|
373
|
+
rows: list[DiagnosticTableRow] = []
|
|
374
|
+
if cfsr is None:
|
|
375
|
+
rows.append(
|
|
376
|
+
DiagnosticTableRow(
|
|
377
|
+
("CFSR", "Unavailable", "Not present on this target or memory inaccessible")
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
mmfsr, bfsr, ufsr = cfsr & 0xFF, (cfsr >> 8) & 0xFF, (cfsr >> 16) & 0xFFFF
|
|
382
|
+
flags = [
|
|
383
|
+
f"{name}: {detail} (MemManage)"
|
|
384
|
+
for name, detail in _decode_flags(mmfsr, _MMFSR_BITS)
|
|
385
|
+
]
|
|
386
|
+
flags.extend(
|
|
387
|
+
f"{name}: {detail} (BusFault)" for name, detail in _decode_flags(bfsr, _BFSR_BITS)
|
|
388
|
+
)
|
|
389
|
+
flags.extend(
|
|
390
|
+
f"{name}: {detail} (UsageFault)" for name, detail in _decode_flags(ufsr, _UFSR_BITS)
|
|
391
|
+
)
|
|
392
|
+
rows.extend(
|
|
393
|
+
(
|
|
394
|
+
DiagnosticTableRow(
|
|
395
|
+
(
|
|
396
|
+
"CFSR",
|
|
397
|
+
f"0x{cfsr:08X}",
|
|
398
|
+
"\n".join(flags) if flags else "No active error flags",
|
|
399
|
+
)
|
|
400
|
+
),
|
|
401
|
+
DiagnosticTableRow(
|
|
402
|
+
(
|
|
403
|
+
" ├─ MMFSR",
|
|
404
|
+
f"0x{mmfsr:02X}",
|
|
405
|
+
f"{len(_decode_flags(mmfsr, _MMFSR_BITS))} active flag(s)",
|
|
406
|
+
)
|
|
407
|
+
),
|
|
408
|
+
DiagnosticTableRow(
|
|
409
|
+
(
|
|
410
|
+
" ├─ BFSR",
|
|
411
|
+
f"0x{bfsr:02X}",
|
|
412
|
+
f"{len(_decode_flags(bfsr, _BFSR_BITS))} active flag(s)",
|
|
413
|
+
)
|
|
414
|
+
),
|
|
415
|
+
DiagnosticTableRow(
|
|
416
|
+
(
|
|
417
|
+
" └─ UFSR",
|
|
418
|
+
f"0x{ufsr:04X}",
|
|
419
|
+
f"{len(_decode_flags(ufsr, _UFSR_BITS))} active flag(s)",
|
|
420
|
+
)
|
|
421
|
+
),
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
status_registers: tuple[tuple[str, dict[int, tuple[str, str]], str], ...] = (
|
|
425
|
+
("HFSR", _HFSR_BITS, "No active error flags"),
|
|
426
|
+
("DFSR", _DFSR_BITS, "No debug event"),
|
|
427
|
+
)
|
|
428
|
+
for name, table, empty in status_registers:
|
|
429
|
+
value = get(name)
|
|
430
|
+
if value is not None:
|
|
431
|
+
decoded_flags = _decode_flags(value, table)
|
|
432
|
+
rows.append(
|
|
433
|
+
DiagnosticTableRow(
|
|
434
|
+
(
|
|
435
|
+
name,
|
|
436
|
+
f"0x{value:08X}",
|
|
437
|
+
"\n".join(f"{key}: {text}" for key, text in decoded_flags) or empty,
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
mmfar, bfar = get("MMFAR"), get("BFAR")
|
|
442
|
+
if mmfar is not None:
|
|
443
|
+
rows.append(
|
|
444
|
+
DiagnosticTableRow(
|
|
445
|
+
(
|
|
446
|
+
"MMFAR",
|
|
447
|
+
f"0x{mmfar:08X}",
|
|
448
|
+
f"{'[VALID]' if cfsr is not None and cfsr & 0x80 else '[INVALID]'} "
|
|
449
|
+
f"Region: {_memory_region(mmfar)}",
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
if bfar is not None:
|
|
454
|
+
rows.append(
|
|
455
|
+
DiagnosticTableRow(
|
|
456
|
+
(
|
|
457
|
+
"BFAR",
|
|
458
|
+
f"0x{bfar:08X}",
|
|
459
|
+
f"{'[VALID]' if cfsr is not None and cfsr & (1 << 15) else '[INVALID]'} "
|
|
460
|
+
f"Region: {_memory_region(bfar)}",
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
shcsr = get("SHCSR")
|
|
465
|
+
if shcsr is not None:
|
|
466
|
+
enabled = [
|
|
467
|
+
name
|
|
468
|
+
for bit, name in (
|
|
469
|
+
(16, "MemManage"),
|
|
470
|
+
(17, "BusFault"),
|
|
471
|
+
(18, "UsageFault"),
|
|
472
|
+
(19, "SecureFault"),
|
|
473
|
+
)
|
|
474
|
+
if shcsr & (1 << bit)
|
|
475
|
+
]
|
|
476
|
+
rows.append(
|
|
477
|
+
DiagnosticTableRow(
|
|
478
|
+
(
|
|
479
|
+
"SHCSR",
|
|
480
|
+
f"0x{shcsr:08X}",
|
|
481
|
+
"Enabled configurable handlers: "
|
|
482
|
+
+ (
|
|
483
|
+
", ".join(enabled)
|
|
484
|
+
if enabled
|
|
485
|
+
else "None (direct escalation to HardFault)"
|
|
486
|
+
),
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
vtor = get("VTOR")
|
|
491
|
+
if vtor is not None:
|
|
492
|
+
rows.append(
|
|
493
|
+
DiagnosticTableRow(
|
|
494
|
+
("VTOR", f"0x{vtor:08X}", f"Vector table @ {_memory_region(vtor)}")
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
sfsr = get("SFSR")
|
|
498
|
+
if sfsr is not None and sfsr not in (0, 0xFFFFFFFF):
|
|
499
|
+
decoded_flags = _decode_flags(sfsr, _SFSR_BITS)
|
|
500
|
+
rows.append(
|
|
501
|
+
DiagnosticTableRow(
|
|
502
|
+
(
|
|
503
|
+
"SFSR",
|
|
504
|
+
f"0x{sfsr:08X}",
|
|
505
|
+
"\n".join(f"{key}: {text}" for key, text in decoded_flags),
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
sfar = get("SFAR")
|
|
510
|
+
if sfar is not None and sfsr & (1 << 6):
|
|
511
|
+
rows.append(
|
|
512
|
+
DiagnosticTableRow(
|
|
513
|
+
("SFAR", f"0x{sfar:08X}", f"[VALID] Region: {_memory_region(sfar)}")
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
return DiagnosticTable(
|
|
517
|
+
"SCB (System Control Block) Status Registers",
|
|
518
|
+
("Register", "Value", "Active Flags & Meaning"),
|
|
519
|
+
tuple(rows),
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
@staticmethod
|
|
523
|
+
def _diagnostic_panels(
|
|
524
|
+
scb: SystemRegisterSet,
|
|
525
|
+
frame: StackedFrame | str | None,
|
|
526
|
+
resolve: Callable[[int], str],
|
|
527
|
+
) -> tuple[DiagnosticPanel, ...]:
|
|
528
|
+
"""Derive ordered probable-cause lines from SCB fault information."""
|
|
529
|
+
get: Callable[[str], int | None] = lambda name: CortexMFaultCollector._scb_value(scb, name)
|
|
530
|
+
cfsr, hfsr = get("CFSR") or 0, get("HFSR") or 0
|
|
531
|
+
bfar, mmfar, cpacr = get("BFAR"), get("MMFAR"), get("CPACR")
|
|
532
|
+
mmfsr, bfsr, ufsr = cfsr & 0xFF, (cfsr >> 8) & 0xFF, (cfsr >> 16) & 0xFFFF
|
|
533
|
+
lines: list[str] = []
|
|
534
|
+
if hfsr & (1 << 30):
|
|
535
|
+
source = (
|
|
536
|
+
"BusFault"
|
|
537
|
+
if bfsr
|
|
538
|
+
else "MemManage Fault" if mmfsr else "An UsageFault" if ufsr else None
|
|
539
|
+
)
|
|
540
|
+
lines.append(
|
|
541
|
+
"• HardFault escalation: "
|
|
542
|
+
+ (
|
|
543
|
+
f"A {source} was forced to HardFault (source handler disabled in SHCSR)."
|
|
544
|
+
if source is not None
|
|
545
|
+
else "Forced HardFault without configurable cause bits (handler masked by PRIMASK/FAULTMASK)."
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
if bfsr & (1 << 1):
|
|
549
|
+
if bfsr & (1 << 7) and bfar is not None:
|
|
550
|
+
if bfar < 0x1000:
|
|
551
|
+
lines.append(
|
|
552
|
+
f"• NULL pointer dereference: Invalid memory access at 0x{bfar:08X} ({_memory_region(bfar)})."
|
|
553
|
+
)
|
|
554
|
+
elif 0x40000000 <= bfar < 0x60000000:
|
|
555
|
+
lines.append(
|
|
556
|
+
f"• Peripheral bus error (0x{bfar:08X}): Verify that the peripheral clock (RCC/PCLK) is enabled before any access."
|
|
557
|
+
)
|
|
558
|
+
else:
|
|
559
|
+
lines.append(
|
|
560
|
+
f"• Precise BusFault at 0x{bfar:08X}: Invalid memory access in {_memory_region(bfar)} region."
|
|
561
|
+
)
|
|
562
|
+
elif bfsr & (1 << 2):
|
|
563
|
+
lines.append(
|
|
564
|
+
"• Imprecise (asynchronous) BusFault: Caused by a write buffer. The stacked PC is downstream of the actual faulting access. To locate the exact access, temporarily disable write buffering (e.g. ACTLR.DISDEFWBUF)."
|
|
565
|
+
)
|
|
566
|
+
if bfsr & 1:
|
|
567
|
+
lines.append(
|
|
568
|
+
"• Instruction Fetch BusFault: Attempted execution from an invalid or inaccessible memory region (corrupted function pointer, overwritten vtable)."
|
|
569
|
+
)
|
|
570
|
+
if bfsr & (1 << 4):
|
|
571
|
+
lines.append(
|
|
572
|
+
"• Bus error during Stacking: The stack (MSP/PSP) overflowed or points to invalid memory."
|
|
573
|
+
)
|
|
574
|
+
if bfsr & (1 << 3):
|
|
575
|
+
lines.append("• Bus error during Unstacking: Corrupted stack upon exception return.")
|
|
576
|
+
if mmfsr & (1 << 1):
|
|
577
|
+
address = f" at 0x{mmfar:08X}" if mmfsr & (1 << 7) and mmfar is not None else ""
|
|
578
|
+
lines.append(
|
|
579
|
+
f"• MPU violation on data{address}: Access prohibited by MPU region permissions."
|
|
580
|
+
)
|
|
581
|
+
if mmfsr & 1:
|
|
582
|
+
lines.append(
|
|
583
|
+
"• MPU violation on instruction: Attempted execution in an MPU region marked eXecute-Never (XN)."
|
|
584
|
+
)
|
|
585
|
+
for bit, line in (
|
|
586
|
+
(
|
|
587
|
+
0,
|
|
588
|
+
"• Undefined instruction (UNDEFINSTR): Unknown or corrupted opcode (branching into data/NULL or incorrect instruction alignment).",
|
|
589
|
+
),
|
|
590
|
+
(
|
|
591
|
+
1,
|
|
592
|
+
"• Invalid Thumb state (INVSTATE): Branch to an even address (bit T=0). On Cortex-M, function pointers must have the least significant bit (LSB) set to 1.",
|
|
593
|
+
),
|
|
594
|
+
(
|
|
595
|
+
2,
|
|
596
|
+
"• Invalid EXC_RETURN (INVPC): Illegal exception return value loaded into PC (LR or stack corruption).",
|
|
597
|
+
),
|
|
598
|
+
(
|
|
599
|
+
4,
|
|
600
|
+
"• Hardware stack overflow (STKOF): Stack pointer exceeded configured limit (MSPLIM/PSPLIM).",
|
|
601
|
+
),
|
|
602
|
+
(8, "• Unaligned access (UNALIGNED): Unaligned access trapped by CCR.UNALIGN_TRP."),
|
|
603
|
+
(9, "• Divide by zero (DIVBYZERO): Integer division by zero trapped by CCR.DIV_0_TRP."),
|
|
604
|
+
):
|
|
605
|
+
if ufsr & (1 << bit):
|
|
606
|
+
lines.append(line)
|
|
607
|
+
if ufsr & (1 << 3):
|
|
608
|
+
lines.append(
|
|
609
|
+
"• Disabled FPU coprocessor (NOCP): FPU instruction executed while FPU is disabled. Add SCB->CPACR |= (0xF << 20); during initialization."
|
|
610
|
+
if cpacr is not None and cpacr & 0x00F00000 != 0x00F00000
|
|
611
|
+
else "• Unimplemented coprocessor access (NOCP): Access to a non-existent or unconfigured coprocessor."
|
|
612
|
+
)
|
|
613
|
+
if hfsr & (1 << 1):
|
|
614
|
+
lines.append(
|
|
615
|
+
"• Vector Table read error (VECTTBL): Vector table is unreadable. Verify VTOR register configuration and Flash memory."
|
|
616
|
+
)
|
|
617
|
+
if isinstance(frame, StackedFrame):
|
|
618
|
+
lines.append(
|
|
619
|
+
f"• Crash location: Instruction at 0x{frame.pc:08X} ({resolve(frame.pc)}), called from 0x{frame.lr:08X} ({resolve(frame.lr)})."
|
|
620
|
+
)
|
|
621
|
+
if not lines:
|
|
622
|
+
lines.append("• No obvious error conditions detected in SCB registers.")
|
|
623
|
+
return (DiagnosticPanel("Diagnostics & Probable Causes", tuple(lines)),)
|
|
624
|
+
|
|
625
|
+
@staticmethod
|
|
626
|
+
def _scb_value(scb: SystemRegisterSet, name: str) -> int | None:
|
|
627
|
+
"""Return the available value of a typed SCB register."""
|
|
628
|
+
register = scb.get(name)
|
|
629
|
+
return register.value if register is not None else None
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Arm target-report models shared by device providers and renderers."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from .coresight import CoreSightDiscovery
|
|
11
|
+
from .cortex_m import CortexMTargetDescription
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class FieldValue:
|
|
16
|
+
"""A report field that is either known or explicitly unavailable."""
|
|
17
|
+
|
|
18
|
+
value: str | None = None
|
|
19
|
+
unavailable_reason: str | None = None
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
"""Ensure that a field has exactly one state."""
|
|
23
|
+
if (self.value is None) == (self.unavailable_reason is None):
|
|
24
|
+
raise ValueError("a field must have either a value or an unavailable reason")
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def known(cls, value: str) -> FieldValue:
|
|
28
|
+
"""Create a known report field."""
|
|
29
|
+
return cls(value=value)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def unavailable(cls, reason: str) -> FieldValue:
|
|
33
|
+
"""Create a field with an explicit unavailable-state reason."""
|
|
34
|
+
return cls(unavailable_reason=reason)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_available(self) -> bool:
|
|
38
|
+
"""Whether this field holds a known value."""
|
|
39
|
+
return self.value is not None
|
|
40
|
+
|
|
41
|
+
def display(self) -> str:
|
|
42
|
+
"""Return the report-ready field text."""
|
|
43
|
+
if self.value is not None:
|
|
44
|
+
return self.value
|
|
45
|
+
return f"Unavailable: {self.unavailable_reason}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class DeviceReport:
|
|
50
|
+
"""Arm CPU and optional vendor device information for ``lscpu``."""
|
|
51
|
+
|
|
52
|
+
target: CortexMTargetDescription
|
|
53
|
+
discovery: CoreSightDiscovery
|
|
54
|
+
vendor: str
|
|
55
|
+
product_line: FieldValue
|
|
56
|
+
part_number: FieldValue
|
|
57
|
+
ram: FieldValue
|
|
58
|
+
flash: FieldValue
|
|
59
|
+
package: FieldValue
|
|
60
|
+
serial_number: FieldValue
|