tools: rename tools/gdb/nuttxgdb to tools/pynuttx/nxgdb

Signed-off-by: yinshengkai <yinshengkai@xiaomi.com>
This commit is contained in:
yinshengkai
2024-11-26 16:38:06 +08:00
committed by Xiang Xiao
parent 08fe636001
commit 50fb43e23b
32 changed files with 54 additions and 36 deletions
+2
View File
@@ -0,0 +1,2 @@
dist/
nxgdb.egg-info/
+37
View File
@@ -0,0 +1,37 @@
############################################################################
# tools/pynuttx/gdbinit.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import sys
from os import path
here = path.dirname(path.abspath(__file__))
if __name__ == "__main__":
if here not in sys.path:
sys.path.insert(0, here)
if "nxgdb" in sys.modules:
for key in list(sys.modules.keys()):
if key.startswith("nxgdb"):
del sys.modules[key]
import nxgdb # noqa: F401
+87
View File
@@ -0,0 +1,87 @@
############################################################################
# tools/pynuttx/nxgdb/__init__.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import importlib
import traceback
from os import path
import gdb
if __name__ == "__main__":
import sys
gdb.write("Use nuttx/tools/pynuttx/gdbinit.py instead")
sys.exit(1)
here = path.dirname(path.abspath(__file__))
def register_commands(event):
if getattr(register_commands, "registered", False):
return
register_commands.registered = True
gdb.write(f"Registering NuttX GDB commands from {here}\n")
gdb.execute("set pagination off")
gdb.write("set pagination off\n")
gdb.execute("set python print-stack full")
gdb.write("set python print-stack full\n")
gdb.execute('handle SIGUSR1 "nostop" "pass" "noprint"')
gdb.write('"handle SIGUSR1 "nostop" "pass" "noprint"\n')
def init_gdb_commands(m: str):
try:
module = importlib.import_module(f"{__package__}.{m}")
except Exception as e:
gdb.write(f"\x1b[31;1mIgnore module: {m}, error: {e}\n\x1b[m")
traceback.print_exc()
return
for c in module.__dict__.values():
if isinstance(c, type) and issubclass(c, gdb.Command):
try:
c()
except Exception as e:
gdb.write(f"\x1b[31;1mIgnore command: {c}, e: {e}\n\x1b[m")
# import utils module
utils = importlib.import_module(f"{__package__}.utils")
modules = utils.gather_modules(here)
# Register prefix commands firstly
init_gdb_commands("prefix")
modules.remove("prefix")
modules.remove("__init__")
# Register all other modules
for m in modules:
init_gdb_commands(m)
utils.check_version()
if len(gdb.objfiles()) == 0:
gdb.write("No objectfile, defer to register NuttX commands.\n")
gdb.events.new_objfile.connect(register_commands)
else:
register_commands(None)
+64
View File
@@ -0,0 +1,64 @@
############################################################################
# tools/pynuttx/nxgdb/debug.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import argparse
import gdb
from . import utils
class DebugPy(gdb.Command):
"""Start debugpy server, so we can debug python code from IDE like VSCode"""
def __init__(self):
debugpy = utils.import_check("debugpy", errmsg="Please pip install debugpy")
if not debugpy:
return
self.debugpy = debugpy
super().__init__("debugpy", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
debugpy = self.debugpy
if debugpy.is_client_connected():
gdb.write("debugpy is already running.\n")
return
parser = argparse.ArgumentParser(description=DebugPy.__doc__)
parser.add_argument(
"-p",
"--port",
default=5678,
type=int,
help="Server listening port",
)
try:
args = parser.parse_args(gdb.string_to_argv(args))
except SystemExit:
return
debugpy.listen(args.port)
gdb.write(f"Waiting for connection at localhost:{args.port}\n")
debugpy.wait_for_client()
gdb.write("Debugger connected.\n")
+93
View File
@@ -0,0 +1,93 @@
############################################################################
# tools/pynuttx/nxgdb/diagnose.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import argparse
import gdb
from . import utils
class DiagnosePrefix(gdb.Command):
"""Diagnostic related commands."""
def __init__(self):
super().__init__("diagnose", gdb.COMMAND_USER, prefix=True)
class DiagnoseReport(gdb.Command):
"""Run diagnostics to generate reports."""
def __init__(self):
super().__init__("diagnose report", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
parser = argparse.ArgumentParser(description=self.__doc__)
parser.add_argument(
"-o",
"--output",
type=str,
help="report output file name",
)
try:
args = parser.parse_args(gdb.string_to_argv(args))
except SystemExit:
return
reportfile = (
args.output
if args.output
else gdb.objfiles()[0].filename + ".diagnostics.json"
)
modules = utils.gather_modules()
modules.remove("prefix")
modules.remove("__init__")
commands = utils.gather_gdbcommands(modules=modules)
results = []
for clz in commands:
if hasattr(clz, "diagnose"):
command = clz()
name = clz.__name__.lower()
gdb.write(f"Run command: {name}\n")
try:
result = command.diagnose()
except gdb.error as e:
result = {
"title": f"Command {name} failed",
"summary": "Command execution failed",
"result": "info",
"command": name,
"message": str(e),
}
gdb.write(f"Failed: {e}\n")
result.setdefault("command", name)
results.append(result)
gdb.write(f"Write report to {reportfile}\n")
with open(reportfile, "w") as f:
f.write(utils.jsonify(results, indent=4))
+71
View File
@@ -0,0 +1,71 @@
############################################################################
# tools/pynuttx/nxgdb/dmesg.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import gdb
from . import utils
CONFIG_RAMLOG_SYSLOG = utils.get_symbol_value("CONFIG_RAMLOG_SYSLOG")
class Dmesg(gdb.Command):
def __init__(self):
if CONFIG_RAMLOG_SYSLOG:
super().__init__("dmesg", gdb.COMMAND_USER)
def _get_buf(self):
sysdev = utils.gdb_eval_or_none("g_sysdev")
if not sysdev:
gdb.write("RAM log not available.\n")
return None
rl_header = sysdev["rl_header"]
rl_bufsize = sysdev["rl_bufsize"]
offset = rl_header["rl_head"] % rl_bufsize # Currently writing to this offset
tail = rl_bufsize - offset # Total size till buffer end.
rl_buffer = int(rl_header["rl_buffer"].address) # rl_buffer is a char array
inf = gdb.selected_inferior()
buf = bytes(inf.read_memory(offset + rl_buffer, tail))
buf = buf.lstrip(b"\0") # Remove leading NULLs
buf += bytes(inf.read_memory(rl_buffer, offset))
buf = buf.replace(
b"\0", "".encode("utf-8")
) # NULL is valid utf-8 but not printable
return buf.decode("utf-8", errors="replace")
def diagnose(self, *args, **kwargs):
buf = self._get_buf()
return {
"title": "RAM log",
"summary": (
f"Buffer length:{len(buf)} bytes" if buf else "Buffer not available"
),
"result": "info",
"command": "dmesg",
"message": buf,
}
def invoke(self, args, from_tty):
gdb.write(f"{self._get_buf()}\n")
+461
View File
@@ -0,0 +1,461 @@
############################################################################
# tools/pynuttx/nxgdb/fs.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import argparse
import enum
from typing import Generator, Tuple
import gdb
from . import utils
from .protocols.fs import File, Inode
from .protocols.thread import Tcb
FSNODEFLAG_TYPE_MASK = utils.get_symbol_value("FSNODEFLAG_TYPE_MASK")
CONFIG_PSEUDOFS_FILE = utils.get_symbol_value("CONFIG_PSEUDOFS_FILE")
CONFIG_PSEUDOFS_ATTRIBUTES = utils.get_symbol_value("CONFIG_PSEUDOFS_ATTRIBUTES")
CONFIG_FS_BACKTRACE = utils.get_symbol_value("CONFIG_FS_BACKTRACE")
CONFIG_NFILE_DESCRIPTORS_PER_BLOCK = int(
utils.get_symbol_value("CONFIG_NFILE_DESCRIPTORS_PER_BLOCK")
)
CONFIG_FS_SHMFS = utils.get_symbol_value("CONFIG_FS_SHMFS")
g_special_inodes = {} # Map of the special inodes including epoll, inotify, etc.
class InodeType(enum.Enum):
# define FSNODEFLAG_TYPE_PSEUDODIR 0x00000000 /* Pseudo dir (default) */
# define FSNODEFLAG_TYPE_DRIVER 0x00000001 /* Character driver */
# define FSNODEFLAG_TYPE_BLOCK 0x00000002 /* Block driver */
# define FSNODEFLAG_TYPE_MOUNTPT 0x00000003 /* Mount point */
# define FSNODEFLAG_TYPE_NAMEDSEM 0x00000004 /* Named semaphore */
# define FSNODEFLAG_TYPE_MQUEUE 0x00000005 /* Message Queue */
# define FSNODEFLAG_TYPE_SHM 0x00000006 /* Shared memory region */
# define FSNODEFLAG_TYPE_MTD 0x00000007 /* Named MTD driver */
# define FSNODEFLAG_TYPE_SOFTLINK 0x00000008 /* Soft link */
# define FSNODEFLAG_TYPE_SOCKET 0x00000009 /* Socket */
# define FSNODEFLAG_TYPE_PIPE 0x0000000a /* Pipe */
# define FSNODEFLAG_TYPE_NAMEDEVENT 0x0000000b /* Named event group */
PSEUDODIR = 0
DRIVER = 1
BLOCK = 2
MOUNTPT = 3
NAMEDSEM = 4
MQUEUE = 5
SHM = 6
MTD = 7
SOFTLINK = 8
SOCKET = 9
PIPE = 10
NAMEDEVENT = 11
UNKNOWN = 12
def get_inode_name(inode: Inode):
if not inode:
return ""
def special_inode_name(inode):
global g_special_inodes
if not g_special_inodes:
g_special_inodes = {}
for special in (
"perf",
"timerfd",
"signalfd",
"dir",
"inotify",
"epoll",
"eventfd",
"sock",
):
value = utils.gdb_eval_or_none(f"g_{special}_inode")
if value:
g_special_inodes[special] = value.address
for name, value in g_special_inodes.items():
if value == inode:
return name
return None
if name := special_inode_name(inode):
return name
ptr = inode.i_name.cast(gdb.lookup_type("char").pointer())
return ptr.string()
def inode_getpath(inode: Inode):
"""get path fron inode"""
if not inode:
return ""
name = get_inode_name(inode)
if inode.i_parent:
return inode_getpath(inode.i_parent) + "/" + name
return name
def inode_gettype(inode: Inode) -> InodeType:
if not inode:
return InodeType.UNKNOWN
type = int(inode.i_flags & FSNODEFLAG_TYPE_MASK)
# check if it's a valid type in InodeType
if type in [e.value for e in InodeType]:
return InodeType(type)
return InodeType.UNKNOWN
def get_file(tcb: Tcb, fd):
group = tcb.group
filelist = group.tg_filelist
fl_files = filelist.fl_files
fl_rows = filelist.fl_rows
row = fd // CONFIG_NFILE_DESCRIPTORS_PER_BLOCK
col = fd % CONFIG_NFILE_DESCRIPTORS_PER_BLOCK
if row >= fl_rows:
return None
return fl_files[row][col]
def foreach_inode(root=None, path="") -> Generator[Tuple[Inode, str], None, None]:
node: Inode = root or utils.parse_and_eval("g_root_inode").i_child
while node:
newpath = path + "/" + get_inode_name(node)
yield node, newpath
if node.i_child:
yield from foreach_inode(node.i_child, newpath)
node = node.i_peer
def foreach_file(tcb: Tcb):
"""Iterate over all file descriptors in a tcb"""
group = tcb.group
filelist = group.tg_filelist
fl_files = filelist.fl_files
fl_rows = filelist.fl_rows
for row in range(fl_rows):
for col in range(CONFIG_NFILE_DESCRIPTORS_PER_BLOCK):
file = fl_files[row][col]
if not file or not file.f_inode:
continue
fd = row * CONFIG_NFILE_DESCRIPTORS_PER_BLOCK + col
yield fd, file
class Fdinfo(gdb.Command):
"""Dump fd info information of process"""
def __init__(self):
super().__init__("fdinfo", gdb.COMMAND_DATA, gdb.COMPLETE_EXPRESSION)
self.total_fd_count = 0
def print_file_info(self, fd, file: File, formatter: str):
backtrace_formatter = "{0:<5} {1:<36} {2}"
oflags = int(file.f_oflags)
pos = int(file.f_pos)
path = inode_getpath(file.f_inode)
output = []
if CONFIG_FS_BACKTRACE:
backtrace = utils.Backtrace(
utils.ArrayIterator(file.f_backtrace), formatter=backtrace_formatter
)
output.append(
formatter.format(
fd, hex(file.address), oflags, pos, path, backtrace.formatted[0]
)
)
output.extend(
formatter.format("", "", "", "", "", bt)
for bt in backtrace.formatted[1:]
)
output.append("") # separate each backtrace
else:
output = [
formatter.format(fd, hex(file.address), hex(oflags), pos, path, "")
]
gdb.write("\n".join(output).rstrip()) # strip trailing spaces
gdb.write("\n")
def print_fdinfo_by_tcb(self, tcb):
"""print fdlist from tcb"""
gdb.write(f"PID: {tcb['pid']}\n")
group = tcb.group
if not group:
return
if group in self.processed_groups:
return
self.processed_groups.add(group)
headers = ["FD", "FILEP", "OFLAGS", "POS", "PATH", "BACKTRACE"]
formatter = "{:<4}{:<12}{:<12}{:<10}{:<48}{:<50}"
gdb.write(formatter.format(*headers) + "\n")
fd_count = 0
for fd, file in foreach_file(tcb):
self.print_file_info(fd, file, formatter)
fd_count += 1
self.total_fd_count += fd_count
gdb.write("\n")
def diagnose(self, *args, **kwargs):
output = gdb.execute("fdinfo", to_string=True)
return {
"title": "fdinfo report",
"summary": f"Total files opened:{self.total_fd_count}",
"result": "info",
"command": "fdinfo",
"message": output,
}
def invoke(self, arg, from_tty):
parser = argparse.ArgumentParser(
description="Get fdinfo for a process or all processes."
)
parser.add_argument("-p", "--pid", type=int, help="Optional process ID")
try:
args = parser.parse_args(gdb.string_to_argv(arg))
except SystemExit:
gdb.write("Invalid arguments.\n")
return
self.processed_groups = set()
self.total_fd_count = 0
tcbs = [utils.get_tcb(args.pid)] if args.pid else utils.get_tcbs()
for tcb in tcbs:
self.print_fdinfo_by_tcb(tcb)
class Mount(gdb.Command):
def __init__(self):
if not utils.get_symbol_value("CONFIG_DISABLE_MOUNTPOINT"):
super().__init__("mount", gdb.COMMAND_USER)
self.mount_count = 0
def diagnose(self, *args, **kwargs):
output = gdb.execute("mount", to_string=True)
return {
"title": "File system mount information",
"summary": f"Total {self.mount_count} mount points",
"command": "mount",
"result": "info",
"message": output or "No mount",
}
def invoke(self, args, from_tty):
self.mount_count = 0
nodes = filter(
lambda x: inode_gettype(x[0]) == InodeType.MOUNTPT, foreach_inode()
)
for node, path in nodes:
statfs = node.u.i_mops.statfs
funcname = gdb.block_for_pc(int(statfs)).function.print_name
fstype = funcname.split("_")[0]
gdb.write(" %s type %s\n" % (path, fstype))
self.mount_count += 1
class ForeachInode(gdb.Command):
"""Dump each inode info"""
def __init__(self):
super().__init__("foreach inode", gdb.COMMAND_USER)
self.level = 4096
def get_root_inode(self, addr_or_expr):
try:
return utils.Value(int(addr_or_expr, 0)).cast(
gdb.lookup_type("struct inode").pointer()
)
except ValueError:
return utils.gdb_eval_or_none(addr_or_expr)
def parse_arguments(self, argv):
parser = argparse.ArgumentParser(description="foreach inode command")
parser.add_argument(
"-L",
"--level",
type=int,
default=None,
help="Only render the tree to a specific depth",
)
parser.add_argument(
"addr_or_expr",
type=str,
nargs="?",
default=None,
help="set the start inode to be tranversed",
)
try:
args = parser.parse_args(argv)
except SystemExit:
return None
return {
"level": args.level if args.level else 4096,
"root_inode": (
self.get_root_inode(args.addr_or_expr)
if args.addr_or_expr
else utils.gdb_eval_or_none("g_root_inode")
),
}
def print_inode_info(self, node: Inode, level, prefix):
if level > self.level:
return
while node:
if node.i_peer:
initial_indent = prefix + "├── "
subsequent_indent = prefix + ""
newprefix = prefix + ""
else:
initial_indent = prefix + "└── "
subsequent_indent = prefix + " "
newprefix = prefix + " "
gdb.write(
"%s [%s], %s, %s\n"
% (initial_indent, get_inode_name(node), node.i_ino, node)
)
gdb.write(
"%s i_crefs: %s, i_flags: %s, i_private: %s\n"
% (
subsequent_indent,
node.i_crefs,
node.i_flags,
node.i_private,
)
)
if CONFIG_PSEUDOFS_FILE:
gdb.write("%s i_size: %s\n" % (subsequent_indent, node.i_size))
if CONFIG_PSEUDOFS_ATTRIBUTES:
gdb.write(
"%s i_mode: %s, i_owner: %s, i_group: %s\n"
% (
subsequent_indent,
node.i_mode,
node.i_owner,
node.i_group,
)
)
gdb.write(
"%s i_atime: %s, i_mtime: %s, i_ctime: %s\n"
% (
subsequent_indent,
node.i_atime,
node.i_mtime,
node.i_ctime,
)
)
if node.i_child:
self.print_inode_info(node.i_child, level + 1, newprefix)
node = node.i_peer
def diagnose(self, *args, **kwargs):
output = gdb.execute("foreach inode", to_string=True)
return {
"title": "File node information",
"summary": "inode formation dump",
"command": "foreach inode",
"result": "info",
"message": output,
}
def invoke(self, args, from_tty):
arg = self.parse_arguments(args.split(" "))
if not arg:
return
self.level = arg["level"]
self.print_inode_info(arg["root_inode"], 1, "")
class InfoShmfs(gdb.Command):
"""Show share memory usage"""
def __init__(self):
if CONFIG_FS_SHMFS:
super().__init__("info shm", gdb.COMMAND_USER)
self.total_size = 0
self.block_count = 0
def shm_filter(self, node: Inode, path):
if inode_gettype(node) != InodeType.SHM:
return
obj = node.i_private.cast(gdb.lookup_type("struct shmfs_object_s").pointer())
length = obj.length
paddr = obj.paddr
print(f" {path} memsize: {length}, paddr: {paddr}")
self.total_size += length / 1024
self.block_count += 1
def diagnose(self, *args, **kwargs):
output = gdb.execute("info shm", to_string=True)
return {
"title": "Share memory usage",
"summary": f"Total used:{self.total_size}kB, {self.block_count}blocks",
"result": "info",
"command": "info shm",
"message": output or "No InfoShmfs",
}
def invoke(self, args, from_tty):
self.total_size = 0
self.block_count = 0
nodes = filter(lambda x: inode_gettype(x[0]) == InodeType.SHM, foreach_inode())
for node, path in nodes:
obj = node.i_private.cast(
gdb.lookup_type("struct shmfs_object_s").pointer()
)
length = obj.length
paddr = obj.paddr
print(f" {path} memsize: {length}, paddr: {paddr}")
self.total_size += length / 1024
self.block_count += 1
+160
View File
@@ -0,0 +1,160 @@
############################################################################
# tools/pynuttx/nxgdb/gcore.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import argparse
import os
import shutil
import gdb
from . import utils
def create_file_with_size(filename, size):
with open(filename, "wb") as f:
f.write(b"\0" * size)
def parse_args(args):
parser = argparse.ArgumentParser()
parser.add_argument("-o", "--output", help="Gcore output file")
parser.add_argument(
"-t",
"--objcopy",
help="Select the appropriate architecture for the objcopy tool",
type=str,
)
parser.add_argument(
"--trust-readonly",
help="Trust readonly section from elf, bypass read from device.",
action="store_true",
default=True,
)
parser.add_argument(
"--no-trust-readonly",
help="Do not trust readonly section from elf, read from device.",
action="store_false",
dest="trust_readonly",
)
parser.add_argument(
"-r",
"--memrange",
type=str,
)
return parser.parse_args(args)
class NXGcore(gdb.Command):
def __init__(self):
self.tempfile = utils.import_check(
"tempfile",
errmsg="No tempfile module found, please try gdb-multiarch instead.\n",
)
if not self.tempfile:
return
super().__init__("nxgcore", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
try:
args = parse_args(gdb.string_to_argv(args))
except SystemExit:
return
objfile = gdb.current_progspace().objfiles()[0]
elffile = objfile.filename
tmpfile = self.tempfile.NamedTemporaryFile(suffix=".elf")
# Create temporary ELF file with sections for each memory region
shutil.copy(elffile, tmpfile.name)
# If no parameters are passed in
if args.output:
corefile = args.output
else:
corefile = elffile + ".core"
# Obtain memory range from macros or parameters
if args.memrange:
memregion = args.memrange
else:
memregion = str(utils.get_symbol_value("CONFIG_BOARD_MEMORY_RANGE"))
if len(memregion) < 3:
print(
"Please set CONFIG_BOARD_MEMORY_RANGE.\n"
"Memory range of board. format: <start>,<end>,<flags>,....\n"
"start: start address of memory range\n"
"end: end address of memory range\n"
"flags: Readable 0x1, writable 0x2, executable 0x4\n"
"example:0x1000,0x2000,0x1,0x2000,0x3000,0x3,0x3000,0x4000,0x7"
)
return
# Resolve memory range and shell run commands
values = memregion.replace('"', "").split(",")
for i in range(0, len(values), 3):
start = int(values[i], 16)
end = int(values[i + 1], 16)
# Create a random section name
section = tmpfile.name + f"{i // 3}"
# Add objcopy insertion segment command and modify segment start address command
create_file_with_size(section, end - start)
os.system(
f"{args.objcopy} --add-section {section}={section} "
f"--set-section-flags {section}=noload,alloc {tmpfile.name}"
)
os.system(
f"{args.objcopy} --change-section-address "
f"{section}={hex(start)} {tmpfile.name}"
)
os.remove(section)
gdb.execute(f"file {tmpfile.name}")
if args.trust_readonly:
gdb.execute("set trust-readonly-sections on")
gdb.execute(f"gcore {corefile}")
if args.trust_readonly:
# Restore trust-readonly-sections to default off state
gdb.execute("set trust-readonly-sections off")
gdb.execute(f'file "{elffile}"')
tmpfile.close()
print(f"Please run gdbserver.py to parse {corefile}")
+421
View File
@@ -0,0 +1,421 @@
############################################################################
# tools/pynuttx/nxgdb/lists.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import argparse
import gdb
from . import utils
list_node_type = utils.lookup_type("struct list_node")
sq_queue_type = utils.lookup_type("sq_queue_t")
sq_entry_type = utils.lookup_type("sq_entry_t")
dq_queue_type = utils.lookup_type("dq_queue_t")
class NxList:
def __init__(self, list, container_type=None, member=None, reverse=False):
"""Initialize the list iterator. Optionally specify the container type and member name."""
if not list:
raise ValueError("The head cannot be None.\n")
if list.type.code != gdb.TYPE_CODE_PTR:
list = list.address # Make sure list is a pointer.
if container_type and not member:
raise ValueError("Must specify the member name in container.\n")
self.list = list
self.reverse = reverse
self.container_type = container_type
self.member = member
self.current = self._get_first()
def _get_first(self):
"""Get the initial node based on the direction of traversal."""
prev = self.list["prev"]
next = self.list["next"]
first = prev if self.reverse else next
return first if first and first != self.list else None
def _get_next(self, node):
# for(node = (list)->next; node != (list); node = node->next)
return node["next"] if node["next"] != self.list else None
def _get_prev(self, node):
# for(node = (list)->next; node != (list); node = node->prev)
return node["prev"] if node["prev"] != self.list else None
def __iter__(self):
return self
def __next__(self):
if self.current is None:
raise StopIteration
node = self.current
self.current = self._get_prev(node) if self.reverse else self._get_next(node)
return (
utils.container_of(node, self.container_type, self.member)
if self.container_type
else node
)
class NxSQueue(NxList):
def __init__(self, list, container_type=None, member=None, reverse=False):
"""Initialize the singly linked list iterator. Optionally specify the container type and member name."""
if reverse:
raise ValueError(
"Reverse iteration is not supported for singly linked lists.\n"
)
super().__init__(list, container_type, member, reverse)
def _get_first(self):
# for ((p) = (q)->head; (p) != NULL; (p) = (p)->flink)
return self.list["head"] or None
def _get_next(self, node):
# if not node["flink"], then return None, to indicate end of list
return node["flink"] or None
class NxDQueue(NxList):
def __init__(self, list, container_type=None, member=None, reverse=False):
"""Initialize the doubly linked list iterator. Optionally specify the container type and member name."""
super().__init__(list, container_type, member, reverse)
def _get_first(self):
head = self.list["head"]
tail = self.list["tail"]
first = head if not self.reverse else tail
return first or None
def _get_next(self, node):
# for ((p) = (q)->head; (p) != NULL; (p) = (p)->flink)
return node["flink"] or None
def _get_prev(self, node):
# for ((p) = (q)->tail; (p) != NULL; (p) = (p)->blink)
return node["blink"] or None
def list_check(head):
"""Check the consistency of a list"""
nb = 0
if head.type == list_node_type.pointer():
head = head.dereference()
elif head.type != list_node_type:
raise gdb.GdbError("argument must be of type (struct list_node [*])")
c = head
try:
gdb.write("Starting with: {}\n".format(c))
except gdb.MemoryError:
gdb.write("head is not accessible\n")
return
while True:
p = c["prev"].dereference()
n = c["next"].dereference()
try:
if p["next"] != c.address:
gdb.write(
"prev.next != current: "
"current@{current_addr}={current} "
"prev@{p_addr}={p}\n".format(
current_addr=c.address,
current=c,
p_addr=p.address,
p=p,
)
)
return
except gdb.MemoryError:
gdb.write(
"prev is not accessible: "
"current@{current_addr}={current}\n".format(
current_addr=c.address, current=c
)
)
return
try:
if n["prev"] != c.address:
gdb.write(
"next.prev != current: "
"current@{current_addr}={current} "
"next@{n_addr}={n}\n".format(
current_addr=c.address,
current=c,
n_addr=n.address,
n=n,
)
)
return
except gdb.MemoryError:
gdb.write(
"next is not accessible: "
"current@{current_addr}={current}\n".format(
current_addr=c.address, current=c
)
)
return
c = n
nb += 1
if c == head:
gdb.write("list is consistent: {} node(s)\n".format(nb))
return
def sq_is_empty(sq):
"""Check if a singly linked list is empty"""
if sq.type == sq_queue_type.pointer():
sq = sq.dereference()
elif sq.type != sq_queue_type:
return False
if sq["head"] == 0:
return True
else:
return False
def sq_check(sq, verbose=True) -> int:
"""Check the consistency of a singly linked list"""
nb = 0
if sq.type == sq_queue_type.pointer():
sq = sq.dereference()
elif sq.type != sq_queue_type:
gdb.write("Must be struct sq_queue not {}".format(sq.type))
return nb
if sq["head"] == 0:
if verbose:
gdb.write("sq_queue head is empty {}\n".format(sq.address))
return nb
nodes = set()
entry = sq["head"].dereference()
try:
while entry.address:
nb += 1
if int(entry.address) in nodes:
gdb.write("sq_queue is circular: {}\n".format(entry.address))
return nb
nodes.add(int(entry.address))
entry = entry["flink"].dereference()
except gdb.MemoryError:
gdb.write("entry address is unaccessible {}\n".format(entry.address))
return nb
if int(sq["tail"]) not in nodes:
gdb.write("sq_queue tail is not in the list {}\n".format(sq["tail"]))
return nb
if sq["tail"]["flink"] != 0:
gdb.write("sq_queue tail->flink is not null {}\n".format(sq["tail"]))
return nb
if verbose:
gdb.write("sq_queue is consistent: {} node(s)\n".format(nb))
return nb
def sq_count(sq) -> int:
"""Count sq elements, abort if check failed"""
return sq_check(sq, verbose=False)
def dq_for_every(dq, entry=None):
"""Iterate over a doubly linked list"""
if dq.type == dq_queue_type.pointer():
dq = dq.dereference()
elif dq.type != dq_queue_type:
gdb.write("Must be struct dq_queue not {}".format(dq.type))
return
if dq["head"] == 0:
return
if not entry:
entry = dq["head"].dereference()
while entry.address:
yield entry.address
entry = entry["flink"].dereference()
def dq_check(dq):
"""Check the consistency of a doubly linked list"""
nb = 0
if dq.type == dq_queue_type.pointer():
dq = dq.dereference()
elif dq.type != dq_queue_type:
gdb.write("Must be struct dq_queue not {}".format(dq.type))
return
if dq["head"] == 0:
gdb.write("dq_queue head is empty {}\n".format(dq.address))
return
nodes = set()
entry = dq["head"].dereference()
try:
while entry.address:
nb += 1
if int(entry.address) in nodes:
gdb.write("dq_queue is circular: {}\n".format(entry.address))
return
nodes.add(int(entry.address))
entry = entry["flink"].dereference()
except gdb.MemoryError:
gdb.write("entry address is unaccessible {}\n".format(entry.address))
return
if int(dq["tail"]) not in nodes:
gdb.write("dq_queue tail is not in the list {}\n".format(dq["tail"]))
return
if dq["tail"]["flink"] != 0:
gdb.write("dq_queue tail->flink is not null {}\n".format(dq["tail"]))
return
gdb.write("dq_queue is consistent: {} node(s)\n".format(nb))
class ListCheck(gdb.Command):
"""Verify a list consistency"""
def __init__(self):
super().__init__("list_check", gdb.COMMAND_DATA, gdb.COMPLETE_EXPRESSION)
def invoke(self, arg, from_tty):
argv = gdb.string_to_argv(arg)
if len(argv) != 1:
raise gdb.GdbError("nx-list-check takes one argument")
obj = gdb.parse_and_eval(argv[0])
if obj.type == list_node_type.pointer():
list_check(obj)
elif obj.type == sq_queue_type.pointer():
sq_check(obj)
elif obj.type == dq_queue_type.pointer():
dq_check(obj)
else:
raise gdb.GdbError("Invalid argument type: {}".format(obj.type))
class ForeachListEntry(gdb.Command):
"""Dump list members for a given list"""
def __init__(self):
super().__init__("foreach list", gdb.COMMAND_DATA, gdb.COMPLETE_EXPRESSION)
def invoke(self, arg, from_tty):
argv = gdb.string_to_argv(arg)
parser = argparse.ArgumentParser(description="Iterate the items in list")
parser.add_argument("head", type=str, help="List head")
parser.add_argument(
"-n",
"--next",
type=str,
help="The name of the next pointer in the list node",
default="next",
)
parser.add_argument(
"-c", "--container", type=str, default=None, help="Optional container type"
)
parser.add_argument(
"-m", "--member", type=str, default=None, help="Member name in container"
)
parser.add_argument(
"-e",
"--element",
type=str,
help="Only dump this element in array member struct.",
default=None,
)
try:
args = parser.parse_args(argv)
except SystemExit:
gdb.write("Invalid arguments\n")
return
pointer = gdb.parse_and_eval(args.head)
node = pointer
i = 0
while node:
entry = (
utils.container_of(node, args.container, args.member)
if args.container
else node
)
entry = entry.dereference()
entry = entry[args.element] if args.element else entry
gdb.write(
f"{i} *({entry.type} *){hex(entry.address)} {entry.format_string(styling=True)}\n"
)
i += 1
node = node[args.next]
if node == pointer:
break
class ForeachArray(gdb.Command):
"""Dump array members."""
def __init__(self):
super().__init__("foreach array", gdb.COMMAND_DATA, gdb.COMPLETE_EXPRESSION)
def invoke(self, arg, from_tty):
argv = gdb.string_to_argv(arg)
parser = argparse.ArgumentParser(description="Iterate the items in array")
parser.add_argument("head", type=str, help="List head")
parser.add_argument(
"-l",
"--length",
type=int,
help="The array length",
default=None,
)
parser.add_argument(
"-e",
"--element",
type=str,
help="Only dump this element in array member struct.",
default=None,
)
try:
args = parser.parse_args(argv)
except SystemExit:
gdb.write("Invalid arguments\n")
return
pointer = gdb.parse_and_eval(args.head)
node = pointer
len = args.length if args.length else utils.nitems(pointer)
for i in range(len):
entry = node[i][args.element] if args.element else node[i]
gdb.write(f"{i}: {entry.format_string(styling=True)}\n")
+289
View File
@@ -0,0 +1,289 @@
############################################################################
# tools/pynuttx/nxgdb/macros.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
# NOTE: GDB stores macro information based on the current stack frame's scope,
# including the source file and line number. Therefore, there may be missing
# macro definitions when you are at different stack frames.
#
# To resolve this issue, we need to retrieve all macro information from the ELF file
# then parse and evaluate it by ourselves.
#
# There might be two ways to achieve this, one is to leverage the C preprocessor
# to directly preprocess all the macros interpreted into python constants
# gcc -E -x c -P <file_with_macros> -I/path/to/nuttx/include
#
# While the other way is to leverage the dwarf info stored in the ELF file,
# with -g3 switch, we have a `.debug_macro` section containing all the information
# about the macros.
#
# Currently, we are using the second method.
import hashlib
import json
import os
import re
import tempfile
import time
from os import path
PUNCTUATORS = [
r"\[",
r"\]",
r"\(",
r"\)",
r"\{",
r"\}",
r"\?",
r";",
r",",
r"~",
r"\.\.\.",
r"\.",
r"\-\>",
r"\-\-",
r"\-\=",
r"\-",
r"\+\+",
r"\+\=",
r"\+",
r"\*\=",
r"\*",
r"\!\=",
r"\!",
r"\&\&",
r"\&\=",
r"\&",
r"\/\=",
r"\/",
r"\%\>",
r"%:%:",
r"%:",
r"%=",
r"%",
r"\^\=",
r"\^",
r"\#\#",
r"\#",
r"\:\>",
r"\:",
r"\|\|",
r"\|\=",
r"\|",
r"<<=",
r"<<",
r"<=",
r"<:",
r"<%",
r"<",
r">>=",
r">>",
r">=",
r">",
r"\=\=",
r"\=",
]
def parse_macro(line, macros, pattern):
# grep name, value
# the first group matches the token, the second matches the replacement
m = pattern.match(line)
if not m:
return False
name, value = m.group(1), m.group(2)
if name in macros:
# FIXME: what should we do if we got a redefinition/duplication here?
# for now I think it's ok just overwrite the old value
pass
# emplace, for all undefined macros we evalute it to zero
macros[name] = value if value else "0"
return True
def fetch_macro_info(file):
if not path.isfile(file):
raise FileNotFoundError("No given ELF target found")
# FIXME: we don't use subprocess here because
# it's broken on some GDB distribution :(, I haven't
# found a solution to it.
with open(file, "rb") as f:
hash = hashlib.md5(f.read()).hexdigest()
macros = {}
p = re.compile(r".*macro[ ]*:[ ]*([\S]+\(.*?\)|[\w]+)[ ]*(.*)")
cache = path.join(tempfile.gettempdir(), f"{hash}.json")
print(f"Load macro: {cache}")
if not path.isfile(cache):
t = time.time()
os.system(f'readelf -wm "{file}" > "{cache}"')
print(f"readelf took {time.time() - t:.1f} seconds")
t = time.time()
with open(cache, "r") as f2:
for line in f2.readlines():
if not line.startswith(" DW_MACRO_define") and not line.startswith(
" DW_MACRO_undef"
):
continue
if not parse_macro(line, macros, p):
print(f"Failed to parse {line}")
print(f"Parse macro took {time.time() - t:.1f} seconds")
with open(cache, "w") as f2:
dump = json.dumps(macros, indent=4, sort_keys=True)
f2.write(dump)
print(f"Cache macro info to {cache}")
else:
with open(cache, "r") as f2:
macros = json.load(f2)
return macros
def split_tokens(expr):
p = "(" + "|".join(PUNCTUATORS) + ")"
res = list(
filter(lambda e: e != "", map(lambda e: e.rstrip().lstrip(), re.split(p, expr)))
)
return res
def do_expand(expr, macro_map):
if expr in PUNCTUATORS:
return expr
tokens = split_tokens(expr)
res = []
for t in tokens:
if t not in macro_map:
res.append(t)
continue
res += do_expand(macro_map[t], macro_map)
return res
# NOTE: Implement a fully functional parser which can
# preprocess all the C marcos according to ISO 9899 standard
# may be an overkill, what we really care about are those
# macros that can be evaluated to a constant value.
#
# #define A (B + C + D)
# #define B 1
# #define C 2
# #define D 3
# invoking try_expand('A', macro_map) will give you "(1 + 2 + 3)"
#
# However,
# #define SUM(B,C,D) (B + C + D)
# invoking try_expand('SUM(1,2,3)', macro_map) will give you "SUM(1,2,3)"
#
# We have not implemented this feature as we have not found a practical
# use case for it in our GDB plugin.
#
# However, you can switch to the correct stack frame that has this macro defined
# and let GDB expand and evaluate it for you if you really want to evaluate some very
# complex macros.
def try_expand(expr, macro):
res = []
res += do_expand(expr, macro)
return "".join(res)
class Macro:
"""
This is a singleton class which only initializes once to
cache a context of macro definition which can be queried later
TODO: we only deal with single ELF at the moment for simplicity
If you load more object files while debugging, only the first one gets loaded
will be used to retrieve macro information
Usage:
macro = Macro("nuttx/nuttx")
print(macro.CONFIG_MM_BACKTRACE)
if macro.CONFIG_MM_BACKTRACE:
print("mm backtrace is enabled")
else:
print("mm backtrace is disabled")
"""
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "instance"):
cls.instance = super(Macro, cls).__new__(cls)
return cls.instance
def __init__(self, filename):
self._macro_map = {}
self._file = filename
self._macro_map = fetch_macro_info(filename)
def is_defined(self, macro_name):
"""
Check if a macro is defined
"""
return macro_name in self._macro_map
def get_value(self, macro_name, default=None):
"""
Get the value of a macro, return default if macro is not defined
"""
if not self.is_defined(macro_name):
return default
value = self._macro_map[macro_name]
# Try to convert to numeric type
try:
# Handle hexadecimal
if isinstance(value, str) and value.startswith("0x"):
return int(value, 16)
# Handle integer
return int(value)
except (ValueError, TypeError):
# Return original value if conversion fails
return value
def __getattr__(self, name):
"""
Allow using dot notation to access macros
"""
return self.get_value(name)
def __call__(self, macro_name):
"""
Allow using function call syntax to get macro
"""
return self.get_value(macro_name)
+175
View File
@@ -0,0 +1,175 @@
############################################################################
# tools/pynuttx/nxgdb/memcheck.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import traceback
from collections import defaultdict
from typing import Dict, List, Tuple
import gdb
from . import memdump, mm, utils
class MMCheck(gdb.Command):
"""Check memory manager and pool integrity"""
def __init__(self):
super().__init__("mm check", gdb.COMMAND_USER)
utils.alias("memcheck", "mm check")
def check_heap(self, heap: mm.MMHeap) -> Dict[int, List[str]]: # noqa: C901
"""Check heap integrity and return list of issues in string"""
issues = defaultdict(list) # key: address, value: list of issues
def report(e, heap, node: mm.MMNode) -> None:
gdb.write(f"Error happened during heap check: {e}\n")
try:
gdb.write(f" heap: {heap}\n")
gdb.write(f"current node: {node}")
if node.prevnode:
gdb.write(f" prev node: {node.prevnode}")
if node.nextnode:
gdb.write(f" next node: {node.nextnode}")
gdb.write("\n")
except gdb.error as e:
gdb.write(f"Error happened during report: {e}\n")
def is_node_corrupted(node: mm.MMNode) -> Tuple[bool, str]:
# Must be in this heap
if not heap.contains(node.address):
return True, f"node@{hex(node.address)} not in heap"
# Check next node
if node.nodesize > node.MM_SIZEOF_ALLOCNODE:
nextnode = node.nextnode
if not heap.contains(nextnode.address):
return True, f"nexnode@{hex(nextnode.address)} not in heap"
if node.is_free:
if not nextnode.is_prev_free:
# This node is free, then next node must have prev free set
return (
True,
f"nextnode@{hex(nextnode.address)} not marked as prev free",
)
if nextnode.prevsize != node.nodesize:
return (
True,
f"nextnode @{hex(nextnode.address)} prevsize not match",
)
if node.is_free:
if node.nodesize < node.MM_MIN_CHUNK:
return True, f"nodesize {int(node.nodesize)} too small"
if node.flink and node.flink.blink != node:
return (
True,
f"flink not intact: {hex(node.flink.blink)}, node: {hex(node.address)}",
)
if node.blink.flink != node:
return (
True,
f"blink not intact: {hex(node.blink.flink)}, node: {hex(node.address)}",
)
# Node should be in correctly sorted order
if (blinksize := mm.MMNode(node.blink).nodesize) > node.nodesize:
return (
True,
f"blink node not in sorted order: {blinksize} > {node.nodesize}",
)
fnode = mm.MMNode(node.flink) if node.flink else None
if fnode and fnode.nodesize and fnode.nodesize < node.nodesize:
return (
True,
f"flink node not in sorted order: {fnode.nodesize} < {node.nodesize}",
)
else:
# Node is allocated.
if node.nodesize < node.MM_SIZEOF_ALLOCNODE:
return True, f"nodesize {node.nodesize} too small"
return False, ""
try:
# Check nodes in physical memory order
for node in heap.nodes:
corrupted, reason = is_node_corrupted(node)
if corrupted:
issues[node.address].append(reason)
# Check free list
for node in utils.ArrayIterator(heap.mm_nodelist):
# node is in type of gdb.Value, struct mm_freenode_s
while node:
address = int(node.address)
if node["flink"] and not heap.contains(node["flink"]):
issues[address].append(
f"flink {hex(node['flink'])} not in heap"
)
break
if address in issues or node["size"] == 0:
# This node is already checked or size is 0, which is a node in node table
node = node["flink"]
continue
# Check if this node is corrupted
corrupted, reason = is_node_corrupted(mm.MMNode(node))
if corrupted:
issues[address].append(reason)
break
# Continue to it's flink
node = node["flink"]
except Exception as e:
report(e, heap, node)
traceback.print_exc()
return issues
def dump_issues(self, heap, issues: Dict[int, List[str]]) -> None:
for address, reasons in issues.items():
gdb.write(
f"{len(reasons)} issues @{hex(address)}: " f"{','.join(reasons)}\n"
)
def invoke(self, arg: str, from_tty: bool) -> None:
try:
heaps = memdump.get_heaps()
for heap in heaps:
issues = self.check_heap(heap)
if not issues:
continue
print(f"Found {len(issues)} issues in heap {heap}")
self.dump_issues(heap, issues)
except Exception as e:
print(f"Error happened during check: {e}")
traceback.print_exc()
print("Check done.")
File diff suppressed because it is too large Load Diff
+219
View File
@@ -0,0 +1,219 @@
############################################################################
# tools/pynuttx/nxgdb/memleak.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import bisect
import json
import time
from os import path
from typing import Dict, Generator, List, Tuple
import gdb
from . import memdump, mm, utils
class GlobalNode(memdump.MMNodeDump):
def __init__(self, address: int, nodesize: int):
self.address = address
self.nodesize = nodesize
self.pid = None
self.seqno = None
self.overhead = 0
self.backtrace = ()
def __repr__(self):
return f"GlobalVar@{self.address:x}:{self.nodesize}Bytes"
def contains(self, addr: int) -> bool:
pass
def read_memory(self) -> memoryview:
return gdb.selected_inferior().read_memory(self.address, self.nodesize)
class MMLeak(gdb.Command):
"""Dump memory manager heap"""
def __init__(self):
self.elf = utils.import_check(
"elftools.elf.elffile", "ELFFile", "Please pip install pyelftools\n"
)
if not self.elf:
return
super().__init__("mm leak", gdb.COMMAND_USER)
utils.alias("memleak", "mm leak")
def global_nodes(self) -> List[GlobalNode]:
cache = path.join(
path.dirname(path.abspath(gdb.objfiles()[0].filename)),
f"{utils.get_elf_md5()}-globals.json",
)
nodes: List[GlobalNode] = []
if path.isfile(cache):
with open(cache, "r") as f:
variables = json.load(f)
for var in variables:
nodes.append(GlobalNode(var["address"], var["size"]))
return nodes
longsize = utils.get_long_type().sizeof
for objfile in gdb.objfiles():
elf = self.elf.load_from_path(objfile.filename)
symtab = elf.get_section_by_name(".symtab")
symbols = filter(
lambda s: s["st_info"]["type"] == "STT_OBJECT"
and s["st_size"] >= longsize,
symtab.iter_symbols(),
)
for symbol in symbols:
size = symbol["st_size"] // longsize * longsize
address = symbol["st_value"]
nodes.append(GlobalNode(address, size))
with open(cache, "w") as f:
variables = [
{"address": node.address, "size": node.nodesize} for node in nodes
]
str = utils.jsonify(variables)
f.write(str)
return nodes
def collect_leaks(
self, heaps: List[mm.MMHeap]
) -> Dict[memdump.MMNodeDump, List[memdump.MMNodeDump]]:
t = time.time()
print("Loading globals from elf...", flush=True, end="")
good_nodes = self.global_nodes() # Global memory are all good.
print(f" {time.time() - t:.2f}s", flush=True, end="\n")
nodes_dict: Dict[int, memdump.MMNodeDump] = {}
sorted_addr = set()
t = time.time()
print("Gather memory nodes...", flush=True, end="")
for node in memdump.dump_nodes(heaps, {"no_pid": mm.PID_MM_MEMPOOL}):
nodes_dict[node.address] = node
sorted_addr.add(node.address)
sorted_addr = sorted(sorted_addr)
print(f" {time.time() - t:.2f}s", flush=True, end="\n")
regions = [
{"start": start.address, "end": end.address}
for heap in heaps
for start, end in heap.regions
]
longsize = utils.get_long_type().sizeof
def pointers(node: memdump.MMNodeDump) -> Generator[int, None, None]:
# Return all possible pointers stored in this node
size = node.nodesize - node.overhead
memory = node.read_memory()
while size > 0:
size -= longsize
ptr = int.from_bytes(memory[size : size + longsize], "little")
if any(region["start"] <= ptr < region["end"] for region in regions):
yield ptr
print("Leak analyzing...", flush=True, end="")
t = time.time()
for good in good_nodes:
for ptr in pointers(good):
if not (idx := bisect.bisect_right(sorted_addr, ptr)):
continue
node = nodes_dict[sorted_addr[idx - 1]]
if node.contains(ptr):
del sorted_addr[idx - 1]
good_nodes.append(node)
print(f" {time.time() - t:.2f}s", flush=True, end="\n")
return memdump.group_nodes((nodes_dict[addr] for addr in sorted_addr))
def _iterate_leaks(
self, nodes: Dict[memdump.MMNodeDump, List[memdump.MMNodeDump]]
) -> Generator[Tuple[memdump.MMNodeDump, bool, int], None, None]:
pids = [int(tcb["pid"]) for tcb in utils.get_tcbs()]
def is_pid_alive(pid):
return pid in pids
for node in nodes.keys():
count = len(nodes[node])
yield node, is_pid_alive(node.pid), count
def invoke(self, arg: str, from_tty: bool) -> None:
heaps = memdump.get_heaps("g_mmheap")
leak_nodes = self.collect_leaks(heaps)
memdump.print_header()
total_blk = total_size = 0
for node, alive, count in self._iterate_leaks(leak_nodes):
total_blk += count
total_size += count * node.nodesize
memdump.print_node(node, alive, count=count)
print(f"Leaked {total_blk} blks, {total_size} bytes")
def diagnose(self, *args, **kwargs):
heaps = memdump.get_heaps("g_mmheap")
leak_nodes = self.collect_leaks(heaps)
total_blk = total_size = 0
data = []
for node, alive, count in self._iterate_leaks(leak_nodes):
total_blk += count
total_size += count * node.nodesize
info = {
"count": count,
"pid": node.pid,
"size": node.nodesize,
"address": node.address,
"seqno": node.seqno,
"alive": alive,
"backtrace": [],
}
if mm.CONFIG_MM_BACKTRACE and node.backtrace and node.backtrace[0]:
bt = utils.Backtrace(node.backtrace)
info["backtrace"] = [
{
"address": addr,
"function": func,
"source": source,
}
for addr, func, source in bt.backtrace
]
data.append(info)
return {
"title": "Memory Leak Report",
"summary": f"Total {total_blk} blks, {total_size} bytes leaked",
"result": "fail" if total_blk else "pass",
"command": "mm leak",
"data": data,
}
File diff suppressed because it is too large Load Diff
+320
View File
@@ -0,0 +1,320 @@
############################################################################
# tools/pynuttx/nxgdb/net.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
from enum import Enum
import gdb
from . import utils
from .lists import NxDQueue, NxSQueue
socket = utils.import_check(
"socket", errmsg="No socket module found, please try gdb-multiarch instead.\n"
)
NET_IPv4 = utils.get_symbol_value("CONFIG_NET_IPv4")
NET_IPv6 = utils.get_symbol_value("CONFIG_NET_IPv6")
AF_INET = utils.get_symbol_value("AF_INET")
AF_INET6 = utils.get_symbol_value("AF_INET6")
def ntohs(val):
"""Convert a 16-bit value from network byte order to host byte order"""
if utils.get_target_endianness() == utils.BIG_ENDIAN:
return val
return utils.swap16(val)
def get_ip_port(conn):
"""Get the IP address and port of a network connection"""
domain = utils.get_field(conn, "domain", AF_INET if NET_IPv4 else AF_INET6)
ip_binding = conn["u"]["ipv4" if domain == AF_INET else "ipv6"]
lport = ntohs(conn["lport"])
rport = ntohs(conn["rport"])
laddr = inet_ntop(domain, ip_binding["laddr"])
raddr = inet_ntop(domain, ip_binding["raddr"])
return laddr, lport, raddr, rport
def inet_ntop(domain, addr):
"""Convert a network address to a string"""
addr_len = 16 if domain == AF_INET6 else 4
if socket:
return socket.inet_ntop(domain, utils.get_bytes(addr, addr_len))
else:
separator = "." if domain == AF_INET else ""
fmt = "%d" if domain == AF_INET else "%02x"
return separator.join([fmt % byte for byte in utils.get_bytes(addr, addr_len)])
def socket_for_each_entry(proto):
"""Iterate over a dq of socket structs, usage:
for conn in socket_for_each_entry("icmp"):
readahead = conn["readahead"]
"""
g_active_connections = gdb.parse_and_eval("g_active_%s_connections" % proto)
for node in NxDQueue(g_active_connections, "struct socket_conn_s", "node"):
# udp_conn_s::socket_conn_s sconn
yield utils.container_of(
node,
"struct %s_conn_s" % proto,
"sconn",
)
def wrbuffer_inqueue_size(queue=None, protocol="tcp"):
"""Calculate the total size of all iob in the write queue of a udp connection"""
if not queue:
return 0
type = "struct %s_wrbuffer_s" % protocol
node = "wb_node"
return sum(entry["wb_iob"]["io_pktlen"] for entry in NxSQueue(queue, type, node))
def tcp_ofoseg_bufsize(conn):
"""Calculate the pending size of out-of-order buffer of a tcp connection"""
total = 0
if utils.get_symbol_value("CONFIG_NET_TCP_OUT_OF_ORDER"):
total = sum(
seg["data"]["io_pktlen"]
for seg in utils.ArrayIterator(conn["ofosegs"], conn["nofosegs"])
)
return total
class NetStats(gdb.Command):
"""Network statistics
Usage: netstats [iob|pkt|tcp|udp|all]
Examples: netstats - Show all stats
netstats all - Show all stats
netstats iob - Show IOB stats
netstats tcp udp - Show both TCP and UDP stats
"""
def __init__(self):
if utils.get_symbol_value("CONFIG_NET"):
super().__init__("netstats", gdb.COMMAND_USER)
def iob_stats(self):
try:
size = utils.get_symbol_value("CONFIG_IOB_BUFSIZE")
ntotal = utils.get_symbol_value("CONFIG_IOB_NBUFFERS")
nfree = gdb.parse_and_eval("g_iob_sem")["semcount"]
nwait, nfree = (0, nfree) if nfree >= 0 else (-nfree, 0)
nthrottle = (
gdb.parse_and_eval("g_throttle_sem")["semcount"]
if utils.get_symbol_value("CONFIG_IOB_THROTTLE") > 0
else 0
)
gdb.write(
"IOB: %10s%10s%10s%10s%10s\n"
% ("size", "ntotal", "nfree", "nwait", "nthrottle")
)
gdb.write(
" %10d%10d%10d%10d%10d\n" % (size, ntotal, nfree, nwait, nthrottle)
)
except gdb.error as e:
gdb.write("Failed to get IOB stats: %s\n" % e)
def pkt_stats(self):
try:
netstats = gdb.parse_and_eval("g_netstats")
gdb.write(
"Packets:%7s%7s%7s%7s%7s%7s\n"
% ("IPv4", "IPv6", "TCP", "UDP", "ICMP", "ICMPv6")
)
def stats_line(title, member):
gdb.write("%-8s" % title)
for proto in ("ipv4", "ipv6", "tcp", "udp", "icmp", "icmpv6"):
gdb.write(
"%7s"
% utils.get_field(utils.get_field(netstats, proto), member, "-")
)
gdb.write("\n")
stats_line("Received", "recv")
stats_line("Dropped", "drop")
stats_line(" VHL", "vhlerr")
stats_line(" Frag", "fragerr")
stats_line(" Chksum", "chkerr")
stats_line(" Type", "typeerr")
stats_line(" Proto", "protoerr")
# TODO: Maybe print TCP's ackerr, rst, syndrop, synrst here
stats_line("Sent", "sent")
stats_line(" Rexmit", "rexmit")
except gdb.error as e:
gdb.write("Failed to get Net Stats: %s\n" % e)
def tcp_stats(self):
try:
gdb.write(
"TCP Conn: %3s %3s %3s %3s %4s %3s"
% ("st", "flg", "ref", "tmr", "uack", "nrt")
)
gdb.write(
" %11s %11s+%-5s %21s %21s\n"
% ("txbuf", "rxbuf", "ofo", "local_address", "remote_address")
)
for idx, conn in enumerate(socket_for_each_entry("tcp")):
state = conn["tcpstateflags"]
flags = conn["sconn"]["s_flags"]
ref = conn["crefs"]
timer = conn["timer"]
unacked = conn["tx_unacked"]
nrtx = conn["nrtx"]
txbuf = utils.get_field(conn, "snd_bufs", -1)
rxbuf = utils.get_field(conn, "rcv_bufs", -1)
txsz = wrbuffer_inqueue_size(
utils.get_field(conn, "unacked_q"), "tcp"
) + wrbuffer_inqueue_size(utils.get_field(conn, "write_q"), "tcp")
rxsz = conn["readahead"]["io_pktlen"] if conn["readahead"] else 0
ofosz = tcp_ofoseg_bufsize(conn)
laddr, lport, raddr, rport = get_ip_port(conn)
gdb.write(
"%-4d %3x %3x %3d %3d %4d %3d"
% (idx, state, flags, ref, timer, unacked, nrtx)
)
gdb.write(
" %5d/%-5d %5d/%-5d+%-5d %15s:%-5d %15s:%-5d\n"
% (txsz, txbuf, rxsz, rxbuf, ofosz, laddr, lport, raddr, rport)
)
except gdb.error as e:
gdb.write("Failed to get TCP stats: %s\n" % e)
def udp_stats(self):
try:
gdb.write(
"UDP Conn: %4s %11s %11s %21s %21s\n"
% ("flg", "txbuf", "rxbuf", "local_address", "remote_address")
)
for idx, conn in enumerate(socket_for_each_entry("udp")):
flags = conn["sconn"]["s_flags"]
txbuf = utils.get_field(conn, "sndbufs", -1)
rxbuf = utils.get_field(conn, "rcvbufs", -1)
txsz = wrbuffer_inqueue_size(utils.get_field(conn, "write_q"), "udp")
rxsz = conn["readahead"]["io_pktlen"] if conn["readahead"] else 0
laddr, lport, raddr, rport = get_ip_port(conn)
gdb.write(
"%-4d %4x %5d/%-5d %5d/%-5d %15s:%-5d %15s:%-5d\n"
% (idx, flags, txsz, txbuf, rxsz, rxbuf, laddr, lport, raddr, rport)
)
except gdb.error as e:
gdb.write("Failed to get UDP stats: %s\n" % e)
def invoke(self, args, from_tty):
# Parse the arguments in a simple way
if not args or "all" in args:
args = "iob pkt tcp udp"
# Call the corresponding function
if utils.get_symbol_value("CONFIG_MM_IOB") and "iob" in args:
self.iob_stats()
gdb.write("\n")
if utils.get_symbol_value("CONFIG_NET_STATISTICS") and "pkt" in args:
self.pkt_stats()
gdb.write("\n")
if utils.get_symbol_value("CONFIG_NET_TCP") and "tcp" in args:
self.tcp_stats()
gdb.write("\n")
if utils.get_symbol_value("CONFIG_NET_UDP") and "udp" in args:
self.udp_stats()
gdb.write("\n")
class NetCheckResult(Enum):
PASS = 0
WARN = 1
FAILED = 2
def __lt__(self, other):
return self.value < other.value
class NetCheck(gdb.Command):
"""Network check"""
def __init__(self):
if utils.get_symbol_value("CONFIG_NET"):
super().__init__("netcheck", gdb.COMMAND_USER)
def diagnose(self, *args, **kwargs):
result, message = NetCheckResult.PASS, []
if utils.get_symbol_value("CONFIG_MM_IOB"):
ret, msg = self.check_iob()
result = max(result, ret)
message.extend(msg)
return {
"title": "Netcheck Report",
"summary": "Net status check",
"result": "pass" if result else "fail",
"command": "netcheck",
"data": message,
}
def check_iob(self):
result = NetCheckResult.PASS
message = []
try:
nfree = gdb.parse_and_eval("g_iob_sem")["semcount"]
nthrottle = (
gdb.parse_and_eval("g_throttle_sem")["semcount"]
if utils.get_symbol_value("CONFIG_IOB_THROTTLE") > 0
else 0
)
if nfree < 0 or nthrottle < 0:
result = max(result, NetCheckResult.WARN)
message.append(
"[WARNING] IOB used up: free %d throttle %d" % (nfree, nthrottle)
)
except gdb.error as e:
result = max(result, NetCheckResult.FAILED)
message.append("[FAILED] Failed to check IOB: %s" % e)
finally:
return result, message
def invoke(self, args, from_tty):
if utils.get_symbol_value("CONFIG_MM_IOB"):
result, message = self.check_iob()
gdb.write("IOB check: %s\n" % result.name)
gdb.write("\n".join(message))
+37
View File
@@ -0,0 +1,37 @@
############################################################################
# tools/pynuttx/nxgdb/prefix.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import gdb
class ForeachPrefix(gdb.Command):
"""foreach commands prefix."""
def __init__(self):
super(ForeachPrefix, self).__init__("foreach", gdb.COMMAND_USER, prefix=True)
class MMPrefixCommand(gdb.Command):
"""Memory manager related commands prefix."""
def __init__(self):
super().__init__("mm", gdb.COMMAND_USER, prefix=True)
+62
View File
@@ -0,0 +1,62 @@
############################################################################
# tools/pynuttx/nxgdb/profile.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import gdb
from .utils import import_check
class Profile(gdb.Command):
"""Profile a gdb command
Usage: profile <gdb command>
"""
def __init__(self):
self.cProfile = import_check(
"cProfile", errmsg="cProfile module not found, try gdb-multiarch.\n"
)
if not self.cProfile:
return
super().__init__("profile", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
self.cProfile.run(f"gdb.execute('{args}')", sort="cumulative")
class Time(gdb.Command):
"""Time a gdb command
Usage: time <gdb command>
"""
def __init__(self):
super().__init__("time", gdb.COMMAND_USER)
def invoke(self, args, from_tty):
import time
start = time.time()
gdb.execute(args)
end = time.time()
gdb.write(f"Time elapsed: {end - start:.6f}s\n")
+65
View File
@@ -0,0 +1,65 @@
############################################################################
# tools/pynuttx/nxgdb/protocols/fs.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
from .value import Value
class File(Value):
"""struct file"""
f_oflags: Value
f_refs: Value
f_pos: Value
f_inode: Value
f_priv: Value
f_tag_fdsan: Value
f_tag_fdcheck: Value
f_backtrace: Value
locked: Value
class Inode(Value):
"""struct inode"""
i_parent: Value
i_peer: Value
i_child: Value
i_crefs: Value
i_flags: Value
u: Value
i_ino: Value
i_size: Value
i_mode: Value
i_owner: Value
i_group: Value
i_atime: Value
i_mtime: Value
i_ctime: Value
i_private: Value
i_name: Value
class FileList(Value):
"""struct filelist_s"""
fl_rows: Value
fl_files: Value
+122
View File
@@ -0,0 +1,122 @@
############################################################################
# tools/gdb/nuttxgdb/protocols/mm.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
from __future__ import annotations
from typing import List
from .value import Value
class ProcfsMeminfoEntry(Value):
"""struct procfs_meminfo_entry_s"""
name: Value
heap: Value
next: ProcfsMeminfoEntry
class MMAllocNode(Value):
"""struct mm_allocnode_s"""
preceding: Value
size: Value
pid: Value
seqno: Value
backtrace: Value
class MMFreeNode(Value):
"""struct mm_freenode_s"""
preceding: Value
size: Value
pid: Value
seqno: Value
backtrace: Value
flink: MMFreeNode
blink: MMFreeNode
class MMHeap(Value):
"""struct mm_heap_s"""
mm_lock: Value
mm_heapsize: Value
mm_maxused: Value
mm_curused: Value
mm_heapstart: List[MMAllocNode]
mm_heapend: List[MMAllocNode]
mm_nregions: Value
mm_nodelist: Value
class MemPool(Value):
"""struct mempool_s"""
initialsize: Value
interruptsize: Value
expandsize: Value
wait: Value
priv: Value
alloc: Value
free: Value
check: Value
ibase: Value
queue: Value
iqueue: Value
equeue: Value
nalloc: Value
lock: Value
waitsem: Value
procfs: Value
class MemPoolMultiple(Value):
"""struct mempool_multiple_s"""
pools: List[MemPool]
npools: Value
expandsize: Value
minpoolsize: Value
arg: Value
alloc: Value
alloc_size: Value
free: Value
alloced: Value
delta: Value
lock: Value
chunk_queue: Value
chunk_size: Value
dict_used: Value
dict_col_num_log2: Value
dict_row_num: Value
dict: Value
class MemPoolBlock(Value):
"""struct mempool_backtrace_s"""
magic: Value
pid: Value
seqno: Value
backtrace: Value
+125
View File
@@ -0,0 +1,125 @@
############################################################################
# tools/pynuttx/nxgdb/protocols/thread.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
from .fs import FileList
from .value import Value
class Group(Value):
"""struct group_s"""
tg_pid: Value
tg_ppid: Value
tg_flags: Value
tg_uid: Value
tg_gid: Value
tg_euid: Value
tg_egid: Value
tg_members: Value
tg_bininfo: Value
tg_children: Value
tg_nchildren: Value
tg_exitcode: Value
tg_nwaiters: Value
tg_waitflags: Value
tg_exitsem: Value
tg_statloc: Value
tg_joinlock: Value
tg_joinqueue: Value
tg_info: Value
tg_sigactionq: Value
tg_sigpendingq: Value
tg_sigdefault: Value
tg_envp: Value
tg_envc: Value
itimer: Value
tg_filelist: FileList
tg_mm_map: Value
class Tcb(Value):
"""struct tcb_s"""
flink: Value
blink: Value
group: Group
member: Value
join_queue: Value
join_entry: Value
join_sem: Value
join_val: Value
addrenv_own: Value
addrenv_curr: Value
pid: Value
sched_priority: Value
init_priority: Value
start: Value
entry: Value
task_state: Value
boost_priority: Value
base_priority: Value
holdsem: Value
cpu: Value
affinity: Value
flags: Value
lockcount: Value
irqcount: Value
errcode: Value
timeslice: Value
sporadic: Value
waitdog: Value
adj_stack_size: Value
stack_alloc_ptr: Value
stack_base_ptr: Value
dspace: Value
waitobj: Value
sigprocmask: Value
sigwaitmask: Value
sigpendactionq: Value
sigpostedq: Value
sigunbinfo: Value
mhead: Value
ticks: Value
run_start: Value
run_max: Value
run_time: Value
premp_start: Value
premp_max: Value
premp_caller: Value
premp_max_caller: Value
crit_start: Value
crit_max: Value
crit_caller: Value
crit_max_caller: Value
perf_event_ctx: Value
perf_event_mutex: Value
xcp: Value
sigdeliver: Value
name: Value
stackrecord_pc: Value
stackrecord_sp: Value
stackrecord_pc_deepest: Value
stackrecord_sp_deepest: Value
sp_deepest: Value
caller_deepest: Value
level_deepest: Value
level: Value
+67
View File
@@ -0,0 +1,67 @@
############################################################################
# tools/pynuttx/nxgdb/protocols/value.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
from __future__ import annotations
from typing import Protocol
import gdb
class Value(Protocol):
address: Value
is_optimized_out: bool
type: gdb.Type
dynamic_type: gdb.Type
is_lazy: bool
bytes: bytes
def cast(self, type: gdb.Type) -> Value: ...
def dereference(self) -> Value: ...
def referenced_value(self) -> Value: ...
def reference_value(self) -> Value: ...
def rvalue_reference_value(self) -> Value: ...
def const_value(self) -> Value: ...
def dynamic_cast(self, type: gdb.Type) -> Value: ...
def reinterpret_cast(self, type: gdb.Type) -> Value: ...
def format_string(
self,
raw: bool = ...,
pretty_arrays: bool = ...,
pretty_structs: bool = ...,
array_indexes: bool = ...,
symbols: bool = ...,
unions: bool = ...,
address: bool = ...,
deref_refs: bool = ...,
actual_objects: bool = ...,
static_members: bool = ...,
max_elements: int = ...,
max_depth: int = ...,
repeat_threshold: int = ...,
format: str = ...,
) -> str: ...
def string(
self, encoding: str = ..., errors: str = ..., length: int = ...
) -> str: ...
+35
View File
@@ -0,0 +1,35 @@
############################################################################
# tools/pynuttx/nxgdb/protocols/wdog.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
from __future__ import annotations
from .value import Value
class WDog(Value):
"""struct wdog_s"""
node: Value
arg: Value
func: Value
picbase: Value
expired: Value
+71
View File
@@ -0,0 +1,71 @@
############################################################################
# tools/pynuttx/nxgdb/protocols/wqueue.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
from __future__ import annotations
from typing import List
from .value import Value
class Work(Value):
"""struct work_s"""
class U(Value):
class S(Value):
dq: Value
qtime: Value
s: S
timer: Value # wdog_s
u: U
worker: Value # void (*worker_t)(FAR void *arg);
arg: Value
wq: KWorkQueue
class KWorker(Value):
"""struct kworker_s"""
pid: Value
work: Value
wait: Value
class KWorkQueue(Value):
"""struct kwork_wqueue_s"""
q: Value
sem: Value
exsem: Value
nthreads: int
exit: bool
worker: List[Value]
class HPWorkQueue(KWorkQueue):
"""struct hp_wqueue_s"""
class LPWorkQueue(KWorkQueue):
"""struct lp_wqueue_s"""
+107
View File
@@ -0,0 +1,107 @@
############################################################################
# tools/pynuttx/rpmsg.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import gdb
from . import utils
from .lists import NxList
class RPMsgDump(gdb.Command):
"""Dump rpmsg service"""
CALLBACK_HEADER = ["rpmsg_cb_s at", "ns_match", "ns_bind"]
ENDPOINT_HEADER = [
"endpoint_addr",
"name",
"addr",
"dest_addr",
"cb",
"ns_bound_cb",
"ns_unbind_cb",
]
CALLBACK_FORAMTTER = "{:<20} {:<40} {:<40}"
ENDPOINT_FORMATTER = "{:<20} {:<20} {:<12} {:<12} {:<40} {:<40} {:<40}"
def __init__(self):
if utils.get_symbol_value("CONFIG_RPMSG"):
super(RPMsgDump, self).__init__("rpmsgdump", gdb.COMMAND_USER)
def print_headers(self, headers, formatter):
gdb.write(formatter.format(*headers) + "\n")
gdb.write(formatter.format(*["-" * len(header) for header in headers]) + "\n")
def dump_rdev_epts(self, endpoints_head):
gdb.write(f"dump_rdev_epts:{endpoints_head}\n")
self.print_headers(self.ENDPOINT_HEADER, self.ENDPOINT_FORMATTER)
output = []
for endpoint in NxList(endpoints_head, "struct rpmsg_endpoint", "node"):
output.append(
self.ENDPOINT_FORMATTER.format(
f"{endpoint}",
f"{endpoint['name'].string()}",
f"{endpoint['addr']}",
f"{endpoint['dest_addr']}",
f"{endpoint['cb']}",
f"{endpoint['ns_bound_cb']}",
f"{endpoint['ns_unbind_cb']}",
)
)
gdb.write("\n".join(output) + "\n")
def dump_rdev_bitmap(self, rdev):
bitmap_values = [hex(bit) for bit in utils.ArrayIterator(rdev["bitmap"])]
gdb.write(
f"bitmap:{' '.join(bitmap_values):<20} bitmaplast: {rdev['bitmap']}\n"
)
def dump_rdev(self, rdev):
gdb.write(f"device:{rdev}\n")
self.dump_rdev_bitmap(rdev)
self.dump_rdev_epts(rdev["endpoints"])
def dump_rpmsg_cb(self):
gdb.write("g_rpmsg_cb:\n")
self.print_headers(self.CALLBACK_HEADER, self.CALLBACK_FORAMTTER)
output = []
for cb in NxList(gdb.parse_and_eval("g_rpmsg_cb"), "struct rpmsg_cb_s", "node"):
output.append(
self.CALLBACK_FORAMTTER.format(
str(cb), str(cb["ns_match"]), str(cb["ns_bind"])
)
)
gdb.write("\n".join(output) + "\n")
def dump_rpmsg(self):
gdb.write("g_rpmsg:\n")
for rpmsg in NxList(gdb.parse_and_eval("g_rpmsg"), "struct rpmsg_s", "node"):
self.dump_rdev(rpmsg["rdev"])
def invoke(self, args, from_tty):
self.dump_rpmsg_cb()
self.dump_rpmsg()
+219
View File
@@ -0,0 +1,219 @@
############################################################################
# tools/pynuttx/nxgdb/stack.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import traceback
import gdb
from . import utils
STACK_COLORATION_PATTERN = utils.get_symbol_value(
"STACK_COLOR", locspec="up_create_stack"
)
class Stack(object):
def __init__(self, name, entry, base, alloc, size, cursp, align):
# We don't care about the stack growth here, base always point to the lower address!
self._thread_name = name
self._thread_entry = entry
self._stack_base = base
self._stack_alloc = alloc
self._stack_top = base + size
self._cur_sp = cursp
self._stack_size = size
self._align = align
self._pattern = STACK_COLORATION_PATTERN
self._sanity_check()
def _sanity_check(self):
# do some basic sanity checking to make sure we have a sane stack object
if (
self._stack_base < self._stack_alloc
or not self._stack_size
or self._cur_sp <= self._stack_base
or self._cur_sp > self._stack_base + self._stack_size
):
gdb.write(
f"base: {self._stack_base}, \
size: {self._stack_size}, sp: {self._cur_sp}\n"
)
raise gdb.GdbError("Inconsistant stack size...Maybe memory corruption?")
# TODO: check if stack ptr is located at a sane address range!
def cur_usage(self):
usage = self._stack_top - self._cur_sp
if self.is_stackof():
gdb.write("An overflow detected, dumping the stack:\n")
ptr_4bytes = gdb.Value(self._stack_base).cast(
utils.lookup_type("unsigned int").pointer()
)
for i in range(0, self._stack_size // 4):
if i % 8 == 0:
gdb.write(f"{hex(self._stack_base + 4 * i)}: ")
gdb.write(f"{hex(ptr_4bytes[i]):10} ")
if i % 8 == 7:
gdb.write("\n")
gdb.write("\n")
raise gdb.GdbError(
"pls check your stack size! @ {0} sp:{1:x} base:{2:x}".format(
self._thread_name, self._cur_sp, self._stack_base
)
)
return usage
def check_max_usage(self):
ptr_4bytes = gdb.Value(self._stack_base).cast(
utils.lookup_type("unsigned int").pointer()
)
spare = 0
for i in range(0, self._stack_size // 4):
if int(ptr_4bytes[i]) != self._pattern:
spare = i * 4
break
return self._stack_size - spare
def max_usage(self):
if not utils.get_symbol_value("CONFIG_STACK_COLORATION"):
return 0
return self.check_max_usage()
def avalaible(self):
cur_usage = self.cur_usage()
return self._stack_size - cur_usage
def maxdepth_backtrace(self):
raise gdb.GdbError("Not implemented yet", traceback.print_stack())
def cur_sp(self):
return self._cur_sp
def is_stackof(self):
# we should notify the user if the stack overflow is about to happen as well!
return self._cur_sp <= self._stack_base
def has_stackof(self):
max_usage = self.max_usage()
return max_usage >= self._stack_size
# Always refetch the stack infos, never cached as we may have threads created/destroyed
# dynamically!
def fetch_stacks():
stacks = dict()
for tcb in utils.get_tcbs():
# We have no way to detect if we are in an interrupt context for now.
# Originally we use `and not utils.in_interrupt_context()`
if tcb["task_state"] == gdb.parse_and_eval("TSTATE_TASK_RUNNING"):
sp = utils.get_sp()
else:
sp = utils.get_sp(tcb=tcb)
try:
stacks[int(tcb["pid"])] = Stack(
utils.get_task_name(tcb),
hex(tcb["entry"]["pthread"]), # should use main?
int(tcb["stack_base_ptr"]),
int(tcb["stack_alloc_ptr"]),
int(tcb["adj_stack_size"]),
sp,
4,
)
except gdb.GdbError as e:
pid = tcb["pid"]
gdb.write(
f"Failed to construction stack object for tcb {pid} due to: {e}\n"
)
return stacks
class StackUsage(gdb.Command):
"""Display the stack usage of each thread, similar to cat /proc/<pid>/stack"""
def __init__(self):
super().__init__("stack-usage", gdb.COMMAND_USER)
self._stacks = []
# format template
self._fmt = (
"{0: <4} | {1: <10} | {2: <10} | {3: <20} | {4: <10} | {5: <10} | {6: <10}"
)
def format_print(self, pid, stack):
def gen_info_str(x):
usage = x / stack._stack_size
res = ",".join([str(x), "{0:.2%}".format(usage)])
if usage > 0.8:
res += "!"
return res
gdb.write(
self._fmt.format(
pid,
stack._thread_name[:10],
stack._thread_entry,
hex(stack._stack_base),
stack._stack_size,
gen_info_str(stack.cur_usage()),
gen_info_str(stack.max_usage()),
)
)
gdb.write("\n")
def invoke(self, args, from_tty):
stacks = fetch_stacks()
args = [int(arg) for arg in args.split()]
pids = stacks.keys() if len(args) == 0 else args
gdb.write(
self._fmt.format(
"Pid", "Name", "Entry", "Base", "Size", "CurUsage", "MaxUsage"
)
)
gdb.write("\n")
for pid in pids:
stack = stacks.get(pid)
if not stack:
continue
self.format_print(pid, stack)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
############################################################################
# tools/pynuttx/nxgdb/wdog.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
from __future__ import annotations
from typing import List
import gdb
from . import lists, utils
from .protocols import wdog as p
from .utils import Value
class WDog(Value, p.WDog):
def __init__(self, wdog: p.WDog):
if wdog.type.code == gdb.TYPE_CODE_PTR:
wdog = wdog.dereference()
super().__init__(wdog)
def __repr__(self) -> str:
return (
f"WDog@{self.address:#x}: tick: {self.expired: <16}"
f" {self.func.format_string(styling=True)} arg: {self.arg:#x}"
)
def __str__(self) -> str:
return self.__repr__()
def get_wdog_list() -> List[WDog]:
wdogs = []
active = utils.parse_and_eval("g_wdactivelist")
for wdog in lists.NxList(active, "struct wdog_s", "node"):
wdogs.append(WDog(wdog))
return wdogs
class WDogDump(gdb.Command):
"""Show wdog timer information"""
def __init__(self):
super().__init__("wdog", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
for wdog in get_wdog_list():
print(wdog)
+170
View File
@@ -0,0 +1,170 @@
############################################################################
# tools/pynuttx/nxgdb/wqueue.py
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
from __future__ import annotations
from typing import List
import gdb
from . import lists, utils
from .protocols import wqueue as p
from .utils import Value
class Work(Value, p.Work):
def __init__(self, work: p.Work):
if work.type.code == gdb.TYPE_CODE_PTR:
work = work.dereference()
super().__init__(work)
@property
def wqueue(self) -> WorkQueue:
return WorkQueue(self.wq)
def __repr__(self) -> str:
return f"work_s@{self.address:#x}: {self.worker.format_string(styling=True)} arg={self.arg}"
def __str__(self) -> str:
return self.__repr__()
class KWorker(Value, p.KWorker):
"""Worker thread information"""
def __init__(self, worker: p.KWorker, wqueue=None):
if worker.type.code == gdb.TYPE_CODE_PTR:
worker = worker.dereference()
super().__init__(worker)
self.wqueue = wqueue
@property
def is_running(self):
return self.work
@property
def work(self) -> Work:
work = self["work"]
return Work(work) if work else None
@property
def name(self):
return utils.get_task_name(utils.get_tcb(self.pid))
def __repr__(self):
return f"kworker_s@{self.address:#x} {self.work or 'idle'}"
def __str__(self):
return self.__repr__()
class WorkQueue(Value, p.KWorkQueue):
"""Work queue information"""
def __init__(self, queue: p.KWorkQueue, name: str = None):
if queue.type.code == gdb.TYPE_CODE_PTR:
queue = queue.dereference()
super().__init__(queue)
self.name = name or "<noname>"
@property
def workers(self) -> List[Work]:
work_s = utils.lookup_type("struct work_s")
return [
Work(worker.cast(work_s.pointer())) for worker in lists.NxDQueue(self.q)
]
@property
def threads(self) -> List[KWorker]:
return [
KWorker(thread, wqueue=self)
for thread in utils.ArrayIterator(self.worker, self.nthreads)
]
@property
def nthreads(self):
return int(self["nthreads"])
@property
def is_running(self):
return any(thread.is_running for thread in self.threads)
@property
def is_exiting(self):
return self.exit
def __repr__(self):
state = "running" if self.is_running else "idle"
return f"{self.name}@{self.address:#x}, {state}, {self.nthreads} threads, {len(self.workers)} work"
def __str__(self):
return self.__repr__()
def get_work_queues() -> List[WorkQueue]:
entry = gdb.parse_and_eval("work_thread")
kwork_wqueue_s = utils.lookup_type("struct kwork_wqueue_s")
tcbs = utils.get_tcbs()
# The function address may be or'ed with 0x01
tcbs = filter(lambda tcb: int(tcb.entry.main) & ~0x01 == entry, tcbs)
queue = []
for tcb in tcbs:
if not (args := utils.get_task_argvstr(tcb)):
continue
# wqueue = (FAR struct kwork_wqueue_s *)
# ((uintptr_t)strtoul(argv[1], NULL, 16));
# kworker = (FAR struct kworker_s *)
# ((uintptr_t)strtoul(argv[2], NULL, 16));
wqueue = gdb.Value(int(args[1], 16)).cast(kwork_wqueue_s.pointer())
wqueue = WorkQueue(wqueue, name=utils.get_task_name(tcb))
if wqueue not in queue:
queue.append(wqueue)
return queue
class WorkQueueDump(gdb.Command):
"""Show work queue information"""
def __init__(self):
if not utils.get_symbol_value("CONFIG_SCHED_WORKQUEUE"):
return
super().__init__("worker", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
queues = get_work_queues()
for queue in queues:
print(f"{queue}")
if not queue.is_running:
continue
print(" Running:") # Dump the work that is running
running = [thread for thread in queue.threads if thread.is_running]
print("\n".join(f" {thread}" for thread in running))
if not queue.workers:
continue
print(" Queued:")
print("\n".join(f" {work}" for work in queue.workers))
+22
View File
@@ -0,0 +1,22 @@
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"
[project]
version = "0.0.1"
name = 'nxgdb'
description = 'NuttX RTOS GDB extensions'
readme = "README.md"
license = { file = 'LICENSE' }
classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
]
dependencies = ["matplotlib", "numpy", "pyelftools"]
requires-python = ">=3.8"
[tool.setuptools.package-dir]
nxgdb = "nxgdb"
+13
View File
@@ -0,0 +1,13 @@
contourpy==1.3.0
cycler==0.12.1
debugpy==1.8.7
fonttools==4.54.1
kiwisolver==1.4.7
matplotlib==3.9.2
numpy==2.1.2
packaging==24.1
pillow==11.0.0
pyelftools==0.31
pyparsing==3.2.0
python-dateutil==2.9.0.post0
six==1.16.0