/[gentoo-projects]/pax-utils/lddtree.py
Gentoo

Contents of /pax-utils/lddtree.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.18 - (show annotations) (download) (as text)
Sat Jan 5 20:39:56 2013 UTC (23 months, 2 weeks ago) by vapier
Branch: MAIN
Changes since 1.17: +15 -3 lines
File MIME type: text/x-python
lddtree.py: leverage the rpaths from the main executable to better locate libs that get used

1 #!/usr/bin/python
2 # Copyright 2012 Gentoo Foundation
3 # Copyright 2012 Mike Frysinger <vapier@gentoo.org>
4 # Distributed under the terms of the GNU General Public License v2
5 # $Header: /var/cvsroot/gentoo-projects/pax-utils/lddtree.py,v 1.17 2012/12/14 04:22:52 vapier Exp $
6
7 """Read the ELF dependency tree and show it
8
9 This does not work like `ldd` in that we do not execute/load code (only read
10 files on disk), and we should the ELFs as a tree rather than a flat list.
11 """
12
13 from __future__ import print_function
14
15 import glob
16 import errno
17 import optparse
18 import os
19 import shutil
20 import sys
21
22 from elftools.elf.elffile import ELFFile
23 from elftools.common import exceptions
24
25
26 def warn(msg, prefix='warning'):
27 """Write |msg| to stderr with a |prefix| before it"""
28 print('%s: %s: %s' % (sys.argv[0], prefix, msg), file=sys.stderr)
29
30
31 def err(msg, status=1):
32 """Write |msg| to stderr and exit with |status|"""
33 warn(msg, prefix='error')
34 sys.exit(status)
35
36
37 def normpath(path):
38 """Normalize a path
39
40 Python's os.path.normpath() doesn't handle some cases:
41 // -> //
42 //..// -> //
43 //..//..// -> ///
44 """
45 return os.path.normpath(path).replace('//', '/')
46
47
48 def dedupe(input):
49 """Remove all duplicates from input (keeping order)"""
50 seen = {}
51 return [seen.setdefault(x, x) for x in input if x not in seen]
52
53
54 def ParseLdPaths(str_ldpaths, root='', path=None):
55 """Parse the colon-delimited list of paths and apply ldso rules to each
56
57 Note the special handling as dictated by the ldso:
58 - Empty paths are equivalent to $PWD
59 - $ORIGIN is expanded to the path of the given file
60 - (TODO) $LIB and friends
61
62 Args:
63 str_ldpath: A colon-delimited string of paths
64 root: The path to prepend to all paths found
65 Returns:
66 list of processed paths
67 """
68 ldpaths = []
69 for ldpath in str_ldpaths.split(':'):
70 if ldpath == '':
71 # The ldso treats "" paths as $PWD.
72 ldpath = os.getcwd()
73 else:
74 ldpath = ldpath.replace('$ORIGIN', os.path.dirname(path))
75 ldpaths.append(normpath(root + ldpath))
76 return dedupe(ldpaths)
77
78
79 def ParseLdSoConf(ldso_conf, root='/', _first=True):
80 """Load all the paths from a given ldso config file
81
82 This should handle comments, whitespace, and "include" statements.
83
84 Args:
85 ldso_conf: The file to scan
86 root: The path to prepend to all paths found
87 _first: Recursive use only; is this the first ELF ?
88 Returns:
89 list of paths found
90 """
91 paths = []
92
93 try:
94 with open(ldso_conf) as f:
95 for line in f.readlines():
96 line = line.split('#', 1)[0].strip()
97 if not line:
98 continue
99 if line.startswith('include '):
100 line = line[8:]
101 if line[0] == '/':
102 line = root + line.lstrip('/')
103 else:
104 line = os.path.dirname(ldso_conf) + '/' + line
105 for path in glob.glob(line):
106 paths += ParseLdSoConf(path, root=root, _first=False)
107 else:
108 paths += [normpath(root + line)]
109 except IOError as e:
110 if e.errno != errno.ENOENT:
111 warn(e)
112
113 if _first:
114 # XXX: Load paths from ldso itself.
115 # Remove duplicate entries to speed things up.
116 paths = dedupe(paths)
117
118 return paths
119
120
121 def LoadLdpaths(root='/'):
122 """Load linker paths from common locations
123
124 This parses the ld.so.conf and LD_LIBRARY_PATH env var.
125
126 Args:
127 root: The root tree to prepend to paths
128 Returns:
129 dict containing library paths to search
130 """
131 ldpaths = {
132 'conf': [],
133 'env': [],
134 'interp': [],
135 }
136
137 # Load up $LD_LIBRARY_PATH.
138 ldpaths['env'] = []
139 env_ldpath = os.environ.get('LD_LIBRARY_PATH')
140 if not env_ldpath is None:
141 if root != '/':
142 warn('ignoring LD_LIBRARY_PATH due to ROOT usage')
143 else:
144 # XXX: If this contains $ORIGIN, we probably have to parse this
145 # on a per-ELF basis so it can get turned into the right thing.
146 ldpaths['env'] = ParseLdPaths(env_ldpath, path='')
147
148 # Load up /etc/ld.so.conf.
149 ldpaths['conf'] = ParseLdSoConf(root + 'etc/ld.so.conf', root=root)
150
151 return ldpaths
152
153
154 def CompatibleELFs(elf1, elf2):
155 """See if two ELFs are compatible
156
157 This compares the aspects of the ELF to see if they're compatible:
158 bit size, endianness, machine type, and operating system.
159
160 Args:
161 elf1: an ELFFile object
162 elf2: an ELFFile object
163 Returns:
164 True if compatible, False otherwise
165 """
166 osabis = frozenset([e.header['e_ident']['EI_OSABI'] for e in (elf1, elf2)])
167 compat_sets = (
168 frozenset(['ELFOSABI_NONE', 'ELFOSABI_SYSV', 'ELFOSABI_LINUX']),
169 )
170 return ((len(osabis) == 1 or any(osabis.issubset(x) for x in compat_sets)) and
171 elf1.elfclass == elf2.elfclass and
172 elf1.little_endian == elf2.little_endian and
173 elf1.header['e_machine'] == elf2.header['e_machine'])
174
175
176 def FindLib(elf, lib, ldpaths):
177 """Try to locate a |lib| that is compatible to |elf| in the given |ldpaths|
178
179 Args:
180 elf: the elf which the library should be compatible with (ELF wise)
181 lib: the library (basename) to search for
182 ldpaths: a list of paths to search
183 Returns:
184 the full path to the desired library
185 """
186 for ldpath in ldpaths:
187 path = os.path.join(ldpath, lib)
188 if os.path.exists(path):
189 with open(path) as f:
190 libelf = ELFFile(f)
191 if CompatibleELFs(elf, libelf):
192 return path
193 return None
194
195
196 def ParseELF(path, root='/', ldpaths={'conf':[], 'env':[], 'interp':[]},
197 _first=True, _all_libs={}):
198 """Parse the ELF dependency tree of the specified file
199
200 Args:
201 path: The ELF to scan
202 root: The root tree to prepend to paths; this applies to interp and rpaths
203 only as |path| and |ldpaths| are expected to be prefixed already
204 ldpaths: dict containing library paths to search; should have the keys:
205 conf, env, interp
206 _first: Recursive use only; is this the first ELF ?
207 _all_libs: Recursive use only; dict of all libs we've seen
208 Returns:
209 a dict containing information about all the ELFs; e.g.
210 {
211 'interp': '/lib64/ld-linux.so.2',
212 'needed': ['libc.so.6', 'libcurl.so.4',],
213 'libs': {
214 'libc.so.6': {
215 'path': '/lib64/libc.so.6',
216 'needed': [],
217 },
218 'libcurl.so.4': {
219 'path': '/usr/lib64/libcurl.so.4',
220 'needed': ['libc.so.6', 'librt.so.1',],
221 },
222 },
223 }
224 """
225 if _first:
226 _all_libs = {}
227 ldpaths = ldpaths.copy()
228 ret = {
229 'interp': None,
230 'path': path,
231 'needed': [],
232 'rpath': [],
233 'runpath': [],
234 'libs': _all_libs,
235 }
236
237 with open(path) as f:
238 elf = ELFFile(f)
239
240 # If this is the first ELF, extract the interpreter.
241 if _first:
242 for segment in elf.iter_segments():
243 if segment.header.p_type != 'PT_INTERP':
244 continue
245
246 interp = segment.get_interp_name()
247 ret['interp'] = normpath(root + interp)
248 ret['libs'][os.path.basename(interp)] = {
249 'path': ret['interp'],
250 'needed': [],
251 }
252 # XXX: Should read it and scan for /lib paths.
253 ldpaths['interp'] = [
254 normpath(root + os.path.dirname(interp)),
255 normpath(root + '/usr' + os.path.dirname(interp)),
256 ]
257 break
258
259 # Parse the ELF's dynamic tags.
260 libs = []
261 rpaths = []
262 runpaths = []
263 for segment in elf.iter_segments():
264 if segment.header.p_type != 'PT_DYNAMIC':
265 continue
266
267 for t in segment.iter_tags():
268 if t.entry.d_tag == 'DT_RPATH':
269 rpaths = ParseLdPaths(t.rpath, root=root, path=path)
270 elif t.entry.d_tag == 'DT_RUNPATH':
271 runpaths = ParseLdPaths(t.runpath, root=root, path=path)
272 elif t.entry.d_tag == 'DT_NEEDED':
273 libs.append(t.needed)
274 if runpaths:
275 # If both RPATH and RUNPATH are set, only the latter is used.
276 rpaths = []
277
278 # XXX: We assume there is only one PT_DYNAMIC. This is
279 # probably fine since the runtime ldso does the same.
280 break
281 if _first:
282 # Propagate the rpaths used by the main ELF since those will be
283 # used at runtime to locate things.
284 ldpaths['rpath'] = rpaths
285 ldpaths['runpath'] = runpaths
286 ret['rpath'] = rpaths
287 ret['runpath'] = runpaths
288 ret['needed'] = libs
289
290 # Search for the libs this ELF uses.
291 all_ldpaths = None
292 for lib in libs:
293 if lib in _all_libs:
294 continue
295 if all_ldpaths is None:
296 all_ldpaths = rpaths + ldpaths['rpath'] + ldpaths['env'] + runpaths + ldpaths['runpath'] + ldpaths['conf'] + ldpaths['interp']
297 fullpath = FindLib(elf, lib, all_ldpaths)
298 _all_libs[lib] = {
299 'path': fullpath,
300 'needed': [],
301 }
302 if fullpath:
303 lret = ParseELF(fullpath, root, ldpaths, False, _all_libs)
304 _all_libs[lib]['needed'] = lret['needed']
305
306 del elf
307
308 return ret
309
310
311 def _NormalizePath(option, _opt, value, parser):
312 setattr(parser.values, option.dest, normpath(value))
313
314
315 def _ShowVersion(_option, _opt, _value, _parser):
316 id = '$Id: lddtree.py,v 1.17 2012/12/14 04:22:52 vapier Exp $'.split()
317 print('%s-%s %s %s' % (id[1].split('.')[0], id[2], id[3], id[4]))
318 sys.exit(0)
319
320
321 def _ActionShow(options, elf):
322 """Show the dependency tree for this ELF"""
323 def _show(lib, depth):
324 chain_libs.append(lib)
325 fullpath = elf['libs'][lib]['path']
326 if options.list:
327 print(fullpath or lib)
328 else:
329 print('%s%s => %s' % (' ' * depth, lib, fullpath))
330
331 new_libs = []
332 for lib in elf['libs'][lib]['needed']:
333 if lib in chain_libs:
334 if not options.list:
335 print('%s%s => !!! circular loop !!!' % (' ' * depth, lib))
336 continue
337 if options.all or not lib in shown_libs:
338 shown_libs.add(lib)
339 new_libs.append(lib)
340
341 for lib in new_libs:
342 _show(lib, depth + 1)
343 chain_libs.pop()
344
345 shown_libs = set(elf['needed'])
346 chain_libs = []
347 interp = elf['interp']
348 if interp:
349 shown_libs.add(os.path.basename(interp))
350 if options.list:
351 print(elf['path'])
352 if not interp is None:
353 print(interp)
354 else:
355 print('%s (interpreter => %s)' % (elf['path'], interp))
356 for lib in elf['needed']:
357 _show(lib, 1)
358
359
360 def _ActionCopy(options, elf):
361 """Copy the ELF and its dependencies to a destination tree"""
362 def _copy(src):
363 if src is None:
364 return
365
366 dst = options.dest + src
367 if os.path.exists(dst):
368 # See if they're the same file.
369 ostat = os.stat(src)
370 nstat = os.stat(dst)
371 for field in ('mode', 'mtime', 'size'):
372 if getattr(ostat, 'st_' + field) != \
373 getattr(nstat, 'st_' + field):
374 break
375 else:
376 return
377
378 if options.verbose:
379 print('%s -> %s' % (src, dst))
380
381 try:
382 os.makedirs(os.path.dirname(dst))
383 except OSError as e:
384 if e.errno != os.errno.EEXIST:
385 raise
386 try:
387 shutil.copy2(src, dst)
388 return
389 except IOError:
390 os.unlink(dst)
391 shutil.copy2(src, dst)
392
393 _copy(elf['path'])
394 _copy(elf['interp'])
395 for lib in elf['libs']:
396 _copy(elf['libs'][lib]['path'])
397
398
399 def main(argv):
400 parser = optparse.OptionParser("""%prog [options] <ELFs>
401
402 Display ELF dependencies as a tree""")
403 parser.add_option('-a', '--all',
404 action='store_true', default=False,
405 help=('Show all duplicated dependencies'))
406 parser.add_option('-R', '--root',
407 dest='root', default=os.environ.get('ROOT', ''), type='string',
408 action='callback', callback=_NormalizePath,
409 help=('Show all duplicated dependencies'))
410 parser.add_option('--copy-to-tree',
411 dest='dest', default=None, type='string',
412 action='callback', callback=_NormalizePath,
413 help=('Copy all files to the specified tree'))
414 parser.add_option('-l', '--list',
415 action='store_true', default=False,
416 help=('Display output in a simple list (easy for copying)'))
417 parser.add_option('-x', '--debug',
418 action='store_true', default=False,
419 help=('Run with debugging'))
420 parser.add_option('-v', '--verbose',
421 action='store_true', default=False,
422 help=('Be verbose'))
423 parser.add_option('-V', '--version',
424 action='callback', callback=_ShowVersion,
425 help=('Show version information'))
426 (options, paths) = parser.parse_args(argv)
427
428 # Throw away argv[0].
429 paths.pop(0)
430 if options.root != '/':
431 options.root += '/'
432
433 if options.debug:
434 print('root =', options.root)
435 if options.dest:
436 print('dest =', options.dest)
437 if not paths:
438 err('missing ELF files to scan')
439
440 ldpaths = LoadLdpaths(options.root)
441 if options.debug:
442 print('ldpaths[conf] =', ldpaths['conf'])
443 print('ldpaths[env] =', ldpaths['env'])
444
445 # Process all the files specified.
446 ret = 0
447 for path in paths:
448 try:
449 elf = ParseELF(path, options.root, ldpaths)
450 except (exceptions.ELFError, IOError) as e:
451 ret = 1
452 warn('%s: %s' % (path, e))
453 continue
454 if options.dest is None:
455 _ActionShow(options, elf)
456 else:
457 _ActionCopy(options, elf)
458 return ret
459
460
461 if __name__ == '__main__':
462 sys.exit(main(sys.argv))

  ViewVC Help
Powered by ViewVC 1.1.20