|  | #!/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)) | 
|  |  | 
|  |  |