1#!/usr/bin/env python
2from __future__ import print_function
3
4import cmd
5import optparse
6import os
7import shlex
8import struct
9import sys
10
11ARMAG = "!<arch>\n"
12SARMAG = 8
13ARFMAG = "`\n"
14AR_EFMT1 = "#1/"
15
16
17def memdump(src, bytes_per_line=16, address=0):
18    FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.'
19                     for x in range(256)])
20    for i in range(0, len(src), bytes_per_line):
21        s = src[i:i+bytes_per_line]
22        hex_bytes = ' '.join(["%02x" % (ord(x)) for x in s])
23        ascii = s.translate(FILTER)
24        print("%#08.8x: %-*s %s" % (address+i, bytes_per_line*3, hex_bytes,
25                                    ascii))
26
27
28class Object(object):
29    def __init__(self, file):
30        def read_str(file, str_len):
31            return file.read(str_len).rstrip('\0 ')
32
33        def read_int(file, str_len, base):
34            return int(read_str(file, str_len), base)
35
36        self.offset = file.tell()
37        self.file = file
38        self.name = read_str(file, 16)
39        self.date = read_int(file, 12, 10)
40        self.uid = read_int(file, 6, 10)
41        self.gid = read_int(file, 6, 10)
42        self.mode = read_int(file, 8, 8)
43        self.size = read_int(file, 10, 10)
44        if file.read(2) != ARFMAG:
45            raise ValueError('invalid BSD object at offset %#08.8x' % (
46                             self.offset))
47        # If we have an extended name read it. Extended names start with
48        name_len = 0
49        if self.name.startswith(AR_EFMT1):
50            name_len = int(self.name[len(AR_EFMT1):], 10)
51            self.name = read_str(file, name_len)
52        self.obj_offset = file.tell()
53        self.obj_size = self.size - name_len
54        file.seek(self.obj_size, 1)
55
56    def dump(self, f=sys.stdout, flat=True):
57        if flat:
58            f.write('%#08.8x: %#08.8x %5u %5u %6o %#08.8x %s\n' % (self.offset,
59                    self.date, self.uid, self.gid, self.mode, self.size,
60                    self.name))
61        else:
62            f.write('%#08.8x: \n' % self.offset)
63            f.write(' name = "%s"\n' % self.name)
64            f.write(' date = %#08.8x\n' % self.date)
65            f.write('  uid = %i\n' % self.uid)
66            f.write('  gid = %i\n' % self.gid)
67            f.write(' mode = %o\n' % self.mode)
68            f.write(' size = %#08.8x\n' % (self.size))
69            self.file.seek(self.obj_offset, 0)
70            first_bytes = self.file.read(4)
71            f.write('bytes = ')
72            memdump(first_bytes)
73
74    def get_bytes(self):
75        saved_pos = self.file.tell()
76        self.file.seek(self.obj_offset, 0)
77        bytes = self.file.read(self.obj_size)
78        self.file.seek(saved_pos, 0)
79        return bytes
80
81    def save(self, path=None, overwrite=False):
82        '''
83            Save the contents of the object to disk using 'path' argument as
84            the path, or save it to the current working directory using the
85            object name.
86        '''
87
88        if path is None:
89            path = self.name
90        if not overwrite and os.path.exists(path):
91            print('error: outfile "%s" already exists' % (path))
92            return
93        print('Saving "%s" to "%s"...' % (self.name, path))
94        with open(path, 'w') as f:
95            f.write(self.get_bytes())
96
97
98class StringTable(object):
99    def __init__(self, bytes):
100        self.bytes = bytes
101
102    def get_string(self, offset):
103        length = len(self.bytes)
104        if offset >= length:
105            return None
106        return self.bytes[offset:self.bytes.find('\0', offset)]
107
108
109class Archive(object):
110    def __init__(self, path):
111        self.path = path
112        self.file = open(path, 'r')
113        self.objects = []
114        self.offset_to_object = {}
115        if self.file.read(SARMAG) != ARMAG:
116            print("error: file isn't a BSD archive")
117        while True:
118            try:
119                self.objects.append(Object(self.file))
120            except ValueError:
121                break
122
123    def get_object_at_offset(self, offset):
124        if offset in self.offset_to_object:
125            return self.offset_to_object[offset]
126        for obj in self.objects:
127            if obj.offset == offset:
128                self.offset_to_object[offset] = obj
129                return obj
130        return None
131
132    def find(self, name, mtime=None, f=sys.stdout):
133        '''
134            Find an object(s) by name with optional modification time. There
135            can be multple objects with the same name inside and possibly with
136            the same modification time within a BSD archive so clients must be
137            prepared to get multiple results.
138        '''
139        matches = []
140        for obj in self.objects:
141            if obj.name == name and (mtime is None or mtime == obj.date):
142                matches.append(obj)
143        return matches
144
145    @classmethod
146    def dump_header(self, f=sys.stdout):
147        f.write('            DATE       UID   GID   MODE   SIZE       NAME\n')
148        f.write('            ---------- ----- ----- ------ ---------- '
149                '--------------\n')
150
151    def get_symdef(self):
152        def get_uint32(file):
153            '''Extract a uint32_t from the current file position.'''
154            v, = struct.unpack('=I', file.read(4))
155            return v
156
157        for obj in self.objects:
158            symdef = []
159            if obj.name.startswith("__.SYMDEF"):
160                self.file.seek(obj.obj_offset, 0)
161                ranlib_byte_size = get_uint32(self.file)
162                num_ranlib_structs = ranlib_byte_size/8
163                str_offset_pairs = []
164                for _ in range(num_ranlib_structs):
165                    strx = get_uint32(self.file)
166                    offset = get_uint32(self.file)
167                    str_offset_pairs.append((strx, offset))
168                strtab_len = get_uint32(self.file)
169                strtab = StringTable(self.file.read(strtab_len))
170                for s in str_offset_pairs:
171                    symdef.append((strtab.get_string(s[0]), s[1]))
172            return symdef
173
174    def get_object_dicts(self):
175        '''
176            Returns an array of object dictionaries that contain they following
177            keys:
178                'object': the actual bsd.Object instance
179                'symdefs': an array of symbol names that the object contains
180                           as found in the "__.SYMDEF" item in the archive
181        '''
182        symdefs = self.get_symdef()
183        symdef_dict = {}
184        if symdefs:
185            for (name, offset) in symdefs:
186                if offset in symdef_dict:
187                    object_dict = symdef_dict[offset]
188                else:
189                    object_dict = {
190                        'object': self.get_object_at_offset(offset),
191                        'symdefs': []
192                    }
193                    symdef_dict[offset] = object_dict
194                object_dict['symdefs'].append(name)
195        object_dicts = []
196        for offset in sorted(symdef_dict):
197            object_dicts.append(symdef_dict[offset])
198        return object_dicts
199
200    def dump(self, f=sys.stdout, flat=True):
201        f.write('%s:\n' % self.path)
202        if flat:
203            self.dump_header(f=f)
204        for obj in self.objects:
205            obj.dump(f=f, flat=flat)
206
207class Interactive(cmd.Cmd):
208    '''Interactive prompt for exploring contents of BSD archive files, type
209      "help" to see a list of supported commands.'''
210    image_option_parser = None
211
212    def __init__(self, archives):
213        cmd.Cmd.__init__(self)
214        self.use_rawinput = False
215        self.intro = ('Interactive  BSD archive prompt, type "help" to see a '
216                      'list of supported commands.')
217        self.archives = archives
218        self.prompt = '% '
219
220    def default(self, line):
221        '''Catch all for unknown command, which will exit the interpreter.'''
222        print("unknown command: %s" % line)
223        return True
224
225    def do_q(self, line):
226        '''Quit command'''
227        return True
228
229    def do_quit(self, line):
230        '''Quit command'''
231        return True
232
233    def do_extract(self, line):
234        args = shlex.split(line)
235        if args:
236            extracted = False
237            for object_name in args:
238                for archive in self.archives:
239                    matches = archive.find(object_name)
240                    if matches:
241                        for object in matches:
242                            object.save(overwrite=False)
243                            extracted = True
244            if not extracted:
245                print('error: no object matches "%s" in any archives' % (
246                        object_name))
247        else:
248            print('error: must specify the name of an object to extract')
249
250    def do_ls(self, line):
251        args = shlex.split(line)
252        if args:
253            for object_name in args:
254                for archive in self.archives:
255                    matches = archive.find(object_name)
256                    if matches:
257                        for object in matches:
258                            object.dump(flat=False)
259                    else:
260                        print('error: no object matches "%s" in "%s"' % (
261                                object_name, archive.path))
262        else:
263            for archive in self.archives:
264                archive.dump(flat=True)
265                print('')
266
267
268
269def main():
270    parser = optparse.OptionParser(
271        prog='bsd',
272        description='Utility for BSD archives')
273    parser.add_option(
274        '--object',
275        type='string',
276        dest='object_name',
277        default=None,
278        help=('Specify the name of a object within the BSD archive to get '
279              'information on'))
280    parser.add_option(
281        '-s', '--symbol',
282        type='string',
283        dest='find_symbol',
284        default=None,
285        help=('Specify the name of a symbol within the BSD archive to get '
286              'information on from SYMDEF'))
287    parser.add_option(
288        '--symdef',
289        action='store_true',
290        dest='symdef',
291        default=False,
292        help=('Dump the information in the SYMDEF.'))
293    parser.add_option(
294        '-v', '--verbose',
295        action='store_true',
296        dest='verbose',
297        default=False,
298        help='Enable verbose output')
299    parser.add_option(
300        '-e', '--extract',
301        action='store_true',
302        dest='extract',
303        default=False,
304        help=('Specify this to extract the object specified with the --object '
305              'option. There must be only one object with a matching name or '
306              'the --mtime option must be specified to uniquely identify a '
307              'single object.'))
308    parser.add_option(
309        '-m', '--mtime',
310        type='int',
311        dest='mtime',
312        default=None,
313        help=('Specify the modification time of the object an object. This '
314              'option is used with either the --object or --extract options.'))
315    parser.add_option(
316        '-o', '--outfile',
317        type='string',
318        dest='outfile',
319        default=None,
320        help=('Specify a different name or path for the file to extract when '
321              'using the --extract option. If this option isn\'t specified, '
322              'then the extracted object file will be extracted into the '
323              'current working directory if a file doesn\'t already exist '
324              'with that name.'))
325    parser.add_option(
326        '-i', '--interactive',
327        action='store_true',
328        dest='interactive',
329        default=False,
330        help=('Enter an interactive shell that allows users to interactively '
331              'explore contents of .a files.'))
332
333    (options, args) = parser.parse_args(sys.argv[1:])
334
335    if options.interactive:
336        archives = []
337        for path in args:
338            archives.append(Archive(path))
339        interpreter = Interactive(archives)
340        interpreter.cmdloop()
341        return
342
343    for path in args:
344        archive = Archive(path)
345        if options.object_name:
346            print('%s:\n' % (path))
347            matches = archive.find(options.object_name, options.mtime)
348            if matches:
349                dump_all = True
350                if options.extract:
351                    if len(matches) == 1:
352                        dump_all = False
353                        matches[0].save(path=options.outfile, overwrite=False)
354                    else:
355                        print('error: multiple objects match "%s". Specify '
356                              'the modification time using --mtime.' % (
357                                options.object_name))
358                if dump_all:
359                    for obj in matches:
360                        obj.dump(flat=False)
361            else:
362                print('error: object "%s" not found in archive' % (
363                      options.object_name))
364        elif options.find_symbol:
365            symdefs = archive.get_symdef()
366            if symdefs:
367                success = False
368                for (name, offset) in symdefs:
369                    obj = archive.get_object_at_offset(offset)
370                    if name == options.find_symbol:
371                        print('Found "%s" in:' % (options.find_symbol))
372                        obj.dump(flat=False)
373                        success = True
374                if not success:
375                    print('Didn\'t find "%s" in any objects' % (
376                          options.find_symbol))
377            else:
378                print("error: no __.SYMDEF was found")
379        elif options.symdef:
380            object_dicts = archive.get_object_dicts()
381            for object_dict in object_dicts:
382                object_dict['object'].dump(flat=False)
383                print("symbols:")
384                for name in object_dict['symdefs']:
385                    print("  %s" % (name))
386        else:
387            archive.dump(flat=not options.verbose)
388
389
390if __name__ == '__main__':
391    main()
392
393
394def print_mtime_error(result, dmap_mtime, actual_mtime):
395    print("error: modification time in debug map (%#08.8x) doesn't "
396                     "match the .o file modification time (%#08.8x)" % (
397                        dmap_mtime, actual_mtime), file=result)
398
399
400def print_file_missing_error(result, path):
401    print("error: file \"%s\" doesn't exist" % (path), file=result)
402
403
404def print_multiple_object_matches(result, object_name, mtime, matches):
405    print("error: multiple matches for object '%s' with with "
406                     "modification time %#08.8x:" % (object_name, mtime), file=result)
407    Archive.dump_header(f=result)
408    for match in matches:
409        match.dump(f=result, flat=True)
410
411
412def print_archive_object_error(result, object_name, mtime, archive):
413    matches = archive.find(object_name, f=result)
414    if len(matches) > 0:
415        print("error: no objects have a modification time that "
416                         "matches %#08.8x for '%s'. Potential matches:" % (
417                            mtime, object_name), file=result)
418        Archive.dump_header(f=result)
419        for match in matches:
420            match.dump(f=result, flat=True)
421    else:
422        print("error: no object named \"%s\" found in archive:" % (
423            object_name), file=result)
424        Archive.dump_header(f=result)
425        for match in archive.objects:
426            match.dump(f=result, flat=True)
427        # archive.dump(f=result, flat=True)
428
429
430class VerifyDebugMapCommand:
431    name = "verify-debug-map-objects"
432
433    def create_options(self):
434        usage = "usage: %prog [options]"
435        description = '''This command reports any .o files that are missing
436or whose modification times don't match in the debug map of an executable.'''
437
438        self.parser = optparse.OptionParser(
439            description=description,
440            prog=self.name,
441            usage=usage,
442            add_help_option=False)
443
444        self.parser.add_option(
445            '-e', '--errors',
446            action='store_true',
447            dest='errors',
448            default=False,
449            help="Only show errors")
450
451    def get_short_help(self):
452        return "Verify debug map object files."
453
454    def get_long_help(self):
455        return self.help_string
456
457    def __init__(self, debugger, unused):
458        self.create_options()
459        self.help_string = self.parser.format_help()
460
461    def __call__(self, debugger, command, exe_ctx, result):
462        import lldb
463        # Use the Shell Lexer to properly parse up command options just like a
464        # shell would
465        command_args = shlex.split(command)
466
467        try:
468            (options, args) = self.parser.parse_args(command_args)
469        except:
470            result.SetError("option parsing failed")
471            return
472
473        # Always get program state from the SBExecutionContext passed in
474        target = exe_ctx.GetTarget()
475        if not target.IsValid():
476            result.SetError("invalid target")
477            return
478        archives = {}
479        for module_spec in args:
480            module = target.module[module_spec]
481            if not (module and module.IsValid()):
482                result.SetError('error: invalid module specification: "%s". '
483                                'Specify the full path, basename, or UUID of '
484                                'a module ' % (module_spec))
485                return
486            num_symbols = module.GetNumSymbols()
487            num_errors = 0
488            for i in range(num_symbols):
489                symbol = module.GetSymbolAtIndex(i)
490                if symbol.GetType() != lldb.eSymbolTypeObjectFile:
491                    continue
492                path = symbol.GetName()
493                if not path:
494                    continue
495                # Extract the value of the symbol by dumping the
496                # symbol. The value is the mod time.
497                dmap_mtime = int(str(symbol).split('value = ')
498                                 [1].split(',')[0], 16)
499                if not options.errors:
500                    print('%s' % (path), file=result)
501                if os.path.exists(path):
502                    actual_mtime = int(os.stat(path).st_mtime)
503                    if dmap_mtime != actual_mtime:
504                        num_errors += 1
505                        if options.errors:
506                            print('%s' % (path), end=' ', file=result)
507                        print_mtime_error(result, dmap_mtime,
508                                          actual_mtime)
509                elif path[-1] == ')':
510                    (archive_path, object_name) = path[0:-1].split('(')
511                    if not archive_path and not object_name:
512                        num_errors += 1
513                        if options.errors:
514                            print('%s' % (path), end=' ', file=result)
515                        print_file_missing_error(path)
516                        continue
517                    if not os.path.exists(archive_path):
518                        num_errors += 1
519                        if options.errors:
520                            print('%s' % (path), end=' ', file=result)
521                        print_file_missing_error(archive_path)
522                        continue
523                    if archive_path in archives:
524                        archive = archives[archive_path]
525                    else:
526                        archive = Archive(archive_path)
527                        archives[archive_path] = archive
528                    matches = archive.find(object_name, dmap_mtime)
529                    num_matches = len(matches)
530                    if num_matches == 1:
531                        print('1 match', file=result)
532                        obj = matches[0]
533                        if obj.date != dmap_mtime:
534                            num_errors += 1
535                            if options.errors:
536                                print('%s' % (path), end=' ', file=result)
537                            print_mtime_error(result, dmap_mtime, obj.date)
538                    elif num_matches == 0:
539                        num_errors += 1
540                        if options.errors:
541                            print('%s' % (path), end=' ', file=result)
542                        print_archive_object_error(result, object_name,
543                                                   dmap_mtime, archive)
544                    elif num_matches > 1:
545                        num_errors += 1
546                        if options.errors:
547                            print('%s' % (path), end=' ', file=result)
548                        print_multiple_object_matches(result,
549                                                      object_name,
550                                                      dmap_mtime, matches)
551            if num_errors > 0:
552                print("%u errors found" % (num_errors), file=result)
553            else:
554                print("No errors detected in debug map", file=result)
555
556
557def __lldb_init_module(debugger, dict):
558    # This initializer is being run from LLDB in the embedded command
559    # interpreter.
560    # Add any commands contained in this module to LLDB
561    debugger.HandleCommand(
562        'command script add -c %s.VerifyDebugMapCommand %s' % (
563            __name__, VerifyDebugMapCommand.name))
564    print('The "%s" command has been installed, type "help %s" for detailed '
565          'help.' % (VerifyDebugMapCommand.name, VerifyDebugMapCommand.name))
566