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

Contents of /pax-utils/lddtree.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.16 - (show annotations) (download) (as text)
Mon Nov 26 20:06:54 2012 UTC (20 months, 3 weeks ago) by vapier
Branch: MAIN
Changes since 1.15: +10 -19 lines
File MIME type: text/x-python
lddtree.py: even simpler CompatibleELFs by David James

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.15 2012/11/24 19:44:03 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 ret = {
228 'interp': None,
229 'path': path,
230 'needed': [],
231 'libs': _all_libs,
232 }
233
234 with open(path) as f:
235 elf = ELFFile(f)
236
237 # If this is the first ELF, extract the interpreter.
238 if _first:
239 for segment in elf.iter_segments():
240 if segment.header.p_type != 'PT_INTERP':
241 continue
242
243 interp = segment.get_interp_name()
244 ret['interp'] = normpath(root + interp)
245 ret['libs'][os.path.basename(interp)] = {
246 'path': ret['interp'],
247 'needed': [],
248 }
249 # XXX: Should read it and scan for /lib paths.
250 ldpaths['interp'] = [
251 normpath(root + os.path.dirname(interp)),
252 normpath(root + '/usr' + os.path.dirname(interp)),
253 ]
254 break
255
256 # Parse the ELF's dynamic tags.
257 libs = []
258 rpaths = []
259 runpaths = []
260 for segment in elf.iter_segments():
261 if segment.header.p_type != 'PT_DYNAMIC':
262 continue
263
264 for t in segment.iter_tags():
265 if t.entry.d_tag == 'DT_RPATH':
266 rpaths = ParseLdPaths(t.rpath, root=root, path=path)
267 elif t.entry.d_tag == 'DT_RUNPATH':
268 runpaths = ParseLdPaths(t.runpath, root=root, path=path)
269 elif t.entry.d_tag == 'DT_NEEDED':
270 libs.append(t.needed)
271 if runpaths:
272 # If both RPATH and RUNPATH are set, only the latter is used.
273 rpath = []
274
275 break
276 ret['needed'] = libs
277
278 # Search for the libs this ELF uses.
279 all_ldpaths = None
280 for lib in libs:
281 if lib in _all_libs:
282 continue
283 if all_ldpaths is None:
284 all_ldpaths = rpaths + ldpaths['env'] + runpaths + ldpaths['conf'] + ldpaths['interp']
285 fullpath = FindLib(elf, lib, all_ldpaths)
286 _all_libs[lib] = {
287 'path': fullpath,
288 'needed': [],
289 }
290 if fullpath:
291 lret = ParseELF(fullpath, root, ldpaths, False, _all_libs)
292 _all_libs[lib]['needed'] = lret['needed']
293
294 del elf
295
296 return ret
297
298
299 def _NormalizePath(option, _opt, value, parser):
300 setattr(parser.values, option.dest, normpath(value))
301
302
303 def _ShowVersion(_option, _opt, _value, _parser):
304 id = '$Id: lddtree.py,v 1.15 2012/11/24 19:44:03 vapier Exp $'.split()
305 print('%s-%s %s %s' % (id[1].split('.')[0], id[2], id[3], id[4]))
306 sys.exit(0)
307
308
309 def _ActionShow(options, elf):
310 """Show the dependency tree for this ELF"""
311 def _show(lib, depth):
312 chain_libs.append(lib)
313 fullpath = elf['libs'][lib]['path']
314 if options.list:
315 print(fullpath or lib)
316 else:
317 print('%s%s => %s' % (' ' * depth, lib, fullpath))
318
319 new_libs = []
320 for lib in elf['libs'][lib]['needed']:
321 if lib in chain_libs:
322 if not options.list:
323 print('%s%s => !!! circular loop !!!' % (' ' * depth, lib))
324 continue
325 if options.all or not lib in shown_libs:
326 shown_libs.add(lib)
327 new_libs.append(lib)
328
329 for lib in new_libs:
330 _show(lib, depth + 1)
331 chain_libs.pop()
332
333 shown_libs = set(elf['needed'])
334 chain_libs = []
335 interp = elf['interp']
336 if interp:
337 shown_libs.add(os.path.basename(interp))
338 if options.list:
339 print(elf['path'])
340 if not interp is None:
341 print(interp)
342 else:
343 print('%s (interpreter => %s)' % (elf['path'], interp))
344 for lib in elf['needed']:
345 _show(lib, 1)
346
347
348 def _ActionCopy(options, elf):
349 """Copy the ELF and its dependencies to a destination tree"""
350 def _copy(src):
351 if src is None:
352 return
353
354 dst = options.dest + src
355 if os.path.exists(dst):
356 # See if they're the same file.
357 ostat = os.stat(src)
358 nstat = os.stat(dst)
359 for field in ('mode', 'mtime', 'size'):
360 if getattr(ostat, 'st_' + field) != \
361 getattr(nstat, 'st_' + field):
362 break
363 else:
364 return
365
366 if options.verbose:
367 print('%s -> %s' % (src, dst))
368
369 try:
370 os.makedirs(os.path.dirname(dst))
371 except OSError as e:
372 if e.errno != os.errno.EEXIST:
373 raise
374 try:
375 shutil.copy2(src, dst)
376 return
377 except IOError:
378 os.unlink(dst)
379 shutil.copy2(src, dst)
380
381 _copy(elf['path'])
382 _copy(elf['interp'])
383 for lib in elf['libs']:
384 _copy(elf['libs'][lib]['path'])
385
386
387 def main(argv):
388 parser = optparse.OptionParser("""%prog [options] <ELFs>
389
390 Display ELF dependencies as a tree""")
391 parser.add_option('-a', '--all',
392 action='store_true', default=False,
393 help=('Show all duplicated dependencies'))
394 parser.add_option('-R', '--root',
395 dest='root', default=os.environ.get('ROOT', ''), type='string',
396 action='callback', callback=_NormalizePath,
397 help=('Show all duplicated dependencies'))
398 parser.add_option('--copy-to-tree',
399 dest='dest', default=None, type='string',
400 action='callback', callback=_NormalizePath,
401 help=('Copy all files to the specified tree'))
402 parser.add_option('-l', '--list',
403 action='store_true', default=False,
404 help=('Display output in a simple list (easy for copying)'))
405 parser.add_option('-x', '--debug',
406 action='store_true', default=False,
407 help=('Run with debugging'))
408 parser.add_option('-v', '--verbose',
409 action='store_true', default=False,
410 help=('Be verbose'))
411 parser.add_option('-V', '--version',
412 action='callback', callback=_ShowVersion,
413 help=('Show version information'))
414 (options, paths) = parser.parse_args(argv)
415
416 # Throw away argv[0].
417 paths.pop(0)
418 if options.root != '/':
419 options.root += '/'
420
421 if options.debug:
422 print('root =', options.root)
423 if options.dest:
424 print('dest =', options.dest)
425 if not paths:
426 err('missing ELF files to scan')
427
428 ldpaths = LoadLdpaths(options.root)
429 if options.debug:
430 print('ldpaths[conf] =', ldpaths['conf'])
431 print('ldpaths[env] =', ldpaths['env'])
432
433 # Process all the files specified.
434 ret = 0
435 for path in paths:
436 try:
437 elf = ParseELF(path, options.root, ldpaths)
438 except (exceptions.ELFError, IOError) as e:
439 ret = 1
440 warn('%s: %s' % (path, e))
441 continue
442 if options.dest is None:
443 _ActionShow(options, elf)
444 else:
445 _ActionCopy(options, elf)
446 return ret
447
448
449 if __name__ == '__main__':
450 sys.exit(main(sys.argv))

  ViewVC Help
Powered by ViewVC 1.1.20