blob: 6627902e2312060607c331e574e0db2aa4f63a0d [file] [edit]
#!/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())