aboutsummaryrefslogtreecommitdiff
blob: 447614ea7dbbe0259c1a1c69932a12309276a4c8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#!/usr/bin/env python

import os, sys
import re, glob
from optparse import OptionParser

from elftools import __version__
from elftools.common.exceptions import ELFError
from elftools.common.py3compat import bytes2str
from elftools.elf.elffile import ELFFile
from elftools.elf.dynamic import DynamicSection
from elftools.elf.descriptions import describe_ei_class

class ReadElf(object):
    def __init__(self, file):
        """ file: stream object with the ELF file to read
        """
        self.elffile = ELFFile(file)


    def elf_class(self):
        """ Return the ELF Class
        """
        header = self.elffile.header
        e_ident = header['e_ident']
        return describe_ei_class(e_ident['EI_CLASS'])

    def dynamic_dt_needed(self):
        """ Return a list of the DT_NEEDED
        """
        dt_needed = []
        for section in self.elffile.iter_sections():
            if not isinstance(section, DynamicSection):
                continue

            for tag in section.iter_tags():
                if tag.entry.d_tag == 'DT_NEEDED':
                    dt_needed.append(bytes2str(tag.needed))
                    #sys.stdout.write('\t%s\n' % bytes2str(tag.needed) )

        return dt_needed


def ldpaths(ld_so_conf='/etc/ld.so.conf'):
    """ Generate paths to search for libraries from ld.so.conf.  Recursively
        parse included files.  We assume correct syntax and the ld.so.cache
        is in sync with ld.so.conf.
    """
    with open(ld_so_conf, 'r') as path_file:
        lines = path_file.read()
    lines = re.sub('#.*', '', lines)                   # kill comments
    lines = list(re.split(':+|\s+|\t+|\n+|,+', lines)) # man 8 ldconfig

    paths = []
    include_globs = []
    for i in range(0,len(lines)):
        if lines[i] == '':
            continue
        if lines[i] == 'include':
            f = lines[i + 1]
            include_globs.append(f)
            continue
        if lines[i] not in include_globs:
            real_path = os.path.realpath(lines[i])
            if os.path.exists(real_path):
                paths.append(real_path)

    include_files = []
    for g in include_globs:
        include_files = include_files + glob.glob('/etc/' + g)
    for c in include_files:
        paths = paths + ldpaths(os.path.realpath(c))

    paths = list(set(paths))
    paths.sort()
    return paths


# We cache the dependencies for speed.  The structure is
# { ELFClass : { SONAME : library, ... }, ELFClass : ... }
cache = {}

def dynamic_dt_needed_paths( dt_needed, eclass, paths):
    """ Search library paths for the library file corresponding
        to the given DT_NEEDED and ELF Class.
    """
    global cache
    if not eclass in cache:
        cache[eclass] = {}

    dt_needed_paths = {}
    for n in dt_needed:
        if n in cache[eclass].keys():
           dt_needed_paths[n] = cache[eclass][n]
        else:
            for p in paths:
                lib = p + os.sep + n
                if os.path.exists(lib):
                    with open(lib, 'rb') as file:
                        try:
                            readlib = ReadElf(file)
                            if eclass == readlib.elf_class():
                                dt_needed_paths[n] = lib
                                cache[eclass][n] = lib
                        except ELFError as ex:
                            sys.stderr.write('ELF error: %s\n' % ex)
                            sys.exit(1)

    return dt_needed_paths


def all_dynamic_dt_needed_paths(f, paths):
    """ Return a dictionary of all the DT_NEEDED => Library Paths for
        a given ELF file obtained by recursively following linkage.
    """
    with open(f, 'rb') as file:
        try:
            readelf = ReadElf(file)
            eclass = readelf.elf_class()
            # This needs to be iterated until we traverse the entire linkage tree
            dt_needed = readelf.dynamic_dt_needed()
            dt_needed_paths = dynamic_dt_needed_paths(dt_needed, eclass, paths)
            for n, lib in dt_needed_paths.items():
                dt_needed_paths = dict(all_dynamic_dt_needed_paths(lib, paths), **dt_needed_paths)
        except ELFError as ex:
            sys.stderr.write('ELF error: %s\n' % ex)
            sys.exit(1)

    return dt_needed_paths


SCRIPT_DESCRIPTION = 'Print shared library dependencies'
VERSION_STRING = '%%prog: based on pyelftools %s' % __version__

def main():
    optparser = OptionParser(
        usage='usage: %prog <elf-file>',
        description=SCRIPT_DESCRIPTION,
        add_help_option=False, # -h is a real option of readelf
        prog='ldd.py',
        version=VERSION_STRING)
    optparser.add_option('-h', '--help',
        action='store_true', dest='help',
        help='Display this information')
    options, args = optparser.parse_args()

    if options.help or len(args) == 0:
        optparser.print_help()
        sys.exit(0)

    paths = ldpaths()

    for f in args:
        if len(args) > 1:
            sys.stdout.write('%s : \n' % f)
        all_dt_needed_paths = all_dynamic_dt_needed_paths(f, paths)
        for n, lib in all_dt_needed_paths.items():
            sys.stdout.write('\t%s => %s\n' % (n, lib))

if __name__ == '__main__':
    main()