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

Contents of /pax-utils/lddtree.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.42 - (show annotations) (download) (as text)
Tue Apr 23 02:16:59 2013 UTC (15 months ago) by vapier
Branch: MAIN
Changes since 1.41: +3 -2 lines
File MIME type: text/x-python
lddtree.py: add docstring for path in ParseLdPaths

1 #!/usr/bin/python
2 # Copyright 2012-2013 Gentoo Foundation
3 # Copyright 2012-2013 Mike Frysinger <vapier@gentoo.org>
4 # Use of this source code is governed by a BSD-style license (BSD-3)
5 # pylint: disable=C0301
6 # $Header: /var/cvsroot/gentoo-projects/pax-utils/lddtree.py,v 1.41 2013/04/22 22:02:43 vapier Exp $
7
8 # TODO: Handle symlinks.
9
10 """Read the ELF dependency tree and show it
11
12 This does not work like `ldd` in that we do not execute/load code (only read
13 files on disk), and we should the ELFs as a tree rather than a flat list.
14 """
15
16 from __future__ import print_function
17
18 import glob
19 import errno
20 import optparse
21 import os
22 import shutil
23 import sys
24
25 from elftools.elf.elffile import ELFFile
26 from elftools.common import exceptions
27
28
29 def warn(msg, prefix='warning'):
30 """Write |msg| to stderr with a |prefix| before it"""
31 print('%s: %s: %s' % (sys.argv[0], prefix, msg), file=sys.stderr)
32
33
34 def err(msg, status=1):
35 """Write |msg| to stderr and exit with |status|"""
36 warn(msg, prefix='error')
37 sys.exit(status)
38
39
40 def bstr(buf):
41 """Decode the byte string into a string"""
42 return buf.decode('utf-8')
43
44
45 def normpath(path):
46 """Normalize a path
47
48 Python's os.path.normpath() doesn't handle some cases:
49 // -> //
50 //..// -> //
51 //..//..// -> ///
52 """
53 return os.path.normpath(path).replace('//', '/')
54
55
56 def makedirs(path):
57 """Like os.makedirs(), but ignore EEXIST errors"""
58 try:
59 os.makedirs(path)
60 except OSError as e:
61 if e.errno != os.errno.EEXIST:
62 raise
63
64
65 def dedupe(items):
66 """Remove all duplicates from |items| (keeping order)"""
67 seen = {}
68 return [seen.setdefault(x, x) for x in items if x not in seen]
69
70
71 def GenerateLdsoWrapper(root, path, interp, libpaths=()):
72 """Generate a shell script wrapper which uses local ldso to run the ELF
73
74 Since we cannot rely on the host glibc (or other libraries), we need to
75 execute the local packaged ldso directly and tell it where to find our
76 copies of libraries.
77
78 Args:
79 root: The root tree to generate scripts inside of
80 path: The full path (inside |root|) to the program to wrap
81 interp: The ldso interpreter that we need to execute
82 libpaths: Extra lib paths to search for libraries
83 """
84 basedir = os.path.dirname(path)
85 interp_dir, interp_name = os.path.split(interp)
86 libpaths = dedupe([interp_dir] + list(libpaths))
87 replacements = {
88 'interp': os.path.join(os.path.relpath(interp_dir, basedir),
89 interp_name),
90 'libpaths': ':'.join(['${basedir}/' + os.path.relpath(p, basedir)
91 for p in libpaths]),
92 }
93 wrapper = """#!/bin/sh
94 if ! base=$(realpath "$0" 2>/dev/null); then
95 case $0 in
96 /*) base=$0;;
97 *) base=${PWD:-`pwd`}/$0;;
98 esac
99 fi
100 basedir=${base%%/*}
101 exec \
102 "${basedir}/%(interp)s" \
103 --library-path "%(libpaths)s" \
104 --inhibit-rpath '' \
105 "${base}.elf" \
106 "$@"
107 """
108 wrappath = root + path
109 os.rename(wrappath, wrappath + '.elf')
110 with open(wrappath, 'w') as f:
111 f.write(wrapper % replacements)
112 os.chmod(wrappath, 0o0755)
113
114
115 def ParseLdPaths(str_ldpaths, root='', path=None):
116 """Parse the colon-delimited list of paths and apply ldso rules to each
117
118 Note the special handling as dictated by the ldso:
119 - Empty paths are equivalent to $PWD
120 - $ORIGIN is expanded to the path of the given file
121 - (TODO) $LIB and friends
122
123 Args:
124 str_ldpath: A colon-delimited string of paths
125 root: The path to prepend to all paths found
126 path: The object actively being parsed (used for $ORIGIN)
127 Returns:
128 list of processed paths
129 """
130 ldpaths = []
131 for ldpath in str_ldpaths.split(':'):
132 if ldpath == '':
133 # The ldso treats "" paths as $PWD.
134 ldpath = os.getcwd()
135 elif '$ORIGIN' in ldpath:
136 ldpath = ldpath.replace('$ORIGIN', os.path.dirname(path))
137 else:
138 ldpath = root + ldpath
139 ldpaths.append(normpath(ldpath))
140 return dedupe(ldpaths)
141
142
143 def ParseLdSoConf(ldso_conf, root='/', _first=True):
144 """Load all the paths from a given ldso config file
145
146 This should handle comments, whitespace, and "include" statements.
147
148 Args:
149 ldso_conf: The file to scan
150 root: The path to prepend to all paths found
151 _first: Recursive use only; is this the first ELF ?
152 Returns:
153 list of paths found
154 """
155 paths = []
156
157 try:
158 with open(ldso_conf) as f:
159 for line in f.readlines():
160 line = line.split('#', 1)[0].strip()
161 if not line:
162 continue
163 if line.startswith('include '):
164 line = line[8:]
165 if line[0] == '/':
166 line = root + line.lstrip('/')
167 else:
168 line = os.path.dirname(ldso_conf) + '/' + line
169 for path in glob.glob(line):
170 paths += ParseLdSoConf(path, root=root, _first=False)
171 else:
172 paths += [normpath(root + line)]
173 except IOError as e:
174 if e.errno != errno.ENOENT:
175 warn(e)
176
177 if _first:
178 # XXX: Load paths from ldso itself.
179 # Remove duplicate entries to speed things up.
180 paths = dedupe(paths)
181
182 return paths
183
184
185 def LoadLdpaths(root='/'):
186 """Load linker paths from common locations
187
188 This parses the ld.so.conf and LD_LIBRARY_PATH env var.
189
190 Args:
191 root: The root tree to prepend to paths
192 Returns:
193 dict containing library paths to search
194 """
195 ldpaths = {
196 'conf': [],
197 'env': [],
198 'interp': [],
199 }
200
201 # Load up $LD_LIBRARY_PATH.
202 ldpaths['env'] = []
203 env_ldpath = os.environ.get('LD_LIBRARY_PATH')
204 if not env_ldpath is None:
205 if root != '/':
206 warn('ignoring LD_LIBRARY_PATH due to ROOT usage')
207 else:
208 # XXX: If this contains $ORIGIN, we probably have to parse this
209 # on a per-ELF basis so it can get turned into the right thing.
210 ldpaths['env'] = ParseLdPaths(env_ldpath, path='')
211
212 # Load up /etc/ld.so.conf.
213 ldpaths['conf'] = ParseLdSoConf(root + 'etc/ld.so.conf', root=root)
214
215 return ldpaths
216
217
218 def CompatibleELFs(elf1, elf2):
219 """See if two ELFs are compatible
220
221 This compares the aspects of the ELF to see if they're compatible:
222 bit size, endianness, machine type, and operating system.
223
224 Args:
225 elf1: an ELFFile object
226 elf2: an ELFFile object
227 Returns:
228 True if compatible, False otherwise
229 """
230 osabis = frozenset([e.header['e_ident']['EI_OSABI'] for e in (elf1, elf2)])
231 compat_sets = (
232 frozenset('ELFOSABI_%s' % x for x in ('NONE', 'SYSV', 'GNU', 'LINUX',)),
233 )
234 return ((len(osabis) == 1 or any(osabis.issubset(x) for x in compat_sets)) and
235 elf1.elfclass == elf2.elfclass and
236 elf1.little_endian == elf2.little_endian and
237 elf1.header['e_machine'] == elf2.header['e_machine'])
238
239
240 def FindLib(elf, lib, ldpaths):
241 """Try to locate a |lib| that is compatible to |elf| in the given |ldpaths|
242
243 Args:
244 elf: the elf which the library should be compatible with (ELF wise)
245 lib: the library (basename) to search for
246 ldpaths: a list of paths to search
247 Returns:
248 the full path to the desired library
249 """
250 for ldpath in ldpaths:
251 path = os.path.join(ldpath, lib)
252 if os.path.exists(path):
253 with open(path, 'rb') as f:
254 libelf = ELFFile(f)
255 if CompatibleELFs(elf, libelf):
256 return path
257 return None
258
259
260 def ParseELF(path, root='/', ldpaths={'conf':[], 'env':[], 'interp':[]},
261 _first=True, _all_libs={}):
262 """Parse the ELF dependency tree of the specified file
263
264 Args:
265 path: The ELF to scan
266 root: The root tree to prepend to paths; this applies to interp and rpaths
267 only as |path| and |ldpaths| are expected to be prefixed already
268 ldpaths: dict containing library paths to search; should have the keys:
269 conf, env, interp
270 _first: Recursive use only; is this the first ELF ?
271 _all_libs: Recursive use only; dict of all libs we've seen
272 Returns:
273 a dict containing information about all the ELFs; e.g.
274 {
275 'interp': '/lib64/ld-linux.so.2',
276 'needed': ['libc.so.6', 'libcurl.so.4',],
277 'libs': {
278 'libc.so.6': {
279 'path': '/lib64/libc.so.6',
280 'needed': [],
281 },
282 'libcurl.so.4': {
283 'path': '/usr/lib64/libcurl.so.4',
284 'needed': ['libc.so.6', 'librt.so.1',],
285 },
286 },
287 }
288 """
289 if _first:
290 _all_libs = {}
291 ldpaths = ldpaths.copy()
292 ret = {
293 'interp': None,
294 'path': path,
295 'needed': [],
296 'rpath': [],
297 'runpath': [],
298 'libs': _all_libs,
299 }
300
301 with open(path, 'rb') as f:
302 elf = ELFFile(f)
303
304 # If this is the first ELF, extract the interpreter.
305 if _first:
306 for segment in elf.iter_segments():
307 if segment.header.p_type != 'PT_INTERP':
308 continue
309
310 interp = bstr(segment.get_interp_name())
311 ret['interp'] = normpath(root + interp)
312 ret['libs'][os.path.basename(interp)] = {
313 'path': ret['interp'],
314 'needed': [],
315 }
316 # XXX: Should read it and scan for /lib paths.
317 ldpaths['interp'] = [
318 normpath(root + os.path.dirname(interp)),
319 normpath(root + '/usr' + os.path.dirname(interp)),
320 ]
321 break
322
323 # Parse the ELF's dynamic tags.
324 libs = []
325 rpaths = []
326 runpaths = []
327 for segment in elf.iter_segments():
328 if segment.header.p_type != 'PT_DYNAMIC':
329 continue
330
331 for t in segment.iter_tags():
332 if t.entry.d_tag == 'DT_RPATH':
333 rpaths = ParseLdPaths(bstr(t.rpath), root=root, path=path)
334 elif t.entry.d_tag == 'DT_RUNPATH':
335 runpaths = ParseLdPaths(bstr(t.runpath), root=root, path=path)
336 elif t.entry.d_tag == 'DT_NEEDED':
337 libs.append(bstr(t.needed))
338 if runpaths:
339 # If both RPATH and RUNPATH are set, only the latter is used.
340 rpaths = []
341
342 # XXX: We assume there is only one PT_DYNAMIC. This is
343 # probably fine since the runtime ldso does the same.
344 break
345 if _first:
346 # Propagate the rpaths used by the main ELF since those will be
347 # used at runtime to locate things.
348 ldpaths['rpath'] = rpaths
349 ldpaths['runpath'] = runpaths
350 ret['rpath'] = rpaths
351 ret['runpath'] = runpaths
352 ret['needed'] = libs
353
354 # Search for the libs this ELF uses.
355 all_ldpaths = None
356 for lib in libs:
357 if lib in _all_libs:
358 continue
359 if all_ldpaths is None:
360 all_ldpaths = rpaths + ldpaths['rpath'] + ldpaths['env'] + runpaths + ldpaths['runpath'] + ldpaths['conf'] + ldpaths['interp']
361 fullpath = FindLib(elf, lib, all_ldpaths)
362 _all_libs[lib] = {
363 'path': fullpath,
364 'needed': [],
365 }
366 if fullpath:
367 lret = ParseELF(fullpath, root, ldpaths, False, _all_libs)
368 _all_libs[lib]['needed'] = lret['needed']
369
370 del elf
371
372 return ret
373
374
375 def _NormalizePath(option, _opt, value, parser):
376 setattr(parser.values, option.dest, normpath(value))
377
378
379 def _ShowVersion(_option, _opt, _value, _parser):
380 d = '$Id: lddtree.py,v 1.41 2013/04/22 22:02:43 vapier Exp $'.split()
381 print('%s-%s %s %s' % (d[1].split('.')[0], d[2], d[3], d[4]))
382 sys.exit(0)
383
384
385 def _ActionShow(options, elf):
386 """Show the dependency tree for this ELF"""
387 def _show(lib, depth):
388 chain_libs.append(lib)
389 fullpath = elf['libs'][lib]['path']
390 if options.list:
391 print(fullpath or lib)
392 else:
393 print('%s%s => %s' % (' ' * depth, lib, fullpath))
394
395 new_libs = []
396 for lib in elf['libs'][lib]['needed']:
397 if lib in chain_libs:
398 if not options.list:
399 print('%s%s => !!! circular loop !!!' % (' ' * depth, lib))
400 continue
401 if options.all or not lib in shown_libs:
402 shown_libs.add(lib)
403 new_libs.append(lib)
404
405 for lib in new_libs:
406 _show(lib, depth + 1)
407 chain_libs.pop()
408
409 shown_libs = set(elf['needed'])
410 chain_libs = []
411 interp = elf['interp']
412 if interp:
413 shown_libs.add(os.path.basename(interp))
414 if options.list:
415 print(elf['path'])
416 if not interp is None:
417 print(interp)
418 else:
419 print('%s (interpreter => %s)' % (elf['path'], interp))
420 for lib in elf['needed']:
421 _show(lib, 1)
422
423
424 def _ActionCopy(options, elf):
425 """Copy the ELF and its dependencies to a destination tree"""
426 def _StripRoot(path):
427 return path[len(options.root) - 1:]
428
429 def _copy(src, striproot=True, wrapit=False, libpaths=(), outdir=None):
430 if src is None:
431 return
432
433 if wrapit:
434 # Static ELFs don't need to be wrapped.
435 if not elf['interp']:
436 wrapit = False
437
438 striproot = _StripRoot if striproot else lambda x: x
439
440 if outdir:
441 subdst = os.path.join(outdir, os.path.basename(src))
442 else:
443 subdst = striproot(src)
444 dst = options.dest + subdst
445
446 try:
447 # See if they're the same file.
448 nstat = os.stat(dst + ('.elf' if wrapit else ''))
449 ostat = os.stat(src)
450 for field in ('mode', 'mtime', 'size'):
451 if getattr(ostat, 'st_' + field) != \
452 getattr(nstat, 'st_' + field):
453 break
454 else:
455 return
456 except OSError as e:
457 if e.errno != errno.ENOENT:
458 raise
459
460 if options.verbose:
461 print('%s -> %s' % (src, dst))
462
463 makedirs(os.path.dirname(dst))
464 try:
465 shutil.copy2(src, dst)
466 except IOError:
467 os.unlink(dst)
468 shutil.copy2(src, dst)
469
470 if wrapit:
471 if options.verbose:
472 print('generate wrapper %s' % (dst,))
473
474 if options.libdir:
475 interp = os.path.join(options.libdir, os.path.basename(elf['interp']))
476 else:
477 interp = _StripRoot(elf['interp'])
478 GenerateLdsoWrapper(options.dest, subdst, interp, libpaths)
479
480 # XXX: We should automatically import libgcc_s.so whenever libpthread.so
481 # is copied over (since we know it can be dlopen-ed by NPTL at runtime).
482 # Similarly, we should provide an option for automatically copying over
483 # the libnsl.so and libnss_*.so libraries, as well as an open ended list
484 # for known libs that get loaded (e.g. curl will dlopen(libresolv)).
485 libpaths = set()
486 for lib in elf['libs']:
487 path = elf['libs'][lib]['path']
488 if not options.libdir:
489 libpaths.add(_StripRoot(os.path.dirname(path)))
490 _copy(path, outdir=options.libdir)
491
492 if not options.libdir:
493 libpaths = list(libpaths)
494 if elf['runpath']:
495 libpaths = elf['runpath'] + libpaths
496 else:
497 libpaths = elf['rpath'] + libpaths
498 else:
499 libpaths.add(options.libdir)
500
501 _copy(elf['interp'], outdir=options.libdir)
502 _copy(elf['path'], striproot=options.auto_root,
503 wrapit=options.generate_wrappers, libpaths=libpaths,
504 outdir=options.bindir)
505
506
507 def main(argv):
508 parser = optparse.OptionParser("""%prog [options] <ELFs>
509
510 Display ELF dependencies as a tree
511
512 <ELFs> can be globs that lddtree will take care of expanding.
513 Useful when you want to glob a path under the ROOT path.
514
515 When using the --root option, all paths are implicitly prefixed by that.
516 e.g. lddtree -R /my/magic/root /bin/bash
517 This will load up the ELF found at /my/magic/root/bin/bash and then resolve
518 all libraries via that path. If you wish to actually read /bin/bash (and
519 so use the ROOT path as an alternative library tree), you can specify the
520 --no-auto-root option.
521
522 When pairing --root with --copy-to-tree, the ROOT path will be stripped.
523 e.g. lddtree -R /my/magic/root --copy-to-tree /foo /bin/bash
524 You will see /foo/bin/bash and /foo/lib/libc.so.6 and not paths like
525 /foo/my/magic/root/bin/bash. If you want that, you'll have to manually
526 add the ROOT path to the output path.
527
528 The --bindir and --libdir flags are used to normalize the output subdirs
529 when used with --copy-to-tree.
530 e.g. lddtree --copy-to-tree /foo /bin/bash /usr/sbin/lspci /usr/bin/lsof
531 This will mirror the input paths in the output. So you will end up with
532 /foo/bin/bash and /foo/usr/sbin/lspci and /foo/usr/bin/lsof. Similarly,
533 the libraries needed will be scattered among /foo/lib/ and /foo/usr/lib/
534 and perhaps other paths (like /foo/lib64/ and /usr/lib/gcc/...). You can
535 collapse all that down into nice directory structure.
536 e.g. lddtree --copy-to-tree /foo /bin/bash /usr/sbin/lspci /usr/bin/lsof \\
537 --bindir /bin --libdir /lib
538 This will place bash, lspci, and lsof into /foo/bin/. All the libraries
539 they need will be placed into /foo/lib/ only.""")
540 parser.add_option('-a', '--all',
541 action='store_true', default=False,
542 help='Show all duplicated dependencies')
543 parser.add_option('-R', '--root',
544 default=os.environ.get('ROOT', ''), type='string',
545 action='callback', callback=_NormalizePath,
546 help='Search for all files/dependencies in ROOT')
547 parser.add_option('--no-auto-root',
548 dest='auto_root', action='store_false', default=True,
549 help='Do not automatically prefix input ELFs with ROOT')
550 parser.add_option('-l', '--list',
551 action='store_true', default=False,
552 help='Display output in a simple list (easy for copying)')
553 parser.add_option('-x', '--debug',
554 action='store_true', default=False,
555 help='Run with debugging')
556 parser.add_option('-v', '--verbose',
557 action='store_true', default=False,
558 help='Be verbose')
559 parser.add_option('--skip-non-elfs',
560 action='store_true', default=False,
561 help='Skip plain (non-ELF) files instead of warning')
562 parser.add_option('-V', '--version',
563 action='callback', callback=_ShowVersion,
564 help='Show version information')
565
566 group = optparse.OptionGroup(parser, 'Copying options')
567 group.add_option('--copy-to-tree',
568 dest='dest', default=None, type='string',
569 action='callback', callback=_NormalizePath,
570 help='Copy all files to the specified tree')
571 group.add_option('--bindir',
572 default=None, type='string',
573 action='callback', callback=_NormalizePath,
574 help='Dir to store all ELFs specified on the command line')
575 group.add_option('--libdir',
576 default=None, type='string',
577 action='callback', callback=_NormalizePath,
578 help='Dir to store all ELF libs')
579 group.add_option('--generate-wrappers',
580 action='store_true', default=False,
581 help='Wrap executable ELFs with scripts for local ldso')
582 group.add_option('--copy-non-elfs',
583 action='store_true', default=False,
584 help='Copy over plain (non-ELF) files instead of warn+ignore')
585 parser.add_option_group(group)
586
587 (options, paths) = parser.parse_args(argv)
588
589 if options.root != '/':
590 options.root += '/'
591
592 if options.bindir and options.bindir[0] != '/':
593 parser.error('--bindir accepts absolute paths only')
594 if options.libdir and options.libdir[0] != '/':
595 parser.error('--libdir accepts absolute paths only')
596
597 if options.skip_non_elfs and options.copy_non_elfs:
598 parser.error('pick one handler for non-ELFs: skip or copy')
599
600 if options.debug:
601 print('root =', options.root)
602 if options.dest:
603 print('dest =', options.dest)
604 if not paths:
605 err('missing ELF files to scan')
606
607 ldpaths = LoadLdpaths(options.root)
608 if options.debug:
609 print('ldpaths[conf] =', ldpaths['conf'])
610 print('ldpaths[env] =', ldpaths['env'])
611
612 # Process all the files specified.
613 ret = 0
614 for path in paths:
615 # Only auto-prefix the path if the ELF is absolute.
616 # If it's a relative path, the user most likely wants
617 # the local path.
618 if options.auto_root and path.startswith('/'):
619 path = options.root + path.lstrip('/')
620
621 matched = False
622 for p in glob.iglob(path):
623 matched = True
624 try:
625 elf = ParseELF(p, options.root, ldpaths)
626 except exceptions.ELFError as e:
627 if options.skip_non_elfs:
628 continue
629 # XXX: Ugly. Should unify with _Action* somehow.
630 if options.dest is not None and options.copy_non_elfs:
631 if os.path.exists(p):
632 elf = {
633 'interp': None,
634 'libs': [],
635 'runpath': [],
636 'rpath': [],
637 'path': p,
638 }
639 _ActionCopy(options, elf)
640 continue
641 ret = 1
642 warn('%s: %s' % (p, e))
643 continue
644 except IOError as e:
645 ret = 1
646 warn('%s: %s' % (p, e))
647 continue
648
649 if options.dest is None:
650 _ActionShow(options, elf)
651 else:
652 _ActionCopy(options, elf)
653
654 if not matched:
655 ret = 1
656 warn('%s: did not match any paths' % (path,))
657
658 return ret
659
660
661 if __name__ == '__main__':
662 sys.exit(main(sys.argv[1:]))

  ViewVC Help
Powered by ViewVC 1.1.20