You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
566 lines
21 KiB
566 lines
21 KiB
#!/usr/bin/env python
|
|
from __future__ import print_function
|
|
|
|
import cmd
|
|
import optparse
|
|
import os
|
|
import shlex
|
|
import struct
|
|
import sys
|
|
|
|
ARMAG = "!<arch>\n"
|
|
SARMAG = 8
|
|
ARFMAG = "`\n"
|
|
AR_EFMT1 = "#1/"
|
|
|
|
|
|
def memdump(src, bytes_per_line=16, address=0):
|
|
FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.'
|
|
for x in range(256)])
|
|
for i in range(0, len(src), bytes_per_line):
|
|
s = src[i:i+bytes_per_line]
|
|
hex_bytes = ' '.join(["%02x" % (ord(x)) for x in s])
|
|
ascii = s.translate(FILTER)
|
|
print("%#08.8x: %-*s %s" % (address+i, bytes_per_line*3, hex_bytes,
|
|
ascii))
|
|
|
|
|
|
class Object(object):
|
|
def __init__(self, file):
|
|
def read_str(file, str_len):
|
|
return file.read(str_len).rstrip('\0 ')
|
|
|
|
def read_int(file, str_len, base):
|
|
return int(read_str(file, str_len), base)
|
|
|
|
self.offset = file.tell()
|
|
self.file = file
|
|
self.name = read_str(file, 16)
|
|
self.date = read_int(file, 12, 10)
|
|
self.uid = read_int(file, 6, 10)
|
|
self.gid = read_int(file, 6, 10)
|
|
self.mode = read_int(file, 8, 8)
|
|
self.size = read_int(file, 10, 10)
|
|
if file.read(2) != ARFMAG:
|
|
raise ValueError('invalid BSD object at offset %#08.8x' % (
|
|
self.offset))
|
|
# If we have an extended name read it. Extended names start with
|
|
name_len = 0
|
|
if self.name.startswith(AR_EFMT1):
|
|
name_len = int(self.name[len(AR_EFMT1):], 10)
|
|
self.name = read_str(file, name_len)
|
|
self.obj_offset = file.tell()
|
|
self.obj_size = self.size - name_len
|
|
file.seek(self.obj_size, 1)
|
|
|
|
def dump(self, f=sys.stdout, flat=True):
|
|
if flat:
|
|
f.write('%#08.8x: %#08.8x %5u %5u %6o %#08.8x %s\n' % (self.offset,
|
|
self.date, self.uid, self.gid, self.mode, self.size,
|
|
self.name))
|
|
else:
|
|
f.write('%#08.8x: \n' % self.offset)
|
|
f.write(' name = "%s"\n' % self.name)
|
|
f.write(' date = %#08.8x\n' % self.date)
|
|
f.write(' uid = %i\n' % self.uid)
|
|
f.write(' gid = %i\n' % self.gid)
|
|
f.write(' mode = %o\n' % self.mode)
|
|
f.write(' size = %#08.8x\n' % (self.size))
|
|
self.file.seek(self.obj_offset, 0)
|
|
first_bytes = self.file.read(4)
|
|
f.write('bytes = ')
|
|
memdump(first_bytes)
|
|
|
|
def get_bytes(self):
|
|
saved_pos = self.file.tell()
|
|
self.file.seek(self.obj_offset, 0)
|
|
bytes = self.file.read(self.obj_size)
|
|
self.file.seek(saved_pos, 0)
|
|
return bytes
|
|
|
|
def save(self, path=None, overwrite=False):
|
|
'''
|
|
Save the contents of the object to disk using 'path' argument as
|
|
the path, or save it to the current working directory using the
|
|
object name.
|
|
'''
|
|
|
|
if path is None:
|
|
path = self.name
|
|
if not overwrite and os.path.exists(path):
|
|
print('error: outfile "%s" already exists' % (path))
|
|
return
|
|
print('Saving "%s" to "%s"...' % (self.name, path))
|
|
with open(path, 'w') as f:
|
|
f.write(self.get_bytes())
|
|
|
|
|
|
class StringTable(object):
|
|
def __init__(self, bytes):
|
|
self.bytes = bytes
|
|
|
|
def get_string(self, offset):
|
|
length = len(self.bytes)
|
|
if offset >= length:
|
|
return None
|
|
return self.bytes[offset:self.bytes.find('\0', offset)]
|
|
|
|
|
|
class Archive(object):
|
|
def __init__(self, path):
|
|
self.path = path
|
|
self.file = open(path, 'r')
|
|
self.objects = []
|
|
self.offset_to_object = {}
|
|
if self.file.read(SARMAG) != ARMAG:
|
|
print("error: file isn't a BSD archive")
|
|
while True:
|
|
try:
|
|
self.objects.append(Object(self.file))
|
|
except ValueError:
|
|
break
|
|
|
|
def get_object_at_offset(self, offset):
|
|
if offset in self.offset_to_object:
|
|
return self.offset_to_object[offset]
|
|
for obj in self.objects:
|
|
if obj.offset == offset:
|
|
self.offset_to_object[offset] = obj
|
|
return obj
|
|
return None
|
|
|
|
def find(self, name, mtime=None, f=sys.stdout):
|
|
'''
|
|
Find an object(s) by name with optional modification time. There
|
|
can be multple objects with the same name inside and possibly with
|
|
the same modification time within a BSD archive so clients must be
|
|
prepared to get multiple results.
|
|
'''
|
|
matches = []
|
|
for obj in self.objects:
|
|
if obj.name == name and (mtime is None or mtime == obj.date):
|
|
matches.append(obj)
|
|
return matches
|
|
|
|
@classmethod
|
|
def dump_header(self, f=sys.stdout):
|
|
f.write(' DATE UID GID MODE SIZE NAME\n')
|
|
f.write(' ---------- ----- ----- ------ ---------- '
|
|
'--------------\n')
|
|
|
|
def get_symdef(self):
|
|
def get_uint32(file):
|
|
'''Extract a uint32_t from the current file position.'''
|
|
v, = struct.unpack('=I', file.read(4))
|
|
return v
|
|
|
|
for obj in self.objects:
|
|
symdef = []
|
|
if obj.name.startswith("__.SYMDEF"):
|
|
self.file.seek(obj.obj_offset, 0)
|
|
ranlib_byte_size = get_uint32(self.file)
|
|
num_ranlib_structs = ranlib_byte_size/8
|
|
str_offset_pairs = []
|
|
for _ in range(num_ranlib_structs):
|
|
strx = get_uint32(self.file)
|
|
offset = get_uint32(self.file)
|
|
str_offset_pairs.append((strx, offset))
|
|
strtab_len = get_uint32(self.file)
|
|
strtab = StringTable(self.file.read(strtab_len))
|
|
for s in str_offset_pairs:
|
|
symdef.append((strtab.get_string(s[0]), s[1]))
|
|
return symdef
|
|
|
|
def get_object_dicts(self):
|
|
'''
|
|
Returns an array of object dictionaries that contain they following
|
|
keys:
|
|
'object': the actual bsd.Object instance
|
|
'symdefs': an array of symbol names that the object contains
|
|
as found in the "__.SYMDEF" item in the archive
|
|
'''
|
|
symdefs = self.get_symdef()
|
|
symdef_dict = {}
|
|
if symdefs:
|
|
for (name, offset) in symdefs:
|
|
if offset in symdef_dict:
|
|
object_dict = symdef_dict[offset]
|
|
else:
|
|
object_dict = {
|
|
'object': self.get_object_at_offset(offset),
|
|
'symdefs': []
|
|
}
|
|
symdef_dict[offset] = object_dict
|
|
object_dict['symdefs'].append(name)
|
|
object_dicts = []
|
|
for offset in sorted(symdef_dict):
|
|
object_dicts.append(symdef_dict[offset])
|
|
return object_dicts
|
|
|
|
def dump(self, f=sys.stdout, flat=True):
|
|
f.write('%s:\n' % self.path)
|
|
if flat:
|
|
self.dump_header(f=f)
|
|
for obj in self.objects:
|
|
obj.dump(f=f, flat=flat)
|
|
|
|
class Interactive(cmd.Cmd):
|
|
'''Interactive prompt for exploring contents of BSD archive files, type
|
|
"help" to see a list of supported commands.'''
|
|
image_option_parser = None
|
|
|
|
def __init__(self, archives):
|
|
cmd.Cmd.__init__(self)
|
|
self.use_rawinput = False
|
|
self.intro = ('Interactive BSD archive prompt, type "help" to see a '
|
|
'list of supported commands.')
|
|
self.archives = archives
|
|
self.prompt = '% '
|
|
|
|
def default(self, line):
|
|
'''Catch all for unknown command, which will exit the interpreter.'''
|
|
print("unknown command: %s" % line)
|
|
return True
|
|
|
|
def do_q(self, line):
|
|
'''Quit command'''
|
|
return True
|
|
|
|
def do_quit(self, line):
|
|
'''Quit command'''
|
|
return True
|
|
|
|
def do_extract(self, line):
|
|
args = shlex.split(line)
|
|
if args:
|
|
extracted = False
|
|
for object_name in args:
|
|
for archive in self.archives:
|
|
matches = archive.find(object_name)
|
|
if matches:
|
|
for object in matches:
|
|
object.save(overwrite=False)
|
|
extracted = True
|
|
if not extracted:
|
|
print('error: no object matches "%s" in any archives' % (
|
|
object_name))
|
|
else:
|
|
print('error: must specify the name of an object to extract')
|
|
|
|
def do_ls(self, line):
|
|
args = shlex.split(line)
|
|
if args:
|
|
for object_name in args:
|
|
for archive in self.archives:
|
|
matches = archive.find(object_name)
|
|
if matches:
|
|
for object in matches:
|
|
object.dump(flat=False)
|
|
else:
|
|
print('error: no object matches "%s" in "%s"' % (
|
|
object_name, archive.path))
|
|
else:
|
|
for archive in self.archives:
|
|
archive.dump(flat=True)
|
|
print('')
|
|
|
|
|
|
|
|
def main():
|
|
parser = optparse.OptionParser(
|
|
prog='bsd',
|
|
description='Utility for BSD archives')
|
|
parser.add_option(
|
|
'--object',
|
|
type='string',
|
|
dest='object_name',
|
|
default=None,
|
|
help=('Specify the name of a object within the BSD archive to get '
|
|
'information on'))
|
|
parser.add_option(
|
|
'-s', '--symbol',
|
|
type='string',
|
|
dest='find_symbol',
|
|
default=None,
|
|
help=('Specify the name of a symbol within the BSD archive to get '
|
|
'information on from SYMDEF'))
|
|
parser.add_option(
|
|
'--symdef',
|
|
action='store_true',
|
|
dest='symdef',
|
|
default=False,
|
|
help=('Dump the information in the SYMDEF.'))
|
|
parser.add_option(
|
|
'-v', '--verbose',
|
|
action='store_true',
|
|
dest='verbose',
|
|
default=False,
|
|
help='Enable verbose output')
|
|
parser.add_option(
|
|
'-e', '--extract',
|
|
action='store_true',
|
|
dest='extract',
|
|
default=False,
|
|
help=('Specify this to extract the object specified with the --object '
|
|
'option. There must be only one object with a matching name or '
|
|
'the --mtime option must be specified to uniquely identify a '
|
|
'single object.'))
|
|
parser.add_option(
|
|
'-m', '--mtime',
|
|
type='int',
|
|
dest='mtime',
|
|
default=None,
|
|
help=('Specify the modification time of the object an object. This '
|
|
'option is used with either the --object or --extract options.'))
|
|
parser.add_option(
|
|
'-o', '--outfile',
|
|
type='string',
|
|
dest='outfile',
|
|
default=None,
|
|
help=('Specify a different name or path for the file to extract when '
|
|
'using the --extract option. If this option isn\'t specified, '
|
|
'then the extracted object file will be extracted into the '
|
|
'current working directory if a file doesn\'t already exist '
|
|
'with that name.'))
|
|
parser.add_option(
|
|
'-i', '--interactive',
|
|
action='store_true',
|
|
dest='interactive',
|
|
default=False,
|
|
help=('Enter an interactive shell that allows users to interactively '
|
|
'explore contents of .a files.'))
|
|
|
|
(options, args) = parser.parse_args(sys.argv[1:])
|
|
|
|
if options.interactive:
|
|
archives = []
|
|
for path in args:
|
|
archives.append(Archive(path))
|
|
interpreter = Interactive(archives)
|
|
interpreter.cmdloop()
|
|
return
|
|
|
|
for path in args:
|
|
archive = Archive(path)
|
|
if options.object_name:
|
|
print('%s:\n' % (path))
|
|
matches = archive.find(options.object_name, options.mtime)
|
|
if matches:
|
|
dump_all = True
|
|
if options.extract:
|
|
if len(matches) == 1:
|
|
dump_all = False
|
|
matches[0].save(path=options.outfile, overwrite=False)
|
|
else:
|
|
print('error: multiple objects match "%s". Specify '
|
|
'the modification time using --mtime.' % (
|
|
options.object_name))
|
|
if dump_all:
|
|
for obj in matches:
|
|
obj.dump(flat=False)
|
|
else:
|
|
print('error: object "%s" not found in archive' % (
|
|
options.object_name))
|
|
elif options.find_symbol:
|
|
symdefs = archive.get_symdef()
|
|
if symdefs:
|
|
success = False
|
|
for (name, offset) in symdefs:
|
|
obj = archive.get_object_at_offset(offset)
|
|
if name == options.find_symbol:
|
|
print('Found "%s" in:' % (options.find_symbol))
|
|
obj.dump(flat=False)
|
|
success = True
|
|
if not success:
|
|
print('Didn\'t find "%s" in any objects' % (
|
|
options.find_symbol))
|
|
else:
|
|
print("error: no __.SYMDEF was found")
|
|
elif options.symdef:
|
|
object_dicts = archive.get_object_dicts()
|
|
for object_dict in object_dicts:
|
|
object_dict['object'].dump(flat=False)
|
|
print("symbols:")
|
|
for name in object_dict['symdefs']:
|
|
print(" %s" % (name))
|
|
else:
|
|
archive.dump(flat=not options.verbose)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|
|
|
|
def print_mtime_error(result, dmap_mtime, actual_mtime):
|
|
print("error: modification time in debug map (%#08.8x) doesn't "
|
|
"match the .o file modification time (%#08.8x)" % (
|
|
dmap_mtime, actual_mtime), file=result)
|
|
|
|
|
|
def print_file_missing_error(result, path):
|
|
print("error: file \"%s\" doesn't exist" % (path), file=result)
|
|
|
|
|
|
def print_multiple_object_matches(result, object_name, mtime, matches):
|
|
print("error: multiple matches for object '%s' with with "
|
|
"modification time %#08.8x:" % (object_name, mtime), file=result)
|
|
Archive.dump_header(f=result)
|
|
for match in matches:
|
|
match.dump(f=result, flat=True)
|
|
|
|
|
|
def print_archive_object_error(result, object_name, mtime, archive):
|
|
matches = archive.find(object_name, f=result)
|
|
if len(matches) > 0:
|
|
print("error: no objects have a modification time that "
|
|
"matches %#08.8x for '%s'. Potential matches:" % (
|
|
mtime, object_name), file=result)
|
|
Archive.dump_header(f=result)
|
|
for match in matches:
|
|
match.dump(f=result, flat=True)
|
|
else:
|
|
print("error: no object named \"%s\" found in archive:" % (
|
|
object_name), file=result)
|
|
Archive.dump_header(f=result)
|
|
for match in archive.objects:
|
|
match.dump(f=result, flat=True)
|
|
# archive.dump(f=result, flat=True)
|
|
|
|
|
|
class VerifyDebugMapCommand:
|
|
name = "verify-debug-map-objects"
|
|
|
|
def create_options(self):
|
|
usage = "usage: %prog [options]"
|
|
description = '''This command reports any .o files that are missing
|
|
or whose modification times don't match in the debug map of an executable.'''
|
|
|
|
self.parser = optparse.OptionParser(
|
|
description=description,
|
|
prog=self.name,
|
|
usage=usage,
|
|
add_help_option=False)
|
|
|
|
self.parser.add_option(
|
|
'-e', '--errors',
|
|
action='store_true',
|
|
dest='errors',
|
|
default=False,
|
|
help="Only show errors")
|
|
|
|
def get_short_help(self):
|
|
return "Verify debug map object files."
|
|
|
|
def get_long_help(self):
|
|
return self.help_string
|
|
|
|
def __init__(self, debugger, unused):
|
|
self.create_options()
|
|
self.help_string = self.parser.format_help()
|
|
|
|
def __call__(self, debugger, command, exe_ctx, result):
|
|
import lldb
|
|
# Use the Shell Lexer to properly parse up command options just like a
|
|
# shell would
|
|
command_args = shlex.split(command)
|
|
|
|
try:
|
|
(options, args) = self.parser.parse_args(command_args)
|
|
except:
|
|
result.SetError("option parsing failed")
|
|
return
|
|
|
|
# Always get program state from the SBExecutionContext passed in
|
|
target = exe_ctx.GetTarget()
|
|
if not target.IsValid():
|
|
result.SetError("invalid target")
|
|
return
|
|
archives = {}
|
|
for module_spec in args:
|
|
module = target.module[module_spec]
|
|
if not (module and module.IsValid()):
|
|
result.SetError('error: invalid module specification: "%s". '
|
|
'Specify the full path, basename, or UUID of '
|
|
'a module ' % (module_spec))
|
|
return
|
|
num_symbols = module.GetNumSymbols()
|
|
num_errors = 0
|
|
for i in range(num_symbols):
|
|
symbol = module.GetSymbolAtIndex(i)
|
|
if symbol.GetType() != lldb.eSymbolTypeObjectFile:
|
|
continue
|
|
path = symbol.GetName()
|
|
if not path:
|
|
continue
|
|
# Extract the value of the symbol by dumping the
|
|
# symbol. The value is the mod time.
|
|
dmap_mtime = int(str(symbol).split('value = ')
|
|
[1].split(',')[0], 16)
|
|
if not options.errors:
|
|
print('%s' % (path), file=result)
|
|
if os.path.exists(path):
|
|
actual_mtime = int(os.stat(path).st_mtime)
|
|
if dmap_mtime != actual_mtime:
|
|
num_errors += 1
|
|
if options.errors:
|
|
print('%s' % (path), end=' ', file=result)
|
|
print_mtime_error(result, dmap_mtime,
|
|
actual_mtime)
|
|
elif path[-1] == ')':
|
|
(archive_path, object_name) = path[0:-1].split('(')
|
|
if not archive_path and not object_name:
|
|
num_errors += 1
|
|
if options.errors:
|
|
print('%s' % (path), end=' ', file=result)
|
|
print_file_missing_error(path)
|
|
continue
|
|
if not os.path.exists(archive_path):
|
|
num_errors += 1
|
|
if options.errors:
|
|
print('%s' % (path), end=' ', file=result)
|
|
print_file_missing_error(archive_path)
|
|
continue
|
|
if archive_path in archives:
|
|
archive = archives[archive_path]
|
|
else:
|
|
archive = Archive(archive_path)
|
|
archives[archive_path] = archive
|
|
matches = archive.find(object_name, dmap_mtime)
|
|
num_matches = len(matches)
|
|
if num_matches == 1:
|
|
print('1 match', file=result)
|
|
obj = matches[0]
|
|
if obj.date != dmap_mtime:
|
|
num_errors += 1
|
|
if options.errors:
|
|
print('%s' % (path), end=' ', file=result)
|
|
print_mtime_error(result, dmap_mtime, obj.date)
|
|
elif num_matches == 0:
|
|
num_errors += 1
|
|
if options.errors:
|
|
print('%s' % (path), end=' ', file=result)
|
|
print_archive_object_error(result, object_name,
|
|
dmap_mtime, archive)
|
|
elif num_matches > 1:
|
|
num_errors += 1
|
|
if options.errors:
|
|
print('%s' % (path), end=' ', file=result)
|
|
print_multiple_object_matches(result,
|
|
object_name,
|
|
dmap_mtime, matches)
|
|
if num_errors > 0:
|
|
print("%u errors found" % (num_errors), file=result)
|
|
else:
|
|
print("No errors detected in debug map", file=result)
|
|
|
|
|
|
def __lldb_init_module(debugger, dict):
|
|
# This initializer is being run from LLDB in the embedded command
|
|
# interpreter.
|
|
# Add any commands contained in this module to LLDB
|
|
debugger.HandleCommand(
|
|
'command script add -c %s.VerifyDebugMapCommand %s' % (
|
|
__name__, VerifyDebugMapCommand.name))
|
|
print('The "%s" command has been installed, type "help %s" for detailed '
|
|
'help.' % (VerifyDebugMapCommand.name, VerifyDebugMapCommand.name))
|