Home | History | Annotate | Download | only in lib
      1 # Copyright 2014 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 """Helper script to close over all transitive dependencies of a given .nexe
      6 executable.
      7 
      8 e.g. Given
      9 A -> B
     10 B -> C
     11 B -> D
     12 C -> E
     13 
     14 where "A -> B" means A depends on B, then GetNeeded(A) will return A, B, C, D
     15 and E.
     16 """
     17 
     18 import os
     19 import re
     20 import subprocess
     21 
     22 import elf
     23 
     24 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
     25 SDK_DIR = os.path.dirname(os.path.dirname(SCRIPT_DIR))
     26 
     27 NeededMatcher = re.compile('^ *NEEDED *([^ ]+)\n$')
     28 FormatMatcher = re.compile('^(.+):\\s*file format (.+)\n$')
     29 
     30 RUNNABLE_LD = 'runnable-ld.so'  # Name of the dynamic loader
     31 
     32 OBJDUMP_ARCH_MAP = {
     33     # Names returned by Linux's objdump:
     34     'elf64-x86-64': 'x86-64',
     35     'elf32-i386': 'x86-32',
     36     'elf32-little': 'arm',
     37     'elf32-littlearm': 'arm',
     38     # Names returned by old x86_64-nacl-objdump:
     39     'elf64-nacl': 'x86-64',
     40     'elf32-nacl': 'x86-32',
     41     # Names returned by new x86_64-nacl-objdump:
     42     'elf64-x86-64-nacl': 'x86-64',
     43     'elf32-x86-64-nacl': 'x86-64',
     44     'elf32-i386-nacl': 'x86-32',
     45     'elf32-littlearm-nacl': 'arm',
     46 }
     47 
     48 # The proper name of the dynamic linker, as kept in the IRT.  This is
     49 # excluded from the nmf file by convention.
     50 LD_NACL_MAP = {
     51     'x86-32': 'ld-nacl-x86-32.so.1',
     52     'x86-64': 'ld-nacl-x86-64.so.1',
     53     'arm': None,
     54 }
     55 
     56 
     57 class Error(Exception):
     58   '''Local Error class for this file.'''
     59   pass
     60 
     61 
     62 class NoObjdumpError(Error):
     63   '''Error raised when objdump is needed but not found'''
     64   pass
     65 
     66 
     67 def GetNeeded(main_files, objdump, lib_path):
     68   '''Collect the list of dependencies for the main_files
     69 
     70   Args:
     71     main_files: A list of files to find dependencies of.
     72     objdump: Path to the objdump executable.
     73     lib_path: A list of paths to search for shared libraries.
     74 
     75   Returns:
     76     A dict with key=filename and value=architecture. The architecture will be
     77     one of ('x86_32', 'x86_64', 'arm').
     78   '''
     79 
     80   dynamic = any(elf.ParseElfHeader(f)[1] for f in main_files)
     81 
     82   if dynamic:
     83     return _GetNeededDynamic(main_files, objdump, lib_path)
     84   else:
     85     return _GetNeededStatic(main_files)
     86 
     87 
     88 def _GetNeededDynamic(main_files, objdump, lib_path):
     89   examined = set()
     90   all_files, unexamined = GleanFromObjdump(main_files, None, objdump, lib_path)
     91   for arch in all_files.itervalues():
     92     if unexamined:
     93       unexamined.add((RUNNABLE_LD, arch))
     94 
     95   while unexamined:
     96     files_to_examine = {}
     97 
     98     # Take all the currently unexamined files and group them
     99     # by architecture.
    100     for name, arch in unexamined:
    101       files_to_examine.setdefault(arch, []).append(name)
    102 
    103     # Call GleanFromObjdump() for each architecture.
    104     needed = set()
    105     for arch, files in files_to_examine.iteritems():
    106       new_files, new_needed = GleanFromObjdump(files, arch, objdump, lib_path)
    107       all_files.update(new_files)
    108       needed |= new_needed
    109 
    110     examined |= unexamined
    111     unexamined = needed - examined
    112 
    113   # With the runnable-ld.so scheme we have today, the proper name of
    114   # the dynamic linker should be excluded from the list of files.
    115   ldso = [LD_NACL_MAP[arch] for arch in set(OBJDUMP_ARCH_MAP.values())]
    116   for filename, arch in all_files.items():
    117     name = os.path.basename(filename)
    118     if name in ldso:
    119       del all_files[filename]
    120 
    121   return all_files
    122 
    123 
    124 def GleanFromObjdump(files, arch, objdump, lib_path):
    125   '''Get architecture and dependency information for given files
    126 
    127   Args:
    128     files: A list of files to examine.
    129         [ '/path/to/my.nexe',
    130           '/path/to/lib64/libmy.so',
    131           '/path/to/mydata.so',
    132           '/path/to/my.data' ]
    133     arch: The architecure we are looking for, or None to accept any
    134           architecture.
    135     objdump: Path to the objdump executable.
    136     lib_path: A list of paths to search for shared libraries.
    137 
    138   Returns: A tuple with the following members:
    139     input_info: A dict with key=filename and value=architecture. The
    140         architecture will be one of ('x86_32', 'x86_64', 'arm').
    141     needed: A set of strings formatted as "arch/name".  Example:
    142         set(['x86-32/libc.so', 'x86-64/libgcc.so'])
    143   '''
    144   if not objdump:
    145     raise NoObjdumpError('No objdump executable found!')
    146 
    147   full_paths = set()
    148   for filename in files:
    149     if os.path.exists(filename):
    150       full_paths.add(filename)
    151     else:
    152       for path in _FindLibsInPath(filename, lib_path):
    153         full_paths.add(path)
    154 
    155   cmd = [objdump, '-p'] + list(full_paths)
    156   env = {'LANG': 'en_US.UTF-8'}
    157   proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
    158                           stderr=subprocess.PIPE, bufsize=-1,
    159                           env=env)
    160 
    161   input_info = {}
    162   found_basenames = set()
    163   needed = set()
    164   output, err_output = proc.communicate()
    165   if proc.returncode:
    166     raise Error('%s\nStdError=%s\nobjdump failed with error code: %d' %
    167                 (output, err_output, proc.returncode))
    168 
    169   file_arch = None
    170   for line in output.splitlines(True):
    171     # Objdump should display the architecture first and then the dependencies
    172     # second for each file in the list.
    173     matched = FormatMatcher.match(line)
    174     if matched:
    175       filename = matched.group(1)
    176       file_arch = OBJDUMP_ARCH_MAP[matched.group(2)]
    177       if arch and file_arch != arch:
    178         continue
    179       name = os.path.basename(filename)
    180       found_basenames.add(name)
    181       input_info[filename] = file_arch
    182     matched = NeededMatcher.match(line)
    183     if matched:
    184       if arch and file_arch != arch:
    185         continue
    186       filename = matched.group(1)
    187       new_needed = (filename, file_arch)
    188       needed.add(new_needed)
    189 
    190   for filename in files:
    191     if os.path.basename(filename) not in found_basenames:
    192       raise Error('Library not found [%s]: %s' % (arch, filename))
    193 
    194   return input_info, needed
    195 
    196 
    197 def _FindLibsInPath(name, lib_path):
    198   '''Finds the set of libraries matching |name| within lib_path
    199 
    200   Args:
    201     name: name of library to find
    202     lib_path: A list of paths to search for shared libraries.
    203 
    204   Returns:
    205     A list of system paths that match the given name within the lib_path'''
    206   files = []
    207   for dirname in lib_path:
    208     # The libc.so files in the the glibc toolchain is actually a linker
    209     # script which references libc.so.<SHA1>.  This means the lib.so itself
    210     # does not end up in the NEEDED section for glibc.  However with bionic
    211     # the SONAME is actually libc.so.  If we pass glibc's libc.so to objdump
    212     # if fails to parse it, os this filters out libc.so expept for within
    213     # the bionic toolchain.
    214     # TODO(noelallen): Remove this once the SONAME in bionic is made to be
    215     # unique in the same it is under glibc:
    216     # https://code.google.com/p/nativeclient/issues/detail?id=3833
    217     rel_dirname = os.path.relpath(dirname, SDK_DIR)
    218     if name == 'libc.so' and 'bionic' not in rel_dirname:
    219       continue
    220     filename = os.path.join(dirname, name)
    221     if os.path.exists(filename):
    222       files.append(filename)
    223   if not files:
    224     raise Error('cannot find library %s' % name)
    225   return files
    226 
    227 
    228 def _GetNeededStatic(main_files):
    229   needed = {}
    230   for filename in main_files:
    231     arch = elf.ParseElfHeader(filename)[0]
    232     needed[filename] = arch
    233   return needed
    234