1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3# 4# Copyright (c) 2018 Free Software Foundation 5# Contributed by Bernhard Reutner-Fischer <aldot@gcc.gnu.org> 6# Inspired by bloat-o-meter from busybox. 7 8# This software may be used and distributed according to the terms and 9# conditions of the GNU General Public License as published by the Free 10# Software Foundation. 11 12# For a set of object-files, determine symbols that are 13# - public but should be static 14 15# Examples: 16# unused_functions.py ./gcc/fortran 17# unused_functions.py gcc/c gcc/c-family/ gcc/*-c.o | grep -v "'gt_" 18# unused_functions.py gcc/cp gcc/c-family/ gcc/*-c.o | grep -v "'gt_" 19 20import sys, os 21from tempfile import mkdtemp 22from subprocess import Popen, PIPE 23 24def usage(): 25 sys.stderr.write("usage: %s [-v] [dirs | files] [-- <readelf options>]\n" 26 % sys.argv[0]) 27 sys.stderr.write("\t-v\tVerbose output\n"); 28 sys.exit(1) 29 30(odir, sym_args, tmpd, verbose) = (set(), "", None, False) 31 32for i in range(1, len(sys.argv)): 33 f = sys.argv[i] 34 if f == '--': # sym_args 35 sym_args = ' '.join(sys.argv[i + 1:]) 36 break 37 if f == '-v': 38 verbose = True 39 continue 40 if not os.path.exists(f): 41 sys.stderr.write("Error: No such file or directory '%s'\n" % f) 42 usage() 43 else: 44 if f.endswith('.a') and tmpd is None: 45 tmpd = mkdtemp(prefix='unused_fun') 46 odir.add(f) 47 48def dbg(args): 49 if not verbose: return 50 print(args) 51 52def get_symbols(file): 53 syms = {} 54 rargs = "readelf -W -s %s %s" % (sym_args, file) 55 p0 = Popen((a for a in rargs.split(' ') if a.strip() != ''), stdout=PIPE) 56 p1 = Popen(["c++filt"], stdin=p0.stdout, stdout=PIPE, 57 universal_newlines=True) 58 lines = p1.communicate()[0] 59 for l in lines.split('\n'): 60 l = l.strip() 61 if not len(l) or not l[0].isdigit(): continue 62 larr = l.split() 63 if len(larr) != 8: continue 64 num, value, size, typ, bind, vis, ndx, name = larr 65 if typ == 'SECTION' or typ == 'FILE': continue 66 # I don't think we have many aliases in gcc, re-instate the addr 67 # lut otherwise. 68 if vis != 'DEFAULT': continue 69 #value = int(value, 16) 70 #size = int(size, 16) if size.startswith('0x') else int(size) 71 defined = ndx != 'UND' 72 globl = bind == 'GLOBAL' 73 # c++ RID_FUNCTION_NAME dance. FORNOW: Handled as local use 74 # Is that correct? 75 if name.endswith('::__FUNCTION__') and typ == 'OBJECT': 76 name = name[0:(len(name) - len('::__FUNCTION__'))] 77 if defined: defined = False 78 if defined and not globl: continue 79 syms.setdefault(name, {}) 80 syms[name][['use','def'][defined]] = True 81 syms[name][['local','global'][globl]] = True 82 # Note: we could filter out e.g. debug_* symbols by looking for 83 # value in the debug_macro sections. 84 if p1.returncode != 0: 85 print("Warning: Reading file '%s' exited with %r|%r" 86 % (file, p0.returncode, p1.returncode)) 87 p0.kill() 88 return syms 89 90(oprog, nprog) = ({}, {}) 91 92def walker(paths): 93 def ar_x(archive): 94 dbg("Archive %s" % path) 95 f = os.path.abspath(archive) 96 f = os.path.splitdrive(f)[1] 97 d = tmpd + os.path.sep + f 98 d = os.path.normpath(d) 99 owd = os.getcwd() 100 try: 101 os.makedirs(d) 102 os.chdir(d) 103 p0 = Popen(["ar", "x", "%s" % os.path.join(owd, archive)], 104 stderr=PIPE, universal_newlines=True) 105 p0.communicate() 106 if p0.returncode > 0: d = None # assume thin archive 107 except: 108 dbg("ar x: Error: %s: %s" % (archive, sys.exc_info()[0])) 109 os.chdir(owd) 110 raise 111 os.chdir(owd) 112 if d: dbg("Extracted to %s" % (d)) 113 return (archive, d) 114 115 def ar_t(archive): 116 dbg("Thin archive, using existing files:") 117 try: 118 p0 = Popen(["ar", "t", "%s" % archive], stdout=PIPE, 119 universal_newlines=True) 120 ret = p0.communicate()[0] 121 return ret.split('\n') 122 except: 123 dbg("ar t: Error: %s: %s" % (archive, sys.exc_info()[0])) 124 raise 125 126 prog = {} 127 for path in paths: 128 if os.path.isdir(path): 129 for r, dirs, files in os.walk(path): 130 if files: dbg("Files %s" % ", ".join(files)) 131 if dirs: dbg("Dirs %s" % ", ".join(dirs)) 132 prog.update(walker([os.path.join(r, f) for f in files])) 133 prog.update(walker([os.path.join(r, d) for d in dirs])) 134 else: 135 if path.endswith('.a'): 136 if ar_x(path)[1] is not None: continue # extract worked 137 prog.update(walker(ar_t(path))) 138 if not path.endswith('.o'): continue 139 dbg("Reading symbols from %s" % (path)) 140 prog[os.path.normpath(path)] = get_symbols(path) 141 return prog 142 143def resolve(prog): 144 x = prog.keys() 145 use = set() 146 # for each unique pair of different files 147 for (f, g) in ((f,g) for f in x for g in x if f != g): 148 refs = set() 149 # for each defined symbol 150 for s in (s for s in prog[f] if prog[f][s].get('def') and s in prog[g]): 151 if prog[g][s].get('use'): 152 refs.add(s) 153 for s in refs: 154 # Prune externally referenced symbols as speed optimization only 155 for i in (i for i in x if s in prog[i]): del prog[i][s] 156 use |= refs 157 return use 158 159try: 160 oprog = walker(odir) 161 if tmpd is not None: 162 oprog.update(walker([tmpd])) 163 oused = resolve(oprog) 164finally: 165 try: 166 p0 = Popen(["rm", "-r", "-f", "%s" % (tmpd)], stderr=PIPE, stdout=PIPE) 167 p0.communicate() 168 if p0.returncode != 0: raise "rm '%s' didn't work out" % (tmpd) 169 except: 170 from shutil import rmtree 171 rmtree(tmpd, ignore_errors=True) 172 173for (i,s) in ((i,s) for i in oprog.keys() for s in oprog[i] if oprog[i][s]): 174 if oprog[i][s].get('def') and not oprog[i][s].get('use'): 175 print("%s: Symbol '%s' declared extern but never referenced externally" 176 % (i,s)) 177 178 179