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

Contents of /pax-utils/lddtree.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.12 - (show annotations) (download) (as text)
Sat Nov 17 00:11:39 2012 UTC (2 years, 8 months ago) by vapier
Branch: MAIN
Changes since 1.11: +4 -4 lines
File MIME type: text/x-python
lddtree.py: fix indentation in previous commit

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

  ViewVC Help
Powered by ViewVC 1.1.20