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