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

Contents of /pax-utils/lddtree.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.44 - (show annotations) (download) (as text)
Thu Mar 20 08:25:45 2014 UTC (9 months, 1 week ago) by vapier
Branch: MAIN
Changes since 1.43: +17 -9 lines
File MIME type: text/x-python
lddtree.py: initial prefix support by Benda Xu #488460

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.43 2014/03/20 08:18:06 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_ldpaths: 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
128 Returns:
129 list of processed paths
130 """
131 ldpaths = []
132 for ldpath in str_ldpaths.split(':'):
133 if ldpath == '':
134 # The ldso treats "" paths as $PWD.
135 ldpath = os.getcwd()
136 elif '$ORIGIN' in ldpath:
137 ldpath = ldpath.replace('$ORIGIN', os.path.dirname(path))
138 else:
139 ldpath = root + ldpath
140 ldpaths.append(normpath(ldpath))
141 return dedupe(ldpaths)
142
143
144 def ParseLdSoConf(ldso_conf, root='/', _first=True):
145 """Load all the paths from a given ldso config file
146
147 This should handle comments, whitespace, and "include" statements.
148
149 Args:
150 ldso_conf: The file to scan
151 root: The path to prepend to all paths found
152 _first: Recursive use only; is this the first ELF ?
153
154 Returns:
155 list of paths found
156 """
157 paths = []
158
159 try:
160 with open(ldso_conf) as f:
161 for line in f.readlines():
162 line = line.split('#', 1)[0].strip()
163 if not line:
164 continue
165 if line.startswith('include '):
166 line = line[8:]
167 if line[0] == '/':
168 line = root + line.lstrip('/')
169 else:
170 line = os.path.dirname(ldso_conf) + '/' + line
171 for path in glob.glob(line):
172 paths += ParseLdSoConf(path, root=root, _first=False)
173 else:
174 paths += [normpath(root + line)]
175 except IOError as e:
176 if e.errno != errno.ENOENT:
177 warn(e)
178
179 if _first:
180 # XXX: Load paths from ldso itself.
181 # Remove duplicate entries to speed things up.
182 paths = dedupe(paths)
183
184 return paths
185
186
187 def LoadLdpaths(root='/', prefix=''):
188 """Load linker paths from common locations
189
190 This parses the ld.so.conf and LD_LIBRARY_PATH env var.
191
192 Args:
193 root: The root tree to prepend to paths
194 prefix: The path under |root| to search
195
196 Returns:
197 dict containing library paths to search
198 """
199 ldpaths = {
200 'conf': [],
201 'env': [],
202 'interp': [],
203 }
204
205 # Load up $LD_LIBRARY_PATH.
206 ldpaths['env'] = []
207 env_ldpath = os.environ.get('LD_LIBRARY_PATH')
208 if not env_ldpath is None:
209 if root != '/':
210 warn('ignoring LD_LIBRARY_PATH due to ROOT usage')
211 else:
212 # XXX: If this contains $ORIGIN, we probably have to parse this
213 # on a per-ELF basis so it can get turned into the right thing.
214 ldpaths['env'] = ParseLdPaths(env_ldpath, path='')
215
216 # Load up /etc/ld.so.conf.
217 ldpaths['conf'] = ParseLdSoConf(root + prefix + '/etc/ld.so.conf', root=root)
218
219 return ldpaths
220
221
222 def CompatibleELFs(elf1, elf2):
223 """See if two ELFs are compatible
224
225 This compares the aspects of the ELF to see if they're compatible:
226 bit size, endianness, machine type, and operating system.
227
228 Args:
229 elf1: an ELFFile object
230 elf2: an ELFFile object
231
232 Returns:
233 True if compatible, False otherwise
234 """
235 osabis = frozenset([e.header['e_ident']['EI_OSABI'] for e in (elf1, elf2)])
236 compat_sets = (
237 frozenset('ELFOSABI_%s' % x for x in ('NONE', 'SYSV', 'GNU', 'LINUX',)),
238 )
239 return ((len(osabis) == 1 or any(osabis.issubset(x) for x in compat_sets)) and
240 elf1.elfclass == elf2.elfclass and
241 elf1.little_endian == elf2.little_endian and
242 elf1.header['e_machine'] == elf2.header['e_machine'])
243
244
245 def FindLib(elf, lib, ldpaths):
246 """Try to locate a |lib| that is compatible to |elf| in the given |ldpaths|
247
248 Args:
249 elf: the elf which the library should be compatible with (ELF wise)
250 lib: the library (basename) to search for
251 ldpaths: a list of paths to search
252
253 Returns:
254 the full path to the desired library
255 """
256 for ldpath in ldpaths:
257 path = os.path.join(ldpath, lib)
258 if os.path.exists(path):
259 with open(path, 'rb') as f:
260 libelf = ELFFile(f)
261 if CompatibleELFs(elf, libelf):
262 return path
263 return None
264
265
266 def ParseELF(path, root='/', prefix='', ldpaths={'conf':[], 'env':[], 'interp':[]},
267 _first=True, _all_libs={}):
268 """Parse the ELF dependency tree of the specified file
269
270 Args:
271 path: The ELF to scan
272 root: The root tree to prepend to paths; this applies to interp and rpaths
273 only as |path| and |ldpaths| are expected to be prefixed already
274 prefix: The path under |root| to search
275 ldpaths: dict containing library paths to search; should have the keys:
276 conf, env, interp
277 _first: Recursive use only; is this the first ELF ?
278 _all_libs: Recursive use only; dict of all libs we've seen
279
280 Returns:
281 a dict containing information about all the ELFs; e.g.
282 {
283 'interp': '/lib64/ld-linux.so.2',
284 'needed': ['libc.so.6', 'libcurl.so.4',],
285 'libs': {
286 'libc.so.6': {
287 'path': '/lib64/libc.so.6',
288 'needed': [],
289 },
290 'libcurl.so.4': {
291 'path': '/usr/lib64/libcurl.so.4',
292 'needed': ['libc.so.6', 'librt.so.1',],
293 },
294 },
295 }
296 """
297 if _first:
298 _all_libs = {}
299 ldpaths = ldpaths.copy()
300 ret = {
301 'interp': None,
302 'path': path,
303 'needed': [],
304 'rpath': [],
305 'runpath': [],
306 'libs': _all_libs,
307 }
308
309 with open(path, 'rb') as f:
310 elf = ELFFile(f)
311
312 # If this is the first ELF, extract the interpreter.
313 if _first:
314 for segment in elf.iter_segments():
315 if segment.header.p_type != 'PT_INTERP':
316 continue
317
318 interp = bstr(segment.get_interp_name())
319 ret['interp'] = normpath(root + interp)
320 ret['libs'][os.path.basename(interp)] = {
321 'path': ret['interp'],
322 'needed': [],
323 }
324 # XXX: Should read it and scan for /lib paths.
325 ldpaths['interp'] = [
326 normpath(root + os.path.dirname(interp)),
327 normpath(root + prefix + '/usr' + os.path.dirname(interp).lstrip(prefix)),
328 ]
329 break
330
331 # Parse the ELF's dynamic tags.
332 libs = []
333 rpaths = []
334 runpaths = []
335 for segment in elf.iter_segments():
336 if segment.header.p_type != 'PT_DYNAMIC':
337 continue
338
339 for t in segment.iter_tags():
340 if t.entry.d_tag == 'DT_RPATH':
341 rpaths = ParseLdPaths(bstr(t.rpath), root=root, path=path)
342 elif t.entry.d_tag == 'DT_RUNPATH':
343 runpaths = ParseLdPaths(bstr(t.runpath), root=root, path=path)
344 elif t.entry.d_tag == 'DT_NEEDED':
345 libs.append(bstr(t.needed))
346 if runpaths:
347 # If both RPATH and RUNPATH are set, only the latter is used.
348 rpaths = []
349
350 # XXX: We assume there is only one PT_DYNAMIC. This is
351 # probably fine since the runtime ldso does the same.
352 break
353 if _first:
354 # Propagate the rpaths used by the main ELF since those will be
355 # used at runtime to locate things.
356 ldpaths['rpath'] = rpaths
357 ldpaths['runpath'] = runpaths
358 ret['rpath'] = rpaths
359 ret['runpath'] = runpaths
360 ret['needed'] = libs
361
362 # Search for the libs this ELF uses.
363 all_ldpaths = None
364 for lib in libs:
365 if lib in _all_libs:
366 continue
367 if all_ldpaths is None:
368 all_ldpaths = rpaths + ldpaths['rpath'] + ldpaths['env'] + runpaths + ldpaths['runpath'] + ldpaths['conf'] + ldpaths['interp']
369 fullpath = FindLib(elf, lib, all_ldpaths)
370 _all_libs[lib] = {
371 'path': fullpath,
372 'needed': [],
373 }
374 if fullpath:
375 lret = ParseELF(fullpath, root, prefix, ldpaths, False, _all_libs)
376 _all_libs[lib]['needed'] = lret['needed']
377
378 del elf
379
380 return ret
381
382
383 def _NormalizePath(option, _opt, value, parser):
384 setattr(parser.values, option.dest, normpath(value))
385
386
387 def _ShowVersion(_option, _opt, _value, _parser):
388 d = '$Id: lddtree.py,v 1.43 2014/03/20 08:18:06 vapier Exp $'.split()
389 print('%s-%s %s %s' % (d[1].split('.')[0], d[2], d[3], d[4]))
390 sys.exit(0)
391
392
393 def _ActionShow(options, elf):
394 """Show the dependency tree for this ELF"""
395 def _show(lib, depth):
396 chain_libs.append(lib)
397 fullpath = elf['libs'][lib]['path']
398 if options.list:
399 print(fullpath or lib)
400 else:
401 print('%s%s => %s' % (' ' * depth, lib, fullpath))
402
403 new_libs = []
404 for lib in elf['libs'][lib]['needed']:
405 if lib in chain_libs:
406 if not options.list:
407 print('%s%s => !!! circular loop !!!' % (' ' * depth, lib))
408 continue
409 if options.all or not lib in shown_libs:
410 shown_libs.add(lib)
411 new_libs.append(lib)
412
413 for lib in new_libs:
414 _show(lib, depth + 1)
415 chain_libs.pop()
416
417 shown_libs = set(elf['needed'])
418 chain_libs = []
419 interp = elf['interp']
420 if interp:
421 shown_libs.add(os.path.basename(interp))
422 if options.list:
423 print(elf['path'])
424 if not interp is None:
425 print(interp)
426 else:
427 print('%s (interpreter => %s)' % (elf['path'], interp))
428 for lib in elf['needed']:
429 _show(lib, 1)
430
431
432 def _ActionCopy(options, elf):
433 """Copy the ELF and its dependencies to a destination tree"""
434 def _StripRoot(path):
435 return path[len(options.root) - 1:]
436
437 def _copy(src, striproot=True, wrapit=False, libpaths=(), outdir=None):
438 if src is None:
439 return
440
441 if wrapit:
442 # Static ELFs don't need to be wrapped.
443 if not elf['interp']:
444 wrapit = False
445
446 striproot = _StripRoot if striproot else lambda x: x
447
448 if outdir:
449 subdst = os.path.join(outdir, os.path.basename(src))
450 else:
451 subdst = striproot(src)
452 dst = options.dest + subdst
453
454 try:
455 # See if they're the same file.
456 nstat = os.stat(dst + ('.elf' if wrapit else ''))
457 ostat = os.stat(src)
458 for field in ('mode', 'mtime', 'size'):
459 if getattr(ostat, 'st_' + field) != \
460 getattr(nstat, 'st_' + field):
461 break
462 else:
463 return
464 except OSError as e:
465 if e.errno != errno.ENOENT:
466 raise
467
468 if options.verbose:
469 print('%s -> %s' % (src, dst))
470
471 makedirs(os.path.dirname(dst))
472 try:
473 shutil.copy2(src, dst)
474 except IOError:
475 os.unlink(dst)
476 shutil.copy2(src, dst)
477
478 if wrapit:
479 if options.verbose:
480 print('generate wrapper %s' % (dst,))
481
482 if options.libdir:
483 interp = os.path.join(options.libdir, os.path.basename(elf['interp']))
484 else:
485 interp = _StripRoot(elf['interp'])
486 GenerateLdsoWrapper(options.dest, subdst, interp, libpaths)
487
488 # XXX: We should automatically import libgcc_s.so whenever libpthread.so
489 # is copied over (since we know it can be dlopen-ed by NPTL at runtime).
490 # Similarly, we should provide an option for automatically copying over
491 # the libnsl.so and libnss_*.so libraries, as well as an open ended list
492 # for known libs that get loaded (e.g. curl will dlopen(libresolv)).
493 libpaths = set()
494 for lib in elf['libs']:
495 path = elf['libs'][lib]['path']
496 if not options.libdir:
497 libpaths.add(_StripRoot(os.path.dirname(path)))
498 _copy(path, outdir=options.libdir)
499
500 if not options.libdir:
501 libpaths = list(libpaths)
502 if elf['runpath']:
503 libpaths = elf['runpath'] + libpaths
504 else:
505 libpaths = elf['rpath'] + libpaths
506 else:
507 libpaths.add(options.libdir)
508
509 _copy(elf['interp'], outdir=options.libdir)
510 _copy(elf['path'], striproot=options.auto_root,
511 wrapit=options.generate_wrappers, libpaths=libpaths,
512 outdir=options.bindir)
513
514
515 def main(argv):
516 parser = optparse.OptionParser("""%prog [options] <ELFs>
517
518 Display ELF dependencies as a tree
519
520 <ELFs> can be globs that lddtree will take care of expanding.
521 Useful when you want to glob a path under the ROOT path.
522
523 When using the --root option, all paths are implicitly prefixed by that.
524 e.g. lddtree -R /my/magic/root /bin/bash
525 This will load up the ELF found at /my/magic/root/bin/bash and then resolve
526 all libraries via that path. If you wish to actually read /bin/bash (and
527 so use the ROOT path as an alternative library tree), you can specify the
528 --no-auto-root option.
529
530 When pairing --root with --copy-to-tree, the ROOT path will be stripped.
531 e.g. lddtree -R /my/magic/root --copy-to-tree /foo /bin/bash
532 You will see /foo/bin/bash and /foo/lib/libc.so.6 and not paths like
533 /foo/my/magic/root/bin/bash. If you want that, you'll have to manually
534 add the ROOT path to the output path.
535
536 The --bindir and --libdir flags are used to normalize the output subdirs
537 when used with --copy-to-tree.
538 e.g. lddtree --copy-to-tree /foo /bin/bash /usr/sbin/lspci /usr/bin/lsof
539 This will mirror the input paths in the output. So you will end up with
540 /foo/bin/bash and /foo/usr/sbin/lspci and /foo/usr/bin/lsof. Similarly,
541 the libraries needed will be scattered among /foo/lib/ and /foo/usr/lib/
542 and perhaps other paths (like /foo/lib64/ and /usr/lib/gcc/...). You can
543 collapse all that down into nice directory structure.
544 e.g. lddtree --copy-to-tree /foo /bin/bash /usr/sbin/lspci /usr/bin/lsof \\
545 --bindir /bin --libdir /lib
546 This will place bash, lspci, and lsof into /foo/bin/. All the libraries
547 they need will be placed into /foo/lib/ only.""")
548 parser.add_option('-a', '--all',
549 action='store_true', default=False,
550 help='Show all duplicated dependencies')
551 parser.add_option('-R', '--root',
552 default=os.environ.get('ROOT', ''), type='string',
553 action='callback', callback=_NormalizePath,
554 help='Search for all files/dependencies in ROOT')
555 parser.add_option('-P', '--prefix',
556 default=os.environ.get('EPREFIX', '@GENTOO_PORTAGE_EPREFIX@'), type='string',
557 action='callback', callback=_NormalizePath,
558 help='Specify EPREFIX for binaries (for Gentoo Prefix)')
559 parser.add_option('--no-auto-root',
560 dest='auto_root', action='store_false', default=True,
561 help='Do not automatically prefix input ELFs with ROOT')
562 parser.add_option('-l', '--list',
563 action='store_true', default=False,
564 help='Display output in a simple list (easy for copying)')
565 parser.add_option('-x', '--debug',
566 action='store_true', default=False,
567 help='Run with debugging')
568 parser.add_option('-v', '--verbose',
569 action='store_true', default=False,
570 help='Be verbose')
571 parser.add_option('--skip-non-elfs',
572 action='store_true', default=False,
573 help='Skip plain (non-ELF) files instead of warning')
574 parser.add_option('-V', '--version',
575 action='callback', callback=_ShowVersion,
576 help='Show version information')
577
578 group = optparse.OptionGroup(parser, 'Copying options')
579 group.add_option('--copy-to-tree',
580 dest='dest', default=None, type='string',
581 action='callback', callback=_NormalizePath,
582 help='Copy all files to the specified tree')
583 group.add_option('--bindir',
584 default=None, type='string',
585 action='callback', callback=_NormalizePath,
586 help='Dir to store all ELFs specified on the command line')
587 group.add_option('--libdir',
588 default=None, type='string',
589 action='callback', callback=_NormalizePath,
590 help='Dir to store all ELF libs')
591 group.add_option('--generate-wrappers',
592 action='store_true', default=False,
593 help='Wrap executable ELFs with scripts for local ldso')
594 group.add_option('--copy-non-elfs',
595 action='store_true', default=False,
596 help='Copy over plain (non-ELF) files instead of warn+ignore')
597 parser.add_option_group(group)
598
599 (options, paths) = parser.parse_args(argv)
600
601 if options.root != '/':
602 options.root += '/'
603 if options.prefix == '@''GENTOO_PORTAGE_EPREFIX''@':
604 options.prefix = ''
605
606 if options.bindir and options.bindir[0] != '/':
607 parser.error('--bindir accepts absolute paths only')
608 if options.libdir and options.libdir[0] != '/':
609 parser.error('--libdir accepts absolute paths only')
610
611 if options.skip_non_elfs and options.copy_non_elfs:
612 parser.error('pick one handler for non-ELFs: skip or copy')
613
614 if options.debug:
615 print('root =', options.root)
616 if options.dest:
617 print('dest =', options.dest)
618 if not paths:
619 err('missing ELF files to scan')
620
621 ldpaths = LoadLdpaths(options.root, options.prefix)
622 if options.debug:
623 print('ldpaths[conf] =', ldpaths['conf'])
624 print('ldpaths[env] =', ldpaths['env'])
625
626 # Process all the files specified.
627 ret = 0
628 for path in paths:
629 # Only auto-prefix the path if the ELF is absolute.
630 # If it's a relative path, the user most likely wants
631 # the local path.
632 if options.auto_root and path.startswith('/'):
633 path = options.root + path.lstrip('/')
634
635 matched = False
636 for p in glob.iglob(path):
637 matched = True
638 try:
639 elf = ParseELF(p, options.root, options.prefix, ldpaths)
640 except exceptions.ELFError as e:
641 if options.skip_non_elfs:
642 continue
643 # XXX: Ugly. Should unify with _Action* somehow.
644 if options.dest is not None and options.copy_non_elfs:
645 if os.path.exists(p):
646 elf = {
647 'interp': None,
648 'libs': [],
649 'runpath': [],
650 'rpath': [],
651 'path': p,
652 }
653 _ActionCopy(options, elf)
654 continue
655 ret = 1
656 warn('%s: %s' % (p, e))
657 continue
658 except IOError as e:
659 ret = 1
660 warn('%s: %s' % (p, e))
661 continue
662
663 if options.dest is None:
664 _ActionShow(options, elf)
665 else:
666 _ActionCopy(options, elf)
667
668 if not matched:
669 ret = 1
670 warn('%s: did not match any paths' % (path,))
671
672 return ret
673
674
675 if __name__ == '__main__':
676 sys.exit(main(sys.argv[1:]))

  ViewVC Help
Powered by ViewVC 1.1.20