dump-static-initializers.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2012 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Dump functions called by static intializers in a Linux Release binary.
  6. Usage example:
  7. tools/linux/dump-static-intializers.py out/Release/chrome
  8. A brief overview of static initialization:
  9. 1) the compiler writes out, per object file, a function that contains
  10. the static intializers for that file.
  11. 2) the compiler also writes out a pointer to that function in a special
  12. section.
  13. 3) at link time, the linker concatenates the function pointer sections
  14. into a single list of all initializers.
  15. 4) at run time, on startup the binary runs all function pointers.
  16. The functions in (1) all have mangled names of the form
  17. _GLOBAL__I_foobar.cc or __cxx_global_var_initN
  18. using objdump, we can disassemble those functions and dump all symbols that
  19. they reference.
  20. """
  21. # Needed so pylint does not complain about print('', end='').
  22. from __future__ import print_function
  23. import optparse
  24. import os
  25. import re
  26. import subprocess
  27. import sys
  28. # A map of symbol => informative text about it.
  29. NOTES = {
  30. '__cxa_atexit@plt': 'registers a dtor to run at exit',
  31. 'std::__ioinit': '#includes <iostream>, use <ostream> instead',
  32. }
  33. # Determine whether this is a git checkout (as opposed to e.g. svn).
  34. IS_GIT_WORKSPACE = (subprocess.Popen(
  35. ['git', 'rev-parse'], stderr=subprocess.PIPE).wait() == 0)
  36. class Demangler:
  37. """A wrapper around c++filt to provide a function to demangle symbols."""
  38. def __init__(self, toolchain):
  39. # llvm toolchain uses cxx rather than c++.
  40. path = toolchain + 'cxxfilt'
  41. if not os.path.exists(path):
  42. path = toolchain + 'c++filt'
  43. if not os.path.exists(path):
  44. # Android currently has an issue where the llvm toolchain in the ndk does
  45. # not contain c++filt. Hopefully fixed in next NDK update...
  46. path = 'c++filt'
  47. self.cppfilt = subprocess.Popen([path],
  48. stdin=subprocess.PIPE,
  49. stdout=subprocess.PIPE,
  50. universal_newlines=True)
  51. def Demangle(self, sym):
  52. """Given mangled symbol |sym|, return its demangled form."""
  53. self.cppfilt.stdin.write(sym + '\n')
  54. self.cppfilt.stdin.flush()
  55. return self.cppfilt.stdout.readline().strip()
  56. # Matches for example: "cert_logger.pb.cc", capturing "cert_logger".
  57. protobuf_filename_re = re.compile(r'(.*)\.pb\.cc$')
  58. def QualifyFilenameAsProto(filename):
  59. """Attempt to qualify a bare |filename| with a src-relative path, assuming it
  60. is a protoc-generated file. If a single match is found, it is returned.
  61. Otherwise the original filename is returned."""
  62. if not IS_GIT_WORKSPACE:
  63. return filename
  64. match = protobuf_filename_re.match(filename)
  65. if not match:
  66. return filename
  67. basename = match.groups(0)
  68. cmd = ['git', 'ls-files', '--', '*/%s.proto' % basename]
  69. gitlsfiles = subprocess.Popen(cmd,
  70. stdout=subprocess.PIPE,
  71. universal_newlines=True)
  72. candidate = filename
  73. for line in gitlsfiles.stdout:
  74. if candidate != filename:
  75. return filename # Multiple hits, can't help.
  76. candidate = line.strip()
  77. return candidate
  78. # Regex matching the substring of a symbol's demangled text representation most
  79. # likely to appear in a source file.
  80. # Example: "v8::internal::Builtins::InitBuiltinFunctionTable()" becomes
  81. # "InitBuiltinFunctionTable", since the first (optional & non-capturing) group
  82. # picks up any ::-qualification and the last fragment picks up a suffix that
  83. # starts with an opener.
  84. symbol_code_name_re = re.compile(r'^(?:[^(<[]*::)?([^:(<[]*).*?$')
  85. def QualifyFilename(filename, symbol):
  86. """Given a bare filename and a symbol that occurs in it, attempt to qualify
  87. it with a src-relative path. If more than one file matches, return the
  88. original filename."""
  89. if not IS_GIT_WORKSPACE:
  90. return filename
  91. match = symbol_code_name_re.match(symbol)
  92. if not match:
  93. return filename
  94. symbol = match.group(1)
  95. cmd = ['git', 'grep', '-l', symbol, '--', '*/' + filename]
  96. gitgrep = subprocess.Popen(cmd,
  97. stdout=subprocess.PIPE,
  98. universal_newlines=True)
  99. candidate = filename
  100. for line in gitgrep.stdout:
  101. if candidate != filename: # More than one candidate; return bare filename.
  102. return filename
  103. candidate = line.strip()
  104. return candidate
  105. # Regex matching nm output for the symbols we're interested in. The two formats
  106. # we are interested in are _GLOBAL__sub_I_<filename> and _cxx_global_var_initN.
  107. # See test_ParseNmLine for examples.
  108. nm_re = re.compile(
  109. r'''(\S+)\s(\S+)\st\s # Symbol start address and size
  110. (
  111. (?:_ZN12)?_GLOBAL__(?:sub_)?I_ # Pattern with filename
  112. |
  113. __cxx_global_var_init\d* # Pattern without filename
  114. )(.*) # capture the filename''',
  115. re.X)
  116. def ParseNmLine(line):
  117. """Parse static initializers from a line of nm output.
  118. Given a line of nm output, parse static initializers as a
  119. (file, start, size, symbol) tuple."""
  120. match = nm_re.match(line)
  121. if match:
  122. addr, size, prefix, filename = match.groups()
  123. return (filename, int(addr, 16), int(size, 16), prefix+filename)
  124. return None
  125. def test_ParseNmLine():
  126. """Verify the nm_re regex matches some sample lines."""
  127. parse = ParseNmLine(
  128. '0000000001919920 0000000000000008 t '
  129. '_ZN12_GLOBAL__I_safe_browsing_service.cc')
  130. assert parse == ('safe_browsing_service.cc', 26319136, 8,
  131. '_ZN12_GLOBAL__I_safe_browsing_service.cc'), parse
  132. parse = ParseNmLine(
  133. '00000000026b9eb0 0000000000000024 t '
  134. '_GLOBAL__sub_I_extension_specifics.pb.cc')
  135. assert parse == ('extension_specifics.pb.cc', 40607408, 36,
  136. '_GLOBAL__sub_I_extension_specifics.pb.cc'), parse
  137. parse = ParseNmLine(
  138. '0000000002e75a60 0000000000000016 t __cxx_global_var_init')
  139. assert parse == ('', 48716384, 22, '__cxx_global_var_init'), parse
  140. parse = ParseNmLine(
  141. '0000000002e75a60 0000000000000016 t __cxx_global_var_init89')
  142. assert parse == ('', 48716384, 22, '__cxx_global_var_init89'), parse
  143. # Just always run the test; it is fast enough.
  144. test_ParseNmLine()
  145. def ParseNm(toolchain, binary):
  146. """Yield static initializers for the given binary.
  147. Given a binary, yield static initializers as (file, start, size, symbol)
  148. tuples."""
  149. nm = subprocess.Popen([toolchain + 'nm', '-S', binary],
  150. stdout=subprocess.PIPE,
  151. universal_newlines=True)
  152. for line in nm.stdout:
  153. parse = ParseNmLine(line)
  154. if parse:
  155. yield parse
  156. # Regex matching objdump output for the symbols we're interested in.
  157. # Example line:
  158. # 12354ab: (disassembly, including <FunctionReference>)
  159. disassembly_re = re.compile(r'^\s+[0-9a-f]+:.*<(\S+)>')
  160. def ExtractSymbolReferences(toolchain, binary, start, end, symbol):
  161. """Given a span of addresses, returns symbol references from disassembly."""
  162. cmd = [toolchain + 'objdump', binary, '--disassemble',
  163. '--start-address=0x%x' % start, '--stop-address=0x%x' % end]
  164. objdump = subprocess.Popen(cmd,
  165. stdout=subprocess.PIPE,
  166. universal_newlines=True)
  167. refs = set()
  168. for line in objdump.stdout:
  169. if '__static_initialization_and_destruction' in line:
  170. raise RuntimeError('code mentions '
  171. '__static_initialization_and_destruction; '
  172. 'did you accidentally run this on a Debug binary?')
  173. match = disassembly_re.search(line)
  174. if match:
  175. (ref,) = match.groups()
  176. if ref.startswith('.LC') or ref.startswith('_DYNAMIC'):
  177. # Ignore these, they are uninformative.
  178. continue
  179. if re.match(symbol, ref):
  180. # Probably a relative jump within this function.
  181. continue
  182. refs.add(ref)
  183. return sorted(refs)
  184. def main():
  185. parser = optparse.OptionParser(usage='%prog [option] filename')
  186. parser.add_option('-d', '--diffable', dest='diffable',
  187. action='store_true', default=False,
  188. help='Prints the filename on each line, for more easily '
  189. 'diff-able output. (Used by sizes.py)')
  190. parser.add_option('-t', '--toolchain-prefix', dest='toolchain',
  191. action='store', default='',
  192. help='Toolchain prefix to append to all tool invocations '
  193. '(nm, objdump).')
  194. opts, args = parser.parse_args()
  195. if len(args) != 1:
  196. parser.error('missing filename argument')
  197. return 1
  198. binary = args[0]
  199. demangler = Demangler(opts.toolchain)
  200. file_count = 0
  201. initializer_count = 0
  202. files = ParseNm(opts.toolchain, binary)
  203. if opts.diffable:
  204. files = sorted(files)
  205. for filename, addr, size, symbol in files:
  206. file_count += 1
  207. ref_output = []
  208. qualified_filename = QualifyFilenameAsProto(filename)
  209. if size == 2:
  210. # gcc generates a two-byte 'repz retq' initializer when there is a
  211. # ctor even when the ctor is empty. This is fixed in gcc 4.6, but
  212. # Android uses gcc 4.4.
  213. ref_output.append('[empty ctor, but it still has cost on gcc <4.6]')
  214. else:
  215. for ref in ExtractSymbolReferences(opts.toolchain, binary, addr,
  216. addr+size, symbol):
  217. initializer_count += 1
  218. ref = demangler.Demangle(ref)
  219. if qualified_filename == filename:
  220. qualified_filename = QualifyFilename(filename, ref)
  221. note = ''
  222. if ref in NOTES:
  223. note = NOTES[ref]
  224. elif ref.endswith('_2eproto()'):
  225. note = 'protocol compiler bug: crbug.com/105626'
  226. if note:
  227. ref_output.append('%s [%s]' % (ref, note))
  228. else:
  229. ref_output.append(ref)
  230. if opts.diffable:
  231. if ref_output:
  232. print('\n'.join(
  233. '# ' + qualified_filename + ' ' + r for r in ref_output))
  234. else:
  235. print('# %s: (empty initializer list)' % qualified_filename)
  236. else:
  237. print('%s (initializer offset 0x%x size 0x%x)' % (qualified_filename,
  238. addr, size))
  239. print(''.join(' %s\n' % r for r in ref_output))
  240. if opts.diffable:
  241. print('#', end=' ')
  242. print('Found %d static initializers in %d files.' % (initializer_count,
  243. file_count))
  244. return 0
  245. if '__main__' == __name__:
  246. sys.exit(main())