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:
Christopher Haster
2024-12-10 02:25:01 -06:00
parent 0e658b8246
commit c8c12ffae8
3 changed files with 271 additions and 624 deletions
+25 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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(),