| #!/usr/bin/env python |
| # |
| # Copyright 2017, Data61, CSIRO (ABN 41 687 119 230) |
| # |
| # SPDX-License-Identifier: BSD-2-Clause |
| # |
| |
| # |
| # To use this coverage script, run a binary with qemu, passing the |
| # "-singlestep" and "-d exec" options. You will probably also want to use -D to |
| # prevent the log going to stderr. Note that this logging only works on newer |
| # versions of qemu. |
| # |
| # coverage.py staging/arm/imx31/kernel.elf /tmp/qemu.log --functions --objdump | less -R |
| # |
| |
| import sys |
| import os |
| import re |
| import argparse |
| from subprocess import Popen, PIPE |
| |
| |
| class Colors(object): |
| def __init__(self, use_color): |
| c = {} |
| c['NORMAL'] = '\033[0m' |
| c['BLACK'] = '\033[0;30m' |
| c['DARK_RED'] = '\033[0;31m' |
| c['DARK_GREEN'] = '\033[0;32m' |
| c['DARK_YELLOW'] = '\033[0;33m' |
| c['DARK_BLUE'] = '\033[0;34m' |
| c['DARK_MAGENTA'] = '\033[0;35m' |
| c['DARK_CYAN'] = '\033[0;36m' |
| c['GREY'] = '\033[0;37m' |
| c['LIGHT_GREY'] = '\033[1;30m' |
| c['LIGHT_RED'] = '\033[1;31m' |
| c['LIGHT_GREEN'] = '\033[1;32m' |
| c['LIGHT_YELLOW'] = '\033[1;33m' |
| c['LIGHT_BLUE'] = '\033[1;34m' |
| c['LIGHT_MAGENTA'] = '\033[1;35m' |
| c['LIGHT_CYAN'] = '\033[1;36m' |
| c['WHITE'] = '\033[1;37m' |
| |
| if use_color: |
| self.c = c |
| else: |
| self.c = dict([(x, '') for x in c.keys()]) |
| |
| def __getattr__(self, name): |
| if name in self.c: |
| return self.c[name] |
| else: |
| return object.__getattribute__(self, name) |
| |
| |
| def get_tool(toolname): |
| default_prefix = 'arm-none-eabi-' |
| |
| return os.environ.get('TOOLPREFIX', default_prefix) + toolname |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description='Generate coverage information of a binary.') |
| parser.add_argument('kernel_elf_filename', metavar='<kernel ELF>', |
| type=str, help='The kernel ELF file used for the log.') |
| parser.add_argument('coverage_filename', metavar='<qemu log>', |
| type=str, help='The qemu logfile containing the instruction trace.') |
| parser.add_argument('--functions', action='store_true', |
| help='Produce a summary of the functions covered.') |
| parser.add_argument('--objdump', action='store_true', |
| help='Produce an objdump with coverage information.') |
| parser.add_argument('--no-color', action='store_true', default=False, |
| help='Produce coloured output.') |
| |
| args = parser.parse_args() |
| colors = Colors(not args.no_color) |
| |
| # We will need to run objdump on the ELF file. |
| kernel_elf_filename = args.kernel_elf_filename |
| |
| # This is the raw qemu.log file. |
| coverage_filename = args.coverage_filename |
| |
| # Run objdump on the kernel binary. |
| objdump_proc = Popen([get_tool('objdump'), '-d', '-j', '.text', |
| kernel_elf_filename], stdout=PIPE) |
| objdump_lines = objdump_proc.stdout.readlines() |
| |
| seL4_arm_vector_table_address = None |
| |
| # Now, parse the objdump file and retain the following data: |
| |
| # A map from address to line number within the objdump file. |
| addr2lineno = {} |
| |
| # Can be seen as a map from line number in objdump file -> address. |
| objdump_addreses = [] |
| |
| # A map from function name to a set of all executable instructions within |
| # it. |
| function_instructions = {} |
| |
| current_function = None |
| line_re = re.compile(r'^([0-9a-f]+):') |
| ignore_re = re.compile(r'\.word|\.short|\.byte|undefined instruction') |
| function_name_re = re.compile(r'^([0-9a-f]+) <([^>]+)>') |
| for i, line in enumerate(objdump_lines): |
| addr = None |
| g = line_re.match(line) |
| if g: |
| g2 = ignore_re.search(line) |
| if not g2: |
| addr = int(g.group(1), 16) |
| addr2lineno[addr] = i |
| |
| objdump_addreses.append(addr) |
| |
| if current_function is not None and addr is not None: |
| function_instructions[current_function].add(addr) |
| |
| g = function_name_re.search(line) |
| if g: |
| current_function = g.group(2) |
| function_instructions[current_function] = set() |
| |
| if current_function == 'arm_vector_table': |
| seL4_arm_vector_table_address = int(g.group(1), 16) |
| |
| try: |
| coverage_file = open(coverage_filename, 'r') |
| except: |
| print >>sys.stderr, 'Failed to open %s' % coverage_filename |
| return -1 |
| |
| # Record all executable instructions in the ELF file into a set. |
| covered_instructions = set() |
| trace_entry = re.compile(r'^Trace 0x[0-f]+ \[([0-f]+)\]') |
| for line in coverage_file.readlines(): |
| entry = re.search(trace_entry, line) |
| if not entry: |
| continue |
| addr = int(entry.group(1), 16) |
| if addr in addr2lineno: |
| covered_instructions.add(addr) |
| |
| # Sigh. And of course, here are some seL4-specific hacks. The vectors page |
| # is not at the correct address in the binary. It is mapped at 0xffff0000 |
| # in memory, but starts at arm_vector_table in the binary. Account for that |
| # here. |
| if 0xffff0000 <= addr <= 0xffff1000 and seL4_arm_vector_table_address is not None: |
| covered_instructions.add(addr - 0xffff0000 + seL4_arm_vector_table_address) |
| coverage_file.close() |
| |
| # Print basic information. |
| num_covered = len(covered_instructions) |
| num_total = len(addr2lineno) |
| print '%d/%d instructions covered (%.1f%%)' % ( |
| num_covered, num_total, |
| 100.0 * num_covered / num_total) |
| |
| if args.functions: |
| # For each function, calculate how many instructions were covered. |
| function_coverage = {} |
| for f, instructions in function_instructions.iteritems(): |
| num_instructions = len(instructions) |
| if num_instructions > 0: |
| covered = len(instructions.intersection(covered_instructions)) |
| function_coverage[f] = (covered, num_instructions) |
| |
| # Sort by coverage and print. |
| for f, x in sorted(function_coverage.items(), key=lambda (f, x): 1.0 * x[0] / x[1]): |
| pct = 100.0 * x[0] / x[1] |
| |
| if pct == 0.0: |
| colour = colors.LIGHT_RED |
| elif pct == 100.0: |
| colour = colors.DARK_GREEN |
| else: |
| colour = colors.DARK_YELLOW |
| |
| line = " %4d/%-4d %3.1f%% %s\n" % (x[0], x[1], pct, f) |
| sys.stdout.write(colour + line + colors.NORMAL) |
| |
| if args.objdump: |
| # Print a coloured objdump. |
| for i, line in enumerate(objdump_lines): |
| addr = objdump_addreses[i] |
| covered = addr in covered_instructions |
| valid = addr in addr2lineno |
| if covered: |
| colour = colors.DARK_GREEN |
| elif valid: |
| colour = colors.LIGHT_RED |
| else: |
| colour = colors.LIGHT_GREY |
| |
| sys.stdout.write(colour + line + colors.NORMAL) |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |