| # !/usr/bin/env python |
| ############################################################################### |
| # Copyright (c) 2020-2023, ARM Limited and Contributors. All rights reserved. |
| # |
| # SPDX-License-Identifier: BSD-3-Clause |
| ############################################################################### |
| |
| ############################################################################### |
| # FILE: intermediate_layer.py |
| # |
| # DESCRIPTION: Creates an intermediate json file with information provided |
| # by the configuration json file, dwarf signatures and trace |
| # files. |
| # |
| ############################################################################### |
| |
| import os |
| import re |
| import glob |
| import argparse |
| import subprocess |
| import json |
| from argparse import RawTextHelpFormatter |
| import cc_logger |
| import time |
| from typing import Dict |
| from typing import List |
| from typing import Generator |
| from typing import Union |
| from typing import Tuple |
| import logging |
| |
| __version__ = "7.0" |
| |
| # Static map that defines the elf file source type in the intermediate json |
| ELF_MAP = { |
| "bl1": 0, |
| "bl2": 1, |
| "bl31": 2, |
| "bl32": 3, |
| "scp_ram": 10, |
| "scp_rom": 11, |
| "mcp_rom": 12, |
| "mcp_ram": 13, |
| "secure_hafnium": 14, |
| "hafium": 15, |
| "custom_offset": 100 |
| } |
| |
| |
| def os_command(command, show_command=False): |
| """ |
| Function that execute an os command, on fail exit the program |
| |
| :param command: OS command as string |
| :param show_command: Optional argument to print the command in stdout |
| :return: The string output of the os command |
| """ |
| try: |
| if show_command: |
| print("OS command: {}".format(command)) |
| out = subprocess.check_output( |
| command, stderr=subprocess.STDOUT, shell=True) |
| except subprocess.CalledProcessError as ex: |
| raise Exception( |
| "Exception running command '{}': {}({})".format( |
| command, ex.output, ex.returncode)) |
| return out.decode("utf8") |
| |
| |
| def load_stats_from_traces(trace_globs): |
| """ |
| Function to process and consolidate statistics from trace files |
| |
| :param trace_globs: List of trace file patterns |
| :return: Dictionary with stats from trace files i.e. |
| {mem address in decimal}=(times executed, inst size) |
| """ |
| stats = {} |
| stat_size = {} |
| |
| # Make a list of unique trace files |
| trace_files = [] |
| for tg in trace_globs: |
| trace_files.extend(glob.glob(tg)) |
| trace_files = set(trace_files) |
| |
| if not trace_files: |
| raise Exception("No trace files found for '{}'".format(trace_globs)) |
| # Load stats from the trace files |
| for trace_file in trace_files: |
| try: |
| with open(trace_file, 'r') as f: |
| for line in f: |
| data = line.split() |
| address = int(data[0], 16) |
| stat = int(data[1]) |
| size = int(data[2]) |
| stat_size[address] = size |
| if address in stats: |
| stats[address] += stat |
| else: |
| stats[address] = stat |
| except Exception as ex: |
| logger.error("@Loading stats from trace files:{}".format(ex)) |
| # Merge the two dicts |
| for address in stats: |
| stats[address] = (stats[address], stat_size[address]) |
| return stats |
| |
| |
| def get_code_sections_for_binary(elf_name): |
| """ |
| Function to return the ranges of memory address for sections of code |
| in the elf file |
| |
| :param elf_name: Elf binary file name |
| :return: List of code sections tuples, i.e. (section type, initial |
| address, end address) |
| """ |
| command = """%s -h %s | grep -B 1 CODE | grep -v CODE \ |
| | awk '{print $2" "$4" "$3}'""" % (OBJDUMP, elf_name) |
| text_out = os_command(command) |
| sections = text_out.split('\n') |
| sections.pop() |
| secs = [] |
| for sec in sections: |
| try: |
| d = sec.split() |
| secs.append((d[0], int(d[1], 16), int(d[2], 16))) |
| except Exception as ex: |
| logger.error( |
| "@Returning memory address code sections:".format(ex)) |
| return secs |
| |
| |
| def get_executable_ranges_for_binary(elf_name): |
| """ |
| Get function ranges from an elf file |
| |
| :param elf_name: Elf binary file name |
| :return: List of tuples for ranges i.e. (range start, range end) |
| """ |
| # Parse all $x / $d symbols |
| symbol_table = [] |
| address = None |
| _type = None |
| command = r"""%s -s %s | awk '/\$[xatd]/ {print $2" "$8}'""" % ( |
| READELF, elf_name) |
| text_out = os_command(command) |
| lines = text_out.split('\n') |
| lines.pop() |
| for line in lines: |
| try: |
| data = line.split() |
| address = int(data[0], 16) |
| _type = 'X' if data[1] in ['$x', '$t', '$a'] else 'D' |
| except Exception as ex: |
| logger.error("@Getting executable ranges:".format(ex)) |
| symbol_table.append((address, _type)) |
| |
| # Add markers for end of code sections |
| sections = get_code_sections_for_binary(elf_name) |
| for sec in sections: |
| symbol_table.append((sec[1] + sec[2], 'S')) |
| |
| # Sort by address |
| symbol_table = sorted(symbol_table, key=lambda tup: tup[0]) |
| |
| # Create ranges (list of START/END tuples) |
| ranges = [] |
| range_start = symbol_table[0][0] |
| rtype = symbol_table[0][1] |
| for sym in symbol_table: |
| if sym[1] != rtype: |
| if rtype == 'X': |
| # Subtract one because the first address of the |
| # next range belongs to the next range. |
| ranges.append((range_start, sym[0] - 1)) |
| range_start = sym[0] |
| rtype = sym[1] |
| return ranges |
| |
| |
| def remove_workspace(path, workspace): |
| """ |
| Get the relative path to a given workspace |
| |
| :param path: Path relative to the workspace to be returned |
| :param workspace: Path. |
| """ |
| ret = path if workspace is None else os.path.relpath(path, workspace) |
| return ret |
| |
| |
| def get_function_line_numbers(source_file: str) -> Dict[str, int]: |
| """ |
| Using ctags get all the function names with their line numbers |
| within the source_file |
| |
| :return: Dictionary with function name as key and line number as value |
| """ |
| command = "ctags -x --c-kinds=f {}".format(source_file) |
| fln = {} |
| try: |
| function_lines = os_command(command).split("\n") |
| for line in function_lines: |
| cols = line.split() |
| if len(cols) < 3: |
| continue |
| if cols[1] == "function": |
| fln[cols[0]] = int(cols[2]) |
| elif cols[1] == "label": |
| if cols[0] == "func": |
| fln[cols[-1]] = int(cols[2]) |
| elif cols[0] + ":" == cols[-1]: |
| fln[cols[0]] = int(cols[2]) |
| except BaseException: |
| logger.warning("Warning: Can't get all function line numbers from %s" % |
| source_file) |
| except Exception as ex: |
| logger.warning(f"Warning: Unknown error '{ex}' when executing command " |
| f"'{command}'") |
| return {} |
| return fln |
| |
| |
| class FunctionLineNumbers(object): |
| """Helper class used to get a function start line number within |
| a source code file""" |
| |
| def __init__(self, workspace: str): |
| """ |
| Initialise dictionary to allocate source code files with the |
| corresponding function start line numbers. |
| |
| :param workspace: The folder where the source files are deployed |
| """ |
| self.filenames = {} |
| self.workspace = workspace |
| |
| def get_line_number(self, filename: str, function_name: str) -> int: |
| if not FUNCTION_LINES_ENABLED: |
| return 0 |
| if filename not in self.filenames: |
| source_file = os.path.join(self.workspace, filename) |
| # Get all functions with their lines in the source file |
| self.filenames[filename] = get_function_line_numbers(source_file) |
| return 0 if function_name not in self.filenames[filename] else \ |
| self.filenames[filename][function_name] |
| |
| |
| class BinaryParser(object): |
| """Class used to create an instance to parse the binary files with a |
| dwarf signature in order to produce logical information to be matched with |
| traces and produce a code coverage report""" |
| |
| def __init__(self, dump: str, _workspace: str, _remove_workspace: bool, |
| local_workspace: str): |
| """ |
| Initialisation of the instance to parse binary files. |
| |
| :param dump: Binary dump (string) containing assembly code and source |
| code metadata, i.e. source code location and line number. |
| :param _workspace: Workspace (folder) where the source files were |
| built from. |
| :param _remove_workspace: Boolean to indicate if the build of |
| source files was local (false) or from a CI (true). |
| :param local_workspace: Path to the local workspace where the source |
| files reside |
| """ |
| self.dump = dump |
| self.no_source_functions = self.get_no_source_functions() |
| self.workspace = _workspace |
| self.remove_workspace = _remove_workspace |
| self.local_workspace = local_workspace |
| self.function_line_numbers = FunctionLineNumbers(self.local_workspace) |
| |
| def get_no_source_functions(self) -> Dict[int, Dict]: |
| """Find in the dwarf dump all the functions with no source code i.e.: |
| function_name(): |
| start_hex_address opcode |
| .... |
| end_hex_address opcode |
| |
| :returns: Dictionary of functions indexed by start address function's |
| location |
| """ |
| # The functions dict is [start_dec_address]={function name, function |
| # end address in decimal} |
| _functions = {} |
| groups = re.findall(r"(.+?)\(\):\n\s+([a-f0-9]+):." |
| r"+?\n(\s+([a-f0-9]+):.+?\n)*", self.dump) |
| for group in groups: |
| function_name, start_hex_address, _, end_hex_address = group |
| if not end_hex_address: |
| end_hex_address = start_hex_address |
| _functions[int(start_hex_address, 16)] = {'name': function_name, |
| 'end_address': int( |
| end_hex_address, 16)} |
| return _functions |
| |
| class SourceCodeBlock(object): |
| """Class used to represent a source code block of information within |
| a function block in a binary dump file. |
| The source code block contains the following components: |
| - Source code file that contains the source code corresponding |
| to the assembly code. |
| - Line number within the source code file corresponding to the source |
| code. |
| - Assembly code block. |
| """ |
| |
| def __init__(self, source_code_block_dump): |
| """ |
| Create an instance of a source code block within a function block. |
| |
| :param source_code_block: Tuple of 3 elements that contains the |
| components of a source code block. |
| """ |
| self.source_file, self.line_number, self.asm_code \ |
| = source_code_block_dump |
| |
| @staticmethod |
| def get(dwarf_data: str) -> Generator['BinaryParser.SourceCodeBlock', |
| None, None]: |
| source_block_groups = re.findall(r"(?s)(/[a-zA-Z_0-9][^\n]+?):" |
| r"([0-9]+)(?: [^\n]+)?\n(.+?)" |
| r"\n(?=/[a-zA-Z_0-9][^\n]+?" |
| r":[0-9]+[^\n]+?\n|\n$)", |
| dwarf_data) |
| for source_block_group in source_block_groups: |
| if len(source_block_group) != 3: |
| logger.warning(f"Source code incomplete:" |
| f"{source_block_group}") |
| continue |
| source_block_dump = list(source_block_group) |
| source_block_dump[-1] += "\n\n" # For parsing assembly lines |
| yield BinaryParser.SourceCodeBlock(source_block_dump) |
| |
| def __str__(self): |
| return f"'{self.source_file}:{self.line_number}'" |
| |
| class FunctionBlock(object): |
| """Class used to parse and obtain a function block from the |
| binary dump file that corresponds to a function declaration in the |
| source code file and a block of assembly code mixed with corresponding |
| source code lines, i.e. dwarf information. |
| The function block has the following components: |
| - Function name at source code. |
| - DWARF data. |
| - Function declaration's line number at source code. |
| This comes from dump blocks like these: |
| 0000000000000230 <_setup>: |
| read_el(): <---- Function name at source code |
| /home/user/aarch64/setup.c:238 <------ Source file and line number |
| 230: d53e1100 mrs x0, scr_el3 <----- Assembly lines belonging to |
| the source code |
| no_setup(): |
| /home/user/no_setup.c:618 |
| 234: b2760000 orr x0, x0, #0x400 |
| """ |
| |
| def __init__(self, function_group: List[str]): |
| """ |
| Create an instance of a function block within a binary dump. |
| |
| :param function_group: List containing the function name and |
| dwarf data of the block. |
| """ |
| self.name, self.dwarf = function_group |
| # Now obtain the function's source file |
| m = re.search(r"(/.+?):([0-9]+)(?: [^\n]+)?\n", self.dwarf) |
| self.source_file = m.groups()[0].strip() \ |
| if m and len(m.groups()) == 2 else None |
| # Computed later |
| self.function_line_number = None |
| |
| @staticmethod |
| def get(dump: str) -> Generator['BinaryParser.FunctionBlock', None, |
| None]: |
| """ |
| Static method generator to extract a function block from the binary |
| dump. |
| |
| :param dump: Binary dump (string) that contains the binary file |
| information. |
| :return: A FunctionBlock object that is a logical representation |
| of a function declaration within the binary dump. |
| """ |
| function_groups = re.findall(r"(?s)([a-zA-Z0-9_]+?)\(\):" |
| r"\n(/.+?:[0-9]+?.+?)\n" |
| r"(?=[a-zA-Z0-9_]+?\(\):\n|\n\n$)", |
| dump) |
| for group in function_groups: |
| if len(group) != 2: |
| continue |
| function_group = list(group) |
| function_group[-1] += "\n\n" # For parsing source code blocks |
| yield BinaryParser.FunctionBlock(function_group) |
| |
| @property |
| def values(self): |
| return self.name, self.source_file, self.function_line_number |
| |
| def __str__(self): |
| return f"'{self.name}:{self.function_line_number}'" |
| |
| class AssemblyLine(object): |
| """Class used to represent an assembly code line within an |
| assembly code block. |
| The assembly line instruction is formed by the following components: |
| - Hexadecimal address of the assembly instruction. |
| - Assembly instruction. |
| """ |
| |
| def __init__(self, line): |
| """ |
| Create an instance representing an assembly code line within an |
| assembly code block. |
| |
| :param line: Tuple of 2 elements [Hexadecimal number, |
| and assembly code] |
| """ |
| self.hex_line_number, self.opcode = line |
| self.dec_address = int(self.hex_line_number, 16) |
| self.times_executed = 0 |
| |
| @staticmethod |
| def get(asm_code: str) -> Generator['BinaryParser.AssemblyLine', |
| None, None]: |
| """ |
| Static method generator to extract an assembly code line from an |
| assembly code block. |
| |
| :param asm_code: Lines of assembly code within the dump |
| :return: AssemblyLine object. |
| """ |
| lines = re.findall(r"^(?:\s+)?([a-fA-F0-9]+):\t(.+?)\n", asm_code, |
| re.DOTALL | re.MULTILINE) |
| for line in lines: |
| if len(line) != 2: |
| logger.warning(f"Assembly code incomplete: {line}") |
| continue |
| yield BinaryParser.AssemblyLine(line) |
| |
| @staticmethod |
| def get_asm_line(source_code_block: 'BinaryParser.SourceCodeBlock', |
| traces_stats) -> \ |
| Generator['BinaryParser.AssemblyLine', None, None]: |
| """Generator method to obtain all assembly line codes within a source |
| code line """ |
| traces_stats = traces_stats |
| for asm_line in BinaryParser.AssemblyLine.get( |
| source_code_block.asm_code): |
| asm_line.times_executed = traces_stats.get(asm_line.dec_address, |
| [0])[0] |
| yield asm_line |
| |
| def get_source_code_block(self, function_block: FunctionBlock) -> \ |
| Generator['BinaryParser.SourceCodeBlock', None, None]: |
| """ |
| Generator method to obtain all the source code blocks within a |
| function block. |
| |
| :param function_block: FunctionBlock object that contains the code |
| the source code blocks. |
| :return: A SourceCodeBlock object. |
| """ |
| for source_code_block in BinaryParser.SourceCodeBlock.get( |
| function_block.dwarf): |
| if self.remove_workspace: |
| source_code_block.source_file = remove_workspace( |
| source_code_block.source_file, self.workspace) |
| yield source_code_block |
| |
| def get_function_block(self) -> Generator['BinaryParser.FunctionBlock', |
| None, None]: |
| """Generator method to obtain all the function blocks contained in |
| the binary dump file. |
| """ |
| for function_block in BinaryParser.FunctionBlock.get(self.dump): |
| if function_block.source_file is None: |
| logger.warning(f"Source file not found for function " |
| f"{function_block.name}, will not be covered") |
| continue |
| if self.remove_workspace: |
| function_block.source_file = remove_workspace( |
| function_block.source_file, self.workspace) |
| function_block.function_line_number = \ |
| self.function_line_numbers.get_line_number( |
| function_block.source_file, function_block.name) |
| yield function_block |
| |
| |
| class CoverageHandler(object): |
| """ Class used to handle source files coverage linked with their functions |
| and line code coverage from function blocks obtained from DWARF data and |
| trace code coverage from CC plugin""" |
| |
| def __init__(self): |
| self._source_files = {} |
| |
| def add_function_coverage(self, function_data: |
| Union[BinaryParser.FunctionBlock, |
| Tuple[str, str, int]]): |
| """ Add a function coverage block and a source file coverage block, |
| if not already created and link them""" |
| # Unpack function data either as an FunctionBlock object property or a |
| # tuple |
| name, source_file, function_line_number = function_data.values if \ |
| isinstance(function_data, BinaryParser.FunctionBlock) else \ |
| function_data |
| |
| # Add source file coverage block it if not already there |
| self._source_files.setdefault(source_file, |
| {"functions": {}, "lines": {}}) |
| # Add a function coverage block (if not existent) from a function |
| # block using the function block name as key and link it to the source |
| # file coverage block |
| self._source_files[source_file]["functions"].setdefault( |
| name, {"covered": False, "line_number": function_line_number}) |
| |
| def add_line_coverage(self, source_code_block: |
| BinaryParser.SourceCodeBlock): |
| """ Add a line coverage block and a source file coverage block, |
| if not already created and link them""" |
| # Add source file coverage block it if not already there |
| self._source_files.setdefault(source_code_block.source_file, |
| {"functions": {}, "lines": {}}) |
| # Add a line coverage block (if not existent) from a source block |
| # using the source code line number as a key and link it to the source |
| # file coverage block |
| self._source_files[source_code_block.source_file]["lines"].setdefault( |
| source_code_block.line_number, {"covered": False, "elf_index": {}}) |
| |
| def add_asm_line(self, source_code_block: BinaryParser.SourceCodeBlock, |
| asm_line: BinaryParser.AssemblyLine, elf_index: int): |
| """Add an assembly line from the DWARF data linked to a source code |
| line""" |
| self._source_files[source_code_block.source_file]["lines"][ |
| source_code_block.line_number]["elf_index"].setdefault( |
| elf_index, {}) |
| self._source_files[source_code_block.source_file]["lines"][ |
| source_code_block.line_number]["elf_index"][ |
| elf_index].setdefault(asm_line.dec_address, |
| (asm_line.opcode, asm_line.times_executed)) |
| |
| def set_line_coverage(self, source_code_block: |
| BinaryParser.SourceCodeBlock, value: bool): |
| self._source_files[source_code_block.source_file]["lines"][ |
| source_code_block.line_number]["covered"] = value |
| |
| def set_function_coverage(self, function_block: |
| Union[BinaryParser.FunctionBlock, |
| Tuple[str, str]], value: bool): |
| name, source_file = (function_block.name, function_block.source_file)\ |
| if isinstance(function_block, BinaryParser.FunctionBlock) else \ |
| function_block |
| self._source_files[source_file]["functions"][name]["covered"] = value |
| |
| @property |
| def source_files(self): |
| return self._source_files |
| |
| |
| class IntermediateCodeCoverage(object): |
| """Class used to process the trace data along with the dwarf |
| signature files to produce an intermediate layer in json with |
| code coverage in assembly and c source code. |
| """ |
| |
| def __init__(self, _config, local_workspace): |
| self._data = {} |
| self.config = _config |
| self.workspace = self.config['parameters']['workspace'] |
| self.remove_workspace = self.config['configuration']['remove_workspace'] |
| self.local_workspace = local_workspace |
| self.elfs = self.config['elfs'] |
| # Dictionary with stats from trace files {address}=(times executed, |
| # inst size) |
| self.traces_stats = {} |
| # Dictionary of unique assembly line memory address against source |
| # file location |
| # {assembly address} = (opcode, source file location, line number in |
| # the source file, times executed) |
| self.asm_lines = {} |
| # Dictionary of {source file location}=>{'lines': {'covered':Boolean, |
| # 'elf_index'; {elf index}=>{assembly address}=>(opcode, |
| # times executed), |
| # 'functions': {function name}=>is covered(boolean)} |
| self.coverage = CoverageHandler() |
| self.functions = [] |
| # Unique set of elf list of files |
| self.elf_map = {} |
| # For elf custom mappings |
| self.elf_custom = None |
| |
| def process(self): |
| """ |
| Public method to process the trace files and dwarf signatures |
| using the information contained in the json configuration file. |
| This method writes the intermediate json file output linking |
| the trace data and c source and assembly code. |
| """ |
| self.asm_lines = {} |
| # Initialize for unknown elf files |
| self.elf_custom = ELF_MAP["custom_offset"] |
| print("Generating intermediate json layer '{}'...".format( |
| self.config['parameters']['output_file'])) |
| for elf in self.elfs: |
| # Gather information |
| elf_name = elf['name'] |
| # Obtain trace data |
| self.traces_stats = load_stats_from_traces(elf['traces']) |
| # Produce code coverage |
| self._process_binary(elf_name) |
| # Write to the intermediate json file |
| data = {"source_files": self.coverage.source_files, |
| "configuration": { |
| "sources": self.config['parameters']['sources'], |
| "metadata": "" if 'metadata' not in |
| self.config['parameters'] else |
| self.config['parameters']['metadata'], |
| "elf_map": self.elf_map} |
| } |
| json_data = json.dumps(data, indent=4, sort_keys=True) |
| with open(self.config['parameters']['output_file'], "w") as f: |
| f.write(json_data) |
| |
| def get_elf_index(self, elf_name: str) -> int: |
| """Obtains the elf index and fills the elf_map instance variable""" |
| if elf_name not in self.elf_map: |
| if elf_name in ELF_MAP: |
| self.elf_map[elf_name] = ELF_MAP[elf_name] |
| else: |
| self.elf_map[elf_name] = ELF_MAP["custom_offset"] |
| ELF_MAP["custom_offset"] += 1 |
| return self.elf_map[elf_name] |
| |
| def _process_binary(self, elf_filename: str) -> BinaryParser: |
| """ |
| Process an elf file i.e. match the source code and asm lines against |
| trace files (coverage). |
| |
| :param elf_filename: Elf binary file name |
| """ |
| command = "%s -Sl %s | tee %s" % (OBJDUMP, elf_filename, |
| elf_filename.replace(".elf", ".dump")) |
| dump = os_command(command, show_command=True) |
| dump += "\n\n" # For pattern matching the last function |
| logger.info(f"Parsing assembly file {elf_filename}") |
| elf_name = os.path.splitext(os.path.basename(elf_filename))[0] |
| elf_index = self.get_elf_index(elf_name) |
| parser = BinaryParser(dump, self.workspace, self.remove_workspace, |
| self.local_workspace) |
| total_number_functions = 0 |
| functions_covered = 0 |
| for function_block in parser.get_function_block(): |
| total_number_functions += 1 |
| # Function contains source code |
| self.coverage.add_function_coverage(function_block) |
| is_function_covered = False |
| for source_code_block in parser.get_source_code_block( |
| function_block): |
| self.coverage.add_line_coverage(source_code_block) |
| is_line_covered = False |
| for asm_line in parser.get_asm_line(source_code_block, |
| self.traces_stats): |
| # Here it is checked the line coverage |
| is_line_covered = asm_line.times_executed > 0 or \ |
| is_line_covered |
| self.coverage.add_asm_line(source_code_block, asm_line, |
| elf_index) |
| logger.debug(f"Source file {source_code_block} is " |
| f"{'' if is_line_covered else 'not '}covered") |
| if is_line_covered: |
| self.coverage.set_line_coverage(source_code_block, True) |
| is_function_covered = True |
| logger.debug(f"\tFunction '{function_block.name}' at '" |
| f"{function_block.source_file} is " |
| f"{'' if is_function_covered else 'not '}covered") |
| if is_function_covered: |
| self.coverage.set_function_coverage(function_block, True) |
| functions_covered += 1 |
| logger.info(f"Total functions: {total_number_functions}, Functions " |
| f"covered:{functions_covered}") |
| # Now check code coverage in the functions with no dwarf signature |
| self._process_fn_no_sources(parser) |
| return parser |
| |
| def _process_fn_no_sources(self, parser: BinaryParser): |
| """ |
| Checks function coverage for functions with no dwarf signature i.e. |
| sources. |
| |
| :param parser: Binary parser that contains objects needed |
| to check function line numbers including the dictionary of functions |
| to be checked i.e [start_dec_address]={'name', 'end_address'} |
| """ |
| if not FUNCTION_LINES_ENABLED: |
| return # No source code at the workspace |
| traces_addresses = sorted(self.traces_stats.keys()) |
| traces_address_pointer = 0 |
| _functions = parser.no_source_functions |
| functions_addresses = sorted(_functions.keys()) |
| address_size = 4 |
| for start_address in functions_addresses: |
| function_covered = False |
| function_name = _functions[start_address]['name'] |
| # Get all files in the source code where the function is defined |
| source_files = os_command("grep --include '*.c' --include '*.s' " |
| "--include '*.S' -nrw '{}' {}" |
| "| cut -d: -f1". |
| format(function_name, |
| self.local_workspace)) |
| unique_files = set(source_files.split()) |
| sources_found = [] |
| for source_file in unique_files: |
| line_number = parser.function_line_numbers.get_line_number( |
| source_file, function_name) |
| if line_number > 0: |
| sources_found.append((source_file, line_number)) |
| if len(sources_found) == 0: |
| logger.debug(f"'{function_name}' not found in sources") |
| elif len(sources_found) > 1: |
| logger.warning(f"'{function_name}' declared in " |
| f"{len(sources_found)} files") |
| else: |
| source_file_found, function_line_number = sources_found[0] |
| function_source_file = remove_workspace(source_file_found, |
| self.local_workspace) |
| self.coverage.add_function_coverage((function_name, |
| function_source_file, |
| function_line_number)) |
| for in_function_address in \ |
| range(start_address, |
| _functions[start_address]['end_address'] |
| + address_size, address_size): |
| if in_function_address in traces_addresses[ |
| traces_address_pointer:]: |
| function_covered = True |
| traces_address_pointer = traces_addresses.index( |
| in_function_address) + 1 |
| break |
| logger.info(f"Added non-sources function '{function_name}' " |
| f"with coverage: {function_covered}") |
| if function_covered: |
| self.coverage.set_function_coverage((function_name, |
| function_source_file), |
| function_covered) |
| |
| |
| json_conf_help = """ |
| Produces an intermediate json layer for code coverage reporting |
| using an input json configuration file. |
| |
| Input json configuration file format: |
| { |
| "configuration": |
| { |
| "remove_workspace": <true if 'workspace' must be from removed from the |
| path of the source files>, |
| "include_assembly": <true to include assembly source code in the |
| intermediate layer> |
| }, |
| "parameters": |
| { |
| "objdump": "<Path to the objdump binary to handle dwarf signatures>", |
| "readelf: "<Path to the readelf binary to handle dwarf signatures>", |
| "sources": [ <List of source code origins, one or more of the next |
| options> |
| { |
| "type": "git", |
| "URL": "<URL git repo>", |
| "COMMIT": "<Commit id>", |
| "REFSPEC": "<Refspec>", |
| "LOCATION": "<Folder within 'workspace' where this source |
| is located>" |
| }, |
| { |
| "type": "http", |
| "URL": <URL link to file>", |
| "COMPRESSION": "xz", |
| "LOCATION": "<Folder within 'workspace' where this source |
| is located>" |
| } |
| ], |
| "workspace": "<Workspace folder where the source code was located to |
| produce the elf/axf files>", |
| "output_file": "<Intermediate layer output file name and location>", |
| "metadata": {<Metadata objects to be passed to the intermediate json |
| files>} |
| }, |
| "elfs": [ <List of elf files to be traced/parsed> |
| { |
| "name": "<Full path name to elf/axf file>", |
| "traces": [ <List of trace files to be parsed for this |
| elf/axf file> |
| "Full path name to the trace file," |
| ] |
| } |
| ] |
| } |
| """ |
| OBJDUMP = None |
| READELF = None |
| FUNCTION_LINES_ENABLED = None |
| |
| |
| def main(): |
| global OBJDUMP |
| global READELF |
| global FUNCTION_LINES_ENABLED |
| |
| parser = argparse.ArgumentParser(epilog=json_conf_help, |
| formatter_class=RawTextHelpFormatter) |
| parser.add_argument('--config-json', metavar='PATH', |
| dest="config_json", default='config_file.json', |
| help='JSON configuration file', required=True) |
| parser.add_argument('--local-workspace', default="", |
| help=('Local workspace folder where source code files' |
| ' and folders resides')) |
| args = parser.parse_args() |
| try: |
| with open(args.config_json, 'r') as f: |
| config = json.load(f) |
| except Exception as ex: |
| print("Error at opening and processing JSON: {}".format(ex)) |
| return |
| print(json.dumps(config, indent=4)) |
| # Setting toolchain binary tools variables |
| OBJDUMP = config['parameters']['objdump'] |
| READELF = config['parameters']['readelf'] |
| # Checking if are installed |
| os_command("{} --version".format(OBJDUMP)) |
| os_command("{} --version".format(READELF)) |
| |
| if args.local_workspace != "": |
| # Checking ctags installed |
| try: |
| os_command("ctags --version") |
| except BaseException: |
| print("Warning!: ctags not installed/working function line numbers\ |
| will be set to 0. [{}]".format( |
| "sudo apt install exuberant-ctags")) |
| else: |
| FUNCTION_LINES_ENABLED = True |
| |
| intermediate_layer = IntermediateCodeCoverage(config, args.local_workspace) |
| intermediate_layer.process() |
| |
| |
| if __name__ == '__main__': |
| logger = cc_logger.logger |
| start_time = time.time() |
| main() |
| elapsed_time = time.time() - start_time |
| print("Elapsed time: {}s".format(elapsed_time)) |