123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504 |
- #!/usr/bin/env python
- """A tool for extracting a list of symbols to export
- When exporting symbols from a dll or exe we either need to mark the symbols in
- the source code as __declspec(dllexport) or supply a list of symbols to the
- linker. This program automates the latter by inspecting the symbol tables of a
- list of link inputs and deciding which of those symbols need to be exported.
- We can't just export all the defined symbols, as there's a limit of 65535
- exported symbols and in clang we go way over that, particularly in a debug
- build. Therefore a large part of the work is pruning symbols either which can't
- be imported, or which we think are things that have definitions in public header
- files (i.e. template instantiations) and we would get defined in the thing
- importing these symbols anyway.
- """
- from __future__ import print_function
- import sys
- import re
- import os
- import subprocess
- import multiprocessing
- import argparse
- # Define functions which extract a list of symbols from a library using several
- # different tools. We use subprocess.Popen and yield a symbol at a time instead
- # of using subprocess.check_output and returning a list as, especially on
- # Windows, waiting for the entire output to be ready can take a significant
- # amount of time.
- def dumpbin_get_symbols(lib):
- process = subprocess.Popen(['dumpbin','/symbols',lib], bufsize=1,
- stdout=subprocess.PIPE, stdin=subprocess.PIPE,
- universal_newlines=True)
- process.stdin.close()
- for line in process.stdout:
- # Look for external symbols that are defined in some section
- match = re.match("^.+SECT.+External\s+\|\s+(\S+).*$", line)
- if match:
- yield match.group(1)
- process.wait()
- def nm_get_symbols(lib):
- process = subprocess.Popen(['nm',lib], bufsize=1,
- stdout=subprocess.PIPE, stdin=subprocess.PIPE,
- universal_newlines=True)
- process.stdin.close()
- for line in process.stdout:
- # Look for external symbols that are defined in some section
- match = re.match("^\S+\s+[BDGRSTVW]\s+(\S+)$", line)
- if match:
- yield match.group(1)
- process.wait()
- def readobj_get_symbols(lib):
- process = subprocess.Popen(['llvm-readobj','-symbols',lib], bufsize=1,
- stdout=subprocess.PIPE, stdin=subprocess.PIPE,
- universal_newlines=True)
- process.stdin.close()
- for line in process.stdout:
- # When looking through the output of llvm-readobj we expect to see Name,
- # Section, then StorageClass, so record Name and Section when we see
- # them and decide if this is a defined external symbol when we see
- # StorageClass.
- match = re.search('Name: (\S+)', line)
- if match:
- name = match.group(1)
- match = re.search('Section: (\S+)', line)
- if match:
- section = match.group(1)
- match = re.search('StorageClass: (\S+)', line)
- if match:
- storageclass = match.group(1)
- if section != 'IMAGE_SYM_ABSOLUTE' and \
- section != 'IMAGE_SYM_UNDEFINED' and \
- storageclass == 'External':
- yield name
- process.wait()
- # Define functions which determine if the target is 32-bit Windows (as that's
- # where calling convention name decoration happens).
- def dumpbin_is_32bit_windows(lib):
- # dumpbin /headers can output a huge amount of data (>100MB in a debug
- # build) so we read only up to the 'machine' line then close the output.
- process = subprocess.Popen(['dumpbin','/headers',lib], bufsize=1,
- stdout=subprocess.PIPE, stdin=subprocess.PIPE,
- universal_newlines=True)
- process.stdin.close()
- retval = False
- for line in process.stdout:
- match = re.match('.+machine \((\S+)\)', line)
- if match:
- retval = (match.group(1) == 'x86')
- break
- process.stdout.close()
- process.wait()
- return retval
- def objdump_is_32bit_windows(lib):
- output = subprocess.check_output(['objdump','-f',lib],
- universal_newlines=True)
- for line in output:
- match = re.match('.+file format (\S+)', line)
- if match:
- return (match.group(1) == 'pe-i386')
- return False
- def readobj_is_32bit_windows(lib):
- output = subprocess.check_output(['llvm-readobj','-file-headers',lib],
- universal_newlines=True)
- for line in output:
- match = re.match('Format: (\S+)', line)
- if match:
- return (match.group(1) == 'COFF-i386')
- return False
- # MSVC mangles names to ?<identifier_mangling>@<type_mangling>. By examining the
- # identifier/type mangling we can decide which symbols could possibly be
- # required and which we can discard.
- def should_keep_microsoft_symbol(symbol, calling_convention_decoration):
- # Keep unmangled (i.e. extern "C") names
- if not '?' in symbol:
- if calling_convention_decoration:
- # Remove calling convention decoration from names
- match = re.match('[_@]([^@]+)', symbol)
- if match:
- return match.group(1)
- return symbol
- # Function template instantiations start with ?$; keep the instantiations of
- # clang::Type::getAs, as some of them are explipict specializations that are
- # defined in clang's lib/AST/Type.cpp; discard the rest as it's assumed that
- # the definition is public
- elif re.match('\?\?\$getAs@.+@Type@clang@@', symbol):
- return symbol
- elif symbol.startswith('??$'):
- return None
- # Deleting destructors start with ?_G or ?_E and can be discarded because
- # link.exe gives you a warning telling you they can't be exported if you
- # don't
- elif symbol.startswith('??_G') or symbol.startswith('??_E'):
- return None
- # Constructors (?0) and destructors (?1) of templates (?$) are assumed to be
- # defined in headers and not required to be kept
- elif symbol.startswith('??0?$') or symbol.startswith('??1?$'):
- return None
- # An anonymous namespace is mangled as ?A(maybe hex number)@. Any symbol
- # that mentions an anonymous namespace can be discarded, as the anonymous
- # namespace doesn't exist outside of that translation unit.
- elif re.search('\?A(0x\w+)?@', symbol):
- return None
- # Keep mangled llvm:: and clang:: function symbols. How we detect these is a
- # bit of a mess and imprecise, but that avoids having to completely demangle
- # the symbol name. The outermost namespace is at the end of the identifier
- # mangling, and the identifier mangling is followed by the type mangling, so
- # we look for (llvm|clang)@@ followed by something that looks like a
- # function type mangling. To spot a function type we use (this is derived
- # from clang/lib/AST/MicrosoftMangle.cpp):
- # <function-type> ::= <function-class> <this-cvr-qualifiers>
- # <calling-convention> <return-type>
- # <argument-list> <throw-spec>
- # <function-class> ::= [A-Z]
- # <this-cvr-qualifiers> ::= [A-Z0-9_]*
- # <calling-convention> ::= [A-JQ]
- # <return-type> ::= .+
- # <argument-list> ::= X (void)
- # ::= .+@ (list of types)
- # ::= .*Z (list of types, varargs)
- # <throw-spec> ::= exceptions are not allowed
- elif re.search('(llvm|clang)@@[A-Z][A-Z0-9_]*[A-JQ].+(X|.+@|.*Z)$', symbol):
- return symbol
- return None
- # Itanium manglings are of the form _Z<identifier_mangling><type_mangling>. We
- # demangle the identifier mangling to identify symbols that can be safely
- # discarded.
- def should_keep_itanium_symbol(symbol, calling_convention_decoration):
- # Start by removing any calling convention decoration (which we expect to
- # see on all symbols, even mangled C++ symbols)
- if calling_convention_decoration and symbol.startswith('_'):
- symbol = symbol[1:]
- # Keep unmangled names
- if not symbol.startswith('_') and not symbol.startswith('.'):
- return symbol
- # Discard manglings that aren't nested names
- match = re.match('_Z(T[VTIS])?(N.+)', symbol)
- if not match:
- return None
- # Demangle the name. If the name is too complex then we don't need to keep
- # it, but it the demangling fails then keep the symbol just in case.
- try:
- names, _ = parse_itanium_nested_name(match.group(2))
- except TooComplexName:
- return None
- if not names:
- return symbol
- # Constructors and destructors of templates classes are assumed to be
- # defined in headers and not required to be kept
- if re.match('[CD][123]', names[-1][0]) and names[-2][1]:
- return None
- # Keep the instantiations of clang::Type::getAs, as some of them are
- # explipict specializations that are defined in clang's lib/AST/Type.cpp;
- # discard any other function template instantiations as it's assumed that
- # the definition is public
- elif symbol.startswith('_ZNK5clang4Type5getAs'):
- return symbol
- elif names[-1][1]:
- return None
- # Keep llvm:: and clang:: names
- elif names[0][0] == '4llvm' or names[0][0] == '5clang':
- return symbol
- # Discard everything else
- else:
- return None
- # Certain kinds of complex manglings we assume cannot be part of a public
- # interface, and we handle them by raising an exception.
- class TooComplexName(Exception):
- pass
- # Parse an itanium mangled name from the start of a string and return a
- # (name, rest of string) pair.
- def parse_itanium_name(arg):
- # Check for a normal name
- match = re.match('(\d+)(.+)', arg)
- if match:
- n = int(match.group(1))
- name = match.group(1)+match.group(2)[:n]
- rest = match.group(2)[n:]
- return name, rest
- # Check for constructor/destructor names
- match = re.match('([CD][123])(.+)', arg)
- if match:
- return match.group(1), match.group(2)
- # Assume that a sequence of characters that doesn't end a nesting is an
- # operator (this is very imprecise, but appears to be good enough)
- match = re.match('([^E]+)(.+)', arg)
- if match:
- return match.group(1), match.group(2)
- # Anything else: we can't handle it
- return None, arg
- # Parse an itanium mangled template argument list from the start of a string
- # and throw it away, returning the rest of the string.
- def skip_itanium_template(arg):
- # A template argument list starts with I
- assert arg.startswith('I'), arg
- tmp = arg[1:]
- while tmp:
- # Check for names
- match = re.match('(\d+)(.+)', tmp)
- if match:
- n = int(match.group(1))
- tmp = match.group(2)[n:]
- continue
- # Check for substitutions
- match = re.match('S[A-Z0-9]*_(.+)', tmp)
- if match:
- tmp = match.group(1)
- # Start of a template
- elif tmp.startswith('I'):
- tmp = skip_itanium_template(tmp)
- # Start of a nested name
- elif tmp.startswith('N'):
- _, tmp = parse_itanium_nested_name(tmp)
- # Start of an expression: assume that it's too complicated
- elif tmp.startswith('L') or tmp.startswith('X'):
- raise TooComplexName
- # End of the template
- elif tmp.startswith('E'):
- return tmp[1:]
- # Something else: probably a type, skip it
- else:
- tmp = tmp[1:]
- return None
- # Parse an itanium mangled nested name and transform it into a list of pairs of
- # (name, is_template), returning (list, rest of string).
- def parse_itanium_nested_name(arg):
- # A nested name starts with N
- assert arg.startswith('N'), arg
- ret = []
- # Skip past the N, and possibly a substitution
- match = re.match('NS[A-Z0-9]*_(.+)', arg)
- if match:
- tmp = match.group(1)
- else:
- tmp = arg[1:]
- # Skip past CV-qualifiers and ref qualifiers
- match = re.match('[rVKRO]*(.+)', tmp);
- if match:
- tmp = match.group(1)
- # Repeatedly parse names from the string until we reach the end of the
- # nested name
- while tmp:
- # An E ends the nested name
- if tmp.startswith('E'):
- return ret, tmp[1:]
- # Parse a name
- name_part, tmp = parse_itanium_name(tmp)
- if not name_part:
- # If we failed then we don't know how to demangle this
- return None, None
- is_template = False
- # If this name is a template record that, then skip the template
- # arguments
- if tmp.startswith('I'):
- tmp = skip_itanium_template(tmp)
- is_template = True
- # Add the name to the list
- ret.append((name_part, is_template))
- # If we get here then something went wrong
- return None, None
- def extract_symbols(arg):
- get_symbols, should_keep_symbol, calling_convention_decoration, lib = arg
- symbols = dict()
- for symbol in get_symbols(lib):
- symbol = should_keep_symbol(symbol, calling_convention_decoration)
- if symbol:
- symbols[symbol] = 1 + symbols.setdefault(symbol,0)
- return symbols
- if __name__ == '__main__':
- tool_exes = ['dumpbin','nm','objdump','llvm-readobj']
- parser = argparse.ArgumentParser(
- description='Extract symbols to export from libraries')
- parser.add_argument('--mangling', choices=['itanium','microsoft'],
- required=True, help='expected symbol mangling scheme')
- parser.add_argument('--tools', choices=tool_exes, nargs='*',
- help='tools to use to extract symbols and determine the'
- ' target')
- parser.add_argument('libs', metavar='lib', type=str, nargs='+',
- help='libraries to extract symbols from')
- parser.add_argument('-o', metavar='file', type=str, help='output to file')
- args = parser.parse_args()
- # Determine the function to use to get the list of symbols from the inputs,
- # and the function to use to determine if the target is 32-bit windows.
- tools = { 'dumpbin' : (dumpbin_get_symbols, dumpbin_is_32bit_windows),
- 'nm' : (nm_get_symbols, None),
- 'objdump' : (None, objdump_is_32bit_windows),
- 'llvm-readobj' : (readobj_get_symbols, readobj_is_32bit_windows) }
- get_symbols = None
- is_32bit_windows = None
- # If we have a tools argument then use that for the list of tools to check
- if args.tools:
- tool_exes = args.tools
- # Find a tool to use by trying each in turn until we find one that exists
- # (subprocess.call will throw OSError when the program does not exist)
- get_symbols = None
- for exe in tool_exes:
- try:
- # Close std streams as we don't want any output and we don't
- # want the process to wait for something on stdin.
- p = subprocess.Popen([exe], stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- stdin=subprocess.PIPE,
- universal_newlines=True)
- p.stdout.close()
- p.stderr.close()
- p.stdin.close()
- p.wait()
- # Keep going until we have a tool to use for both get_symbols and
- # is_32bit_windows
- if not get_symbols:
- get_symbols = tools[exe][0]
- if not is_32bit_windows:
- is_32bit_windows = tools[exe][1]
- if get_symbols and is_32bit_windows:
- break
- except OSError:
- continue
- if not get_symbols:
- print("Couldn't find a program to read symbols with", file=sys.stderr)
- exit(1)
- if not is_32bit_windows:
- print("Couldn't find a program to determining the target", file=sys.stderr)
- exit(1)
- # How we determine which symbols to keep and which to discard depends on
- # the mangling scheme
- if args.mangling == 'microsoft':
- should_keep_symbol = should_keep_microsoft_symbol
- else:
- should_keep_symbol = should_keep_itanium_symbol
- # Get the list of libraries to extract symbols from
- libs = list()
- for lib in args.libs:
- # When invoked by cmake the arguments are the cmake target names of the
- # libraries, so we need to add .lib/.a to the end and maybe lib to the
- # start to get the filename. Also allow objects.
- suffixes = ['.lib','.a','.obj','.o']
- if not any([lib.endswith(s) for s in suffixes]):
- for s in suffixes:
- if os.path.exists(lib+s):
- lib = lib+s
- break
- if os.path.exists('lib'+lib+s):
- lib = 'lib'+lib+s
- break
- if not any([lib.endswith(s) for s in suffixes]):
- print("Don't know what to do with argument "+lib, file=sys.stderr)
- exit(1)
- libs.append(lib)
- # Check if calling convention decoration is used by inspecting the first
- # library in the list
- calling_convention_decoration = is_32bit_windows(libs[0])
- # Extract symbols from libraries in parallel. This is a huge time saver when
- # doing a debug build, as there are hundreds of thousands of symbols in each
- # library.
- pool = multiprocessing.Pool()
- try:
- # Only one argument can be passed to the mapping function, and we can't
- # use a lambda or local function definition as that doesn't work on
- # windows, so create a list of tuples which duplicates the arguments
- # that are the same in all calls.
- vals = [(get_symbols, should_keep_symbol, calling_convention_decoration, x) for x in libs]
- # Do an async map then wait for the result to make sure that
- # KeyboardInterrupt gets caught correctly (see
- # http://bugs.python.org/issue8296)
- result = pool.map_async(extract_symbols, vals)
- pool.close()
- libs_symbols = result.get(3600)
- except KeyboardInterrupt:
- # On Ctrl-C terminate everything and exit
- pool.terminate()
- pool.join()
- exit(1)
- # Merge everything into a single dict
- symbols = dict()
- for this_lib_symbols in libs_symbols:
- for k,v in list(this_lib_symbols.items()):
- symbols[k] = v + symbols.setdefault(k,0)
- # Count instances of member functions of template classes, and map the
- # symbol name to the function+class. We do this under the assumption that if
- # a member function of a template class is instantiated many times it's
- # probably declared in a public header file.
- template_function_count = dict()
- template_function_mapping = dict()
- template_function_count[""] = 0
- for k in symbols:
- name = None
- if args.mangling == 'microsoft':
- # Member functions of templates start with
- # ?<fn_name>@?$<class_name>@, so we map to <fn_name>@?$<class_name>.
- # As manglings go from the innermost scope to the outermost scope
- # this means:
- # * When we have a function member of a subclass of a template
- # class then <fn_name> will actually contain the mangling of
- # both the subclass and the function member. This is fine.
- # * When we have a function member of a template subclass of a
- # (possibly template) class then it's the innermost template
- # subclass that becomes <class_name>. This should be OK so long
- # as we don't have multiple classes with a template subclass of
- # the same name.
- match = re.search("^\?(\??\w+\@\?\$\w+)\@", k)
- if match:
- name = match.group(1)
- else:
- # Find member functions of templates by demangling the name and
- # checking if the second-to-last name in the list is a template.
- match = re.match('_Z(T[VTIS])?(N.+)', k)
- if match:
- try:
- names, _ = parse_itanium_nested_name(match.group(2))
- if names and names[-2][1]:
- name = ''.join([x for x,_ in names])
- except TooComplexName:
- # Manglings that are too complex should already have been
- # filtered out, but if we happen to somehow see one here
- # just leave it as-is.
- pass
- if name:
- old_count = template_function_count.setdefault(name,0)
- template_function_count[name] = old_count + 1
- template_function_mapping[k] = name
- else:
- template_function_mapping[k] = ""
- # Print symbols which both:
- # * Appear in exactly one input, as symbols defined in multiple
- # objects/libraries are assumed to have public definitions.
- # * Aren't instances of member functions of templates which have been
- # instantiated 100 times or more, which are assumed to have public
- # definitions. (100 is an arbitrary guess here.)
- if args.o:
- outfile = open(args.o,'w')
- else:
- outfile = sys.stdout
- for k,v in list(symbols.items()):
- template_count = template_function_count[template_function_mapping[k]]
- if v == 1 and template_count < 100:
- print(k, file=outfile)
|