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