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.
@@ -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()