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,52 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""GDB adapters for architecture-neutral diagnostic runtime contracts."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import gdb
|
|
9
|
+
|
|
10
|
+
from .arch.diagnostics import (
|
|
11
|
+
DiagnosticRegisterReader,
|
|
12
|
+
DiagnosticRuntimeAccess,
|
|
13
|
+
DiagnosticSymbolResolver,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GdbDiagnosticRegisterReader:
|
|
18
|
+
"""Read runtime registers from GDB's selected frame."""
|
|
19
|
+
|
|
20
|
+
def read_first(self, names: tuple[str, ...]) -> int | None:
|
|
21
|
+
"""Return the first GDB register exposed under the supplied names."""
|
|
22
|
+
try:
|
|
23
|
+
frame = gdb.selected_frame()
|
|
24
|
+
except gdb.error:
|
|
25
|
+
return None
|
|
26
|
+
for name in names:
|
|
27
|
+
try:
|
|
28
|
+
return int(frame.read_register(name))
|
|
29
|
+
except (gdb.error, ValueError, TypeError):
|
|
30
|
+
continue
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GdbDiagnosticSymbolResolver:
|
|
35
|
+
"""Resolve target addresses through GDB without architecture assumptions."""
|
|
36
|
+
|
|
37
|
+
def resolve(self, address: int) -> str:
|
|
38
|
+
"""Return GDB's symbol description or ``"?"`` when unavailable."""
|
|
39
|
+
try:
|
|
40
|
+
output = gdb.execute(f"info symbol 0x{address:X}", to_string=True).strip()
|
|
41
|
+
except (gdb.error, TypeError, ValueError):
|
|
42
|
+
return "?"
|
|
43
|
+
if "No symbol matches" in output or "not in valid memory" in output:
|
|
44
|
+
return "?"
|
|
45
|
+
return output.split(" in section ")[0]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def gdb_diagnostic_access() -> DiagnosticRuntimeAccess:
|
|
49
|
+
"""Create portable diagnostic access adapters backed by the selected GDB frame."""
|
|
50
|
+
registers: DiagnosticRegisterReader = GdbDiagnosticRegisterReader()
|
|
51
|
+
symbols: DiagnosticSymbolResolver = GdbDiagnosticSymbolResolver()
|
|
52
|
+
return DiagnosticRuntimeAccess(registers=registers, symbols=symbols)
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 H2Lab Development Team
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Camelot RTOS project symbol locations."""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import struct
|
|
10
|
+
import tomllib
|
|
11
|
+
from typing import Literal
|
|
12
|
+
import xml.etree.ElementTree as ET
|
|
13
|
+
|
|
14
|
+
import gdb
|
|
15
|
+
from rich import box
|
|
16
|
+
from rich.console import Group
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
NAME = "camelot"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class TaskSlot:
|
|
25
|
+
"""One task metadata slot and its resolved application name."""
|
|
26
|
+
|
|
27
|
+
index: int
|
|
28
|
+
address: int
|
|
29
|
+
task_name: str | None
|
|
30
|
+
text_address: int
|
|
31
|
+
ram_address: int
|
|
32
|
+
metadata: dict[str, int | tuple[int, ...] | str]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class TaskList:
|
|
37
|
+
"""Location and decoded slots of the kernel's task metadata section."""
|
|
38
|
+
|
|
39
|
+
address: int
|
|
40
|
+
size: int
|
|
41
|
+
entry_size: int
|
|
42
|
+
slots: tuple[TaskSlot, ...]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def task_list_section(kernel_elf: Path) -> tuple[int, bytes]:
|
|
46
|
+
"""Read the linked address and contents of the kernel's task metadata section."""
|
|
47
|
+
data = kernel_elf.read_bytes()
|
|
48
|
+
if data[:4] != b"\x7fELF" or data[4] not in (1, 2) or data[5] not in (1, 2):
|
|
49
|
+
raise ValueError(f"Not a supported ELF: {kernel_elf}")
|
|
50
|
+
byte_order = "<" if data[5] == 1 else ">"
|
|
51
|
+
is_64_bit = data[4] == 2
|
|
52
|
+
header_format = byte_order + ("HHIQQQIHHHHHH" if is_64_bit else "HHIIIIIHHHHHH")
|
|
53
|
+
header = struct.unpack_from(header_format, data, 16)
|
|
54
|
+
section_offset, section_size, section_count, names_index = (
|
|
55
|
+
header[5],
|
|
56
|
+
header[10],
|
|
57
|
+
header[11],
|
|
58
|
+
header[12],
|
|
59
|
+
)
|
|
60
|
+
section_format = byte_order + ("IIQQQQIIQQ" if is_64_bit else "IIIIIIIIII")
|
|
61
|
+
if section_size < struct.calcsize(section_format) or names_index >= section_count:
|
|
62
|
+
raise ValueError(f"Invalid ELF section table: {kernel_elf}")
|
|
63
|
+
sections = [
|
|
64
|
+
struct.unpack_from(section_format, data, section_offset + index * section_size)
|
|
65
|
+
for index in range(section_count)
|
|
66
|
+
]
|
|
67
|
+
names = sections[names_index]
|
|
68
|
+
name_table = data[names[4] : names[4] + names[5]]
|
|
69
|
+
for section in sections:
|
|
70
|
+
name = name_table[section[0] :].split(b"\0", 1)[0]
|
|
71
|
+
if name == b".task_list":
|
|
72
|
+
address, offset, size = section[3:6]
|
|
73
|
+
if offset + size > len(data):
|
|
74
|
+
raise ValueError(f"Truncated .task_list section: {kernel_elf}")
|
|
75
|
+
return address, data[offset : offset + size]
|
|
76
|
+
raise ValueError(f"No .task_list section in {kernel_elf}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_task_list(project_path: Path, kernel_elf: Path) -> TaskList:
|
|
80
|
+
"""Decode the task slots using the generated C type's DWARF layout."""
|
|
81
|
+
header = project_path / "output/build/kernel/kernel/include/sentry/managers/task_metadata.h"
|
|
82
|
+
if not header.is_file():
|
|
83
|
+
raise FileNotFoundError(header)
|
|
84
|
+
address, contents = task_list_section(kernel_elf)
|
|
85
|
+
task_type = gdb.lookup_type("task_meta_t")
|
|
86
|
+
entry_size = task_type.sizeof
|
|
87
|
+
if not entry_size or len(contents) % entry_size:
|
|
88
|
+
raise ValueError(".task_list size is not a multiple of task_meta_t")
|
|
89
|
+
fields = {field.name: field for field in task_type.fields()}
|
|
90
|
+
if not {"s_text", "s_svcexchange"} <= fields.keys():
|
|
91
|
+
raise ValueError("task_meta_t is missing task memory addresses")
|
|
92
|
+
byte_order: Literal["little", "big"] = "little" if kernel_elf.read_bytes()[5] == 1 else "big"
|
|
93
|
+
|
|
94
|
+
with (project_path / "output/build/camelot_private/layout.json").open() as layout_file:
|
|
95
|
+
regions = json.load(layout_file)["regions"]
|
|
96
|
+
text_regions = {
|
|
97
|
+
int(region["start_address"], 0): region["name"]
|
|
98
|
+
for region in regions
|
|
99
|
+
if region["type"] == "text" and region["name"] != "kernel"
|
|
100
|
+
}
|
|
101
|
+
ram_regions = {
|
|
102
|
+
region["name"]: int(region["start_address"], 0)
|
|
103
|
+
for region in regions
|
|
104
|
+
if region["type"] == "ram"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def field_bytes(entry: bytes, field: gdb.Field) -> bytes:
|
|
108
|
+
if field.bitpos is None or field.type is None or field.name is None:
|
|
109
|
+
raise ValueError("Incomplete task_meta_t field in debug symbols")
|
|
110
|
+
if field.bitpos % 8:
|
|
111
|
+
raise ValueError(f"Unaligned task_meta_t field: {field.name}")
|
|
112
|
+
offset = field.bitpos // 8
|
|
113
|
+
return entry[offset : offset + field.type.sizeof]
|
|
114
|
+
|
|
115
|
+
def field_value(entry: bytes, name: str) -> int:
|
|
116
|
+
return int.from_bytes(field_bytes(entry, fields[name]), byte_order)
|
|
117
|
+
|
|
118
|
+
slots = []
|
|
119
|
+
for index in range(len(contents) // entry_size):
|
|
120
|
+
entry = contents[index * entry_size : (index + 1) * entry_size]
|
|
121
|
+
text_address = field_value(entry, "s_text")
|
|
122
|
+
ram_address = field_value(entry, "s_svcexchange")
|
|
123
|
+
name = text_regions.get(text_address) if any(entry) else None
|
|
124
|
+
if any(entry) and (name is None or ram_regions.get(name) != ram_address):
|
|
125
|
+
raise ValueError(f"Cannot match .task_list slot {index} to a task in layout.json")
|
|
126
|
+
metadata: dict[str, int | tuple[int, ...] | str] = {}
|
|
127
|
+
if name is not None:
|
|
128
|
+
for field in task_type.fields():
|
|
129
|
+
raw = field_bytes(entry, field)
|
|
130
|
+
if field.name is None or field.type is None:
|
|
131
|
+
raise ValueError("Incomplete task_meta_t field in debug symbols")
|
|
132
|
+
if field.name in ("task_hmac", "metadata_hmac"):
|
|
133
|
+
metadata[field.name] = raw.hex()
|
|
134
|
+
elif field.type.code == gdb.TYPE_CODE_ARRAY:
|
|
135
|
+
item_type = field.type.target()
|
|
136
|
+
if item_type is None:
|
|
137
|
+
raise ValueError(f"Unknown element type for {field.name}")
|
|
138
|
+
item_size = item_type.sizeof
|
|
139
|
+
metadata[field.name] = tuple(
|
|
140
|
+
int.from_bytes(raw[offset : offset + item_size], byte_order)
|
|
141
|
+
for offset in range(0, len(raw), item_size)
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
metadata[field.name] = int.from_bytes(raw, byte_order)
|
|
145
|
+
slots.append(
|
|
146
|
+
TaskSlot(index, address + index * entry_size, name, text_address, ram_address, metadata)
|
|
147
|
+
)
|
|
148
|
+
return TaskList(address, len(contents), entry_size, tuple(slots))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def project_elfs(project_path: Path) -> list[Path]:
|
|
152
|
+
"""Return the linked kernel and application ELF files declared by a project."""
|
|
153
|
+
with (project_path / "project.toml").open("rb") as project_file:
|
|
154
|
+
project = tomllib.load(project_file)
|
|
155
|
+
|
|
156
|
+
private_build = project_path / "output" / "build" / "camelot_private"
|
|
157
|
+
elfs = []
|
|
158
|
+
if "kernel" in project:
|
|
159
|
+
elfs.append(private_build / "sentry-kernel.elf")
|
|
160
|
+
applications = project.get("application", {})
|
|
161
|
+
if not isinstance(applications, dict):
|
|
162
|
+
raise ValueError("project.toml: application must be a table")
|
|
163
|
+
for name in applications:
|
|
164
|
+
elfs.append(private_build / f"{name}-app.elf")
|
|
165
|
+
if not elfs:
|
|
166
|
+
raise ValueError("project.toml: no kernel or application declared")
|
|
167
|
+
for elf in elfs:
|
|
168
|
+
if not elf.is_file():
|
|
169
|
+
raise FileNotFoundError(elf)
|
|
170
|
+
return elfs
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def elected_task(task_list: TaskList, handle: int) -> str:
|
|
174
|
+
"""Resolve a live scheduler handle through the kernel task table."""
|
|
175
|
+
task_type = gdb.lookup_type("task_t")
|
|
176
|
+
handle_field = next(field for field in task_type.fields() if field.name == "handle")
|
|
177
|
+
if handle_field.bitpos is None or handle_field.type is None:
|
|
178
|
+
raise ValueError("Incomplete task handle in debug symbols")
|
|
179
|
+
if handle_field.bitpos % 8:
|
|
180
|
+
raise ValueError("Unaligned task handle in task_t")
|
|
181
|
+
table = int(gdb.parse_and_eval("&task_table"))
|
|
182
|
+
inferior = gdb.selected_inferior()
|
|
183
|
+
for index in range(len(task_list.slots) + 1):
|
|
184
|
+
entry = table + index * task_type.sizeof
|
|
185
|
+
stored = int.from_bytes(
|
|
186
|
+
inferior.read_memory(entry + handle_field.bitpos // 8, handle_field.type.sizeof),
|
|
187
|
+
"little",
|
|
188
|
+
)
|
|
189
|
+
if stored != handle:
|
|
190
|
+
continue
|
|
191
|
+
task_address = gdb.Value(entry)
|
|
192
|
+
if task_address is None:
|
|
193
|
+
raise ValueError("Cannot resolve task table address")
|
|
194
|
+
task = task_address.cast(task_type.pointer()).dereference()
|
|
195
|
+
label = int(task["metadata"].dereference()["label"])
|
|
196
|
+
if label == 0xCAFE:
|
|
197
|
+
return "idle"
|
|
198
|
+
for slot in task_list.slots:
|
|
199
|
+
if slot.task_name is not None and slot.metadata["label"] == label:
|
|
200
|
+
return slot.task_name
|
|
201
|
+
return f"unknown (label 0x{label:X})"
|
|
202
|
+
return f"unknown (handle 0x{handle:X})"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def scheduling_chart(elections: list[str]) -> Group:
|
|
206
|
+
"""Render chronological election windows without exceeding terminal width."""
|
|
207
|
+
charts = []
|
|
208
|
+
for start in range(0, len(elections), 8):
|
|
209
|
+
window = elections[start : start + 8]
|
|
210
|
+
chart = Table(
|
|
211
|
+
title=f"Task scheduling: elections {start + 1}-{start + len(window)}",
|
|
212
|
+
box=box.SIMPLE_HEAVY,
|
|
213
|
+
header_style="bold cyan",
|
|
214
|
+
)
|
|
215
|
+
chart.add_column("Task", style="bold", no_wrap=True)
|
|
216
|
+
for index in range(start, start + len(window)):
|
|
217
|
+
chart.add_column(str(index + 1), justify="center", no_wrap=True)
|
|
218
|
+
for task in dict.fromkeys(elections):
|
|
219
|
+
chart.add_row(task, *("●" if elected == task else "·" for elected in window))
|
|
220
|
+
charts.append(chart)
|
|
221
|
+
return Group(*charts)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _interrupt_names() -> dict[int, str]:
|
|
225
|
+
"""Resolve IRQ numbers from the SVD currently loaded by the svd command."""
|
|
226
|
+
from ..cmd_svd import SESSION
|
|
227
|
+
from ..svd import _parse_int
|
|
228
|
+
|
|
229
|
+
if SESSION.svd_path is None:
|
|
230
|
+
return {}
|
|
231
|
+
root = ET.parse(SESSION.svd_path).getroot()
|
|
232
|
+
names: dict[int, str] = {}
|
|
233
|
+
for peripheral in root.iter():
|
|
234
|
+
if peripheral.tag.rsplit("}", 1)[-1] != "peripheral":
|
|
235
|
+
continue
|
|
236
|
+
for interrupt in peripheral:
|
|
237
|
+
if interrupt.tag.rsplit("}", 1)[-1] != "interrupt":
|
|
238
|
+
continue
|
|
239
|
+
children = {child.tag.rsplit("}", 1)[-1]: child.text for child in interrupt}
|
|
240
|
+
name = children.get("name")
|
|
241
|
+
value = children.get("value")
|
|
242
|
+
if name and value:
|
|
243
|
+
names.setdefault(_parse_int(value), name)
|
|
244
|
+
return names
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _task_symbol(address: int, text_start: int, text_end: int) -> str | None:
|
|
248
|
+
"""Resolve a Thumb address only within the selected task's text range."""
|
|
249
|
+
code_address = address & ~1
|
|
250
|
+
if not text_start <= code_address < text_end:
|
|
251
|
+
return None
|
|
252
|
+
try:
|
|
253
|
+
symbol = gdb.execute(f"info symbol 0x{code_address:X}", to_string=True).strip()
|
|
254
|
+
except gdb.error:
|
|
255
|
+
return None
|
|
256
|
+
if symbol.startswith("No symbol matches"):
|
|
257
|
+
return None
|
|
258
|
+
return symbol.split(" in section ", 1)[0]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def show_task(project_path: Path, task_list: TaskList, task_name: str) -> Group:
|
|
262
|
+
"""Inspect a named task's live kernel context on a stopped target."""
|
|
263
|
+
layout_path = project_path / "output/build/camelot_private/layout.json"
|
|
264
|
+
with layout_path.open(encoding="utf-8") as layout_file:
|
|
265
|
+
regions = [r for r in json.load(layout_file)["regions"] if r["name"] == task_name]
|
|
266
|
+
if not regions:
|
|
267
|
+
raise ValueError(f"Unknown task: {task_name}")
|
|
268
|
+
slot = next((s for s in task_list.slots if s.task_name == task_name), None)
|
|
269
|
+
task_type = gdb.lookup_type("task_t")
|
|
270
|
+
table_address = int(gdb.parse_and_eval("&task_table"))
|
|
271
|
+
source_names: dict[int, str] = {}
|
|
272
|
+
selected = None
|
|
273
|
+
for index in range(len(task_list.slots) + 1):
|
|
274
|
+
task_address = gdb.Value(table_address + index * task_type.sizeof)
|
|
275
|
+
if task_address is None:
|
|
276
|
+
raise ValueError("Cannot resolve task table address")
|
|
277
|
+
task = task_address.cast(task_type.pointer()).dereference()
|
|
278
|
+
metadata_address = int(task["metadata"])
|
|
279
|
+
if not metadata_address:
|
|
280
|
+
continue
|
|
281
|
+
label = int(task["metadata"].dereference()["label"])
|
|
282
|
+
source_name = next(
|
|
283
|
+
(
|
|
284
|
+
candidate.task_name
|
|
285
|
+
for candidate in task_list.slots
|
|
286
|
+
if candidate.task_name is not None and candidate.metadata.get("label") == label
|
|
287
|
+
),
|
|
288
|
+
"idle" if label == 0xCAFE else f"label 0x{label:X}",
|
|
289
|
+
)
|
|
290
|
+
source_names[index] = source_name
|
|
291
|
+
if (slot is not None and metadata_address == slot.address) or (
|
|
292
|
+
task_name == "idle" and label == 0xCAFE
|
|
293
|
+
):
|
|
294
|
+
selected = task
|
|
295
|
+
if selected is None:
|
|
296
|
+
raise ValueError(f"Task {task_name} is not initialized in task_table")
|
|
297
|
+
|
|
298
|
+
def table(title: str, columns: tuple[str, ...]) -> Table:
|
|
299
|
+
result = Table(title=title, box=box.SIMPLE_HEAVY, header_style="bold cyan")
|
|
300
|
+
for column in columns:
|
|
301
|
+
result.add_column(column)
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
mapping = table(f"Task: {task_name}", ("Region", "Address range", "Access"))
|
|
305
|
+
ram_start = ram_end = 0
|
|
306
|
+
text_start = text_end = 0
|
|
307
|
+
for region in regions:
|
|
308
|
+
start = int(region["start_address"], 0)
|
|
309
|
+
end = start + int(region["size"], 0)
|
|
310
|
+
if region["type"] == "ram":
|
|
311
|
+
ram_start, ram_end = start, end
|
|
312
|
+
if region["type"] == "text":
|
|
313
|
+
text_start, text_end = start, end
|
|
314
|
+
permission = int(region["permission"])
|
|
315
|
+
access = "".join(
|
|
316
|
+
flag if permission & bit else "-" for bit, flag in ((1, "R"), (2, "W"), (4, "X"))
|
|
317
|
+
)
|
|
318
|
+
mapping.add_row(region["type"], f"0x{start:08X}-0x{end:08X}", access)
|
|
319
|
+
|
|
320
|
+
state_number = int(selected["state"])
|
|
321
|
+
state = next(
|
|
322
|
+
(
|
|
323
|
+
field.name
|
|
324
|
+
for field in gdb.lookup_type("job_state_t").fields()
|
|
325
|
+
if field.enumval == state_number
|
|
326
|
+
),
|
|
327
|
+
f"unknown ({state_number})",
|
|
328
|
+
)
|
|
329
|
+
context = table("Kernel context", ("Field", "Value"))
|
|
330
|
+
context.add_row("State", state)
|
|
331
|
+
handle_address = selected["handle"].address
|
|
332
|
+
if handle_address is None:
|
|
333
|
+
raise ValueError("Cannot resolve task handle address")
|
|
334
|
+
context.add_row(
|
|
335
|
+
"Handle",
|
|
336
|
+
f"0x{int(handle_address.cast(gdb.lookup_type('uint32_t').pointer()).dereference()):08X}",
|
|
337
|
+
)
|
|
338
|
+
saved_sp = int(selected["sp"])
|
|
339
|
+
context.add_row("Saved SP", f"0x{saved_sp:08X}")
|
|
340
|
+
|
|
341
|
+
events = table("Pending events", ("Type", "Source", "Value"))
|
|
342
|
+
for field_name in ("ipcs", "sigs"):
|
|
343
|
+
for index in range(
|
|
344
|
+
selected[field_name].type.sizeof // selected[field_name].type.target().sizeof
|
|
345
|
+
):
|
|
346
|
+
value = int(selected[field_name][index])
|
|
347
|
+
if value:
|
|
348
|
+
events.add_row(
|
|
349
|
+
"IPC" if field_name == "ipcs" else "Signal",
|
|
350
|
+
source_names.get(index, f"task #{index}"),
|
|
351
|
+
str(value),
|
|
352
|
+
)
|
|
353
|
+
irq_names = _interrupt_names()
|
|
354
|
+
fields = {field.name for field in task_type.fields()}
|
|
355
|
+
|
|
356
|
+
def queued(field: str, head: str, bottom: str) -> list[gdb.Value]:
|
|
357
|
+
values = selected[field]
|
|
358
|
+
length = values.type.sizeof // values.type.target().sizeof
|
|
359
|
+
cursor, stop = int(selected[bottom]), int(selected[head])
|
|
360
|
+
if cursor >= length or stop >= length:
|
|
361
|
+
raise ValueError(f"Invalid {field} queue indices")
|
|
362
|
+
items = []
|
|
363
|
+
while cursor != stop:
|
|
364
|
+
items.append(values[cursor])
|
|
365
|
+
cursor = (cursor + 1) % length
|
|
366
|
+
return items
|
|
367
|
+
|
|
368
|
+
for event in queued("ints", "ints_head", "ints_bottom"):
|
|
369
|
+
number = int(event)
|
|
370
|
+
events.add_row("IRQ", str(number), irq_names.get(number, "unresolved"))
|
|
371
|
+
if {"dmas", "dmas_head", "dmas_bottom"} <= fields:
|
|
372
|
+
for event in queued("dmas", "dmas_head", "dmas_bottom"):
|
|
373
|
+
events.add_row("DMA", f"0x{int(event['handle']):X}", str(event["event"]))
|
|
374
|
+
if not events.rows:
|
|
375
|
+
events.add_row("None", "", "")
|
|
376
|
+
|
|
377
|
+
stack = table("Stack", ("Measure", "Value"))
|
|
378
|
+
metadata = selected["metadata"].dereference()
|
|
379
|
+
stack_size = int(metadata["stack_size"])
|
|
380
|
+
config_paths = list(
|
|
381
|
+
(project_path / "output/build/kernel/subprojects").glob("kconfig-*/generated_kconfig.json")
|
|
382
|
+
)
|
|
383
|
+
if len(config_paths) != 1:
|
|
384
|
+
raise ValueError("Expected one generated kernel Kconfig file")
|
|
385
|
+
config_path = config_paths[0]
|
|
386
|
+
with config_path.open(encoding="utf-8") as config_file:
|
|
387
|
+
config = json.load(config_file)
|
|
388
|
+
svc_size = int(config["CONFIG_SVC_EXCHANGE_AREA_LEN"])
|
|
389
|
+
stack_bottom = (
|
|
390
|
+
ram_start
|
|
391
|
+
+ svc_size
|
|
392
|
+
+ sum(
|
|
393
|
+
(int(metadata[field]) + 3) & ~3
|
|
394
|
+
for field in ("got_size", "data_size", "bss_size", "heap_size")
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
stack_top = stack_bottom + ((stack_size + 3) & ~3)
|
|
398
|
+
pc = int(gdb.selected_frame().pc())
|
|
399
|
+
current = text_start <= pc < text_end
|
|
400
|
+
stack_pointer = int(gdb.parse_and_eval("$psp")) if current else saved_sp
|
|
401
|
+
if not ram_start or stack_top > ram_end or not stack_bottom <= stack_pointer <= stack_top:
|
|
402
|
+
stack.add_row("Usage", "Unavailable (saved SP outside stack bounds)")
|
|
403
|
+
else:
|
|
404
|
+
used = stack_top - stack_pointer
|
|
405
|
+
stack.add_row("Bounds", f"0x{stack_bottom:08X}-0x{stack_top:08X}")
|
|
406
|
+
stack.add_row("SP", f"0x{stack_pointer:08X} ({'live PSP' if current else 'saved'})")
|
|
407
|
+
stack.add_row("Usage", f"{used}/{stack_size} bytes ({used / stack_size:.1%})")
|
|
408
|
+
|
|
409
|
+
frames = table("Backtrace", ("Frame", "Location"))
|
|
410
|
+
# GDB can unwind the running task only when it is stopped in its own code.
|
|
411
|
+
if current:
|
|
412
|
+
for line in gdb.execute("bt", to_string=True).splitlines():
|
|
413
|
+
frames.add_row("GDB", line)
|
|
414
|
+
elif stack_bottom <= saved_sp < stack_top and task_name != "idle":
|
|
415
|
+
frame = selected["sp"].dereference()
|
|
416
|
+
for field in ("pc", "prev_lr", "lr"):
|
|
417
|
+
address = int(frame[field])
|
|
418
|
+
symbol = _task_symbol(address, text_start, text_end)
|
|
419
|
+
frames.add_row(
|
|
420
|
+
f"saved {field}", f"0x{address:08X} {symbol}" if symbol else f"0x{address:08X}"
|
|
421
|
+
)
|
|
422
|
+
frames.add_row(
|
|
423
|
+
"Note", "Full unwinding of an inactive task requires its register context in GDB"
|
|
424
|
+
)
|
|
425
|
+
else:
|
|
426
|
+
frames.add_row("Note", "No saved frame available for this task")
|
|
427
|
+
return Group(mapping, context, stack, events, frames)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def show_project(project_path: Path, task_list: TaskList) -> Group:
|
|
431
|
+
"""Render the kernel, task mappings and decoded build-time metadata."""
|
|
432
|
+
layout_path = project_path / "output" / "build" / "camelot_private" / "layout.json"
|
|
433
|
+
with layout_path.open(encoding="utf-8") as layout_file:
|
|
434
|
+
layout = json.load(layout_file)
|
|
435
|
+
regions = layout["regions"]
|
|
436
|
+
if not isinstance(regions, list):
|
|
437
|
+
raise ValueError("layout.json: regions must be a list")
|
|
438
|
+
|
|
439
|
+
owners: dict[str, list[tuple[str, str, str]]] = {}
|
|
440
|
+
for region in regions:
|
|
441
|
+
name = region["name"]
|
|
442
|
+
start = int(region["start_address"], 0)
|
|
443
|
+
size = int(region["size"], 0)
|
|
444
|
+
permission = int(region["permission"])
|
|
445
|
+
access = "".join(
|
|
446
|
+
flag if permission & bit else "-" for bit, flag in ((1, "R"), (2, "W"), (4, "X"))
|
|
447
|
+
)
|
|
448
|
+
owners.setdefault(name, []).append(
|
|
449
|
+
(region["type"], f"0x{start:08X}-0x{start + size:08X}", access)
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
if "kernel" not in owners:
|
|
453
|
+
raise ValueError("layout.json: kernel mapping is missing")
|
|
454
|
+
|
|
455
|
+
def mapping_table(title: str, mapping: list[tuple[str, str, str]]) -> Table:
|
|
456
|
+
table = Table(title=Text(title), box=box.SIMPLE_HEAVY, header_style="bold cyan")
|
|
457
|
+
table.add_column("Region", style="bold")
|
|
458
|
+
table.add_column("Address range")
|
|
459
|
+
table.add_column("Access")
|
|
460
|
+
for region_type, address_range, access in mapping:
|
|
461
|
+
table.add_row(region_type, address_range, access)
|
|
462
|
+
return table
|
|
463
|
+
|
|
464
|
+
slots_by_name: dict[str, list[TaskSlot]] = {}
|
|
465
|
+
for slot in task_list.slots:
|
|
466
|
+
if slot.task_name is not None:
|
|
467
|
+
slots_by_name.setdefault(slot.task_name, []).append(slot)
|
|
468
|
+
|
|
469
|
+
output: list[Table | Text] = [mapping_table("Kernel", owners.pop("kernel"))]
|
|
470
|
+
output.append(
|
|
471
|
+
Text(
|
|
472
|
+
f".task_list: 0x{task_list.address:08X}-0x{task_list.address + task_list.size:08X} "
|
|
473
|
+
f"({len(task_list.slots)} slots, {task_list.entry_size} bytes each)",
|
|
474
|
+
style="cyan",
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
for name, mapping in owners.items():
|
|
478
|
+
output.append(mapping_table(f"Task: {name}", mapping))
|
|
479
|
+
for slot in slots_by_name.get(name, []):
|
|
480
|
+
table = Table(
|
|
481
|
+
title=f"Metadata: {name} (slot {slot.index} at 0x{slot.address:08X})",
|
|
482
|
+
box=box.SIMPLE_HEAVY,
|
|
483
|
+
header_style="bold cyan",
|
|
484
|
+
)
|
|
485
|
+
table.add_column("Field", style="bold")
|
|
486
|
+
table.add_column("Value", overflow="fold")
|
|
487
|
+
for field_name, value in slot.metadata.items():
|
|
488
|
+
if isinstance(value, tuple):
|
|
489
|
+
count = slot.metadata[f"num_{field_name}"]
|
|
490
|
+
rendered = ", ".join(f"0x{item:X}" for item in value)
|
|
491
|
+
if isinstance(count, int):
|
|
492
|
+
rendered += f" ({count} active)"
|
|
493
|
+
elif isinstance(value, str):
|
|
494
|
+
rendered = value
|
|
495
|
+
elif field_name in {
|
|
496
|
+
"magic",
|
|
497
|
+
"label",
|
|
498
|
+
"capabilities",
|
|
499
|
+
"flags",
|
|
500
|
+
"s_text",
|
|
501
|
+
"s_got",
|
|
502
|
+
"s_svcexchange",
|
|
503
|
+
"entrypoint_offset",
|
|
504
|
+
"finalize_offset",
|
|
505
|
+
}:
|
|
506
|
+
rendered = f"0x{value:X}"
|
|
507
|
+
else:
|
|
508
|
+
rendered = str(value)
|
|
509
|
+
table.add_row(field_name, rendered)
|
|
510
|
+
output.append(table)
|
|
511
|
+
if name not in slots_by_name:
|
|
512
|
+
output.append(Text("No task metadata in .task_list", style="dim"))
|
|
513
|
+
return Group(*output)
|