(root)/
gcc-13.2.0/
contrib/
unused_functions.py
       1  #!/usr/bin/env python
       2  # -*- coding: utf-8 -*-
       3  #
       4  # Copyright (C) 2018-2023 Free Software Foundation, Inc.
       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  
      20  import sys, os
      21  from tempfile import mkdtemp
      22  from subprocess import Popen, PIPE
      23  
      24  def 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  
      32  for 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  
      48  def dbg(args):
      49      if not verbose: return
      50      print(args)
      51  
      52  def 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  
      92  def 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  
     143  def 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  
     159  try:
     160      oprog = walker(odir)
     161      if tmpd is not None:
     162          oprog.update(walker([tmpd]))
     163      oused = resolve(oprog)
     164  finally:
     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  
     173  for (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