linker_verbose_tracking.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. # Copyright (c) 2016 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """
  5. This script parses the /verbose output from the VC++ linker and uses it to
  6. explain why a particular object file is being linked in. It parses records
  7. like these:
  8. Found "public: static void * __cdecl SkTLS::Get(void * (__cdecl*)(void)...
  9. Referenced in chrome_crash_reporter_client_win.obj
  10. Referenced in skia.lib(SkError.obj)
  11. Loaded skia.lib(SkTLS.obj)
  12. and then uses the information to answer questions such as "why is SkTLS.obj
  13. being linked in. In this case it was requested by SkError.obj, and the process
  14. is then repeated for SkError.obj. It traces the dependency tree back to a file
  15. that was specified on the command line. Typically that file is part of a
  16. source_set, and if that source_set is causing unnecessary code and data to be
  17. pulled in then changing it to a static_library may reduce the binary size. See
  18. crrev.com/2556603002 for an example of a ~900 KB savings from such a change.
  19. In other cases the source_set to static_library fix does not work because some
  20. of the symbols are required, while others are pulling in unwanted object files.
  21. In these cases it can be necessary to see what symbol is causing one object file
  22. to reference another. Removing or moving the problematic symbol can fix the
  23. problem. See crrev.com/2559063002 for an example of such a change.
  24. In some cases a target needs to be a source_set in component builds (so that all
  25. of its functions will be exported) but should be a static_library in
  26. non-component builds. The BUILD.gn pattern for that is:
  27. if (is_component_build) {
  28. link_target_type = "source_set"
  29. } else {
  30. link_target_type = "static_library"
  31. }
  32. target(link_target_type, "filters") {
  33. One complication is that there are sometimes multiple source files with the
  34. same name, such as mime_util.cc, all creating mime_util.obj. The script takes
  35. whatever search criteria you pass and looks for all .obj files that were loaded
  36. that contain that sub-string. It will print the search list that it will use
  37. before reporting on why all of these .obj files were loaded. For instance, the
  38. initial output if mime_util.obj is specified will be something like this:
  39. >python linker_verbose_tracking.py verbose.txt mime_util.obj
  40. Searching for [u'net.lib(mime_util.obj)', u'base.lib(mime_util.obj)']
  41. If you want to restrict the search to just one of these .obj files then you can
  42. give a fully specified name, like this:
  43. >python linker_verbose_tracking.py verbose.txt base.lib(mime_util.obj)
  44. Object file name matching is case sensitive.
  45. Typical output when run on chrome_watcher.dll verbose link output is:
  46. >python tools\win\linker_verbose_tracking.py verbose08.txt drop_data
  47. Database loaded - 3844 xrefs found
  48. Searching for common_sources.lib(drop_data.obj)
  49. common_sources.lib(drop_data.obj).obj pulled in for symbol Metadata::Metadata...
  50. common.lib(content_message_generator.obj)
  51. common.lib(content_message_generator.obj).obj pulled in for symbol ...
  52. Command-line obj file: url_loader.mojom.obj
  53. """
  54. from __future__ import print_function
  55. import io
  56. import pdb
  57. import re
  58. import sys
  59. def ParseVerbose(input_file):
  60. # This matches line like this:
  61. # Referenced in skia.lib(SkError.obj)
  62. # Referenced in cloud_print_helpers.obj
  63. # Loaded libvpx.lib(vp9_encodemb.obj)
  64. # groups()[0] will be 'Referenced in ' or 'Loaded ' and groups()[1] will be
  65. # the fully qualified object-file name (including the .lib name if present).
  66. obj_match = re.compile('.*(Referenced in |Loaded )(.*)')
  67. # Prefix used for symbols that are found and therefore loaded:
  68. found_prefix = ' Found'
  69. # This dictionary is indexed by (fully specified) object file names and the
  70. # payload is the list of object file names that caused the object file that
  71. # is the key name to be pulled in.
  72. cross_refs = {}
  73. # This dictionary has the same index as cross_refs but its payload is the
  74. # simple that caused the object file to be pulled in.
  75. cross_refed_symbols = {}
  76. # None or a list of .obj files that referenced a symbol.
  77. references = None
  78. # When you redirect the linker output to a file from a command prompt the
  79. # result will be a utf-8 (or ASCII?) output file. However if you do the same
  80. # thing from PowerShell you get a utf-16 file. So, we need to handle both
  81. # options. Only the first BOM option (\xff\xfe) has been tested, but it seems
  82. # appropriate to handle the other as well.
  83. file_encoding = 'utf-8'
  84. with open(input_file) as file_handle:
  85. header = file_handle.read(2)
  86. if header == '\xff\xfe' or header == '\xfe\xff':
  87. file_encoding = 'utf-16'
  88. with io.open(input_file, encoding=file_encoding) as file_handle:
  89. for line in file_handle:
  90. if line.startswith(found_prefix):
  91. # Create a list to hold all of the references to this symbol which
  92. # caused the linker to load it.
  93. references = []
  94. # Grab the symbol name
  95. symbol = line[len(found_prefix):].strip()
  96. if symbol[0] == '"':
  97. # Strip off leading and trailing quotes if present.
  98. symbol = symbol[1:-1]
  99. continue
  100. # If we are looking for references to a symbol...
  101. if type(references) == type([]):
  102. sub_line = line.strip()
  103. match = obj_match.match(sub_line)
  104. if match:
  105. match_type, obj_name = match.groups()
  106. # See if the line is part of the list of places where this symbol was
  107. # referenced:
  108. if match_type == 'Referenced in ':
  109. if '.lib' in obj_name:
  110. # This indicates a match that is xxx.lib(yyy.obj), so a
  111. # referencing .obj file that was itself inside of a library.
  112. reference = obj_name
  113. else:
  114. # This indicates a match that is just a pure .obj file name
  115. # I think this means that the .obj file was specified on the
  116. # linker command line.
  117. reference = ('Command-line obj file: ' +
  118. obj_name)
  119. references.append(reference)
  120. else:
  121. assert(match_type == 'Loaded ')
  122. if '.lib' in obj_name and '.obj' in obj_name:
  123. cross_refs[obj_name] = references
  124. cross_refed_symbols[obj_name] = symbol
  125. references = None
  126. if line.startswith('Finished pass 1'):
  127. # Stop now because the remaining 90% of the verbose output is
  128. # not of interest. Could probably use /VERBOSE:REF to trim out
  129. # boring information.
  130. break
  131. return cross_refs, cross_refed_symbols
  132. def TrackObj(cross_refs, cross_refed_symbols, obj_name):
  133. # Keep track of which references we've already followed.
  134. tracked = {}
  135. # Initial set of object files that we are tracking.
  136. targets = []
  137. for key in cross_refs.keys():
  138. # Look for any object files that were pulled in that contain the name
  139. # passed on the command line.
  140. if obj_name in key:
  141. targets.append(key)
  142. if len(targets) == 0:
  143. targets.append(obj_name)
  144. # Print what we are searching for.
  145. if len(targets) == 1:
  146. print('Searching for %s' % targets[0])
  147. else:
  148. print('Searching for %s' % targets)
  149. printed = False
  150. # Follow the chain of references up to an arbitrary maximum level, which has
  151. # so far never been approached.
  152. for i in range(100):
  153. new_targets = {}
  154. for target in targets:
  155. if not target in tracked:
  156. tracked[target] = True
  157. if target in cross_refs.keys():
  158. symbol = cross_refed_symbols[target]
  159. printed = True
  160. print('%s.obj pulled in for symbol "%s" by' % (target, symbol))
  161. for ref in cross_refs[target]:
  162. print('\t%s' % ref)
  163. new_targets[ref] = True
  164. if len(new_targets) == 0:
  165. break
  166. print()
  167. targets = new_targets.keys()
  168. if not printed:
  169. print('No references to %s found. Directly specified in sources or a '
  170. 'source_set?' % obj_name)
  171. def main():
  172. if len(sys.argv) < 3:
  173. print(r'Usage: %s <verbose_output_file> <objfile>' % sys.argv[0])
  174. print(r'Sample: %s chrome_dll_verbose.txt SkTLS' % sys.argv[0])
  175. return 0
  176. cross_refs, cross_refed_symbols = ParseVerbose(sys.argv[1])
  177. print('Database loaded - %d xrefs found' % len(cross_refs))
  178. if not len(cross_refs):
  179. print('No data found to analyze. Exiting')
  180. return 0
  181. TrackObj(cross_refs, cross_refed_symbols, sys.argv[2])
  182. if __name__ == '__main__':
  183. sys.exit(main())