unused_functions.py 6.0 KB

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