123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- #
- # Copyright (c) 2018 Free Software Foundation
- # Contributed by Bernhard Reutner-Fischer <aldot@gcc.gnu.org>
- # Inspired by bloat-o-meter from busybox.
- # This software may be used and distributed according to the terms and
- # conditions of the GNU General Public License as published by the Free
- # Software Foundation.
- # For a set of object-files, determine symbols that are
- # - public but should be static
- # Examples:
- # unused_functions.py ./gcc/fortran
- # unused_functions.py gcc/c gcc/c-family/ gcc/*-c.o | grep -v "'gt_"
- # unused_functions.py gcc/cp gcc/c-family/ gcc/*-c.o | grep -v "'gt_"
- import sys, os
- from tempfile import mkdtemp
- from subprocess import Popen, PIPE
- def usage():
- sys.stderr.write("usage: %s [-v] [dirs | files] [-- <readelf options>]\n"
- % sys.argv[0])
- sys.stderr.write("\t-v\tVerbose output\n");
- sys.exit(1)
- (odir, sym_args, tmpd, verbose) = (set(), "", None, False)
- for i in range(1, len(sys.argv)):
- f = sys.argv[i]
- if f == '--': # sym_args
- sym_args = ' '.join(sys.argv[i + 1:])
- break
- if f == '-v':
- verbose = True
- continue
- if not os.path.exists(f):
- sys.stderr.write("Error: No such file or directory '%s'\n" % f)
- usage()
- else:
- if f.endswith('.a') and tmpd is None:
- tmpd = mkdtemp(prefix='unused_fun')
- odir.add(f)
- def dbg(args):
- if not verbose: return
- print(args)
- def get_symbols(file):
- syms = {}
- rargs = "readelf -W -s %s %s" % (sym_args, file)
- p0 = Popen((a for a in rargs.split(' ') if a.strip() != ''), stdout=PIPE)
- p1 = Popen(["c++filt"], stdin=p0.stdout, stdout=PIPE,
- universal_newlines=True)
- lines = p1.communicate()[0]
- for l in lines.split('\n'):
- l = l.strip()
- if not len(l) or not l[0].isdigit(): continue
- larr = l.split()
- if len(larr) != 8: continue
- num, value, size, typ, bind, vis, ndx, name = larr
- if typ == 'SECTION' or typ == 'FILE': continue
- # I don't think we have many aliases in gcc, re-instate the addr
- # lut otherwise.
- if vis != 'DEFAULT': continue
- #value = int(value, 16)
- #size = int(size, 16) if size.startswith('0x') else int(size)
- defined = ndx != 'UND'
- globl = bind == 'GLOBAL'
- # c++ RID_FUNCTION_NAME dance. FORNOW: Handled as local use
- # Is that correct?
- if name.endswith('::__FUNCTION__') and typ == 'OBJECT':
- name = name[0:(len(name) - len('::__FUNCTION__'))]
- if defined: defined = False
- if defined and not globl: continue
- syms.setdefault(name, {})
- syms[name][['use','def'][defined]] = True
- syms[name][['local','global'][globl]] = True
- # Note: we could filter out e.g. debug_* symbols by looking for
- # value in the debug_macro sections.
- if p1.returncode != 0:
- print("Warning: Reading file '%s' exited with %r|%r"
- % (file, p0.returncode, p1.returncode))
- p0.kill()
- return syms
- (oprog, nprog) = ({}, {})
- def walker(paths):
- def ar_x(archive):
- dbg("Archive %s" % path)
- f = os.path.abspath(archive)
- f = os.path.splitdrive(f)[1]
- d = tmpd + os.path.sep + f
- d = os.path.normpath(d)
- owd = os.getcwd()
- try:
- os.makedirs(d)
- os.chdir(d)
- p0 = Popen(["ar", "x", "%s" % os.path.join(owd, archive)],
- stderr=PIPE, universal_newlines=True)
- p0.communicate()
- if p0.returncode > 0: d = None # assume thin archive
- except:
- dbg("ar x: Error: %s: %s" % (archive, sys.exc_info()[0]))
- os.chdir(owd)
- raise
- os.chdir(owd)
- if d: dbg("Extracted to %s" % (d))
- return (archive, d)
- def ar_t(archive):
- dbg("Thin archive, using existing files:")
- try:
- p0 = Popen(["ar", "t", "%s" % archive], stdout=PIPE,
- universal_newlines=True)
- ret = p0.communicate()[0]
- return ret.split('\n')
- except:
- dbg("ar t: Error: %s: %s" % (archive, sys.exc_info()[0]))
- raise
- prog = {}
- for path in paths:
- if os.path.isdir(path):
- for r, dirs, files in os.walk(path):
- if files: dbg("Files %s" % ", ".join(files))
- if dirs: dbg("Dirs %s" % ", ".join(dirs))
- prog.update(walker([os.path.join(r, f) for f in files]))
- prog.update(walker([os.path.join(r, d) for d in dirs]))
- else:
- if path.endswith('.a'):
- if ar_x(path)[1] is not None: continue # extract worked
- prog.update(walker(ar_t(path)))
- if not path.endswith('.o'): continue
- dbg("Reading symbols from %s" % (path))
- prog[os.path.normpath(path)] = get_symbols(path)
- return prog
- def resolve(prog):
- x = prog.keys()
- use = set()
- # for each unique pair of different files
- for (f, g) in ((f,g) for f in x for g in x if f != g):
- refs = set()
- # for each defined symbol
- for s in (s for s in prog[f] if prog[f][s].get('def') and s in prog[g]):
- if prog[g][s].get('use'):
- refs.add(s)
- for s in refs:
- # Prune externally referenced symbols as speed optimization only
- for i in (i for i in x if s in prog[i]): del prog[i][s]
- use |= refs
- return use
- try:
- oprog = walker(odir)
- if tmpd is not None:
- oprog.update(walker([tmpd]))
- oused = resolve(oprog)
- finally:
- try:
- p0 = Popen(["rm", "-r", "-f", "%s" % (tmpd)], stderr=PIPE, stdout=PIPE)
- p0.communicate()
- if p0.returncode != 0: raise "rm '%s' didn't work out" % (tmpd)
- except:
- from shutil import rmtree
- rmtree(tmpd, ignore_errors=True)
- for (i,s) in ((i,s) for i in oprog.keys() for s in oprog[i] if oprog[i][s]):
- if oprog[i][s].get('def') and not oprog[i][s].get('use'):
- print("%s: Symbol '%s' declared extern but never referenced externally"
- % (i,s))
|