1#!/usr/bin/env python 2# 3# Copyright 2017, Data61 4# Commonwealth Scientific and Industrial Research Organisation (CSIRO) 5# ABN 41 687 119 230. 6# 7# This software may be distributed and modified according to the terms of 8# the BSD 2-Clause license. Note that NO WARRANTY is provided. 9# See "LICENSE_BSD2.txt" for details. 10# 11# @TAG(DATA61_BSD) 12# 13 14# 15# To use this coverage script, run a binary with qemu, passing the 16# "-singlestep" and "-d exec" options. You will probably also want to use -D to 17# prevent the log going to stderr. Note that this logging only works on newer 18# versions of qemu. 19# 20# coverage.py staging/arm/imx31/kernel.elf /tmp/qemu.log --functions --objdump | less -R 21# 22 23import sys 24import os 25import re 26import argparse 27from subprocess import Popen, PIPE 28 29 30class Colors(object): 31 def __init__(self, use_color): 32 c = {} 33 c['NORMAL'] = '\033[0m' 34 c['BLACK'] = '\033[0;30m' 35 c['DARK_RED'] = '\033[0;31m' 36 c['DARK_GREEN'] = '\033[0;32m' 37 c['DARK_YELLOW'] = '\033[0;33m' 38 c['DARK_BLUE'] = '\033[0;34m' 39 c['DARK_MAGENTA'] = '\033[0;35m' 40 c['DARK_CYAN'] = '\033[0;36m' 41 c['GREY'] = '\033[0;37m' 42 c['LIGHT_GREY'] = '\033[1;30m' 43 c['LIGHT_RED'] = '\033[1;31m' 44 c['LIGHT_GREEN'] = '\033[1;32m' 45 c['LIGHT_YELLOW'] = '\033[1;33m' 46 c['LIGHT_BLUE'] = '\033[1;34m' 47 c['LIGHT_MAGENTA'] = '\033[1;35m' 48 c['LIGHT_CYAN'] = '\033[1;36m' 49 c['WHITE'] = '\033[1;37m' 50 51 if use_color: 52 self.c = c 53 else: 54 self.c = dict([(x, '') for x in c.keys()]) 55 56 def __getattr__(self, name): 57 if name in self.c: 58 return self.c[name] 59 else: 60 return object.__getattribute__(self, name) 61 62 63def get_tool(toolname): 64 default_prefix = 'arm-none-eabi-' 65 66 return os.environ.get('TOOLPREFIX', default_prefix) + toolname 67 68 69def main(): 70 parser = argparse.ArgumentParser( 71 description='Generate coverage information of a binary.') 72 parser.add_argument('kernel_elf_filename', metavar='<kernel ELF>', 73 type=str, help='The kernel ELF file used for the log.') 74 parser.add_argument('coverage_filename', metavar='<qemu log>', 75 type=str, help='The qemu logfile containing the instruction trace.') 76 parser.add_argument('--functions', action='store_true', 77 help='Produce a summary of the functions covered.') 78 parser.add_argument('--objdump', action='store_true', 79 help='Produce an objdump with coverage information.') 80 parser.add_argument('--no-color', action='store_true', default=False, 81 help='Produce coloured output.') 82 83 args = parser.parse_args() 84 colors = Colors(not args.no_color) 85 86 # We will need to run objdump on the ELF file. 87 kernel_elf_filename = args.kernel_elf_filename 88 89 # This is the raw qemu.log file. 90 coverage_filename = args.coverage_filename 91 92 # Run objdump on the kernel binary. 93 objdump_proc = Popen([get_tool('objdump'), '-d', '-j', '.text', 94 kernel_elf_filename], stdout=PIPE) 95 objdump_lines = objdump_proc.stdout.readlines() 96 97 seL4_arm_vector_table_address = None 98 99 # Now, parse the objdump file and retain the following data: 100 101 # A map from address to line number within the objdump file. 102 addr2lineno = {} 103 104 # Can be seen as a map from line number in objdump file -> address. 105 objdump_addreses = [] 106 107 # A map from function name to a set of all executable instructions within 108 # it. 109 function_instructions = {} 110 111 current_function = None 112 line_re = re.compile(r'^([0-9a-f]+):') 113 ignore_re = re.compile(r'\.word|\.short|\.byte|undefined instruction') 114 function_name_re = re.compile(r'^([0-9a-f]+) <([^>]+)>') 115 for i, line in enumerate(objdump_lines): 116 addr = None 117 g = line_re.match(line) 118 if g: 119 g2 = ignore_re.search(line) 120 if not g2: 121 addr = int(g.group(1), 16) 122 addr2lineno[addr] = i 123 124 objdump_addreses.append(addr) 125 126 if current_function is not None and addr is not None: 127 function_instructions[current_function].add(addr) 128 129 g = function_name_re.search(line) 130 if g: 131 current_function = g.group(2) 132 function_instructions[current_function] = set() 133 134 if current_function == 'arm_vector_table': 135 seL4_arm_vector_table_address = int(g.group(1), 16) 136 137 try: 138 coverage_file = open(coverage_filename, 'r') 139 except: 140 print >>sys.stderr, 'Failed to open %s' % coverage_filename 141 return -1 142 143 # Record all executable instructions in the ELF file into a set. 144 covered_instructions = set() 145 trace_entry = re.compile(r'^Trace 0x[0-f]+ \[([0-f]+)\]') 146 for line in coverage_file.readlines(): 147 entry = re.search(trace_entry, line) 148 if not entry: 149 continue 150 addr = int(entry.group(1), 16) 151 if addr in addr2lineno: 152 covered_instructions.add(addr) 153 154 # Sigh. And of course, here are some seL4-specific hacks. The vectors page 155 # is not at the correct address in the binary. It is mapped at 0xffff0000 156 # in memory, but starts at arm_vector_table in the binary. Account for that 157 # here. 158 if 0xffff0000 <= addr <= 0xffff1000 and seL4_arm_vector_table_address is not None: 159 covered_instructions.add(addr - 0xffff0000 + seL4_arm_vector_table_address) 160 coverage_file.close() 161 162 # Print basic information. 163 num_covered = len(covered_instructions) 164 num_total = len(addr2lineno) 165 print '%d/%d instructions covered (%.1f%%)' % ( 166 num_covered, num_total, 167 100.0 * num_covered / num_total) 168 169 if args.functions: 170 # For each function, calculate how many instructions were covered. 171 function_coverage = {} 172 for f, instructions in function_instructions.iteritems(): 173 num_instructions = len(instructions) 174 if num_instructions > 0: 175 covered = len(instructions.intersection(covered_instructions)) 176 function_coverage[f] = (covered, num_instructions) 177 178 # Sort by coverage and print. 179 for f, x in sorted(function_coverage.items(), key=lambda (f, x): 1.0 * x[0] / x[1]): 180 pct = 100.0 * x[0] / x[1] 181 182 if pct == 0.0: 183 colour = colors.LIGHT_RED 184 elif pct == 100.0: 185 colour = colors.DARK_GREEN 186 else: 187 colour = colors.DARK_YELLOW 188 189 line = " %4d/%-4d %3.1f%% %s\n" % (x[0], x[1], pct, f) 190 sys.stdout.write(colour + line + colors.NORMAL) 191 192 if args.objdump: 193 # Print a coloured objdump. 194 for i, line in enumerate(objdump_lines): 195 addr = objdump_addreses[i] 196 covered = addr in covered_instructions 197 valid = addr in addr2lineno 198 if covered: 199 colour = colors.DARK_GREEN 200 elif valid: 201 colour = colors.LIGHT_RED 202 else: 203 colour = colors.LIGHT_GREY 204 205 sys.stdout.write(colour + line + colors.NORMAL) 206 207 return 0 208 209 210if __name__ == '__main__': 211 sys.exit(main()) 212