from __future__ import annotations
import argparse
from typing import Generator, List, Tuple
import gdb
from . import autocompeletion, backtrace, lists, utils
from .backtrace import CONFIG_LIBC_BACKTRACE_DEPTH
from .protocols import mm as p
from .utils import Value
CONFIG_MM_RECORD_STACK = utils.has_field("struct mm_freenode_s", "stack")
CONFIG_MM_RECORD_PID = utils.has_field("struct mm_freenode_s", "pid")
CONFIG_MM_RECORD_SEQNO = utils.has_field("struct mm_freenode_s", "seqno")
CONFIG_MM_RECORD = (
CONFIG_MM_RECORD_STACK or CONFIG_MM_RECORD_PID or CONFIG_MM_RECORD_SEQNO
)
MM_RECORD_STACK_DEPTH = CONFIG_LIBC_BACKTRACE_DEPTH if CONFIG_MM_RECORD_STACK else 0
mempool_record_s = utils.lookup_type("struct mempool_record_s")
mm_record_size = 0
g_sections = None
PID_MM_INVALID = -100
PID_MM_MEMPOOL = -1
def get_sections():
global g_sections
if g_sections is None:
g_sections = gdb.execute("maintenance info sections", to_string=True)
return g_sections
def mm_alignup(size: int) -> int:
size_t = utils.lookup_type("uintptr_t")
CONFIG_MM_DEFAULT_ALIGNMENT = 2 * size_t.sizeof
align = CONFIG_MM_DEFAULT_ALIGNMENT
size = (size + align - 1) & ~(align - 1)
return size
if mempool_record_s and CONFIG_MM_RECORD:
mm_record_size = mm_alignup(mempool_record_s.sizeof)
class MemPoolBlock:
"""
Memory pool block instance.
"""
MAGIC_ALLOC = 0xAAAA_AAAA
def __init__(
self, addr: int, blocksize: int, overhead: int, pool: MemPool = None
) -> None:
"""
Initialize the memory pool block instance.
block: must be start address of the block,
blocksize: block size without backtrace overhead,
overhead: backtrace overhead size.
pool: the MemPool instance that this block belongs to.
"""
self.overhead = overhead
self.from_pool = True
self.is_orphan = False
self.address = addr
self.blocksize = int(blocksize)
self.nodesize = int(blocksize) + self.overhead
self.usersize = self.blocksize
self.useraddress = self.address
self.pool = pool
self._backtrace = self._pid = self._seqno = self._magic = self._record = None
def __repr__(self) -> str:
return f"block@{hex(self.address)},size:{self.blocksize},seqno:{self.seqno},pid:{self.pid}"
def __str__(self) -> str:
return self.__repr__()
def __hash__(self) -> int:
return hash((self.pid, self.nodesize, self.backtrace))
def __eq__(self, value: MemPoolBlock) -> bool:
return (
self.pid == value.pid
and self.nodesize == value.nodesize
and self.backtrace == value.backtrace
)
def contains(self, address: int) -> bool:
"""Check if the address is in block's range, excluding overhead"""
return self.address <= address < self.address + self.blocksize
@property
def record(self) -> p.MemPoolBlock:
if not self._record:
addr = int(self.address)
addr -= mm_record_size
self._record = (
gdb.Value(addr).cast(mempool_record_s.pointer()).dereference()
)
return self._record
@property
def is_free(self) -> bool:
if not CONFIG_MM_RECORD:
return self.pool.is_free(self) if self.pool else None
if not self._magic:
self._magic = int(self.record["magic"])
return self._magic != self.MAGIC_ALLOC
@property
def seqno(self) -> int:
if not self._seqno:
self._seqno = (
int(self.record["seqno"]) if CONFIG_MM_RECORD_SEQNO else PID_MM_INVALID
)
return self._seqno
@property
def pid(self) -> int:
if not self._pid:
self._pid = (
int(self.record["pid"]) if CONFIG_MM_RECORD_PID else PID_MM_INVALID
)
return self._pid
@property
def backtrace(self) -> Tuple[int]:
if MM_RECORD_STACK_DEPTH <= 0:
return ()
if not self._backtrace:
self._backtrace = tuple(
backtrace.BacktraceEntry(self.record["stack"]).get()
)
return self._backtrace
@property
def prevnode(self) -> MemPoolBlock:
addr = self.address - self.nodesize
return MemPoolBlock(addr, self.blocksize, self.overhead, pool=self.pool)
@property
def nextnode(self) -> MemPoolBlock:
addr = self.address + self.nodesize
return MemPoolBlock(addr, self.blocksize, self.overhead, pool=self.pool)
def read_memory(self) -> memoryview:
return gdb.selected_inferior().read_memory(self.address, self.blocksize)
class MemPool(Value, p.MemPool):
"""
Memory pool instance.
"""
def __init__(self, mpool: Value, name=None) -> None:
if mpool.type.code == gdb.TYPE_CODE_PTR:
mpool = mpool.dereference()
super().__init__(mpool)
self._blksize = None
self._nfree = None
self._nifree = None
self._overhead = None
self._free_blks = None
def __repr__(self) -> str:
return f"{self.name}@{hex(self.address)},size:{self.size}/{self['blocksize']},nused:{self.nused},nfree:{self.nfree}"
def __str__(self) -> str:
return self.__repr__()
@property
def name(self) -> str:
try:
return self.procfs.name.string()
except Exception:
return "<noname>"
@property
def memranges(self) -> Generator[Tuple[int, int], None, None]:
"""Memory ranges of the pool"""
sq_entry_t = utils.lookup_type("sq_entry_t")
blksize = self.size
if self.ibase:
blks = int(self.interruptsize) // blksize
base = int(self.ibase)
yield (base, base + blks * blksize)
if not self.equeue.head:
return None
ninit = int(self.initialsize)
ninit = ninit and (ninit - sq_entry_t.sizeof) // blksize
nexpand = (int(self.expandsize) - sq_entry_t.sizeof) // blksize
for entry in lists.NxSQueue(self.equeue):
blks = ninit or nexpand
ninit = 0
yield (int(entry) - blks * blksize, int(entry))
@property
def nqueue(self) -> int:
return lists.sq_count(self.equeue)
@property
def size(self) -> int:
"""Real block size including backtrace overhead"""
if not self._blksize:
blksize = self["blocksize"]
if CONFIG_MM_RECORD:
blksize = mm_alignup(blksize + mempool_record_s.sizeof)
self._blksize = int(blksize)
return self._blksize
@property
def overhead(self) -> int:
if not self._overhead:
self._overhead = self.size - int(self["blocksize"])
return self._overhead
@property
def nwaiter(self) -> int:
return (
-int(self.waitsem.val.semcount) if self.wait and self.expandsize == 0 else 0
)
@property
def nused(self) -> int:
return int(self.nalloc)
@property
def free(self) -> int:
return (self.nfree + self.nifree) * self.size
@property
def nfree(self) -> int:
if not self._nfree:
self._nfree = lists.sq_count(self.queue)
return self._nfree + self.nifree
@property
def nifree(self) -> int:
"""Interrupt pool free blocks count"""
if not self._nifree:
self._nifree = lists.sq_count(self.iqueue)
return self._nifree
@property
def total(self) -> int:
nqueue = lists.sq_count(self.equeue)
sq_entry_t = utils.lookup_type("sq_entry_t")
blocks = self.nused + self.nfree
return int(nqueue * sq_entry_t.sizeof + blocks * self.size)
@property
def blks(self) -> Generator[MemPoolBlock, None, None]:
"""Iterate over all blocks in the pool"""
sq_entry_t = utils.lookup_type("sq_entry_t")
blksize = self.size
blocksize = self["blocksize"]
def iterate(entry, nblocks):
base = int(entry) - nblocks * blksize
while nblocks > 0:
yield MemPoolBlock(
base + mm_record_size, blocksize, self.overhead, pool=self
)
base += blksize
nblocks -= 1
if self.ibase:
blks = int(self.interruptsize) // blksize
yield from iterate(self.ibase + blks * blksize, blks)
if not self.equeue.head:
return None
ninit = int(self.initialsize)
ninit = ninit and (ninit - sq_entry_t.sizeof) // blksize
nexpand = (int(self.expandsize) - sq_entry_t.sizeof) // blksize
for entry in lists.NxSQueue(self.equeue):
yield from iterate(entry, ninit or nexpand)
ninit = 0
def contains(self, address: int) -> Tuple[bool, Value]:
ranges = self.memranges
if not ranges:
return False, None
for start, end in ranges:
if start <= address < end:
return True, None
def find(self, address: int) -> Value:
"""Find the block that contains the given address"""
sq_entry_t = utils.lookup_type("sq_entry_t")
blksize = self.size
blocksize = self["blocksize"]
def get_blk(base):
blkstart = base + (address - base) // blksize * blksize
blkstart += mm_record_size
return MemPoolBlock(blkstart, blocksize, self.overhead, pool=self)
if self.ibase:
blks = int(self.interruptsize) // blksize
base = int(self.ibase)
if base <= address < base + blks * blksize:
return get_blk(base)
if not self.equeue.head:
return None
ninit = int(self.initialsize)
ninit = ninit and (ninit - sq_entry_t.sizeof) // blksize
nexpand = (int(self.expandsize) - sq_entry_t.sizeof) // blksize
for entry in lists.NxSQueue(self.equeue):
blks = ninit or nexpand
ninit = 0
base = int(entry) - blks * blksize
if base <= address < int(entry):
return get_blk(base)
def blks_free(self) -> Generator[MemPoolBlock, None, None]:
"""Iterate over all free blocks in the pool"""
blocksize = self["blocksize"]
for entry in lists.NxSQueue(self.queue):
yield MemPoolBlock(int(entry), blocksize, self.overhead, pool=self)
def blks_used(self) -> Generator[MemPoolBlock, None, None]:
"""Iterate over all used blocks in the pool"""
return filter(lambda blk: not blk.is_free, self.blks)
def is_free(self, blk: MemPoolBlock) -> bool:
"""Check if the given block is free in the pool"""
if self._free_blks is None:
try:
self._free_blks = set(free.address for free in self.blks_free())
except Exception:
self._free_blks = set()
def clear_free_blks(event):
self._free_blks = None
gdb.events.stop.connect(clear_free_blks)
if not self._free_blks:
return None
return blk.address in self._free_blks
class MemPoolMultiple(Value, p.MemPoolMultiple):
"""
Multiple level memory pool instance.
"""
def __init__(self, mpool: Value, name=None) -> None:
if mpool.type.code == gdb.TYPE_CODE_PTR:
mpool = mpool.dereference()
super().__init__(mpool)
def __repr__(self) -> str:
return f"Multiple Level Memory Pool: {self.address}"
def __str__(self) -> str:
return self.__repr__()
@property
def pools(self) -> Generator[MemPool, None, None]:
for pool in utils.ArrayIterator(self["pools"], self.npools):
yield MemPool(pool)
@property
def free(self) -> int:
return sum(pool.free for pool in self.pools)
@property
def chunks(self) -> Generator[MemPoolBlock, None, None]:
for chunk in lists.sq_for_every(self.chunk_queue):
chunk = chunk.cast(gdb.lookup_type("struct mpool_chunk_s"))
yield chunk
class MMNode(gdb.Value, p.MMFreeNode):
"""
One memory node in the memory manager heap, either free or allocated.
The instance is always dereferenced to the actual node.
"""
MM_ALLOC_BIT = 0x1
MM_PREVFREE_BIT = 0x2
MM_MASK_BIT = MM_ALLOC_BIT | MM_PREVFREE_BIT
try:
MM_SIZEOF_ALLOCNODE = utils.sizeof("struct mm_allocnode_s")
MM_ALLOCNODE_OVERHEAD = MM_SIZEOF_ALLOCNODE
MM_MIN_SHIFT = utils.log2ceil(utils.sizeof("struct mm_freenode_s"))
MM_MIN_CHUNK = 1 << MM_MIN_SHIFT
except Exception:
MM_SIZEOF_ALLOCNODE = 0
MM_ALLOCNODE_OVERHEAD = 0
MM_MIN_SHIFT = 0
MM_MIN_CHUNK = 0
def __init__(self, node: gdb.Value):
if node.type.code == gdb.TYPE_CODE_PTR:
node = node.dereference()
self._backtrace = None
self._address = None
self._nodesize = None
super().__init__(node)
def __repr__(self):
return (
f"{hex(self.address)}({'F' if self.is_free else 'A'}{'F' if self.is_prev_free else 'A'})"
f" size:{self.nodesize}/{self.prevsize if self.is_prev_free else '-'}"
f" seq:{self.seqno} pid:{self.pid} "
)
def __str__(self) -> str:
return self.__repr__()
def __hash__(self) -> int:
return hash((self.pid, self.nodesize, self.backtrace))
def __eq__(self, value: MMNode) -> bool:
return (
self.pid == value.pid
and self.nodesize == value.nodesize
and self.backtrace == value.backtrace
)
def contains(self, address):
"""Check if the address is in node's range, excluding overhead"""
return self.useraddress <= address < self.useraddress + self.usersize
def read_memory(self):
return gdb.selected_inferior().read_memory(self.useraddress, self.usersize)
@property
def address(self) -> int:
"""Change 'void *' to int"""
if not self._address:
self._address = int(super().address)
return self._address
@property
def useraddress(self) -> int:
"""Address of user memory, excluding overhead"""
return self.address + self.overhead
@property
def prevsize(self) -> int:
"""Size of preceding chunk size"""
return int(self["preceding"]) & ~MMNode.MM_MASK_BIT
@property
def nodesize(self) -> int:
"""Size of this chunk, including overhead"""
if not self._nodesize:
self._nodesize = int(self["size"]) & ~MMNode.MM_MASK_BIT
return self._nodesize
@property
def usersize(self) -> int:
"""Size of this chunk, excluding overhead"""
usersize = self.nodesize - MMNode.MM_ALLOCNODE_OVERHEAD
usersize += 4
return usersize
@property
def flink(self):
return MMNode(self["flink"]) if self.is_free and self["flink"] else None
@property
def blink(self):
return MMNode(self["blink"]) if self.is_free and self["blink"] else None
@property
def pid(self) -> int:
if CONFIG_MM_RECORD_PID:
return int(self["pid"])
return PID_MM_INVALID
@property
def seqno(self) -> int:
return int(self["seqno"]) if CONFIG_MM_RECORD_SEQNO else -1
@property
def backtrace(self) -> List[Tuple[int, str, str]]:
if MM_RECORD_STACK_DEPTH <= 0:
return ()
try:
if not self._backtrace and (
stack := backtrace.BacktraceEntry(self["stack"]).get()
):
self._backtrace = tuple(stack)
except gdb.MemoryError:
self._backtrace = tuple(backtrace.BacktraceEntry(0).get())
return self._backtrace
@property
def prevnode(self) -> MMNode:
if not self.is_prev_free:
return None
addr = int(self.address) - self.prevsize
type = utils.lookup_type("struct mm_freenode_s").pointer()
return MMNode(gdb.Value(addr).cast(type))
@property
def nextnode(self) -> MMNode:
if not self.nodesize:
gdb.write(f"\n\x1b[31;1m Node corrupted: {self} \x1b[m\n")
return None
addr = int(self.address) + self.nodesize
type = utils.lookup_type("struct mm_freenode_s").pointer()
return MMNode(gdb.Value(addr).cast(type))
@property
def is_free(self) -> bool:
return not self["size"] & MMNode.MM_ALLOC_BIT
@property
def is_prev_free(self) -> bool:
return self["size"] & MMNode.MM_PREVFREE_BIT
@property
def is_orphan(self) -> bool:
return self.is_prev_free or self.nextnode.is_free
@property
def from_pool(self) -> bool:
return False
@property
def overhead(self) -> int:
return MMNode.MM_ALLOCNODE_OVERHEAD
class MMHeap(Value, p.MMHeap):
"""
One memory manager heap. It may contains multiple regions.
"""
def __init__(self, heap: Value, name=None) -> None:
mm_heap_s = utils.lookup_type("struct mm_heap_s")
if isinstance(heap, int) or heap.type.code == gdb.TYPE_CODE_INT:
heap = gdb.Value(heap).cast(mm_heap_s.pointer()).dereference()
elif heap.type.code == gdb.TYPE_CODE_PTR:
heap = heap.dereference()
if heap.type != mm_heap_s:
raise ValueError(f"Invalid heap type: {heap.type}")
super().__init__(heap)
self.name = name or "<noname>"
self._regions = None
try:
for start, end in self.regions:
gdb.selected_inferior().read_memory(start.address, 1)
gdb.selected_inferior().read_memory(end.address, 1)
except gdb.MemoryError:
raise ValueError(f"Heap node not accessible: {heap}")
def __repr__(self) -> str:
regions = [
f"{hex(start.address)}~{hex(end.address)}" for start, end in self.regions
]
return f"{self.name}@{self.address}, {int(self.heapsize) / 1024 :.1f}kB {self.nregions}regions: {','.join(regions)}"
def __str__(self) -> str:
return self.__repr__()
@property
def curused(self) -> int:
return int(self.mm_curused)
@property
def heapsize(self) -> int:
return int(self.mm_heapsize)
@property
def free(self) -> int:
return self.heapsize - self.curused
@property
def nregions(self) -> int:
return int(utils.get_field(self, "mm_nregions", default=1))
@property
def mm_mpool(self) -> MemPoolMultiple:
mpool = utils.get_field(self, "mm_mpool", default=None)
return MemPoolMultiple(mpool) if mpool else None
@property
def regions(self) -> List[Tuple[MMNode, MMNode]]:
if not self._regions:
regions = self.nregions
self._regions = []
for start, end in zip(
utils.ArrayIterator(self.mm_heapstart, regions),
utils.ArrayIterator(self.mm_heapend, regions),
):
self._regions.append((MMNode(start), MMNode(end)))
return self._regions
@property
def nodes(self) -> Generator[MMNode, None, None]:
for start, end in self.regions:
node = start
while node and node.address <= end.address:
yield node
node = node.nextnode
def nodes_free(self) -> Generator[MMNode, None, None]:
return filter(lambda node: node.is_free, self.nodes)
def nodes_used(self) -> Generator[MMNode, None, None]:
return filter(lambda node: not node.is_free, self.nodes)
def contains(self, address: int) -> bool:
ranges = [[int(start.address), int(end.address)] for start, end in self.regions]
ranges[0][0] = int(self.address)
return any(start <= address <= end for start, end in ranges)
def find(self, address: int) -> MMNode:
for node in self.nodes:
if node.address <= address < node.address + node.nodesize:
return node
def get_heaps() -> List[MMHeap]:
heaps = []
meminfo: p.ProcfsMeminfoEntry = utils.gdb_eval_or_none("g_procfs_meminfo")
if not meminfo and (heap := utils.parse_and_eval("g_mmheap")):
try:
heaps.append(MMHeap(heap))
except Exception:
pass
try:
while meminfo:
try:
heap = MMHeap(meminfo.heap, name=meminfo.name.string())
heaps.append(heap)
except Exception:
pass
meminfo = meminfo.next
except gdb.MemoryError:
pass
return heaps
def get_pools(heaps: List[Value] = []) -> Generator[MemPool, None, None]:
for heap in heaps or get_heaps():
if not (mm_pool := heap.mm_mpool):
continue
for pool in mm_pool.pools:
yield pool
def memory_range(heap=True, globals=True) -> List[Tuple[int, int]]:
memranges = []
if globals:
sections = get_sections()
for line in sections.splitlines():
if "ALLOC" in line and "READONLY" not in line:
parts = line.split()
start = int(parts[1].split("->")[0], 16)
end = int(parts[1].split("->")[1], 16)
if start == end:
continue
memranges.append((start, end))
idle_topstack = int(utils.parse_and_eval("g_idle_topstack").cast("uintptr_t"))
idle_stacksize = int(utils.parse_and_eval("CONFIG_IDLETHREAD_STACKSIZE"))
memranges.append((idle_topstack - idle_stacksize, idle_topstack))
if heap:
for heap in get_heaps():
for i in range(heap.nregions):
start = int(heap["mm_heapstart"][i])
end = int(heap["mm_heapend"][i]) + MMNode.MM_SIZEOF_ALLOCNODE
if start == end:
continue
if i == 0:
start = int(heap.address)
for r in memranges:
if r[0] <= start and r[1] >= end:
break
elif start <= r[0] and end >= r[1]:
memranges.remove(r)
memranges.append((start, end))
break
else:
memranges.append((start, end))
return sorted(memranges, key=lambda x: x[0])
def get_memrange(
rangestr: str = "", heap_only=False, globals_only=False
) -> List[Tuple[int, int]]:
"""
Parse memory range from string or get from heap and globals,
the string should be in the format of "start1,end1,attr1 start2,end2,attr2".
"""
memrange = []
if rangestr:
values = rangestr.replace('"', "").split(",")
for i in range(0, len(values), 3):
start = utils.parse_arg(values[i])
end = utils.parse_arg(values[i + 1])
memrange.append((start, end))
else:
memrange = memory_range(heap=not globals_only, globals=not heap_only)
merged_ranges = []
for start, end in sorted(memrange):
if merged_ranges and start <= merged_ranges[-1][1]:
merged_ranges[-1] = (merged_ranges[-1][0], max(merged_ranges[-1][1], end))
else:
merged_ranges.append((start, end))
return merged_ranges
def get_nodes_dict():
"""
Return dict of all memory nodes, including memory pool.
"""
if hasattr(get_nodes_dict, "_cached_nodes"):
return get_nodes_dict._cached_nodes
nodes = []
for heap in get_heaps():
nodes.extend(
{
"name": heap.name,
"address": node.address,
"size": node.nodesize,
"seqno": node.seqno,
"pid": node.pid,
"free": node.is_free,
"from_pool": False,
"backtrace": node.backtrace,
}
for node in heap.nodes
)
for pool in get_pools([heap]):
nodes.extend(
{
"name": f"{heap.name}@{blk.nodesize}pool",
"address": blk.address,
"size": blk.nodesize,
"seqno": blk.seqno,
"pid": blk.pid,
"free": blk.is_free,
"from_pool": True,
"backtrace": blk.backtrace,
}
for blk in pool.blks
)
get_nodes_dict._cached_nodes = nodes
return nodes
class MMHeapInfo(gdb.Command):
"""Show basic heap information"""
def __init__(self):
super().__init__("mm heap", gdb.COMMAND_USER)
@utils.dont_repeat_decorator
def invoke(self, arg: str, from_tty: bool) -> None:
for heap in get_heaps():
regions = [(start.address, end.address) for start, end in heap.regions]
gdb.write(f"{heap} - has {len(list(heap.nodes))} nodes, regions:")
gdb.write(" ".join(f"{hex(start)}~{hex(end)}" for start, end in regions))
gdb.write("\n")
@autocompeletion.complete
class MMPoolInfo(gdb.Command):
"""Show basic heap information"""
def get_argparser(self):
parser = argparse.ArgumentParser(description="Dump memory pool information.")
parser.add_argument(
"--heap",
type=str,
metavar="file",
help="Which heap's pool to show",
default=None,
)
return parser
def __init__(self):
super().__init__("mm pool", gdb.COMMAND_USER)
utils.alias("mempool", "mm pool")
self.parser = self.get_argparser()
@utils.dont_repeat_decorator
def invoke(self, arg: str, from_tty: bool) -> None:
try:
args = self.parser.parse_args(gdb.string_to_argv(arg))
except SystemExit:
return
heaps = [utils.parse_and_eval(args.heap)] if args.heap else get_heaps()
if not (pools := list(get_pools(heaps))):
gdb.write("No pools found.\n")
return
count = len(pools)
gdb.write(f"Total {count} pools\n")
name_max = max(len(pool.name) for pool in pools) + 11
formatter = (
"{:>%d} {:>11} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9} {:>9}\n" % name_max
)
head = (
"",
"total",
"blocksize",
"bsize",
"overhead",
"nused",
"nfree",
"nifree",
"nwaiter",
"nqueue",
)
gdb.write(formatter.format(*head))
for pool in pools:
gdb.write(
formatter.format(
f"{pool.name}@{pool.address:#x}",
pool.total,
pool.blocksize,
pool.size,
pool.overhead,
pool.nused,
pool.nfree,
pool.nifree,
pool.nwaiter,
pool.nqueue,
)
)