chromium/tools/win/linker_verbose_tracking.py

# Copyright 2016 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
This script parses the /verbose output from the VC++ linker and uses it to
explain why a particular object file is being linked in. It parses records
like these:

      Found "public: static void * __cdecl SkTLS::Get(void * (__cdecl*)(void)...
        Referenced in chrome_crash_reporter_client_win.obj
        Referenced in skia.lib(SkError.obj)
        Loaded skia.lib(SkTLS.obj)

and then uses the information to answer questions such as "why is SkTLS.obj
being linked in. In this case it was requested by SkError.obj, and the process
is then repeated for SkError.obj. It traces the dependency tree back to a file
that was specified on the command line. Typically that file is part of a
source_set, and if that source_set is causing unnecessary code and data to be
pulled in then changing it to a static_library may reduce the binary size. See
crrev.com/2556603002 for an example of a ~900 KB savings from such a change.

In other cases the source_set to static_library fix does not work because some
of the symbols are required, while others are pulling in unwanted object files.
In these cases it can be necessary to see what symbol is causing one object file
to reference another. Removing or moving the problematic symbol can fix the
problem. See crrev.com/2559063002 for an example of such a change.

In some cases a target needs to be a source_set in component builds (so that all
of its functions will be exported) but should be a static_library in
non-component builds. The BUILD.gn pattern for that is:

  if (is_component_build) {
    link_target_type = "source_set"
  } else {
    link_target_type = "static_library"
  }
  target(link_target_type, "filters") {

One complication is that there are sometimes multiple source files with the
same name, such as mime_util.cc, all creating mime_util.obj. The script takes
whatever search criteria you pass and looks for all .obj files that were loaded
that contain that sub-string. It will print the search list that it will use
before reporting on why all of these .obj files were loaded. For instance, the
initial output if mime_util.obj is specified will be something like this:

>python linker_verbose_tracking.py verbose.txt mime_util.obj
  Searching for [u'net.lib(mime_util.obj)', u'base.lib(mime_util.obj)']

If you want to restrict the search to just one of these .obj files then you can
give a fully specified name, like this:

>python linker_verbose_tracking.py verbose.txt base.lib(mime_util.obj)

Object file name matching is case sensitive.

Typical output when run on chrome_watcher.dll verbose link output is:

>python tools\win\linker_verbose_tracking.py verbose08.txt drop_data
Database loaded - 3844 xrefs found
Searching for common_sources.lib(drop_data.obj)
common_sources.lib(drop_data.obj).obj pulled in for symbol Metadata::Metadata...
        common.lib(content_message_generator.obj)

common.lib(content_message_generator.obj).obj pulled in for symbol ...
        Command-line obj file: url_loader.mojom.obj
"""

from __future__ import print_function

import io
import pdb
import re
import sys


def ParseVerbose(input_file):
    # This matches line like this:
    #   Referenced in skia.lib(SkError.obj)
    #   Referenced in cloud_print_helpers.obj
    #   Loaded libvpx.lib(vp9_encodemb.obj)
    # groups()[0] will be 'Referenced in ' or 'Loaded ' and groups()[1] will be
    # the fully qualified object-file name (including the .lib name if present).
    obj_match = re.compile('.*(Referenced in |Loaded )(.*)')

    # Prefix used for symbols that are found and therefore loaded:
    found_prefix = '      Found'

    # This dictionary is indexed by (fully specified) object file names and the
    # payload is the list of object file names that caused the object file that
    # is the key name to be pulled in.
    cross_refs = {}
    # This dictionary has the same index as cross_refs but its payload is the
    # simple that caused the object file to be pulled in.
    cross_refed_symbols = {}

    # None or a list of .obj files that referenced a symbol.
    references = None

    # When you redirect the linker output to a file from a command prompt the
    # result will be a utf-8 (or ASCII?) output file. However if you do the same
    # thing from PowerShell you get a utf-16 file. So, we need to handle both
    # options. Only the first BOM option (\xff\xfe) has been tested, but it
    # seems appropriate to handle the other as well.
    file_encoding = 'utf-8'
    with open(input_file) as file_handle:
        header = file_handle.read(2)
        if header == '\xff\xfe' or header == '\xfe\xff':
            file_encoding = 'utf-16'
    with io.open(input_file, encoding=file_encoding) as file_handle:
        for line in file_handle:
            if line.startswith(found_prefix):
                # Create a list to hold all of the references to this symbol
                # which caused the linker to load it.
                references = []
                # Grab the symbol name
                symbol = line[len(found_prefix):].strip()
                if symbol[0] == '"':
                    # Strip off leading and trailing quotes if present.
                    symbol = symbol[1:-1]
                continue
            # If we are looking for references to a symbol...
            if type(references) == type([]):
                sub_line = line.strip()
                match = obj_match.match(sub_line)
                if match:
                    match_type, obj_name = match.groups()
                    # See if the line is part of the list of places where this
                    # symbol was referenced:
                    if match_type == 'Referenced in ':
                        if '.lib' in obj_name:
                            # This indicates a match that is xxx.lib(yyy.obj),
                            # so a referencing .obj file that was itself inside
                            # of a library.
                            reference = obj_name
                        else:
                            # This indicates a match that is just a pure .obj
                            # file name I think this means that the .obj file
                            # was specified on the linker command line.
                            reference = ('Command-line obj file: ' + obj_name)
                        references.append(reference)
                    else:
                        assert (match_type == 'Loaded ')
                        if '.lib' in obj_name and '.obj' in obj_name:
                            cross_refs[obj_name] = references
                            cross_refed_symbols[obj_name] = symbol
                        references = None
            if line.startswith('Finished pass 1'):
                # Stop now because the remaining 90% of the verbose output is
                # not of interest. Could probably use /VERBOSE:REF to trim out
                # boring information.
                break
    return cross_refs, cross_refed_symbols


def TrackObj(cross_refs, cross_refed_symbols, obj_name):
    # Keep track of which references we've already followed.
    tracked = {}

    # Initial set of object files that we are tracking.
    targets = []
    for key in cross_refs.keys():
        # Look for any object files that were pulled in that contain the name
        # passed on the command line.
        if obj_name in key:
            targets.append(key)
    if len(targets) == 0:
        targets.append(obj_name)
    # Print what we are searching for.
    if len(targets) == 1:
        print('Searching for %s' % targets[0])
    else:
        print('Searching for %s' % targets)
    printed = False
    # Follow the chain of references up to an arbitrary maximum level, which has
    # so far never been approached.
    for i in range(100):
        new_targets = {}
        for target in targets:
            if not target in tracked:
                tracked[target] = True
                if target in cross_refs.keys():
                    symbol = cross_refed_symbols[target]
                    printed = True
                    print('%s.obj pulled in for symbol "%s" by' %
                          (target, symbol))
                    for ref in cross_refs[target]:
                        print('\t%s' % ref)
                        new_targets[ref] = True
        if len(new_targets) == 0:
            break
        print()
        targets = new_targets.keys()
    if not printed:
        print('No references to %s found. Directly specified in sources or a '
              'source_set?' % obj_name)


def main():
    if len(sys.argv) < 3:
        print(r'Usage: %s <verbose_output_file> <objfile>' % sys.argv[0])
        print(r'Sample: %s chrome_dll_verbose.txt SkTLS' % sys.argv[0])
        return 0
    cross_refs, cross_refed_symbols = ParseVerbose(sys.argv[1])
    print('Database loaded - %d xrefs found' % len(cross_refs))
    if not len(cross_refs):
        print('No data found to analyze. Exiting')
        return 0
    TrackObj(cross_refs, cross_refed_symbols, sys.argv[2])


if __name__ == '__main__':
    sys.exit(main())