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