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,1402 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""The ``profile`` GDB command for firmware peripheral usage analysis."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
import glob
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import gdb
|
|
17
|
+
from rich import box
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
from rich.table import Table
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
|
|
23
|
+
from .arch.arm.session_state import device_report
|
|
24
|
+
from .cmd_svd import SESSION as SVD_SESSION
|
|
25
|
+
from .cmd_svd import is_svd_loaded
|
|
26
|
+
from .session import SESSION as TOOLKIT_SESSION
|
|
27
|
+
from .session import SessionSlice
|
|
28
|
+
from .svd import (
|
|
29
|
+
SvdDevice,
|
|
30
|
+
SvdError,
|
|
31
|
+
SvdPeripheral,
|
|
32
|
+
SvdRegister,
|
|
33
|
+
parse_svd_file,
|
|
34
|
+
resolve_and_fetch_svd,
|
|
35
|
+
)
|
|
36
|
+
from .target_memory import TargetMemory, TargetReadError
|
|
37
|
+
|
|
38
|
+
CONSOLE = Console(force_terminal=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class RegisterUsage:
|
|
43
|
+
"""Detailed hit statistics for a specific peripheral register."""
|
|
44
|
+
|
|
45
|
+
name: str
|
|
46
|
+
address_offset: int
|
|
47
|
+
address: int
|
|
48
|
+
hit_count: int
|
|
49
|
+
hit_addresses: tuple[int, ...]
|
|
50
|
+
reference_types: tuple[str, ...]
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
"""Convert the register usage model to a plain dictionary.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
dict[str, Any]
|
|
58
|
+
Dictionary representation of register usage metrics.
|
|
59
|
+
"""
|
|
60
|
+
return {
|
|
61
|
+
"name": self.name,
|
|
62
|
+
"address_offset": f"0x{self.address_offset:04X}",
|
|
63
|
+
"address": f"0x{self.address:08X}",
|
|
64
|
+
"hit_count": self.hit_count,
|
|
65
|
+
"hit_addresses": [f"0x{addr:08X}" for addr in self.hit_addresses],
|
|
66
|
+
"reference_types": list(self.reference_types),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class PeripheralUsage:
|
|
72
|
+
"""Detailed hit statistics for a peripheral."""
|
|
73
|
+
|
|
74
|
+
name: str
|
|
75
|
+
base_address: int
|
|
76
|
+
group_name: str | None
|
|
77
|
+
description: str
|
|
78
|
+
hit_count: int
|
|
79
|
+
total_registers: int
|
|
80
|
+
used_registers_count: int
|
|
81
|
+
registers: tuple[RegisterUsage, ...]
|
|
82
|
+
unnamed_offset_hits: int = 0
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_active(self) -> bool:
|
|
86
|
+
"""Return whether the peripheral has at least one recorded hit.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
bool
|
|
91
|
+
True if peripheral was referenced, False otherwise.
|
|
92
|
+
"""
|
|
93
|
+
return self.hit_count > 0
|
|
94
|
+
|
|
95
|
+
def to_dict(self) -> dict[str, Any]:
|
|
96
|
+
"""Convert the peripheral usage model to a plain dictionary.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
dict[str, Any]
|
|
101
|
+
Dictionary representation of peripheral usage metrics.
|
|
102
|
+
"""
|
|
103
|
+
return {
|
|
104
|
+
"name": self.name,
|
|
105
|
+
"base_address": f"0x{self.base_address:08X}",
|
|
106
|
+
"group_name": self.group_name,
|
|
107
|
+
"description": self.description,
|
|
108
|
+
"hit_count": self.hit_count,
|
|
109
|
+
"total_registers": self.total_registers,
|
|
110
|
+
"used_registers_count": self.used_registers_count,
|
|
111
|
+
"registers": {r.name: r.to_dict() for r in self.registers},
|
|
112
|
+
"unnamed_offset_hits": self.unnamed_offset_hits,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class ProfileReport:
|
|
118
|
+
"""Comprehensive profiling report generated by scanning target memory."""
|
|
119
|
+
|
|
120
|
+
device_name: str
|
|
121
|
+
vendor: str | None
|
|
122
|
+
region_name: str
|
|
123
|
+
start_address: int
|
|
124
|
+
scanned_bytes: int
|
|
125
|
+
total_words_scanned: int
|
|
126
|
+
elapsed_seconds: float
|
|
127
|
+
total_peripherals: int
|
|
128
|
+
active_peripherals_count: int
|
|
129
|
+
peripherals: tuple[PeripheralUsage, ...]
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def inactive_peripherals_count(self) -> int:
|
|
133
|
+
"""Return the number of inactive peripherals.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
int
|
|
138
|
+
Inactive peripheral count.
|
|
139
|
+
"""
|
|
140
|
+
return self.total_peripherals - self.active_peripherals_count
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def active_ratio_pct(self) -> float:
|
|
144
|
+
"""Return the percentage of active peripherals.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
float
|
|
149
|
+
Percentage of active peripherals.
|
|
150
|
+
"""
|
|
151
|
+
if self.total_peripherals == 0:
|
|
152
|
+
return 0.0
|
|
153
|
+
return (self.active_peripherals_count / self.total_peripherals) * 100.0
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def active_peripherals(self) -> list[PeripheralUsage]:
|
|
157
|
+
"""Return all active peripherals ordered by hit count descending.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
list[PeripheralUsage]
|
|
162
|
+
List of active peripheral usage objects.
|
|
163
|
+
"""
|
|
164
|
+
return [p for p in self.peripherals if p.is_active]
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def inactive_peripherals(self) -> list[PeripheralUsage]:
|
|
168
|
+
"""Return all inactive peripherals ordered by name.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
list[PeripheralUsage]
|
|
173
|
+
List of inactive peripheral usage objects.
|
|
174
|
+
"""
|
|
175
|
+
return [p for p in self.peripherals if not p.is_active]
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> dict[str, Any]:
|
|
178
|
+
"""Convert the complete profile report to a nested dictionary.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
dict[str, Any]
|
|
183
|
+
Dictionary representation of the profile report.
|
|
184
|
+
"""
|
|
185
|
+
return {
|
|
186
|
+
"device": self.device_name,
|
|
187
|
+
"vendor": self.vendor,
|
|
188
|
+
"region": self.region_name,
|
|
189
|
+
"start_address": f"0x{self.start_address:08X}",
|
|
190
|
+
"end_address": f"0x{(self.start_address + self.scanned_bytes):08X}",
|
|
191
|
+
"scanned_bytes": self.scanned_bytes,
|
|
192
|
+
"total_words_scanned": self.total_words_scanned,
|
|
193
|
+
"elapsed_seconds": round(self.elapsed_seconds, 4),
|
|
194
|
+
"summary": {
|
|
195
|
+
"total_peripherals": self.total_peripherals,
|
|
196
|
+
"active_peripherals": self.active_peripherals_count,
|
|
197
|
+
"inactive_peripherals": self.inactive_peripherals_count,
|
|
198
|
+
"active_ratio_pct": round(self.active_ratio_pct, 2),
|
|
199
|
+
},
|
|
200
|
+
"active_peripherals": [p.to_dict() for p in self.active_peripherals],
|
|
201
|
+
"inactive_peripherals": [p.name for p in self.inactive_peripherals],
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class ProfileSessionState(SessionSlice):
|
|
207
|
+
"""Store accumulated profiling state, reports, and active peripherals across the GDB session."""
|
|
208
|
+
|
|
209
|
+
device_name: str | None = None
|
|
210
|
+
vendor: str | None = None
|
|
211
|
+
reports: list[ProfileReport] = field(default_factory=list)
|
|
212
|
+
active_peripherals: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
213
|
+
scanned_regions: list[dict[str, Any]] = field(default_factory=list)
|
|
214
|
+
|
|
215
|
+
def reset(self) -> None:
|
|
216
|
+
"""Drop every accumulated profiling result."""
|
|
217
|
+
self.device_name = None
|
|
218
|
+
self.vendor = None
|
|
219
|
+
self.reports.clear()
|
|
220
|
+
self.active_peripherals.clear()
|
|
221
|
+
self.scanned_regions.clear()
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def last_report(self) -> ProfileReport | None:
|
|
225
|
+
"""Return the most recent profiling report.
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
ProfileReport | None
|
|
230
|
+
Latest report or None.
|
|
231
|
+
"""
|
|
232
|
+
return self.reports[-1] if self.reports else None
|
|
233
|
+
|
|
234
|
+
def record_report(self, report: ProfileReport) -> None:
|
|
235
|
+
"""Accumulate a new profiling report into the session state.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
report : ProfileReport
|
|
240
|
+
New profiling report to merge.
|
|
241
|
+
"""
|
|
242
|
+
self.device_name = report.device_name
|
|
243
|
+
self.vendor = report.vendor
|
|
244
|
+
self.reports.append(report)
|
|
245
|
+
self.scanned_regions.append(
|
|
246
|
+
{
|
|
247
|
+
"region": report.region_name,
|
|
248
|
+
"start_address": f"0x{report.start_address:08X}",
|
|
249
|
+
"end_address": f"0x{(report.start_address + report.scanned_bytes):08X}",
|
|
250
|
+
"scanned_bytes": report.scanned_bytes,
|
|
251
|
+
"words_scanned": report.total_words_scanned,
|
|
252
|
+
"elapsed_seconds": round(report.elapsed_seconds, 4),
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
for periph in report.active_peripherals:
|
|
257
|
+
p_name = periph.name
|
|
258
|
+
if p_name not in self.active_peripherals:
|
|
259
|
+
self.active_peripherals[p_name] = {
|
|
260
|
+
"name": periph.name,
|
|
261
|
+
"base_address": f"0x{periph.base_address:08X}",
|
|
262
|
+
"base_address_int": periph.base_address,
|
|
263
|
+
"group_name": periph.group_name,
|
|
264
|
+
"description": periph.description,
|
|
265
|
+
"hit_count": 0,
|
|
266
|
+
"total_registers": periph.total_registers,
|
|
267
|
+
"regions": set(),
|
|
268
|
+
"registers": {},
|
|
269
|
+
"unnamed_offset_hits": 0,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
p_entry = self.active_peripherals[p_name]
|
|
273
|
+
p_entry["hit_count"] += periph.hit_count
|
|
274
|
+
p_entry["regions"].add(report.region_name.upper())
|
|
275
|
+
p_entry["unnamed_offset_hits"] += periph.unnamed_offset_hits
|
|
276
|
+
|
|
277
|
+
for reg in periph.registers:
|
|
278
|
+
if reg.hit_count == 0:
|
|
279
|
+
continue
|
|
280
|
+
r_name = reg.name
|
|
281
|
+
if r_name not in p_entry["registers"]:
|
|
282
|
+
p_entry["registers"][r_name] = {
|
|
283
|
+
"name": reg.name,
|
|
284
|
+
"address_offset": f"0x{reg.address_offset:04X}",
|
|
285
|
+
"address": f"0x{reg.address:08X}",
|
|
286
|
+
"hit_count": 0,
|
|
287
|
+
"hit_addresses": set(),
|
|
288
|
+
"reference_types": set(),
|
|
289
|
+
"regions": set(),
|
|
290
|
+
}
|
|
291
|
+
r_entry = p_entry["registers"][r_name]
|
|
292
|
+
r_entry["hit_count"] += reg.hit_count
|
|
293
|
+
r_entry["hit_addresses"].update(reg.hit_addresses)
|
|
294
|
+
r_entry["reference_types"].update(reg.reference_types)
|
|
295
|
+
r_entry["regions"].add(report.region_name.upper())
|
|
296
|
+
|
|
297
|
+
def to_dict(self) -> dict[str, Any]:
|
|
298
|
+
"""Convert the accumulated session profiling findings to a dictionary.
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
dict[str, Any]
|
|
303
|
+
Dictionary representation of all accumulated findings.
|
|
304
|
+
"""
|
|
305
|
+
active_list: list[dict[str, Any]] = []
|
|
306
|
+
for _, p_data in sorted(
|
|
307
|
+
self.active_peripherals.items(),
|
|
308
|
+
key=lambda item: int(item[1]["hit_count"]),
|
|
309
|
+
reverse=True,
|
|
310
|
+
):
|
|
311
|
+
regs_dict: dict[str, Any] = {}
|
|
312
|
+
for r_name, r_data in p_data["registers"].items():
|
|
313
|
+
regs_dict[r_name] = {
|
|
314
|
+
"name": r_data["name"],
|
|
315
|
+
"address_offset": r_data["address_offset"],
|
|
316
|
+
"address": r_data["address"],
|
|
317
|
+
"hit_count": r_data["hit_count"],
|
|
318
|
+
"hit_addresses": [f"0x{a:08X}" for a in sorted(r_data["hit_addresses"])],
|
|
319
|
+
"reference_types": sorted(r_data["reference_types"]),
|
|
320
|
+
"regions": sorted(r_data["regions"]),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
active_list.append(
|
|
324
|
+
{
|
|
325
|
+
"name": p_data["name"],
|
|
326
|
+
"base_address": p_data["base_address"],
|
|
327
|
+
"group_name": p_data["group_name"],
|
|
328
|
+
"description": p_data["description"],
|
|
329
|
+
"total_hits": p_data["hit_count"],
|
|
330
|
+
"total_registers": p_data["total_registers"],
|
|
331
|
+
"active_registers_count": len(regs_dict),
|
|
332
|
+
"regions": sorted(p_data["regions"]),
|
|
333
|
+
"registers": regs_dict,
|
|
334
|
+
"unnamed_offset_hits": p_data["unnamed_offset_hits"],
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
"device": self.device_name,
|
|
340
|
+
"vendor": self.vendor,
|
|
341
|
+
"scanned_regions": self.scanned_regions,
|
|
342
|
+
"total_active_peripherals": len(self.active_peripherals),
|
|
343
|
+
"active_peripherals": active_list,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
PROFILE_SESSION = TOOLKIT_SESSION.state(ProfileSessionState)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def resolve_memory_region(device: SvdDevice, region: str) -> tuple[int, int]:
|
|
351
|
+
"""Determine the default base address and size for a memory region from SVD and architecture.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
device : SvdDevice
|
|
356
|
+
Loaded SVD device definition.
|
|
357
|
+
region : str
|
|
358
|
+
Target memory region name ('flash' or 'sram').
|
|
359
|
+
|
|
360
|
+
Returns
|
|
361
|
+
-------
|
|
362
|
+
tuple[int, int]
|
|
363
|
+
Tuple of (start_address, default_size_in_bytes).
|
|
364
|
+
"""
|
|
365
|
+
region_norm = region.strip().lower()
|
|
366
|
+
vendor = (device.vendor or "").strip().lower()
|
|
367
|
+
|
|
368
|
+
if region_norm == "flash":
|
|
369
|
+
for periph in device.peripherals:
|
|
370
|
+
p_name = periph.name.upper()
|
|
371
|
+
if periph.base_address < 0x20000000 and any(
|
|
372
|
+
k in p_name for k in ("FLASH", "ROM", "MEM", "NVMC")
|
|
373
|
+
):
|
|
374
|
+
return periph.base_address, 0x80000
|
|
375
|
+
|
|
376
|
+
if "stmicro" in vendor or "stm" in vendor:
|
|
377
|
+
return 0x08000000, 0x80000
|
|
378
|
+
if "raspberry" in vendor:
|
|
379
|
+
return 0x10000000, 0x200000
|
|
380
|
+
if "microchip" in vendor or "atmel" in vendor:
|
|
381
|
+
return 0x00400000, 0x80000
|
|
382
|
+
|
|
383
|
+
return 0x00000000, 0x80000
|
|
384
|
+
|
|
385
|
+
if region_norm in ("sram", "ram"):
|
|
386
|
+
for periph in device.peripherals:
|
|
387
|
+
p_name = periph.name.upper()
|
|
388
|
+
if 0x20000000 <= periph.base_address < 0x40000000 and any(
|
|
389
|
+
k in p_name for k in ("RAM", "SRAM", "AXISRAM", "DTCM")
|
|
390
|
+
):
|
|
391
|
+
return periph.base_address, 0x20000
|
|
392
|
+
|
|
393
|
+
return 0x20000000, 0x20000
|
|
394
|
+
|
|
395
|
+
return 0x00000000, 0x10000
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _decode_thumb_movw_movt(chunk: bytes, offset: int) -> tuple[int, int] | None:
|
|
399
|
+
"""Decode a Thumb-2 MOVW or MOVT 32-bit instruction at a given offset.
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
chunk : bytes
|
|
404
|
+
Memory buffer.
|
|
405
|
+
offset : int
|
|
406
|
+
Byte offset in buffer.
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
tuple[int, int] | None
|
|
411
|
+
Tuple of (destination_register_id, imm16_value) or None if not matching.
|
|
412
|
+
"""
|
|
413
|
+
if offset + 4 > len(chunk):
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
hw1 = int.from_bytes(chunk[offset : offset + 2], byteorder="little")
|
|
417
|
+
hw2 = int.from_bytes(chunk[offset + 2 : offset + 4], byteorder="little")
|
|
418
|
+
|
|
419
|
+
is_movw = (hw1 & 0xFBF0) == 0xF240 and (hw2 & 0x8000) == 0
|
|
420
|
+
is_movt = (hw1 & 0xFBF0) == 0xF2C0 and (hw2 & 0x8000) == 0
|
|
421
|
+
|
|
422
|
+
if not (is_movw or is_movt):
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
i_bit = (hw1 >> 10) & 1
|
|
426
|
+
imm4 = hw1 & 0x000F
|
|
427
|
+
imm3 = (hw2 >> 12) & 0x07
|
|
428
|
+
rd = (hw2 >> 8) & 0x0F
|
|
429
|
+
imm8 = hw2 & 0x00FF
|
|
430
|
+
|
|
431
|
+
imm16 = (imm4 << 12) | (i_bit << 11) | (imm3 << 8) | imm8
|
|
432
|
+
kind = 1 if is_movt else 0
|
|
433
|
+
return rd, (kind << 16) | imm16
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _decode_arm_movw_movt(word: int) -> tuple[int, int, int] | None:
|
|
437
|
+
"""Decode an ARM 32-bit MOVW or MOVT instruction.
|
|
438
|
+
|
|
439
|
+
Parameters
|
|
440
|
+
----------
|
|
441
|
+
word : int
|
|
442
|
+
Raw 32-bit ARM instruction word.
|
|
443
|
+
|
|
444
|
+
Returns
|
|
445
|
+
-------
|
|
446
|
+
tuple[int, int, int] | None
|
|
447
|
+
Tuple of (is_movt_flag, destination_register_id, imm16_value) or None.
|
|
448
|
+
"""
|
|
449
|
+
if (word & 0x0FF00000) == 0x03000000:
|
|
450
|
+
imm4 = (word >> 16) & 0x0F
|
|
451
|
+
rd = (word >> 12) & 0x0F
|
|
452
|
+
imm12 = word & 0x0FFF
|
|
453
|
+
imm16 = (imm4 << 12) | imm12
|
|
454
|
+
return 0, rd, imm16
|
|
455
|
+
|
|
456
|
+
if (word & 0x0FF00000) == 0x03400000:
|
|
457
|
+
imm4 = (word >> 16) & 0x0F
|
|
458
|
+
rd = (word >> 12) & 0x0F
|
|
459
|
+
imm12 = word & 0x0FFF
|
|
460
|
+
imm16 = (imm4 << 12) | imm12
|
|
461
|
+
return 1, rd, imm16
|
|
462
|
+
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def analyze_memory_profile(
|
|
467
|
+
device: SvdDevice,
|
|
468
|
+
reader: TargetMemory,
|
|
469
|
+
region_name: str,
|
|
470
|
+
start_address: int,
|
|
471
|
+
size: int,
|
|
472
|
+
chunk_size: int = 4096,
|
|
473
|
+
) -> ProfileReport:
|
|
474
|
+
"""Scan a target memory region to identify active peripherals and register manipulations.
|
|
475
|
+
|
|
476
|
+
Parameters
|
|
477
|
+
----------
|
|
478
|
+
device : SvdDevice
|
|
479
|
+
Loaded SVD device definition.
|
|
480
|
+
reader : TargetMemory
|
|
481
|
+
Target memory reader interface.
|
|
482
|
+
region_name : str
|
|
483
|
+
Memory region identifier ('flash' or 'sram').
|
|
484
|
+
start_address : int
|
|
485
|
+
Starting physical address to scan.
|
|
486
|
+
size : int
|
|
487
|
+
Total number of bytes to scan.
|
|
488
|
+
chunk_size : int
|
|
489
|
+
Chunk size in bytes for batched memory reads.
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
ProfileReport
|
|
494
|
+
Detailed profiling report.
|
|
495
|
+
"""
|
|
496
|
+
start_time = time.time()
|
|
497
|
+
|
|
498
|
+
exact_registers: dict[int, tuple[SvdPeripheral, SvdRegister]] = {}
|
|
499
|
+
peripheral_bases: dict[int, SvdPeripheral] = {}
|
|
500
|
+
peripheral_ranges: list[tuple[int, int, SvdPeripheral]] = []
|
|
501
|
+
|
|
502
|
+
for periph in device.peripherals:
|
|
503
|
+
peripheral_bases[periph.base_address] = periph
|
|
504
|
+
max_offset = 0
|
|
505
|
+
for reg in periph.registers:
|
|
506
|
+
addr = periph.base_address + reg.address_offset
|
|
507
|
+
exact_registers[addr] = (periph, reg)
|
|
508
|
+
if reg.address_offset > max_offset:
|
|
509
|
+
max_offset = reg.address_offset
|
|
510
|
+
|
|
511
|
+
span = max(0x400, max_offset + 4)
|
|
512
|
+
peripheral_ranges.append((periph.base_address, periph.base_address + span, periph))
|
|
513
|
+
|
|
514
|
+
periph_hits: dict[str, dict[str, Any]] = {}
|
|
515
|
+
for periph in device.peripherals:
|
|
516
|
+
reg_map: dict[str, dict[str, Any]] = {}
|
|
517
|
+
for r in periph.registers:
|
|
518
|
+
reg_map[r.name] = {
|
|
519
|
+
"name": r.name,
|
|
520
|
+
"address_offset": r.address_offset,
|
|
521
|
+
"address": periph.base_address + r.address_offset,
|
|
522
|
+
"hits": [],
|
|
523
|
+
"ref_types": set(),
|
|
524
|
+
}
|
|
525
|
+
periph_hits[periph.name] = {
|
|
526
|
+
"periph": periph,
|
|
527
|
+
"total_hits": 0,
|
|
528
|
+
"registers": reg_map,
|
|
529
|
+
"unnamed_hits": 0,
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
scanned_bytes = 0
|
|
533
|
+
words_scanned = 0
|
|
534
|
+
current_addr = start_address
|
|
535
|
+
end_address = start_address + size
|
|
536
|
+
|
|
537
|
+
while current_addr < end_address:
|
|
538
|
+
read_len = min(chunk_size, end_address - current_addr)
|
|
539
|
+
try:
|
|
540
|
+
chunk = reader.read_bytes(current_addr, read_len)
|
|
541
|
+
except (TargetReadError, ValueError):
|
|
542
|
+
break
|
|
543
|
+
|
|
544
|
+
if not chunk:
|
|
545
|
+
break
|
|
546
|
+
|
|
547
|
+
chunk_len = len(chunk)
|
|
548
|
+
scanned_bytes += chunk_len
|
|
549
|
+
words_scanned += chunk_len // 4
|
|
550
|
+
|
|
551
|
+
# 1. Scan 32-bit aligned words (Literal Pools & Pointer tables)
|
|
552
|
+
for offset in range(0, chunk_len - 3, 4):
|
|
553
|
+
word_val = int.from_bytes(chunk[offset : offset + 4], byteorder="little")
|
|
554
|
+
hit_addr = current_addr + offset
|
|
555
|
+
|
|
556
|
+
if word_val in exact_registers:
|
|
557
|
+
p, r = exact_registers[word_val]
|
|
558
|
+
p_data = periph_hits[p.name]
|
|
559
|
+
p_data["total_hits"] += 1
|
|
560
|
+
r_entry = p_data["registers"][r.name]
|
|
561
|
+
r_entry["hits"].append(hit_addr)
|
|
562
|
+
r_entry["ref_types"].add("literal_pointer")
|
|
563
|
+
elif word_val in peripheral_bases:
|
|
564
|
+
p = peripheral_bases[word_val]
|
|
565
|
+
p_data = periph_hits[p.name]
|
|
566
|
+
p_data["total_hits"] += 1
|
|
567
|
+
if p.registers:
|
|
568
|
+
first_reg = p.registers[0]
|
|
569
|
+
r_entry = p_data["registers"][first_reg.name]
|
|
570
|
+
r_entry["hits"].append(hit_addr)
|
|
571
|
+
r_entry["ref_types"].add("literal_base_pointer")
|
|
572
|
+
else:
|
|
573
|
+
p_data["unnamed_hits"] += 1
|
|
574
|
+
|
|
575
|
+
# 2. Scan Thumb-2 MOVW + MOVT pairs (16-bit aligned)
|
|
576
|
+
for offset in range(0, chunk_len - 7, 2):
|
|
577
|
+
res1 = _decode_thumb_movw_movt(chunk, offset)
|
|
578
|
+
if res1 is not None and (res1[1] >> 16) == 0:
|
|
579
|
+
rd1, low_imm16 = res1[0], res1[1] & 0xFFFF
|
|
580
|
+
res2 = _decode_thumb_movw_movt(chunk, offset + 4)
|
|
581
|
+
if res2 is not None and (res2[1] >> 16) == 1 and res2[0] == rd1:
|
|
582
|
+
high_imm16 = res2[1] & 0xFFFF
|
|
583
|
+
target_addr = (high_imm16 << 16) | low_imm16
|
|
584
|
+
hit_addr = current_addr + offset
|
|
585
|
+
|
|
586
|
+
if target_addr in exact_registers:
|
|
587
|
+
p, r = exact_registers[target_addr]
|
|
588
|
+
p_data = periph_hits[p.name]
|
|
589
|
+
p_data["total_hits"] += 1
|
|
590
|
+
r_entry = p_data["registers"][r.name]
|
|
591
|
+
r_entry["hits"].append(hit_addr)
|
|
592
|
+
r_entry["ref_types"].add("thumb_movw_movt")
|
|
593
|
+
elif target_addr in peripheral_bases:
|
|
594
|
+
p = peripheral_bases[target_addr]
|
|
595
|
+
p_data = periph_hits[p.name]
|
|
596
|
+
p_data["total_hits"] += 1
|
|
597
|
+
if p.registers:
|
|
598
|
+
first_reg = p.registers[0]
|
|
599
|
+
r_entry = p_data["registers"][first_reg.name]
|
|
600
|
+
r_entry["hits"].append(hit_addr)
|
|
601
|
+
r_entry["ref_types"].add("thumb_movw_movt_base")
|
|
602
|
+
else:
|
|
603
|
+
p_data["unnamed_hits"] += 1
|
|
604
|
+
|
|
605
|
+
# 3. Scan ARM 32-bit MOVW + MOVT pairs (32-bit aligned)
|
|
606
|
+
for offset in range(0, chunk_len - 7, 4):
|
|
607
|
+
w1 = int.from_bytes(chunk[offset : offset + 4], byteorder="little")
|
|
608
|
+
arm_dec1 = _decode_arm_movw_movt(w1)
|
|
609
|
+
if arm_dec1 is not None and arm_dec1[0] == 0:
|
|
610
|
+
rd1, low_imm16 = arm_dec1[1], arm_dec1[2]
|
|
611
|
+
w2 = int.from_bytes(chunk[offset + 4 : offset + 8], byteorder="little")
|
|
612
|
+
arm_dec2 = _decode_arm_movw_movt(w2)
|
|
613
|
+
if arm_dec2 is not None and arm_dec2[0] == 1 and arm_dec2[1] == rd1:
|
|
614
|
+
high_imm16 = arm_dec2[2]
|
|
615
|
+
target_addr = (high_imm16 << 16) | low_imm16
|
|
616
|
+
hit_addr = current_addr + offset
|
|
617
|
+
|
|
618
|
+
if target_addr in exact_registers:
|
|
619
|
+
p, r = exact_registers[target_addr]
|
|
620
|
+
p_data = periph_hits[p.name]
|
|
621
|
+
p_data["total_hits"] += 1
|
|
622
|
+
r_entry = p_data["registers"][r.name]
|
|
623
|
+
r_entry["hits"].append(hit_addr)
|
|
624
|
+
r_entry["ref_types"].add("arm_movw_movt")
|
|
625
|
+
elif target_addr in peripheral_bases:
|
|
626
|
+
p = peripheral_bases[target_addr]
|
|
627
|
+
p_data = periph_hits[p.name]
|
|
628
|
+
p_data["total_hits"] += 1
|
|
629
|
+
if p.registers:
|
|
630
|
+
first_reg = p.registers[0]
|
|
631
|
+
r_entry = p_data["registers"][first_reg.name]
|
|
632
|
+
r_entry["hits"].append(hit_addr)
|
|
633
|
+
r_entry["ref_types"].add("arm_movw_movt_base")
|
|
634
|
+
else:
|
|
635
|
+
p_data["unnamed_hits"] += 1
|
|
636
|
+
|
|
637
|
+
current_addr += chunk_len
|
|
638
|
+
if chunk_len < read_len:
|
|
639
|
+
break
|
|
640
|
+
|
|
641
|
+
elapsed = time.time() - start_time
|
|
642
|
+
|
|
643
|
+
periph_usages: list[PeripheralUsage] = []
|
|
644
|
+
active_count = 0
|
|
645
|
+
|
|
646
|
+
for periph in device.peripherals:
|
|
647
|
+
p_data = periph_hits[periph.name]
|
|
648
|
+
reg_usages: list[RegisterUsage] = []
|
|
649
|
+
active_regs = 0
|
|
650
|
+
|
|
651
|
+
for r in periph.registers:
|
|
652
|
+
r_info = p_data["registers"][r.name]
|
|
653
|
+
h_count = len(r_info["hits"])
|
|
654
|
+
if h_count > 0:
|
|
655
|
+
active_regs += 1
|
|
656
|
+
reg_usages.append(
|
|
657
|
+
RegisterUsage(
|
|
658
|
+
name=r.name,
|
|
659
|
+
address_offset=r_info["address_offset"],
|
|
660
|
+
address=r_info["address"],
|
|
661
|
+
hit_count=h_count,
|
|
662
|
+
hit_addresses=tuple(r_info["hits"]),
|
|
663
|
+
reference_types=tuple(sorted(r_info["ref_types"])),
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
p_hit_count = p_data["total_hits"]
|
|
668
|
+
if p_hit_count > 0:
|
|
669
|
+
active_count += 1
|
|
670
|
+
|
|
671
|
+
periph_usages.append(
|
|
672
|
+
PeripheralUsage(
|
|
673
|
+
name=periph.name,
|
|
674
|
+
base_address=periph.base_address,
|
|
675
|
+
group_name=periph.group_name,
|
|
676
|
+
description=periph.description,
|
|
677
|
+
hit_count=p_hit_count,
|
|
678
|
+
total_registers=len(periph.registers),
|
|
679
|
+
used_registers_count=active_regs,
|
|
680
|
+
registers=tuple(reg_usages),
|
|
681
|
+
unnamed_offset_hits=p_data["unnamed_hits"],
|
|
682
|
+
)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
periph_usages.sort(key=lambda pu: pu.hit_count, reverse=True)
|
|
686
|
+
|
|
687
|
+
return ProfileReport(
|
|
688
|
+
device_name=device.name,
|
|
689
|
+
vendor=device.vendor,
|
|
690
|
+
region_name=region_name.lower(),
|
|
691
|
+
start_address=start_address,
|
|
692
|
+
scanned_bytes=scanned_bytes,
|
|
693
|
+
total_words_scanned=words_scanned,
|
|
694
|
+
elapsed_seconds=elapsed,
|
|
695
|
+
total_peripherals=len(device.peripherals),
|
|
696
|
+
active_peripherals_count=active_count,
|
|
697
|
+
peripherals=tuple(periph_usages),
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def render_profile_report(report: ProfileReport) -> None:
|
|
702
|
+
"""Render a comprehensive profiling analysis report using Rich.
|
|
703
|
+
|
|
704
|
+
Parameters
|
|
705
|
+
----------
|
|
706
|
+
report : ProfileReport
|
|
707
|
+
The completed profiling report.
|
|
708
|
+
"""
|
|
709
|
+
kb_scanned = report.scanned_bytes / 1024.0
|
|
710
|
+
end_addr = report.start_address + report.scanned_bytes
|
|
711
|
+
|
|
712
|
+
summary_table = Table(
|
|
713
|
+
box=box.SIMPLE_HEAVY,
|
|
714
|
+
header_style="bold cyan",
|
|
715
|
+
show_header=False,
|
|
716
|
+
)
|
|
717
|
+
summary_table.add_column("Key", style="bold")
|
|
718
|
+
summary_table.add_column("Value")
|
|
719
|
+
|
|
720
|
+
summary_table.add_row("Device / SoC", f"{report.device_name} ({report.vendor or 'Generic'})")
|
|
721
|
+
summary_table.add_row(
|
|
722
|
+
"Scanned Region",
|
|
723
|
+
f"{report.region_name.upper()} @ 0x{report.start_address:08X} - 0x{end_addr:08X} "
|
|
724
|
+
f"({kb_scanned:.1f} KiB)",
|
|
725
|
+
)
|
|
726
|
+
summary_table.add_row(
|
|
727
|
+
"Active Peripherals",
|
|
728
|
+
Text(
|
|
729
|
+
f"{report.active_peripherals_count} / {report.total_peripherals} "
|
|
730
|
+
f"({report.active_ratio_pct:.1f}%)",
|
|
731
|
+
style="bold green" if report.active_peripherals_count > 0 else "yellow",
|
|
732
|
+
),
|
|
733
|
+
)
|
|
734
|
+
summary_table.add_row(
|
|
735
|
+
"Inactive Peripherals",
|
|
736
|
+
f"{report.inactive_peripherals_count} / {report.total_peripherals}",
|
|
737
|
+
)
|
|
738
|
+
summary_table.add_row(
|
|
739
|
+
"Total Words Scanned",
|
|
740
|
+
f"{report.total_words_scanned:,} (in {report.elapsed_seconds:.3f}s)",
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
panel = Panel(
|
|
744
|
+
summary_table,
|
|
745
|
+
title=f"Target Peripheral Profile: {report.region_name.upper()}",
|
|
746
|
+
border_style="cyan",
|
|
747
|
+
box=box.ROUNDED,
|
|
748
|
+
)
|
|
749
|
+
CONSOLE.print(panel)
|
|
750
|
+
|
|
751
|
+
if report.active_peripherals:
|
|
752
|
+
table = Table(
|
|
753
|
+
title=f"Active Peripherals Detected in {report.region_name.upper()}",
|
|
754
|
+
box=box.SIMPLE_HEAVY,
|
|
755
|
+
header_style="bold cyan",
|
|
756
|
+
show_header=True,
|
|
757
|
+
)
|
|
758
|
+
table.add_column("Peripheral", style="bold", no_wrap=True)
|
|
759
|
+
table.add_column("Base Address", style="magenta", no_wrap=True)
|
|
760
|
+
table.add_column("Group", style="dim", no_wrap=True)
|
|
761
|
+
table.add_column("Hits", style="bold green", justify="right", no_wrap=True)
|
|
762
|
+
table.add_column("Active Regs", justify="center", no_wrap=True)
|
|
763
|
+
table.add_column("Referenced Registers")
|
|
764
|
+
|
|
765
|
+
for p in report.active_peripherals:
|
|
766
|
+
active_reg_names = [r.name for r in p.registers if r.hit_count > 0]
|
|
767
|
+
if len(active_reg_names) > 6:
|
|
768
|
+
regs_str = ", ".join(active_reg_names[:6]) + f" (+{len(active_reg_names) - 6} more)"
|
|
769
|
+
elif active_reg_names:
|
|
770
|
+
regs_str = ", ".join(active_reg_names)
|
|
771
|
+
else:
|
|
772
|
+
regs_str = "Base address only"
|
|
773
|
+
|
|
774
|
+
regs_ratio = f"{p.used_registers_count}/{p.total_registers}"
|
|
775
|
+
table.add_row(
|
|
776
|
+
p.name,
|
|
777
|
+
f"0x{p.base_address:08X}",
|
|
778
|
+
p.group_name or "--",
|
|
779
|
+
str(p.hit_count),
|
|
780
|
+
regs_ratio,
|
|
781
|
+
regs_str,
|
|
782
|
+
)
|
|
783
|
+
CONSOLE.print(table)
|
|
784
|
+
else:
|
|
785
|
+
CONSOLE.print(
|
|
786
|
+
Text(
|
|
787
|
+
f"No active peripheral references detected in {report.region_name.upper()} scan.",
|
|
788
|
+
style="yellow",
|
|
789
|
+
)
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
if report.inactive_peripherals:
|
|
793
|
+
inactive_names = [p.name for p in report.inactive_peripherals]
|
|
794
|
+
if len(inactive_names) > 20:
|
|
795
|
+
inactive_preview = (
|
|
796
|
+
", ".join(inactive_names[:20]) + f" ... (+{len(inactive_names) - 20} more)"
|
|
797
|
+
)
|
|
798
|
+
else:
|
|
799
|
+
inactive_preview = ", ".join(inactive_names)
|
|
800
|
+
|
|
801
|
+
inactive_table = Table(
|
|
802
|
+
title="Inactive Peripherals Summary",
|
|
803
|
+
box=box.SIMPLE_HEAVY,
|
|
804
|
+
header_style="bold cyan",
|
|
805
|
+
show_header=True,
|
|
806
|
+
)
|
|
807
|
+
inactive_table.add_column("Count", style="yellow", justify="right", no_wrap=True)
|
|
808
|
+
inactive_table.add_column("Unreferenced Peripherals")
|
|
809
|
+
inactive_table.add_row(str(len(inactive_names)), inactive_preview)
|
|
810
|
+
CONSOLE.print(inactive_table)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def dump_profile_to_json(session_state: ProfileSessionState, output_path: Path) -> int:
|
|
814
|
+
"""Serialize the accumulated profiling results to a JSON file on disk.
|
|
815
|
+
|
|
816
|
+
Parameters
|
|
817
|
+
----------
|
|
818
|
+
session_state : ProfileSessionState
|
|
819
|
+
Profile session state to serialize.
|
|
820
|
+
output_path : Path
|
|
821
|
+
Destination file path.
|
|
822
|
+
|
|
823
|
+
Returns
|
|
824
|
+
-------
|
|
825
|
+
int
|
|
826
|
+
Size of the written file in bytes.
|
|
827
|
+
"""
|
|
828
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
829
|
+
with output_path.open("w", encoding="utf-8") as json_file:
|
|
830
|
+
json.dump(session_state.to_dict(), json_file, indent=2)
|
|
831
|
+
return output_path.stat().st_size
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def render_dump_success(
|
|
835
|
+
session_state: ProfileSessionState, file_path: Path, file_size: int
|
|
836
|
+
) -> None:
|
|
837
|
+
"""Render a Rich confirmation table when a profile report is exported to JSON.
|
|
838
|
+
|
|
839
|
+
Parameters
|
|
840
|
+
----------
|
|
841
|
+
session_state : ProfileSessionState
|
|
842
|
+
Profile session state dumped.
|
|
843
|
+
file_path : Path
|
|
844
|
+
Output file path.
|
|
845
|
+
file_size : int
|
|
846
|
+
Size in bytes.
|
|
847
|
+
"""
|
|
848
|
+
table = Table(
|
|
849
|
+
title="Profile Report Exported",
|
|
850
|
+
box=box.SIMPLE_HEAVY,
|
|
851
|
+
header_style="bold cyan",
|
|
852
|
+
show_header=True,
|
|
853
|
+
)
|
|
854
|
+
table.add_column("Property", style="bold", no_wrap=True)
|
|
855
|
+
table.add_column("Value")
|
|
856
|
+
table.add_row("Device", session_state.device_name or "Unknown")
|
|
857
|
+
table.add_row(
|
|
858
|
+
"Scanned Regions",
|
|
859
|
+
", ".join(r["region"].upper() for r in session_state.scanned_regions) or "None",
|
|
860
|
+
)
|
|
861
|
+
table.add_row("Output File", Text(str(file_path), style="bold green"))
|
|
862
|
+
table.add_row("Active Peripherals", str(len(session_state.active_peripherals)))
|
|
863
|
+
table.add_row("File Size", f"{file_size:,} bytes")
|
|
864
|
+
CONSOLE.print(table)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def render_show_peripherals(
|
|
868
|
+
session_state: ProfileSessionState, periph_filter: str | None = None
|
|
869
|
+
) -> None:
|
|
870
|
+
"""Render the accumulated list of active peripherals or details for one peripheral.
|
|
871
|
+
|
|
872
|
+
Parameters
|
|
873
|
+
----------
|
|
874
|
+
session_state : ProfileSessionState
|
|
875
|
+
Active profiling session state.
|
|
876
|
+
periph_filter : str | None
|
|
877
|
+
Optional peripheral name filter.
|
|
878
|
+
"""
|
|
879
|
+
if not session_state.active_peripherals:
|
|
880
|
+
panel = Panel(
|
|
881
|
+
Text.assemble(
|
|
882
|
+
("No Profiling Data: ", "bold yellow"),
|
|
883
|
+
(
|
|
884
|
+
"No peripheral manipulation has been recorded yet in this session.\n",
|
|
885
|
+
"default",
|
|
886
|
+
),
|
|
887
|
+
("Run ", "dim"),
|
|
888
|
+
("profile analyze flash", "bold cyan"),
|
|
889
|
+
(" or ", "dim"),
|
|
890
|
+
("profile analyze sram", "bold cyan"),
|
|
891
|
+
(" to analyze target memory.", "dim"),
|
|
892
|
+
),
|
|
893
|
+
title="Firmware Peripheral Profile",
|
|
894
|
+
box=box.ROUNDED,
|
|
895
|
+
)
|
|
896
|
+
CONSOLE.print(panel)
|
|
897
|
+
return
|
|
898
|
+
|
|
899
|
+
if periph_filter:
|
|
900
|
+
target_name = periph_filter.strip().upper()
|
|
901
|
+
matched_key = next(
|
|
902
|
+
(k for k in session_state.active_peripherals if k.upper() == target_name),
|
|
903
|
+
None,
|
|
904
|
+
)
|
|
905
|
+
if matched_key is None:
|
|
906
|
+
CONSOLE.print(
|
|
907
|
+
Text(
|
|
908
|
+
f"Peripheral '{periph_filter}' is not present among actively used peripherals.",
|
|
909
|
+
style="yellow",
|
|
910
|
+
)
|
|
911
|
+
)
|
|
912
|
+
return
|
|
913
|
+
|
|
914
|
+
p_data = session_state.active_peripherals[matched_key]
|
|
915
|
+
table = Table(
|
|
916
|
+
title=f"Active Registers: {p_data['name']} (Base {p_data['base_address']})",
|
|
917
|
+
box=box.SIMPLE_HEAVY,
|
|
918
|
+
header_style="bold cyan",
|
|
919
|
+
show_header=True,
|
|
920
|
+
)
|
|
921
|
+
table.add_column("Register", style="bold", no_wrap=True)
|
|
922
|
+
table.add_column("Offset", style="dim", no_wrap=True)
|
|
923
|
+
table.add_column("Address", style="magenta", no_wrap=True)
|
|
924
|
+
table.add_column("Hits", style="bold green", justify="right", no_wrap=True)
|
|
925
|
+
table.add_column("Regions", style="bold yellow", no_wrap=True)
|
|
926
|
+
table.add_column("Reference Types", style="dim")
|
|
927
|
+
table.add_column("Hit Locations Preview")
|
|
928
|
+
|
|
929
|
+
regs_dict: dict[str, Any] = p_data["registers"]
|
|
930
|
+
for r_name, r_info in sorted(
|
|
931
|
+
regs_dict.items(), key=lambda item: int(item[1]["hit_count"]), reverse=True
|
|
932
|
+
):
|
|
933
|
+
hit_addrs = [f"0x{a:08X}" for a in sorted(r_info["hit_addresses"])]
|
|
934
|
+
locs_str = (
|
|
935
|
+
", ".join(hit_addrs[:4]) + f" (+{len(hit_addrs) - 4} more)"
|
|
936
|
+
if len(hit_addrs) > 4
|
|
937
|
+
else ", ".join(hit_addrs)
|
|
938
|
+
)
|
|
939
|
+
table.add_row(
|
|
940
|
+
r_name,
|
|
941
|
+
r_info["address_offset"],
|
|
942
|
+
r_info["address"],
|
|
943
|
+
str(r_info["hit_count"]),
|
|
944
|
+
", ".join(sorted(r_info["regions"])),
|
|
945
|
+
", ".join(sorted(r_info["reference_types"])),
|
|
946
|
+
locs_str,
|
|
947
|
+
)
|
|
948
|
+
CONSOLE.print(table)
|
|
949
|
+
return
|
|
950
|
+
|
|
951
|
+
regions_str = ", ".join(r["region"].upper() for r in session_state.scanned_regions)
|
|
952
|
+
summary_table = Table(
|
|
953
|
+
box=box.SIMPLE_HEAVY,
|
|
954
|
+
header_style="bold cyan",
|
|
955
|
+
show_header=False,
|
|
956
|
+
)
|
|
957
|
+
summary_table.add_column("Key", style="bold")
|
|
958
|
+
summary_table.add_column("Value")
|
|
959
|
+
summary_table.add_row(
|
|
960
|
+
"Device / SoC",
|
|
961
|
+
f"{session_state.device_name or 'Unknown'} ({session_state.vendor or 'Generic'})",
|
|
962
|
+
)
|
|
963
|
+
summary_table.add_row("Scanned Regions", regions_str or "--")
|
|
964
|
+
summary_table.add_row(
|
|
965
|
+
"Total Active Peripherals",
|
|
966
|
+
Text(str(len(session_state.active_peripherals)), style="bold green"),
|
|
967
|
+
)
|
|
968
|
+
summary_table.add_row(
|
|
969
|
+
"Total Analysis Runs",
|
|
970
|
+
str(len(session_state.scanned_regions)),
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
panel = Panel(
|
|
974
|
+
summary_table,
|
|
975
|
+
title="Firmware Active Peripheral Profile",
|
|
976
|
+
border_style="cyan",
|
|
977
|
+
box=box.ROUNDED,
|
|
978
|
+
)
|
|
979
|
+
CONSOLE.print(panel)
|
|
980
|
+
|
|
981
|
+
table = Table(
|
|
982
|
+
title="Active Peripherals Used by Firmware (Accumulated)",
|
|
983
|
+
box=box.SIMPLE_HEAVY,
|
|
984
|
+
header_style="bold cyan",
|
|
985
|
+
show_header=True,
|
|
986
|
+
)
|
|
987
|
+
table.add_column("Peripheral", style="bold", no_wrap=True)
|
|
988
|
+
table.add_column("Base Address", style="magenta", no_wrap=True)
|
|
989
|
+
table.add_column("Group", style="dim", no_wrap=True)
|
|
990
|
+
table.add_column("Total Hits", style="bold green", justify="right", no_wrap=True)
|
|
991
|
+
table.add_column("Active Regs", justify="center", no_wrap=True)
|
|
992
|
+
table.add_column("Regions", style="bold yellow", no_wrap=True)
|
|
993
|
+
table.add_column("Referenced Registers")
|
|
994
|
+
|
|
995
|
+
for _, p_data in sorted(
|
|
996
|
+
session_state.active_peripherals.items(),
|
|
997
|
+
key=lambda item: int(item[1]["hit_count"]),
|
|
998
|
+
reverse=True,
|
|
999
|
+
):
|
|
1000
|
+
regs_dict = p_data["registers"]
|
|
1001
|
+
active_reg_names = list(regs_dict.keys())
|
|
1002
|
+
if len(active_reg_names) > 6:
|
|
1003
|
+
regs_str = ", ".join(active_reg_names[:6]) + f" (+{len(active_reg_names) - 6} more)"
|
|
1004
|
+
elif active_reg_names:
|
|
1005
|
+
regs_str = ", ".join(active_reg_names)
|
|
1006
|
+
else:
|
|
1007
|
+
regs_str = "Base address only"
|
|
1008
|
+
|
|
1009
|
+
regs_ratio = f"{len(regs_dict)}/{p_data['total_registers']}"
|
|
1010
|
+
table.add_row(
|
|
1011
|
+
p_data["name"],
|
|
1012
|
+
p_data["base_address"],
|
|
1013
|
+
p_data["group_name"] or "--",
|
|
1014
|
+
str(p_data["hit_count"]),
|
|
1015
|
+
regs_ratio,
|
|
1016
|
+
", ".join(sorted(p_data["regions"])),
|
|
1017
|
+
regs_str,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
CONSOLE.print(table)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def render_help() -> None:
|
|
1024
|
+
"""Render the profile command overview and help table."""
|
|
1025
|
+
table = Table(
|
|
1026
|
+
title="Firmware Peripheral Profiling & Memory Analysis",
|
|
1027
|
+
box=box.SIMPLE_HEAVY,
|
|
1028
|
+
header_style="bold cyan",
|
|
1029
|
+
show_header=True,
|
|
1030
|
+
)
|
|
1031
|
+
table.add_column("Command Syntax", style="bold yellow", no_wrap=True)
|
|
1032
|
+
table.add_column("Description")
|
|
1033
|
+
table.add_row(
|
|
1034
|
+
"profile analyze flash [<addr>] [<size>]",
|
|
1035
|
+
"Analyze Flash memory for register manipulations and summarize with Rich",
|
|
1036
|
+
)
|
|
1037
|
+
table.add_row(
|
|
1038
|
+
"profile analyze sram [<addr>] [<size>]",
|
|
1039
|
+
"Analyze SRAM memory for register manipulations and summarize with Rich",
|
|
1040
|
+
)
|
|
1041
|
+
table.add_row(
|
|
1042
|
+
"profile show [<peripheral>]",
|
|
1043
|
+
"Display the accumulated list of peripherals used by the firmware",
|
|
1044
|
+
)
|
|
1045
|
+
table.add_row(
|
|
1046
|
+
"profile dump <file.json>",
|
|
1047
|
+
"Export the accumulated profiling analysis results to a JSON file",
|
|
1048
|
+
)
|
|
1049
|
+
table.add_row("profile help", "Show this command reference")
|
|
1050
|
+
CONSOLE.print(table)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _ensure_svd_loaded() -> SvdDevice:
|
|
1054
|
+
"""Ensure an SVD device model is loaded in the session, auto-detecting if needed.
|
|
1055
|
+
|
|
1056
|
+
Returns
|
|
1057
|
+
-------
|
|
1058
|
+
SvdDevice
|
|
1059
|
+
The active SVD device model.
|
|
1060
|
+
|
|
1061
|
+
Raises
|
|
1062
|
+
------
|
|
1063
|
+
gdb.GdbError
|
|
1064
|
+
If no SVD file could be loaded or detected.
|
|
1065
|
+
"""
|
|
1066
|
+
if is_svd_loaded() and SVD_SESSION.device is not None:
|
|
1067
|
+
return SVD_SESSION.device
|
|
1068
|
+
|
|
1069
|
+
try:
|
|
1070
|
+
report = device_report()
|
|
1071
|
+
except TargetReadError as err:
|
|
1072
|
+
raise gdb.GdbError(
|
|
1073
|
+
f"No SVD loaded and cannot detect target memory: {err}. "
|
|
1074
|
+
"Run 'svd load' or 'svd read <file.svd>' first."
|
|
1075
|
+
) from err
|
|
1076
|
+
|
|
1077
|
+
try:
|
|
1078
|
+
svd_path, _ = resolve_and_fetch_svd(report)
|
|
1079
|
+
device = parse_svd_file(svd_path)
|
|
1080
|
+
SVD_SESSION.device = device
|
|
1081
|
+
SVD_SESSION.svd_dict = device.to_dict()
|
|
1082
|
+
SVD_SESSION.svd_path = svd_path
|
|
1083
|
+
return device
|
|
1084
|
+
except SvdError as err:
|
|
1085
|
+
raise gdb.GdbError(
|
|
1086
|
+
f"No SVD definition currently loaded. Failed to auto-load SVD: {err}. "
|
|
1087
|
+
"Please run 'svd load' or 'svd read <file.svd>' first."
|
|
1088
|
+
) from err
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def _parse_int_arg(arg_str: str) -> int:
|
|
1092
|
+
"""Parse an integer argument supporting hex, bin, and dec notation.
|
|
1093
|
+
|
|
1094
|
+
Parameters
|
|
1095
|
+
----------
|
|
1096
|
+
arg_str : str
|
|
1097
|
+
Input string.
|
|
1098
|
+
|
|
1099
|
+
Returns
|
|
1100
|
+
-------
|
|
1101
|
+
int
|
|
1102
|
+
Decoded integer.
|
|
1103
|
+
"""
|
|
1104
|
+
arg_str = arg_str.strip()
|
|
1105
|
+
if arg_str.startswith("#"):
|
|
1106
|
+
return int(arg_str[1:], 16)
|
|
1107
|
+
if arg_str.startswith(("0x", "0X")):
|
|
1108
|
+
return int(arg_str, 16)
|
|
1109
|
+
if arg_str.startswith(("0b", "0B")):
|
|
1110
|
+
return int(arg_str, 2)
|
|
1111
|
+
return int(arg_str, 10)
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
class ProfileCmd(gdb.Command):
|
|
1115
|
+
"""Profile target memory (Flash/SRAM) to identify active SVD peripheral usage."""
|
|
1116
|
+
|
|
1117
|
+
def __init__(self) -> None:
|
|
1118
|
+
super().__init__("profile", gdb.COMMAND_USER, gdb.COMPLETE_NONE, True)
|
|
1119
|
+
|
|
1120
|
+
def invoke(self, arg: str, from_tty: bool) -> None:
|
|
1121
|
+
"""Display help overview.
|
|
1122
|
+
|
|
1123
|
+
Parameters
|
|
1124
|
+
----------
|
|
1125
|
+
arg : str
|
|
1126
|
+
Command arguments.
|
|
1127
|
+
from_tty : bool
|
|
1128
|
+
Whether invoked interactively from terminal.
|
|
1129
|
+
"""
|
|
1130
|
+
del arg
|
|
1131
|
+
del from_tty
|
|
1132
|
+
render_help()
|
|
1133
|
+
|
|
1134
|
+
def complete_args(self, subcmd: str, text: str, word: str) -> list[str]:
|
|
1135
|
+
"""Provide intelligent GDB auto-completion for profile subcommands.
|
|
1136
|
+
|
|
1137
|
+
Parameters
|
|
1138
|
+
----------
|
|
1139
|
+
subcmd : str
|
|
1140
|
+
Subcommand name.
|
|
1141
|
+
text : str
|
|
1142
|
+
Full argument string.
|
|
1143
|
+
word : str
|
|
1144
|
+
Word currently being completed.
|
|
1145
|
+
|
|
1146
|
+
Returns
|
|
1147
|
+
-------
|
|
1148
|
+
list[str]
|
|
1149
|
+
Completion matches.
|
|
1150
|
+
"""
|
|
1151
|
+
command_prefix = f"profile {subcmd}"
|
|
1152
|
+
arg_text = text
|
|
1153
|
+
if arg_text.startswith(command_prefix):
|
|
1154
|
+
arg_text = arg_text[len(command_prefix) :].lstrip()
|
|
1155
|
+
tokens = arg_text.split()
|
|
1156
|
+
|
|
1157
|
+
if subcmd in ("analyze", "analyse"):
|
|
1158
|
+
if not tokens or (len(tokens) == 1 and not arg_text.endswith(" ")):
|
|
1159
|
+
options = ["flash", "sram"]
|
|
1160
|
+
return [o for o in options if o.startswith(word.lower())]
|
|
1161
|
+
return []
|
|
1162
|
+
|
|
1163
|
+
if subcmd == "show":
|
|
1164
|
+
if not tokens or (len(tokens) == 1 and not arg_text.endswith(" ")):
|
|
1165
|
+
names = list(PROFILE_SESSION.active_peripherals.keys())
|
|
1166
|
+
if SVD_SESSION.device is not None:
|
|
1167
|
+
for p in SVD_SESSION.device.peripherals:
|
|
1168
|
+
if p.name not in names:
|
|
1169
|
+
names.append(p.name)
|
|
1170
|
+
return [n for n in names if n.upper().startswith(word.upper())]
|
|
1171
|
+
return []
|
|
1172
|
+
|
|
1173
|
+
if subcmd == "dump":
|
|
1174
|
+
if not tokens or (len(tokens) == 1 and not arg_text.endswith(" ")):
|
|
1175
|
+
pattern = f"{word}*"
|
|
1176
|
+
expanded = os.path.expanduser(pattern)
|
|
1177
|
+
matches = glob.glob(expanded)
|
|
1178
|
+
results: list[str] = []
|
|
1179
|
+
for m in matches:
|
|
1180
|
+
if os.path.isdir(m):
|
|
1181
|
+
results.append(f"{m}/")
|
|
1182
|
+
elif m.lower().endswith(".json"):
|
|
1183
|
+
results.append(m)
|
|
1184
|
+
return results
|
|
1185
|
+
return []
|
|
1186
|
+
|
|
1187
|
+
return []
|
|
1188
|
+
|
|
1189
|
+
def _invoke_analyze(self, args: list[str]) -> None:
|
|
1190
|
+
"""Handle 'profile analyze flash|sram [<addr>] [<size>]'.
|
|
1191
|
+
|
|
1192
|
+
Parameters
|
|
1193
|
+
----------
|
|
1194
|
+
args : list[str]
|
|
1195
|
+
Subcommand arguments.
|
|
1196
|
+
|
|
1197
|
+
Raises
|
|
1198
|
+
------
|
|
1199
|
+
gdb.GdbError
|
|
1200
|
+
If arguments are invalid or memory scan fails.
|
|
1201
|
+
"""
|
|
1202
|
+
if not args:
|
|
1203
|
+
raise gdb.GdbError("Usage: profile analyze <flash|sram> [<start_addr>] [<size>]")
|
|
1204
|
+
|
|
1205
|
+
target_region = args[0].lower()
|
|
1206
|
+
if target_region not in ("flash", "sram", "ram"):
|
|
1207
|
+
raise gdb.GdbError(f"Unknown memory region '{args[0]}': expected 'flash' or 'sram'.")
|
|
1208
|
+
|
|
1209
|
+
device = _ensure_svd_loaded()
|
|
1210
|
+
default_start, default_size = resolve_memory_region(device, target_region)
|
|
1211
|
+
|
|
1212
|
+
start_addr = default_start
|
|
1213
|
+
size = default_size
|
|
1214
|
+
|
|
1215
|
+
if len(args) >= 2:
|
|
1216
|
+
try:
|
|
1217
|
+
start_addr = _parse_int_arg(args[1])
|
|
1218
|
+
except ValueError as err:
|
|
1219
|
+
raise gdb.GdbError(f"Invalid start address '{args[1]}': {err}") from err
|
|
1220
|
+
|
|
1221
|
+
if len(args) >= 3:
|
|
1222
|
+
try:
|
|
1223
|
+
size = _parse_int_arg(args[2])
|
|
1224
|
+
except ValueError as err:
|
|
1225
|
+
raise gdb.GdbError(f"Invalid memory size '{args[2]}': {err}") from err
|
|
1226
|
+
|
|
1227
|
+
try:
|
|
1228
|
+
reader = TOOLKIT_SESSION.memory
|
|
1229
|
+
except TargetReadError as err:
|
|
1230
|
+
raise gdb.GdbError(f"Cannot access target memory: {err}") from err
|
|
1231
|
+
|
|
1232
|
+
report = analyze_memory_profile(
|
|
1233
|
+
device=device,
|
|
1234
|
+
reader=reader,
|
|
1235
|
+
region_name=target_region,
|
|
1236
|
+
start_address=start_addr,
|
|
1237
|
+
size=size,
|
|
1238
|
+
)
|
|
1239
|
+
PROFILE_SESSION.record_report(report)
|
|
1240
|
+
render_profile_report(report)
|
|
1241
|
+
|
|
1242
|
+
def _invoke_show(self, args: list[str]) -> None:
|
|
1243
|
+
"""Handle 'profile show [<peripheral>]'.
|
|
1244
|
+
|
|
1245
|
+
Parameters
|
|
1246
|
+
----------
|
|
1247
|
+
args : list[str]
|
|
1248
|
+
Subcommand arguments.
|
|
1249
|
+
|
|
1250
|
+
Raises
|
|
1251
|
+
------
|
|
1252
|
+
gdb.GdbError
|
|
1253
|
+
If too many arguments are supplied.
|
|
1254
|
+
"""
|
|
1255
|
+
if len(args) > 1:
|
|
1256
|
+
raise gdb.GdbError("Usage: profile show [<peripheral>]")
|
|
1257
|
+
|
|
1258
|
+
periph_name = args[0] if args else None
|
|
1259
|
+
render_show_peripherals(PROFILE_SESSION, periph_name)
|
|
1260
|
+
|
|
1261
|
+
def _invoke_dump(self, args: list[str]) -> None:
|
|
1262
|
+
"""Handle 'profile dump <file.json>'.
|
|
1263
|
+
|
|
1264
|
+
Parameters
|
|
1265
|
+
----------
|
|
1266
|
+
args : list[str]
|
|
1267
|
+
Subcommand arguments.
|
|
1268
|
+
|
|
1269
|
+
Raises
|
|
1270
|
+
------
|
|
1271
|
+
gdb.GdbError
|
|
1272
|
+
If arguments are invalid or writing fails.
|
|
1273
|
+
"""
|
|
1274
|
+
if len(args) != 1:
|
|
1275
|
+
raise gdb.GdbError("Usage: profile dump <file.json>")
|
|
1276
|
+
|
|
1277
|
+
out_path = Path(args[0]).expanduser().resolve()
|
|
1278
|
+
|
|
1279
|
+
if not PROFILE_SESSION.active_peripherals:
|
|
1280
|
+
device = _ensure_svd_loaded()
|
|
1281
|
+
default_start, default_size = resolve_memory_region(device, "flash")
|
|
1282
|
+
try:
|
|
1283
|
+
reader = TOOLKIT_SESSION.memory
|
|
1284
|
+
except TargetReadError as err:
|
|
1285
|
+
raise gdb.GdbError(f"Cannot access target memory: {err}") from err
|
|
1286
|
+
|
|
1287
|
+
report = analyze_memory_profile(
|
|
1288
|
+
device=device,
|
|
1289
|
+
reader=reader,
|
|
1290
|
+
region_name="flash",
|
|
1291
|
+
start_address=default_start,
|
|
1292
|
+
size=default_size,
|
|
1293
|
+
)
|
|
1294
|
+
PROFILE_SESSION.record_report(report)
|
|
1295
|
+
|
|
1296
|
+
try:
|
|
1297
|
+
file_size = dump_profile_to_json(PROFILE_SESSION, out_path)
|
|
1298
|
+
render_dump_success(PROFILE_SESSION, out_path, file_size)
|
|
1299
|
+
except OSError as err:
|
|
1300
|
+
raise gdb.GdbError(f"Failed to write dump to '{out_path}': {err}") from err
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
class _ProfileSubcommand(gdb.Command):
|
|
1304
|
+
"""Base class for concrete ``profile`` subcommands registered under the prefix command."""
|
|
1305
|
+
|
|
1306
|
+
def __init__(self, parent: ProfileCmd, name: str) -> None:
|
|
1307
|
+
self.parent = parent
|
|
1308
|
+
self.name = name
|
|
1309
|
+
super().__init__(f"profile {name}", gdb.COMMAND_USER)
|
|
1310
|
+
|
|
1311
|
+
def _argv(self, arg: str) -> list[str]:
|
|
1312
|
+
"""Split command arguments using GDB CLI lexer.
|
|
1313
|
+
|
|
1314
|
+
Parameters
|
|
1315
|
+
----------
|
|
1316
|
+
arg : str
|
|
1317
|
+
Argument string.
|
|
1318
|
+
|
|
1319
|
+
Returns
|
|
1320
|
+
-------
|
|
1321
|
+
list[str]
|
|
1322
|
+
Parsed arguments.
|
|
1323
|
+
"""
|
|
1324
|
+
return list(gdb.string_to_argv(arg)) if arg.strip() else []
|
|
1325
|
+
|
|
1326
|
+
def complete(self, text: str, word: str) -> list[str]:
|
|
1327
|
+
"""Complete arguments for the concrete subcommand.
|
|
1328
|
+
|
|
1329
|
+
Parameters
|
|
1330
|
+
----------
|
|
1331
|
+
text : str
|
|
1332
|
+
Full argument string.
|
|
1333
|
+
word : str
|
|
1334
|
+
Word to complete.
|
|
1335
|
+
|
|
1336
|
+
Returns
|
|
1337
|
+
-------
|
|
1338
|
+
list[str]
|
|
1339
|
+
Completion options.
|
|
1340
|
+
"""
|
|
1341
|
+
return self.parent.complete_args(self.name, text, word)
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
class ProfileAnalyzeCmd(_ProfileSubcommand):
|
|
1345
|
+
"""Analyze target Flash or SRAM for peripheral register usage."""
|
|
1346
|
+
|
|
1347
|
+
def __init__(self, parent: ProfileCmd) -> None:
|
|
1348
|
+
super().__init__(parent, "analyze")
|
|
1349
|
+
|
|
1350
|
+
def invoke(self, arg: str, from_tty: bool) -> None:
|
|
1351
|
+
"""Execute ``profile analyze <flash|sram>``."""
|
|
1352
|
+
del from_tty
|
|
1353
|
+
self.parent._invoke_analyze(self._argv(arg))
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
class ProfileAnalyseCmd(_ProfileSubcommand):
|
|
1357
|
+
"""Analyze target Flash or SRAM for peripheral register usage (French alias)."""
|
|
1358
|
+
|
|
1359
|
+
def __init__(self, parent: ProfileCmd) -> None:
|
|
1360
|
+
super().__init__(parent, "analyse")
|
|
1361
|
+
|
|
1362
|
+
def invoke(self, arg: str, from_tty: bool) -> None:
|
|
1363
|
+
"""Execute ``profile analyse <flash|sram>``."""
|
|
1364
|
+
del from_tty
|
|
1365
|
+
self.parent._invoke_analyze(self._argv(arg))
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
class ProfileShowCmd(_ProfileSubcommand):
|
|
1369
|
+
"""Display the accumulated list of peripherals used by the firmware."""
|
|
1370
|
+
|
|
1371
|
+
def __init__(self, parent: ProfileCmd) -> None:
|
|
1372
|
+
super().__init__(parent, "show")
|
|
1373
|
+
|
|
1374
|
+
def invoke(self, arg: str, from_tty: bool) -> None:
|
|
1375
|
+
"""Execute ``profile show [<peripheral>]``."""
|
|
1376
|
+
del from_tty
|
|
1377
|
+
self.parent._invoke_show(self._argv(arg))
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
class ProfileDumpCmd(_ProfileSubcommand):
|
|
1381
|
+
"""Dump the latest profiling analysis results to a JSON file."""
|
|
1382
|
+
|
|
1383
|
+
def __init__(self, parent: ProfileCmd) -> None:
|
|
1384
|
+
super().__init__(parent, "dump")
|
|
1385
|
+
|
|
1386
|
+
def invoke(self, arg: str, from_tty: bool) -> None:
|
|
1387
|
+
"""Execute ``profile dump <file.json>``."""
|
|
1388
|
+
del from_tty
|
|
1389
|
+
self.parent._invoke_dump(self._argv(arg))
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
class ProfileHelpCmd(_ProfileSubcommand):
|
|
1393
|
+
"""Show the profile command reference."""
|
|
1394
|
+
|
|
1395
|
+
def __init__(self, parent: ProfileCmd) -> None:
|
|
1396
|
+
super().__init__(parent, "help")
|
|
1397
|
+
|
|
1398
|
+
def invoke(self, arg: str, from_tty: bool) -> None:
|
|
1399
|
+
"""Execute ``profile help``."""
|
|
1400
|
+
del arg
|
|
1401
|
+
del from_tty
|
|
1402
|
+
render_help()
|