scripts: Reverted stack.py to use -fcallgraph-info=su again
See previous commit for the issues with stack.py's current approach. I'm convinced dwarf-info simply does not contain enough info to figure out stack usage. There is one last idea, which is to parse the dissassembly. In theory you only need to understand calls, branches (for control-flow), and push/pop instructions to figure out the worst-case stack usage. But this would be ISA-specific and error-prone, so it probably shouldn't _replace_ the -fcallgraph-info=su based stack.py. So, out of ideas, reverting. --- It's worth noting this isn't a trivial revert. There's a couple interesting changes in stack.py: - We now use .o files to map callgraph nodes to relevant symbol names. This should be a bit more robust than relying only on the names in the .ci files, and guarantees function names line up with other symbol-based scripts (code.py, ctx.py, etc). This also lets us warn on missing callgraph nodes, in case the callgraph info is incomplete. - Callgraph parsing should be quite a bit more robust now. Added a small (and reusable?) Parser class. - Moved cycle detection into result collection. This should let us drop cycle detection from the table renderer eventually.
This commit is contained in:
@@ -22,6 +22,7 @@ SRC ?= $(filter-out $(wildcard *.t.* *.b.*),$(wildcard *.c))
|
|||||||
OBJ := $(SRC:%.c=$(BUILDDIR)/%.o)
|
OBJ := $(SRC:%.c=$(BUILDDIR)/%.o)
|
||||||
DEP := $(SRC:%.c=$(BUILDDIR)/%.d)
|
DEP := $(SRC:%.c=$(BUILDDIR)/%.d)
|
||||||
ASM := $(SRC:%.c=$(BUILDDIR)/%.s)
|
ASM := $(SRC:%.c=$(BUILDDIR)/%.s)
|
||||||
|
CI := $(SRC:%.c=$(BUILDDIR)/%.ci)
|
||||||
GCDA := $(SRC:%.c=$(BUILDDIR)/%.t.a.gcda)
|
GCDA := $(SRC:%.c=$(BUILDDIR)/%.t.a.gcda)
|
||||||
|
|
||||||
TESTS ?= $(wildcard tests/*.toml)
|
TESTS ?= $(wildcard tests/*.toml)
|
||||||
@@ -34,6 +35,7 @@ TEST_C := $(TESTS:%.toml=$(BUILDDIR)/%.t.c) \
|
|||||||
TEST_A := $(TEST_C:%.t.c=%.t.a.c)
|
TEST_A := $(TEST_C:%.t.c=%.t.a.c)
|
||||||
TEST_OBJ := $(TEST_A:%.t.a.c=%.t.a.o)
|
TEST_OBJ := $(TEST_A:%.t.a.c=%.t.a.o)
|
||||||
TEST_DEP := $(TEST_A:%.t.a.c=%.t.a.d)
|
TEST_DEP := $(TEST_A:%.t.a.c=%.t.a.d)
|
||||||
|
TEST_CI := $(TEST_A:%.t.a.c=%.t.a.ci)
|
||||||
TEST_GCNO := $(TEST_A:%.t.a.c=%.t.a.gcno)
|
TEST_GCNO := $(TEST_A:%.t.a.c=%.t.a.gcno)
|
||||||
TEST_GCDA := $(TEST_A:%.t.a.c=%.t.a.gcda)
|
TEST_GCDA := $(TEST_A:%.t.a.c=%.t.a.gcda)
|
||||||
TEST_PERF := $(TEST_RUNNER:%=%.perf)
|
TEST_PERF := $(TEST_RUNNER:%=%.perf)
|
||||||
@@ -50,6 +52,7 @@ BENCH_C := $(BENCHES:%.toml=$(BUILDDIR)/%.b.c) \
|
|||||||
BENCH_A := $(BENCH_C:%.b.c=%.b.a.c)
|
BENCH_A := $(BENCH_C:%.b.c=%.b.a.c)
|
||||||
BENCH_OBJ := $(BENCH_A:%.b.a.c=%.b.a.o)
|
BENCH_OBJ := $(BENCH_A:%.b.a.c=%.b.a.o)
|
||||||
BENCH_DEP := $(BENCH_A:%.b.a.c=%.b.a.d)
|
BENCH_DEP := $(BENCH_A:%.b.a.c=%.b.a.d)
|
||||||
|
BENCH_CI := $(BENCH_A:%.b.a.c=%.b.a.ci)
|
||||||
BENCH_GCNO := $(BENCH_A:%.b.a.c=%.b.a.gcno)
|
BENCH_GCNO := $(BENCH_A:%.b.a.c=%.b.a.gcno)
|
||||||
BENCH_GCDA := $(BENCH_A:%.b.a.c=%.b.a.gcda)
|
BENCH_GCDA := $(BENCH_A:%.b.a.c=%.b.a.gcda)
|
||||||
BENCH_PERF := $(BENCH_RUNNER:%=%.perf)
|
BENCH_PERF := $(BENCH_RUNNER:%=%.perf)
|
||||||
@@ -67,6 +70,7 @@ GDB ?= gdb
|
|||||||
PERF ?= perf
|
PERF ?= perf
|
||||||
PRETTYASSERTS ?= ./scripts/prettyasserts.py
|
PRETTYASSERTS ?= ./scripts/prettyasserts.py
|
||||||
|
|
||||||
|
CFLAGS += -fcallgraph-info=su
|
||||||
CFLAGS += -g3
|
CFLAGS += -g3
|
||||||
CFLAGS += -I.
|
CFLAGS += -I.
|
||||||
CFLAGS += -std=c99 -Wall -Wextra -pedantic
|
CFLAGS += -std=c99 -Wall -Wextra -pedantic
|
||||||
@@ -101,7 +105,7 @@ ifdef VERBOSE
|
|||||||
CODEFLAGS += -v
|
CODEFLAGS += -v
|
||||||
DATAFLAGS += -v
|
DATAFLAGS += -v
|
||||||
STACKFLAGS += -v
|
STACKFLAGS += -v
|
||||||
CTXFLAGS += -v
|
CTXFLAGS += -v
|
||||||
STRUCTSFLAGS += -v
|
STRUCTSFLAGS += -v
|
||||||
COVFLAGS += -v
|
COVFLAGS += -v
|
||||||
PERFFLAGS += -v
|
PERFFLAGS += -v
|
||||||
@@ -113,8 +117,8 @@ PERFBDFLAGS += $(filter -j%,$(MAKEFLAGS))
|
|||||||
ifneq ($(OBJDUMP),objdump)
|
ifneq ($(OBJDUMP),objdump)
|
||||||
CODEFLAGS += --objdump-path="$(OBJDUMP)"
|
CODEFLAGS += --objdump-path="$(OBJDUMP)"
|
||||||
DATAFLAGS += --objdump-path="$(OBJDUMP)"
|
DATAFLAGS += --objdump-path="$(OBJDUMP)"
|
||||||
STACKFLAGS += --objdump-path="$(OBJDUMP)"
|
STACKFLAGS += --objdump-path="$(OBJDUMP)"
|
||||||
CTXFLAGS += --objdump-path="$(OBJDUMP)"
|
CTXFLAGS += --objdump-path="$(OBJDUMP)"
|
||||||
STRUCTSFLAGS += --objdump-path="$(OBJDUMP)"
|
STRUCTSFLAGS += --objdump-path="$(OBJDUMP)"
|
||||||
PERFFLAGS += --objdump-path="$(OBJDUMP)"
|
PERFFLAGS += --objdump-path="$(OBJDUMP)"
|
||||||
PERFBDFLAGS += --objdump-path="$(OBJDUMP)"
|
PERFBDFLAGS += --objdump-path="$(OBJDUMP)"
|
||||||
@@ -231,12 +235,12 @@ data-diff: $(OBJ)
|
|||||||
## Find the per-function stack usage
|
## Find the per-function stack usage
|
||||||
.PHONY: stack
|
.PHONY: stack
|
||||||
stack: STACKFLAGS+=-S
|
stack: STACKFLAGS+=-S
|
||||||
stack: $(OBJ) $(BUILDDIR)/lfs.stack.csv
|
stack: $(OBJ) $(CI) $(BUILDDIR)/lfs.stack.csv
|
||||||
./scripts/stack.py $(OBJ) $(STACKFLAGS)
|
./scripts/stack.py $(OBJ) $(CI) $(STACKFLAGS)
|
||||||
|
|
||||||
## Compare per-function stack usage
|
## Compare per-function stack usage
|
||||||
.PHONY: stack-diff
|
.PHONY: stack-diff
|
||||||
stack-diff: $(OBJ)
|
stack-diff: $(OBJ) $(CI)
|
||||||
./scripts/stack.py $^ $(STACKFLAGS) -d $(BUILDDIR)/lfs.stack.csv
|
./scripts/stack.py $^ $(STACKFLAGS) -d $(BUILDDIR)/lfs.stack.csv
|
||||||
|
|
||||||
## Find the per-function context
|
## Find the per-function context
|
||||||
@@ -247,7 +251,7 @@ ctx: $(OBJ) $(BUILDDIR)/lfs.ctx.csv
|
|||||||
|
|
||||||
## Compare per-function context
|
## Compare per-function context
|
||||||
.PHONY: ctx-diff
|
.PHONY: ctx-diff
|
||||||
ctx-diff: $(OBJ)
|
ctx-diff: $(CI)
|
||||||
./scripts/ctx.py $^ $(CTXFLAGS) -d $(BUILDDIR)/lfs.ctx.csv
|
./scripts/ctx.py $^ $(CTXFLAGS) -d $(BUILDDIR)/lfs.ctx.csv
|
||||||
|
|
||||||
## Find function sizes
|
## Find function sizes
|
||||||
@@ -271,16 +275,16 @@ funcs: \
|
|||||||
## Compare function sizes
|
## Compare function sizes
|
||||||
.PHONY: funcs-diff
|
.PHONY: funcs-diff
|
||||||
funcs-diff: SHELL=/bin/bash
|
funcs-diff: SHELL=/bin/bash
|
||||||
funcs-diff: $(OBJ)
|
funcs-diff: $(OBJ) $(CI)
|
||||||
$(strip ./scripts/csv.py \
|
$(strip ./scripts/csv.py \
|
||||||
<(./scripts/csv.py \
|
<(./scripts/csv.py \
|
||||||
<(./scripts/code.py $^ -q $(CODEFLAGS) -o-) \
|
<(./scripts/code.py $(OBJ) -q $(CODEFLAGS) -o-) \
|
||||||
-fcode=size -q -o-) \
|
-fcode=size -q -o-) \
|
||||||
<(./scripts/csv.py \
|
<(./scripts/csv.py \
|
||||||
<(./scripts/stack.py $^ -q $(STACKFLAGS) -o-) \
|
<(./scripts/stack.py $^ -q $(STACKFLAGS) -o-) \
|
||||||
-fstack='max(limit)' -q -o-) \
|
-fstack='max(limit)' -q -o-) \
|
||||||
<(./scripts/csv.py \
|
<(./scripts/csv.py \
|
||||||
<(./scripts/ctx.py $^ -q $(CTXFLAGS) -o-) \
|
<(./scripts/ctx.py $(OBJ) -q $(CTXFLAGS) -o-) \
|
||||||
-fctx='max(size)' -q -o-) \
|
-fctx='max(size)' -q -o-) \
|
||||||
-bfunction -fcode -fstack='max(stack)' -fctx='max(ctx)' \
|
-bfunction -fcode -fstack='max(stack)' -fctx='max(ctx)' \
|
||||||
$(SUMMARYFLAGS) -d <(./scripts/csv.py \
|
$(SUMMARYFLAGS) -d <(./scripts/csv.py \
|
||||||
@@ -372,20 +376,20 @@ summary sizes: \
|
|||||||
## Compare compile-time sizes
|
## Compare compile-time sizes
|
||||||
.PHONY: summary-diff sizes-diff
|
.PHONY: summary-diff sizes-diff
|
||||||
summary-diff sizes-diff: SHELL=/bin/bash
|
summary-diff sizes-diff: SHELL=/bin/bash
|
||||||
summary-diff sizes-diff: $(OBJ)
|
summary-diff sizes-diff: $(OBJ) $(CI)
|
||||||
$(strip ./scripts/csv.py \
|
$(strip ./scripts/csv.py \
|
||||||
<(./scripts/csv.py \
|
<(./scripts/csv.py \
|
||||||
<(./scripts/csv.py \
|
<(./scripts/csv.py \
|
||||||
<(./scripts/code.py $^ -q $(CODEFLAGS) -o-) \
|
<(./scripts/code.py $(OBJ) -q $(CODEFLAGS) -o-) \
|
||||||
-fcode=size -q -o-) \
|
-fcode=size -q -o-) \
|
||||||
<(./scripts/csv.py \
|
<(./scripts/csv.py \
|
||||||
<(./scripts/data.py $^ -q $(DATAFLAGS) -o-) \
|
<(./scripts/data.py $(OBJ) -q $(DATAFLAGS) -o-) \
|
||||||
-fdata=size -q -o-) \
|
-fdata=size -q -o-) \
|
||||||
<(./scripts/csv.py \
|
<(./scripts/csv.py \
|
||||||
<(./scripts/stack.py $^ -q $(STACKFLAGS) -o-) \
|
<(./scripts/stack.py $^ -q $(STACKFLAGS) -o-) \
|
||||||
-fstack='max(limit)' -q -o-) \
|
-fstack='max(limit)' -q -o-) \
|
||||||
<(./scripts/csv.py \
|
<(./scripts/csv.py \
|
||||||
<(./scripts/ctx.py $^ -q $(CTXFLAGS) -o-) \
|
<(./scripts/ctx.py $(OBJ) -q $(CTXFLAGS) -o-) \
|
||||||
-fctx='max(size)' -q -o-) \
|
-fctx='max(size)' -q -o-) \
|
||||||
-fcode -fdata -fstack='max(stack)' -fctx='max(ctx)' \
|
-fcode -fdata -fstack='max(stack)' -fctx='max(ctx)' \
|
||||||
-bbuild='"AFTER"' -q -o-) \
|
-bbuild='"AFTER"' -q -o-) \
|
||||||
@@ -504,7 +508,7 @@ $(BUILDDIR)/lfs.code.csv: $(OBJ)
|
|||||||
$(BUILDDIR)/lfs.data.csv: $(OBJ)
|
$(BUILDDIR)/lfs.data.csv: $(OBJ)
|
||||||
./scripts/data.py $^ -q $(DATAFLAGS) -o $@
|
./scripts/data.py $^ -q $(DATAFLAGS) -o $@
|
||||||
|
|
||||||
$(BUILDDIR)/lfs.stack.csv: $(OBJ)
|
$(BUILDDIR)/lfs.stack.csv: $(OBJ) $(CI)
|
||||||
./scripts/stack.py $^ -q $(STACKFLAGS) -o $@
|
./scripts/stack.py $^ -q $(STACKFLAGS) -o $@
|
||||||
|
|
||||||
$(BUILDDIR)/lfs.ctx.csv: $(OBJ)
|
$(BUILDDIR)/lfs.ctx.csv: $(OBJ)
|
||||||
@@ -540,7 +544,9 @@ $(BUILDDIR)/runners/test_runner: $(TEST_OBJ)
|
|||||||
$(BUILDDIR)/runners/bench_runner: $(BENCH_OBJ)
|
$(BUILDDIR)/runners/bench_runner: $(BENCH_OBJ)
|
||||||
$(CC) $(CFLAGS) $^ $(LFLAGS) -o $@
|
$(CC) $(CFLAGS) $^ $(LFLAGS) -o $@
|
||||||
|
|
||||||
$(BUILDDIR)/%.o: %.c
|
# our main build rule generates .o, .d, and .ci files, the latter
|
||||||
|
# used for stack analysis
|
||||||
|
$(BUILDDIR)/%.o $(BUILDDIR)/%.ci: %.c
|
||||||
$(CC) -c -MMD $(CFLAGS) $< -o $(BUILDDIR)/$*.o
|
$(CC) -c -MMD $(CFLAGS) $< -o $(BUILDDIR)/$*.o
|
||||||
|
|
||||||
$(BUILDDIR)/%.s: %.c
|
$(BUILDDIR)/%.s: %.c
|
||||||
@@ -582,11 +588,13 @@ clean:
|
|||||||
rm -f $(OBJ)
|
rm -f $(OBJ)
|
||||||
rm -f $(DEP)
|
rm -f $(DEP)
|
||||||
rm -f $(ASM)
|
rm -f $(ASM)
|
||||||
|
rm -f $(CI)
|
||||||
rm -f $(TEST_RUNNER)
|
rm -f $(TEST_RUNNER)
|
||||||
rm -f $(TEST_A)
|
rm -f $(TEST_A)
|
||||||
rm -f $(TEST_C)
|
rm -f $(TEST_C)
|
||||||
rm -f $(TEST_OBJ)
|
rm -f $(TEST_OBJ)
|
||||||
rm -f $(TEST_DEP)
|
rm -f $(TEST_DEP)
|
||||||
|
rm -f $(TEST_CI)
|
||||||
rm -f $(TEST_GCNO)
|
rm -f $(TEST_GCNO)
|
||||||
rm -f $(TEST_GCDA)
|
rm -f $(TEST_GCDA)
|
||||||
rm -f $(TEST_PERF)
|
rm -f $(TEST_PERF)
|
||||||
@@ -597,6 +605,7 @@ clean:
|
|||||||
rm -f $(BENCH_C)
|
rm -f $(BENCH_C)
|
||||||
rm -f $(BENCH_OBJ)
|
rm -f $(BENCH_OBJ)
|
||||||
rm -f $(BENCH_DEP)
|
rm -f $(BENCH_DEP)
|
||||||
|
rm -f $(BENCH_CI)
|
||||||
rm -f $(BENCH_GCNO)
|
rm -f $(BENCH_GCNO)
|
||||||
rm -f $(BENCH_GCDA)
|
rm -f $(BENCH_GCDA)
|
||||||
rm -f $(BENCH_PERF)
|
rm -f $(BENCH_PERF)
|
||||||
|
|||||||
+2
-2
@@ -705,10 +705,10 @@ def collect(obj_paths, *,
|
|||||||
size_ = sizeof(type, seen | {entry.off})
|
size_ = sizeof(type, seen | {entry.off})
|
||||||
children_, notes_, dirty_ = childrenof(
|
children_, notes_, dirty_ = childrenof(
|
||||||
type, seen | {entry.off})
|
type, seen | {entry.off})
|
||||||
dirty = dirty or dirty_
|
|
||||||
children.append(CtxResult(file, name_, size_,
|
children.append(CtxResult(file, name_, size_,
|
||||||
children=children_,
|
children=children_,
|
||||||
notes=notes_))
|
notes=notes_))
|
||||||
|
dirty = dirty or dirty_
|
||||||
# struct? union?
|
# struct? union?
|
||||||
elif entry.tag in {
|
elif entry.tag in {
|
||||||
'DW_TAG_structure_type',
|
'DW_TAG_structure_type',
|
||||||
@@ -721,11 +721,11 @@ def collect(obj_paths, *,
|
|||||||
size_ = sizeof(child, seen | {entry.off})
|
size_ = sizeof(child, seen | {entry.off})
|
||||||
children_, notes_, dirty_ = childrenof(
|
children_, notes_, dirty_ = childrenof(
|
||||||
child, seen | {entry.off})
|
child, seen | {entry.off})
|
||||||
dirty = dirty or dirty_
|
|
||||||
children.append(CtxResult(file, name_, size_,
|
children.append(CtxResult(file, name_, size_,
|
||||||
i=child.off,
|
i=child.off,
|
||||||
children=children_,
|
children=children_,
|
||||||
notes=notes_))
|
notes=notes_))
|
||||||
|
dirty = dirty or dirty_
|
||||||
# base type? function pointer?
|
# base type? function pointer?
|
||||||
elif entry.tag in {
|
elif entry.tag in {
|
||||||
'DW_TAG_base_type',
|
'DW_TAG_base_type',
|
||||||
|
|||||||
+244
-606
@@ -21,7 +21,6 @@ import math as mt
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import bisect
|
|
||||||
|
|
||||||
|
|
||||||
OBJDUMP_PATH = ['objdump']
|
OBJDUMP_PATH = ['objdump']
|
||||||
@@ -168,6 +167,11 @@ def openio(path, mode='r', buffering=-1):
|
|||||||
else:
|
else:
|
||||||
return open(path, mode, buffering)
|
return open(path, mode, buffering)
|
||||||
|
|
||||||
|
def iself(path):
|
||||||
|
# check for an elf file's magic string (\x7fELF)
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
return f.read(4) == b'\x7fELF'
|
||||||
|
|
||||||
class Sym(co.namedtuple('Sym', [
|
class Sym(co.namedtuple('Sym', [
|
||||||
'name', 'global_', 'section', 'addr', 'size'])):
|
'name', 'global_', 'section', 'addr', 'size'])):
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
@@ -404,7 +408,7 @@ class DwarfEntry:
|
|||||||
def addr(self):
|
def addr(self):
|
||||||
if (self.tag == 'DW_TAG_subprogram'
|
if (self.tag == 'DW_TAG_subprogram'
|
||||||
and 'DW_AT_low_pc' in self):
|
and 'DW_AT_low_pc' in self):
|
||||||
return int(self['DW_AT_low_pc'].split(':')[-1], 16)
|
return int(self['DW_AT_low_pc'], 0)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -414,7 +418,7 @@ class DwarfEntry:
|
|||||||
and 'DW_AT_high_pc' in self):
|
and 'DW_AT_high_pc' in self):
|
||||||
# this looks wrong, but high_pc does store the size,
|
# this looks wrong, but high_pc does store the size,
|
||||||
# for whatever reason
|
# for whatever reason
|
||||||
return int(self['DW_AT_high_pc'].split(':')[-1], 16)
|
return int(self['DW_AT_high_pc'], 0)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -578,420 +582,218 @@ def collect_dwarf_info(obj_path, tags=None, *,
|
|||||||
|
|
||||||
return DwarfInfo(info)
|
return DwarfInfo(info)
|
||||||
|
|
||||||
class Frame(co.namedtuple('Frame', ['addr', 'frame'])):
|
# a simple general-purpose parser class
|
||||||
__slots__ = ()
|
#
|
||||||
def __new__(cls, addr, frame):
|
# basically just because memoryview doesn't support strs
|
||||||
return super().__new__(cls, addr, frame)
|
class Parser:
|
||||||
|
def __init__(self, data, ws='\s*', ws_flags=0):
|
||||||
|
self.data = data.lstrip()
|
||||||
|
self.i = 0
|
||||||
|
self.m = None
|
||||||
|
# also consume whitespace
|
||||||
|
self.ws = re.compile(ws, ws_flags)
|
||||||
|
self.i = self.ws.match(self.data, self.i).end()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '%s(0x%x, %d)' % (
|
return '%s(%r...)' % (
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
self.addr,
|
self.data[self.i:self.i+32])
|
||||||
self.frame)
|
|
||||||
|
|
||||||
class FrameInfo:
|
def __bool__(self):
|
||||||
def __init__(self, frames):
|
return self.i != len(self.data)
|
||||||
self.frames = frames
|
|
||||||
|
|
||||||
def get(self, k, d=None):
|
def match(self, pattern, flags=0):
|
||||||
import bisect
|
# compile so we can use the pos arg, this is still cached
|
||||||
|
self.m = re.compile(pattern, flags).match(self.data, self.i)
|
||||||
|
return self.m
|
||||||
|
|
||||||
# organize by address
|
def group(self, *groups):
|
||||||
if not hasattr(self, '_by_addr'):
|
return self.m.group(*groups)
|
||||||
# sort and keep largest when duplicates
|
|
||||||
frames = self.frames.copy()
|
|
||||||
frames.sort(key=lambda x: (x.addr, -x.frame))
|
|
||||||
|
|
||||||
by_addr = []
|
def chomp(self, *groups):
|
||||||
for frame in frames:
|
g = self.group(*groups)
|
||||||
if (len(by_addr) == 0
|
self.i = self.m.end()
|
||||||
or by_addr[-1].addr != frame.addr):
|
# also consume whitespace
|
||||||
by_addr.append(frame)
|
self.i = self.ws.match(self.data, self.i).end()
|
||||||
self._by_addr = by_addr
|
return g
|
||||||
|
|
||||||
# allow lookup by addr or range of addrs
|
class Error(Exception):
|
||||||
if not isinstance(k, slice):
|
pass
|
||||||
# find frame by addr
|
|
||||||
i = bisect.bisect(self._by_addr, k,
|
|
||||||
key=lambda x: x.addr)
|
|
||||||
if i > 0:
|
|
||||||
return self._by_addr[i-1]
|
|
||||||
else:
|
|
||||||
return d
|
|
||||||
|
|
||||||
else:
|
def chompmatch(self, pattern, flags=0, *groups):
|
||||||
# find frame by range
|
if not self.match(pattern, flags):
|
||||||
if k.start is None:
|
raise Parser.Error(
|
||||||
start = 0
|
"expected %r, found %r..." % (
|
||||||
else:
|
pattern, self.data[self.i:self.i+32]))
|
||||||
start = max(
|
return self.chomp(*groups)
|
||||||
bisect.bisect(self._by_addr, k.start,
|
|
||||||
key=lambda x: x.addr) - 1,
|
|
||||||
0)
|
|
||||||
if k.stop is None:
|
|
||||||
stop = len(self._by_addr)
|
|
||||||
else:
|
|
||||||
stop = bisect.bisect(self._by_addr, k.stop,
|
|
||||||
key=lambda x: x.addr)
|
|
||||||
|
|
||||||
return FrameInfo(self._by_addr[start:stop])
|
def unexpected(self):
|
||||||
|
raise Parser.Error(
|
||||||
|
"unexpected %r..." % (
|
||||||
|
self.data[self.i:self.i+32]))
|
||||||
|
|
||||||
def __getitem__(self, k):
|
class CGNode(co.namedtuple('CGNode', [
|
||||||
v = self.get(k)
|
'name', 'size', 'qualifiers', 'calls'])):
|
||||||
if v is None:
|
|
||||||
raise KeyError(k)
|
|
||||||
return v
|
|
||||||
|
|
||||||
def __contains__(self, k):
|
|
||||||
return self.get(k) is not None
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.frames)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self.frames)
|
|
||||||
|
|
||||||
def collect_dwarf_frames(obj_path, tags=None, *,
|
|
||||||
objdump_path=OBJDUMP_PATH,
|
|
||||||
**args):
|
|
||||||
frame_pattern = re.compile(
|
|
||||||
'^\s*(?P<cie_off>[0-9a-fA-F]+)'
|
|
||||||
'\s+(?P<cie_size>[0-9a-fA-F]+)'
|
|
||||||
'\s+(?P<cie_id>[0-9a-fA-F]+)'
|
|
||||||
'\s+CIE\s*$'
|
|
||||||
'|' '^\s*(?P<fde_off>[0-9a-fA-F]+)'
|
|
||||||
'\s+(?P<fde_size>[0-9a-fA-F]+)'
|
|
||||||
'\s+(?P<fde_id>[0-9a-fA-F]+)'
|
|
||||||
'\s+FDE'
|
|
||||||
'\s+cie=(?P<fde_cie>[0-9a-fA-F]+)'
|
|
||||||
'\s+pc=(?P<fde_pc_lo>[0-9a-fA-F]+)'
|
|
||||||
'\.\.(?P<fde_pc_hi>[0-9a-fA-F]+)\s*$'
|
|
||||||
'|' '^\s*(?P<op>DW_CFA_[^\s:]*)\s*:?'
|
|
||||||
'\s*(?P<change>.*?)\s*$')
|
|
||||||
|
|
||||||
# collect frame info
|
|
||||||
#
|
|
||||||
# Frame info is encoded in a state machine stored in fde/cie
|
|
||||||
# entries. fde entries can share cie entries, otherwise they are
|
|
||||||
# mostly the same.
|
|
||||||
#
|
|
||||||
cies = co.OrderedDict()
|
|
||||||
fdes = co.OrderedDict()
|
|
||||||
entry = None
|
|
||||||
# note objdump-path may contain extra args
|
|
||||||
cmd = objdump_path + ['--dwarf=frames', obj_path]
|
|
||||||
if args.get('verbose'):
|
|
||||||
print(' '.join(shlex.quote(c) for c in cmd))
|
|
||||||
proc = sp.Popen(cmd,
|
|
||||||
stdout=sp.PIPE,
|
|
||||||
universal_newlines=True,
|
|
||||||
errors='replace',
|
|
||||||
close_fds=False)
|
|
||||||
for line in proc.stdout:
|
|
||||||
# state machine here to find fde/cie entries
|
|
||||||
m = frame_pattern.match(line)
|
|
||||||
if m:
|
|
||||||
# start cie?
|
|
||||||
if m.group('cie_off'):
|
|
||||||
entry = {
|
|
||||||
'type': 'cie',
|
|
||||||
'off': int(m.group('cie_off'), 16),
|
|
||||||
'ops': []}
|
|
||||||
cies[entry['off']] = entry
|
|
||||||
|
|
||||||
# start fde?
|
|
||||||
elif m.group('fde_off'):
|
|
||||||
entry = {
|
|
||||||
'type': 'fde',
|
|
||||||
'off': int(m.group('fde_off'), 16),
|
|
||||||
'cie': int(m.group('fde_cie'), 16),
|
|
||||||
'pc': (
|
|
||||||
int(m.group('fde_pc_lo'), 16),
|
|
||||||
int(m.group('fde_pc_hi'), 16)),
|
|
||||||
'ops': []}
|
|
||||||
fdes[entry['off']] = entry
|
|
||||||
|
|
||||||
# found op?
|
|
||||||
elif m.group('op'):
|
|
||||||
entry['ops'].append((m.group('op'), m.group('change')))
|
|
||||||
|
|
||||||
else:
|
|
||||||
assert False
|
|
||||||
proc.wait()
|
|
||||||
if proc.returncode != 0:
|
|
||||||
raise sp.CalledProcessError(proc.returncode, proc.args)
|
|
||||||
|
|
||||||
# execute the state machine
|
|
||||||
frames = []
|
|
||||||
for _, fde in fdes.items():
|
|
||||||
cie = cies[fde['cie']]
|
|
||||||
|
|
||||||
cfa_loc = fde['pc'][0]
|
|
||||||
cfa_stack = []
|
|
||||||
for op, change in it.chain(cie['ops'], fde['ops']):
|
|
||||||
# advance location
|
|
||||||
if op in {
|
|
||||||
'DW_CFA_advance_loc',
|
|
||||||
'DW_CFA_advance_loc1',
|
|
||||||
'DW_CFA_advance_loc2',
|
|
||||||
'DW_CFA_advance_loc4'}:
|
|
||||||
cfa_loc = int(change.split('to')[-1], 16)
|
|
||||||
# change cfa offset
|
|
||||||
elif op in {
|
|
||||||
'DW_CFA_def_cfa',
|
|
||||||
'DW_CFA_def_cfa_offset'}:
|
|
||||||
cfa_off = int(change.split('ofs')[-1], 0)
|
|
||||||
frames.append(Frame(cfa_loc, cfa_off))
|
|
||||||
# push state, because of course we need a stack
|
|
||||||
elif op == 'DW_CFA_remember_state':
|
|
||||||
cfa_stack.append(cfa_off)
|
|
||||||
# pop state
|
|
||||||
elif op == 'DW_CFA_restore_state':
|
|
||||||
cfa_off = cfa_stack.pop()
|
|
||||||
# ignore these
|
|
||||||
elif op in {
|
|
||||||
'DW_CFA_nop',
|
|
||||||
'DW_CFA_offset',
|
|
||||||
'DW_CFA_restore',
|
|
||||||
'DW_CFA_def_cfa_register'}:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
assert False, "Unknown frame op? %r" % op
|
|
||||||
|
|
||||||
return FrameInfo(frames)
|
|
||||||
|
|
||||||
class Loc(co.namedtuple('Loc', ['addr', 'size', 'ops'])):
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
def __new__(cls, addr, size, ops):
|
def __new__(cls, name, size, qualifiers, calls=None):
|
||||||
return super().__new__(cls, addr, size, ops)
|
return super().__new__(cls, name, size, qualifiers,
|
||||||
|
calls if calls is not None else set())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '%s(0x%x, 0x%x, %r)' % (
|
return '%s(%r, %r, %r, %r)' % (
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
self.addr,
|
self.name,
|
||||||
self.size,
|
self.size,
|
||||||
self.ops)
|
self.qualifiers,
|
||||||
|
self.calls)
|
||||||
|
|
||||||
class LocList:
|
def collect_callgraph(ci_path,
|
||||||
def __init__(self, off, locs):
|
|
||||||
self.off = off
|
|
||||||
self.locs = locs
|
|
||||||
|
|
||||||
def get(self, k, d=None):
|
|
||||||
import bisect
|
|
||||||
|
|
||||||
# organize by address
|
|
||||||
if not hasattr(self, '_by_addr'):
|
|
||||||
# sort and keep largest/first when duplicates
|
|
||||||
locs = self.locs.copy()
|
|
||||||
locs.sort(key=lambda x: (x.addr, -x.size))
|
|
||||||
|
|
||||||
by_addr = []
|
|
||||||
for loc in locs:
|
|
||||||
if (len(by_addr) == 0
|
|
||||||
or by_addr[-1].addr != loc.addr):
|
|
||||||
by_addr.append(loc)
|
|
||||||
self._by_addr = by_addr
|
|
||||||
|
|
||||||
# find loc by range
|
|
||||||
i = bisect.bisect(self._by_addr, k,
|
|
||||||
key=lambda x: x.addr)
|
|
||||||
# check that we're actually in this loc's size
|
|
||||||
if i > 0 and k < self._by_addr[i-1].addr+self._by_addr[i-1].size:
|
|
||||||
return self._by_addr[i-1]
|
|
||||||
else:
|
|
||||||
return d
|
|
||||||
|
|
||||||
def __getitem__(self, k):
|
|
||||||
v = self.get(k)
|
|
||||||
if v is None:
|
|
||||||
raise KeyError(k)
|
|
||||||
return v
|
|
||||||
|
|
||||||
def __contains__(self, k):
|
|
||||||
return self.get(k) is not None
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.locs)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self.locs)
|
|
||||||
|
|
||||||
def collect_dwarf_locs(obj_path, tags=None, *,
|
|
||||||
objdump_path=OBJDUMP_PATH,
|
|
||||||
**args):
|
**args):
|
||||||
loc_pattern = re.compile(
|
with open(ci_path) as f:
|
||||||
'^\s*(?P<begin_off>[0-9a-fA-F]+)'
|
# parse callgraph
|
||||||
'\s+(?P<begin_start>v?[0-9a-fA-F]+)'
|
p = Parser(f.read())
|
||||||
'\s+(?P<begin_stop>v?[0-9a-fA-F]+)'
|
|
||||||
'\s+views.*$'
|
|
||||||
'|' '^\s*(?P<end_off>[0-9a-fA-F]+)'
|
|
||||||
'\s+<End of list>\s*$'
|
|
||||||
'|' '^\s*(?P<loc_start>[0-9a-fA-F]+)'
|
|
||||||
'\s+(?P<loc_stop>[0-9a-fA-F]+)'
|
|
||||||
'\s+\((?P<loc_ops>.+)\)\s*$',
|
|
||||||
re.IGNORECASE)
|
|
||||||
|
|
||||||
# collect location lists
|
def p_cg(p):
|
||||||
locs = co.OrderedDict()
|
node = {}
|
||||||
list_off = None
|
while True:
|
||||||
list_locs = None
|
# key?
|
||||||
# note objdump-path may contain extra args
|
if not p.match('[^\s:{}]+'):
|
||||||
cmd = objdump_path + ['--dwarf=loc', obj_path]
|
break
|
||||||
if args.get('verbose'):
|
k = p.chomp()
|
||||||
print(' '.join(shlex.quote(c) for c in cmd))
|
p.chompmatch(':')
|
||||||
proc = sp.Popen(cmd,
|
|
||||||
stdout=sp.PIPE,
|
# string?
|
||||||
universal_newlines=True,
|
if p.match('"((?:\\.|[^"])*)"'):
|
||||||
errors='replace',
|
v = p.chomp(1)
|
||||||
close_fds=False)
|
# keyword?
|
||||||
for line in proc.stdout:
|
elif p.match('[^\s:{}]+'):
|
||||||
# find localtion lists
|
v = p.chomp()
|
||||||
m = loc_pattern.match(line)
|
# child node?
|
||||||
if m:
|
elif p.match('{'):
|
||||||
# start of list?
|
p.chomp()
|
||||||
if m.group('begin_off'):
|
v = p_cg(p)
|
||||||
# these occur between every entry, so ignore after
|
p.chompmatch('}')
|
||||||
# the first one
|
else:
|
||||||
if list_off is None:
|
p.unexpected()
|
||||||
list_off = int(m.group('begin_off'), 16)
|
|
||||||
list_locs = []
|
if k not in node:
|
||||||
# end of list?
|
node[k] = []
|
||||||
elif m.group('end_off'):
|
node[k].append(v)
|
||||||
assert list_off is not None
|
return node
|
||||||
locs[list_off] = LocList(list_off, list_locs)
|
|
||||||
list_off = None
|
cg = p_cg(p)
|
||||||
list_locs = None
|
# trailing junk?
|
||||||
# found a loc?
|
if p:
|
||||||
elif m.group('loc_start'):
|
p.unexpected()
|
||||||
assert list_off is not None
|
|
||||||
start = int(m.group('loc_start'), 16)
|
# convert to something a bit more useful
|
||||||
stop = int(m.group('loc_stop'), 16)
|
def cg_key(k):
|
||||||
ops = [op.strip() for op in m.group('loc_ops').split(';')]
|
# map relative paths to try to keep cwd agnostic
|
||||||
list_locs.append(Loc(start, stop-start, ops))
|
if ':' not in k:
|
||||||
|
return k
|
||||||
else:
|
else:
|
||||||
assert False
|
file, function = k.split(':', 1)
|
||||||
proc.wait()
|
if not os.path.isabs(file):
|
||||||
if proc.returncode != 0:
|
file = re.sub('(\.ci)?$', '.c', ci_path, 1)
|
||||||
raise sp.CalledProcessError(proc.returncode, proc.args)
|
return ':'.join([file, function])
|
||||||
|
|
||||||
return locs
|
cg_ = {}
|
||||||
|
for node in cg['graph'][0].get('node') or []:
|
||||||
|
m = re.search('([0-9]+) bytes \((.*)\)', node['label'][0])
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
name = cg_key(node['title'][0])
|
||||||
|
size = int(m.group(1))
|
||||||
|
qualifiers = [q.strip() for q in m.group(2).split(',')]
|
||||||
|
cg_[name] = CGNode(name, size, qualifiers)
|
||||||
|
|
||||||
# we basically need a small linker here
|
for edge in cg['graph'][0].get('edge') or []:
|
||||||
class Func(co.namedtuple('Func', ['file', 'sym', 'entry',
|
cg_[cg_key(edge['sourcename'][0])].calls.add(
|
||||||
'frames', 'calls'])):
|
cg_key(edge['targetname'][0]))
|
||||||
__slots__ = ()
|
|
||||||
def __new__(cls, file, sym, entry, frames=None, calls=None):
|
|
||||||
return super().__new__(cls, file, sym, entry,
|
|
||||||
frames if frames is not None else FuncFrameInfo(),
|
|
||||||
calls if calls is not None else co.OrderedDict())
|
|
||||||
|
|
||||||
def __repr__(self):
|
return cg_
|
||||||
return '<%s %s>' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self.sym.name)
|
|
||||||
|
|
||||||
class FuncFrame(co.namedtuple('FuncFrame', ['addr', 'size', 'frame'])):
|
def collect(obj_paths, ci_paths, *,
|
||||||
__slots__ = ()
|
|
||||||
def __new__(cls, addr, size, frame):
|
|
||||||
return super().__new__(cls, addr, size, frame)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '%s(0x%x, 0x%x, %d)' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self.addr,
|
|
||||||
self.size,
|
|
||||||
self.frame)
|
|
||||||
|
|
||||||
class FuncFrameInfo:
|
|
||||||
def __init__(self, frames=None):
|
|
||||||
self.frames = frames if frames is not None else []
|
|
||||||
|
|
||||||
def get(self, k, d=None):
|
|
||||||
# find frame by address
|
|
||||||
i = bisect.bisect(self.frames, k,
|
|
||||||
key=lambda x: x.addr)
|
|
||||||
# check that we're actually in this frame's size
|
|
||||||
if i > 0 and k < self.frames[i-1].addr+self.frames[i-1].size:
|
|
||||||
return self.frames[i-1]
|
|
||||||
else:
|
|
||||||
return d
|
|
||||||
|
|
||||||
def set(self, k, v):
|
|
||||||
# always operate on ranges
|
|
||||||
if not isinstance(k, slice):
|
|
||||||
k = slice(k, 1)
|
|
||||||
|
|
||||||
# insert frame, merging frames to find max frames
|
|
||||||
frames_ = []
|
|
||||||
for f in it.chain(
|
|
||||||
(f for f in self.frames if f.addr < k.start),
|
|
||||||
[FuncFrame(k.start, k.stop-k.start, v)],
|
|
||||||
(f for f in self.frames if f.addr >= k.start)):
|
|
||||||
g = frames_[-1] if frames_ else None
|
|
||||||
|
|
||||||
# new frame?
|
|
||||||
if g is None or f.addr > g.addr+g.size:
|
|
||||||
frames_.append(f)
|
|
||||||
# merge with previous frame?
|
|
||||||
elif f.frame == g.frame:
|
|
||||||
frames_[-1] = FuncFrame(
|
|
||||||
g.addr,
|
|
||||||
max(g.size, (f.addr+f.size) - g.addr),
|
|
||||||
g.frame)
|
|
||||||
# previous frame wins?
|
|
||||||
elif f.frame < g.frame:
|
|
||||||
# slice new frame?
|
|
||||||
if (f.addr+f.size > g.addr+g.size):
|
|
||||||
frames_.append(FuncFrame(
|
|
||||||
g.addr+g.size,
|
|
||||||
f.addr+f.size - (g.addr+g.size),
|
|
||||||
f.frame))
|
|
||||||
# new frame wins?
|
|
||||||
elif f.frame > g.frame:
|
|
||||||
# slice previous frame
|
|
||||||
frames_[-1] = FuncFrame(
|
|
||||||
g.addr,
|
|
||||||
f.addr - g.addr,
|
|
||||||
g.frame)
|
|
||||||
# append new frame
|
|
||||||
frames_.append(f)
|
|
||||||
# slice previous frame tail?
|
|
||||||
if (f.addr+f.size < g.addr+g.size):
|
|
||||||
frames_.append(FuncFrame(
|
|
||||||
f.addr+f.size,
|
|
||||||
(g.addr+g.size) - (f.addr+f.size),
|
|
||||||
g.frame))
|
|
||||||
|
|
||||||
self.frames = frames_
|
|
||||||
|
|
||||||
def __getitem__(self, k):
|
|
||||||
v = self.get(k)
|
|
||||||
if v is None:
|
|
||||||
raise KeyError(k)
|
|
||||||
return v
|
|
||||||
|
|
||||||
def __contains__(self, k):
|
|
||||||
return self.get(k) is not None
|
|
||||||
|
|
||||||
def __setitem__(self, k, v):
|
|
||||||
return self.set(k, v)
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.frames)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self.frames)
|
|
||||||
|
|
||||||
def collect(obj_paths, *,
|
|
||||||
sources=None,
|
sources=None,
|
||||||
everything=False,
|
everything=False,
|
||||||
**args):
|
**args):
|
||||||
funcs = []
|
# parse the callgraphs
|
||||||
globals = co.OrderedDict()
|
cg = {}
|
||||||
incomplete = False
|
for ci_path in ci_paths:
|
||||||
|
cg.update(collect_callgraph(ci_path))
|
||||||
|
|
||||||
|
# cached frame finder
|
||||||
|
def frameof(node):
|
||||||
|
# cached?
|
||||||
|
if not hasattr(frameof, 'cache'):
|
||||||
|
frameof.cache = {}
|
||||||
|
if node.name in frameof.cache:
|
||||||
|
return frameof.cache[node.name]
|
||||||
|
|
||||||
|
# is our stack frame bounded?
|
||||||
|
if ('static' not in node.qualifiers
|
||||||
|
and 'bounded' not in node.qualifiers):
|
||||||
|
print('warning: found unbounded stack for %s' % node.name,
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
frameof.cache[node.name] = node.size
|
||||||
|
return node.size
|
||||||
|
|
||||||
|
# recursive+cached limit finder
|
||||||
|
def limitof(node, seen=set()):
|
||||||
|
# found a cycle? stop here
|
||||||
|
if node.name in seen:
|
||||||
|
return mt.inf
|
||||||
|
# cached?
|
||||||
|
if not hasattr(limitof, 'cache'):
|
||||||
|
limitof.cache = {}
|
||||||
|
if node.name in limitof.cache:
|
||||||
|
return limitof.cache[node.name]
|
||||||
|
|
||||||
|
# find maximum stack limit
|
||||||
|
frame = frameof(node)
|
||||||
|
limit = 0
|
||||||
|
for call in node.calls:
|
||||||
|
if call not in cg:
|
||||||
|
continue
|
||||||
|
limit = max(limit, limitof(cg[call], seen | {node.name}))
|
||||||
|
|
||||||
|
limitof.cache[node.name] = frame + limit
|
||||||
|
return frame + limit
|
||||||
|
|
||||||
|
# recursive+cached children finder
|
||||||
|
def childrenof(node, seen=set()):
|
||||||
|
# found a cycle? stop here
|
||||||
|
if node.name in seen:
|
||||||
|
return [], ['cycle detected'], True
|
||||||
|
# cached?
|
||||||
|
if not hasattr(childrenof, 'cache'):
|
||||||
|
childrenof.cache = {}
|
||||||
|
if node.name in childrenof.cache:
|
||||||
|
return childrenof.cache[node.name]
|
||||||
|
|
||||||
|
children, dirty = [], False
|
||||||
|
for call in node.calls:
|
||||||
|
if call not in cg:
|
||||||
|
continue
|
||||||
|
node_ = cg[call]
|
||||||
|
name_ = node_.name.split(':', 1)[-1]
|
||||||
|
frame_ = frameof(node_)
|
||||||
|
limit_ = limitof(node_, seen | {node.name})
|
||||||
|
children_, notes_, dirty_ = childrenof(node_, seen | {node.name})
|
||||||
|
# TODO this file is a hack... how do we find the correct file?
|
||||||
|
children.append(StackResult(file, name_, frame_, limit_,
|
||||||
|
children=children_,
|
||||||
|
notes=notes_))
|
||||||
|
dirty = dirty or dirty_
|
||||||
|
|
||||||
|
if not dirty:
|
||||||
|
childrenof.cache[node.name] = children, [], dirty
|
||||||
|
return children, [], dirty
|
||||||
|
|
||||||
|
results = []
|
||||||
for obj_path in obj_paths:
|
for obj_path in obj_paths:
|
||||||
# find relevant symbols
|
# find relevant symbols and sizes
|
||||||
syms = collect_syms(obj_path,
|
syms = collect_syms(obj_path,
|
||||||
sections=['.text'],
|
sections=['.text'],
|
||||||
**args)
|
**args)
|
||||||
@@ -999,26 +801,19 @@ def collect(obj_paths, *,
|
|||||||
# find source paths
|
# find source paths
|
||||||
files = collect_dwarf_files(obj_path, **args)
|
files = collect_dwarf_files(obj_path, **args)
|
||||||
|
|
||||||
# find dwarf info, we only care about functions
|
# find dwarf info
|
||||||
info = collect_dwarf_info(obj_path,
|
info = collect_dwarf_info(obj_path,
|
||||||
tags={'DW_TAG_subprogram'},
|
tags={'DW_TAG_subprogram', 'DW_TAG_variable'},
|
||||||
**args)
|
**args)
|
||||||
|
|
||||||
# find frame info
|
# map function sizes to debug symbols
|
||||||
frames = collect_dwarf_frames(obj_path, **args)
|
|
||||||
|
|
||||||
# find location info
|
|
||||||
locs = collect_dwarf_locs(obj_path, **args)
|
|
||||||
|
|
||||||
# find the max stack frame for each function
|
|
||||||
locals = co.OrderedDict()
|
|
||||||
for sym in syms:
|
for sym in syms:
|
||||||
# discard internal functions
|
# discard internal functions
|
||||||
if not everything and sym.name.startswith('__'):
|
if not everything and sym.name.startswith('__'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# find best matching dwarf entry, this may have a slightly
|
# find best matching dwarf entry, this may be slightly different
|
||||||
# different name due to optimizations
|
# due to optimizations
|
||||||
entry = info.get(sym)
|
entry = info.get(sym)
|
||||||
|
|
||||||
# if we have no file guess from obj path
|
# if we have no file guess from obj path
|
||||||
@@ -1039,198 +834,28 @@ def collect(obj_paths, *,
|
|||||||
os.path.abspath(file)]) == os.getcwd():
|
os.path.abspath(file)]) == os.getcwd():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# build our func
|
# find relevant callgraph node, this is either function if
|
||||||
func = Func(file, sym, entry)
|
# global, or file:function if static
|
||||||
|
if sym.name in cg:
|
||||||
# find the relevant stack frames
|
node = cg[sym.name]
|
||||||
if entry is not None:
|
|
||||||
# base frame
|
|
||||||
func.frames[sym.addr:sym.addr+sym.size] = max(
|
|
||||||
(frame.frame
|
|
||||||
for frame in frames[sym.addr:sym.addr+sym.size]),
|
|
||||||
default=0)
|
|
||||||
|
|
||||||
for var in entry.info():
|
|
||||||
# find stack usage of relevant variables
|
|
||||||
if var.tag in {
|
|
||||||
'DW_TAG_variable',
|
|
||||||
'DW_TAG_formal_parameter',
|
|
||||||
'DW_TAG_call_site_parameter'}:
|
|
||||||
# ignore vars with no location, these are usually
|
|
||||||
# globals or synthetic variables
|
|
||||||
if 'DW_AT_location' not in var:
|
|
||||||
continue
|
|
||||||
|
|
||||||
m = re.match(
|
|
||||||
'^\s*(?P<list>[0xX0-9a-fA-F]+)'
|
|
||||||
'\s*\(.*\)\s*$'
|
|
||||||
'|' '^.*?\((?P<ops>.*)\)\s*$',
|
|
||||||
var['DW_AT_location'])
|
|
||||||
if m.group('ops'):
|
|
||||||
# TODO use range of lexical_block
|
|
||||||
locs_ = [Loc(sym.addr, sym.size,
|
|
||||||
[m.group('ops').strip()])]
|
|
||||||
elif m.group('list'):
|
|
||||||
locs_ = locs[int(m.group('list'), 0)]
|
|
||||||
else:
|
|
||||||
assert False, "Unknown loc? %r" % (
|
|
||||||
var['DW_AT_location'])
|
|
||||||
|
|
||||||
for loc in locs_:
|
|
||||||
frame = frames[loc.addr]
|
|
||||||
for op in loc.ops:
|
|
||||||
if op.startswith('DW_OP_fbreg'):
|
|
||||||
off = int(op.split(':')[-1].strip(), 0)
|
|
||||||
func.frames[loc.addr:loc.addr+loc.size] = (
|
|
||||||
frame.frame - off)
|
|
||||||
# ignore these
|
|
||||||
elif var.tag in {
|
|
||||||
'DW_TAG_lexical_block',
|
|
||||||
'DW_TAG_inlined_subroutine',
|
|
||||||
'DW_TAG_call_site',
|
|
||||||
'DW_TAG_label',
|
|
||||||
'DW_TAG_structure_type',
|
|
||||||
'DW_TAG_union_type',
|
|
||||||
'DW_TAG_member'}:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
assert False, "Unknown frame tag? %r" % var.tag
|
|
||||||
|
|
||||||
# keep track of funcs
|
|
||||||
funcs.append(func)
|
|
||||||
|
|
||||||
# keep track of locals/globals
|
|
||||||
if sym.global_:
|
|
||||||
globals[sym.name] = func
|
|
||||||
if entry is not None:
|
|
||||||
locals[entry.off] = func
|
|
||||||
|
|
||||||
# link local function calls via dwarf entries
|
|
||||||
for func in locals.values():
|
|
||||||
if not func.entry:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ((args.get('warn_on_incomplete')
|
|
||||||
or args.get('error_on_incomplete'))
|
|
||||||
and 'DW_AT_call_all_calls' not in func.entry):
|
|
||||||
print('%s: incomplete call info in %s '
|
|
||||||
'(DW_AT_call_all_calls missing)' % (
|
|
||||||
'error' if args.get('error_on_incomplete')
|
|
||||||
else 'warning',
|
|
||||||
func.sym.name),
|
|
||||||
file=sys.stderr)
|
|
||||||
incomplete = True
|
|
||||||
|
|
||||||
for call in func.entry.info(
|
|
||||||
tags={'DW_TAG_call_site'}):
|
|
||||||
if ('DW_AT_call_return_pc' not in call
|
|
||||||
or 'DW_AT_call_origin' not in call):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# note DW_AT_call_return_pc refers to the address
|
|
||||||
# _after_ the call
|
|
||||||
#
|
|
||||||
# we change this to the last byte in the call
|
|
||||||
# instruction, which is a bit weird, but should at least
|
|
||||||
# map to the right stack frame
|
|
||||||
addr = (int(call['DW_AT_call_return_pc'].split(':')[-1], 16)
|
|
||||||
- 1)
|
|
||||||
off = int(call['DW_AT_call_origin'].strip('<>'), 0)
|
|
||||||
|
|
||||||
# callee in locals?
|
|
||||||
if off in locals:
|
|
||||||
func.calls[addr] = locals[off]
|
|
||||||
else:
|
|
||||||
# if not, just keep track of the symbol and try to link
|
|
||||||
# during the global pass
|
|
||||||
func.calls[addr] = info[off].name
|
|
||||||
|
|
||||||
# error on incomplete calls after printing all relevant functions
|
|
||||||
if args.get('error_on_incomplete') and incomplete:
|
|
||||||
sys.exit(3)
|
|
||||||
|
|
||||||
# link global function calls via symbol
|
|
||||||
for func in funcs:
|
|
||||||
for addr, callee in func.calls.copy().items():
|
|
||||||
if isinstance(callee, str):
|
|
||||||
if callee in globals:
|
|
||||||
func.calls[addr] = globals[callee]
|
|
||||||
else:
|
|
||||||
del func.calls[addr]
|
|
||||||
|
|
||||||
# recursive+cached limit finder
|
|
||||||
def limitof(func, seen=set()):
|
|
||||||
# found a cycle? stop here
|
|
||||||
if id(func) in seen:
|
|
||||||
return 0, mt.inf
|
|
||||||
# cached?
|
|
||||||
if not hasattr(limitof, 'cache'):
|
|
||||||
limitof.cache = {}
|
|
||||||
if id(func) in limitof.cache:
|
|
||||||
return limitof.cache[id(func)]
|
|
||||||
|
|
||||||
# find max stack frame
|
|
||||||
frame = max((frame.frame for frame in func.frames), default=0)
|
|
||||||
|
|
||||||
# find stack limit recursively
|
|
||||||
limit = frame
|
|
||||||
for addr, callee in func.calls.items():
|
|
||||||
if args.get('no_shrinkwrap'):
|
|
||||||
frame_ = frame
|
|
||||||
else:
|
else:
|
||||||
# use stack frame at call site
|
name = ':'.join([
|
||||||
frame_ = func.frames.get(addr)
|
re.sub('(\.o)?$', '.c', obj_path, 1),
|
||||||
if frame_ is not None:
|
sym.name])
|
||||||
frame_ = frame_.frame
|
if name in cg:
|
||||||
|
node = cg[name]
|
||||||
else:
|
else:
|
||||||
frame_ = 0
|
print('warning: no callgraph node found for %s' % name,
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
_, limit_ = limitof(callee, seen | {id(func)})
|
# build result
|
||||||
|
name = sym.name
|
||||||
limit = max(limit, frame_ + limit_)
|
frame = frameof(node)
|
||||||
|
limit = limitof(node)
|
||||||
limitof.cache[id(func)] = frame, limit
|
children, notes, _ = childrenof(node)
|
||||||
return frame, limit
|
results.append(StackResult(file, name, frame, limit,
|
||||||
|
children=children,
|
||||||
# recursive+cached children finder
|
notes=notes))
|
||||||
def childrenof(func, seen=set()):
|
|
||||||
# found a cycle? stop here
|
|
||||||
if id(func) in seen:
|
|
||||||
return [], ['cycle detected'], True
|
|
||||||
# cached?
|
|
||||||
if not hasattr(childrenof, 'cache'):
|
|
||||||
childrenof.cache = {}
|
|
||||||
if id(func) in childrenof.cache:
|
|
||||||
return childrenof.cache[id(func)]
|
|
||||||
|
|
||||||
# find children recursively
|
|
||||||
children = []
|
|
||||||
dirty = False
|
|
||||||
for addr, callee in func.calls.items():
|
|
||||||
file_ = callee.file
|
|
||||||
name_ = callee.sym.name
|
|
||||||
frame_, limit_ = limitof(callee, seen | {id(func)})
|
|
||||||
children_, notes_, dirty_ = childrenof(callee, seen | {id(func)})
|
|
||||||
dirty = dirty or dirty_
|
|
||||||
children.append(StackResult(file_, name_, frame_, limit_,
|
|
||||||
children=children_,
|
|
||||||
notes=notes_))
|
|
||||||
|
|
||||||
if not dirty:
|
|
||||||
childrenof.cache[id(func)] = children, [], dirty
|
|
||||||
return children, [], dirty
|
|
||||||
|
|
||||||
# build results
|
|
||||||
results = []
|
|
||||||
for func in funcs:
|
|
||||||
file = func.file
|
|
||||||
name = func.sym.name
|
|
||||||
frame, limit = limitof(func)
|
|
||||||
children, notes, _ = childrenof(func)
|
|
||||||
|
|
||||||
results.append(StackResult(file, name, frame, limit,
|
|
||||||
children=children,
|
|
||||||
notes=notes))
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -1619,7 +1244,7 @@ def table(Result, results, diff_results=None, *,
|
|||||||
for i, x in enumerate(line[1:], 1))))
|
for i, x in enumerate(line[1:], 1))))
|
||||||
|
|
||||||
|
|
||||||
def main(obj_paths,
|
def main(paths,
|
||||||
by=None,
|
by=None,
|
||||||
fields=None,
|
fields=None,
|
||||||
defines=[],
|
defines=[],
|
||||||
@@ -1633,7 +1258,27 @@ def main(obj_paths,
|
|||||||
|
|
||||||
# find sizes
|
# find sizes
|
||||||
if not args.get('use', None):
|
if not args.get('use', None):
|
||||||
results = collect(obj_paths, **args)
|
# figure out paths
|
||||||
|
obj_paths = []
|
||||||
|
ci_paths = []
|
||||||
|
for path in paths:
|
||||||
|
if iself(path):
|
||||||
|
obj_paths.append(path)
|
||||||
|
else:
|
||||||
|
ci_paths.append(path)
|
||||||
|
# not enough info?
|
||||||
|
if not obj_paths:
|
||||||
|
print("error: no *.o files?",
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if not ci_paths:
|
||||||
|
print("error: no *.ci files?",
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# collect info
|
||||||
|
results = collect(obj_paths, ci_paths, **args)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
results = []
|
results = []
|
||||||
with openio(args['use']) as f:
|
with openio(args['use']) as f:
|
||||||
@@ -1736,9 +1381,15 @@ if __name__ == "__main__":
|
|||||||
description="Find stack usage at the function level.",
|
description="Find stack usage at the function level.",
|
||||||
allow_abbrev=False)
|
allow_abbrev=False)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'obj_paths',
|
'paths',
|
||||||
|
metavar='obj_paths',
|
||||||
nargs='*',
|
nargs='*',
|
||||||
help="Input *.o files.")
|
help="Input *.o files.")
|
||||||
|
parser.add_argument(
|
||||||
|
'paths_',
|
||||||
|
metavar='ci_paths',
|
||||||
|
nargs='*',
|
||||||
|
help="Input *.ci files.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-v', '--verbose',
|
'-v', '--verbose',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
@@ -1818,11 +1469,6 @@ if __name__ == "__main__":
|
|||||||
'--everything',
|
'--everything',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="Include builtin and libc specific symbols.")
|
help="Include builtin and libc specific symbols.")
|
||||||
parser.add_argument(
|
|
||||||
'--no-shrinkwrap',
|
|
||||||
action='store_true',
|
|
||||||
help="Ignore the effects of shrinkwrap optimizations (assume one "
|
|
||||||
"big frame per function).")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-z', '--depth',
|
'-z', '--depth',
|
||||||
nargs='?',
|
nargs='?',
|
||||||
@@ -1839,14 +1485,6 @@ if __name__ == "__main__":
|
|||||||
'-e', '--error-on-recursion',
|
'-e', '--error-on-recursion',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="Error if any functions are recursive.")
|
help="Error if any functions are recursive.")
|
||||||
parser.add_argument(
|
|
||||||
'--warn-on-incomplete',
|
|
||||||
action='store_true',
|
|
||||||
help="Warn if callgraph may be incomplete.")
|
|
||||||
parser.add_argument(
|
|
||||||
'--error-on-incomplete',
|
|
||||||
action='store_true',
|
|
||||||
help="Error if callgraph may be incomplete.")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--objdump-path',
|
'--objdump-path',
|
||||||
type=lambda x: x.split(),
|
type=lambda x: x.split(),
|
||||||
|
|||||||
Reference in New Issue
Block a user