diff --git a/tools/executable_analysis_tools/elf-float-checker.py b/tools/executable_analysis_tools/elf-float-checker.py new file mode 100644 index 0000000000..c1185a7820 --- /dev/null +++ b/tools/executable_analysis_tools/elf-float-checker.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 Arm Limited and Contributors. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Script for checking for floating point symbols in ELF files.""" + +import argparse +import logging +import re +import subprocess +import sys + +log = logging.getLogger(__name__) + +LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" +FLOATING_POINT_SYMBOL_REGEX = r"__aeabi_(cd.+|cf.+|h2f.+|d.+|f.+|.+2d|.+2f)" +OBJECT_FILE_ANALYSIS_CMD = ["objdump", "-t"] + +class SymbolParser: + """Parse the given ELF format file.""" + + def get_symbols_from_table(self, symbol_table, symbol_regex): + """Get symbols matching a regular expression pattern from a table.""" + pattern = re.compile(symbol_regex, re.X) + matched_symbols = [] + symbol_table_lines = symbol_table.split("\n") + for symbol_table_line in symbol_table_lines: + match = pattern.search(symbol_table_line) + if match: + log.debug("Symbol line: {}".format(symbol_table_line)) + log.debug("Match found: {}".format(match)) + matched_symbols.append(match.group(0)) + + log.debug("Symbols found:\n'{}'".format(matched_symbols)) + return matched_symbols + + def get_symbol_table(self, elf_file): + """Get the symbol table from an ELF format file.""" + log.debug( + "Get the symbol table for ELF format file '{}'".format(elf_file) + ) + + cmd = [*OBJECT_FILE_ANALYSIS_CMD, elf_file] + log.debug("command: '{}'".format(cmd)) + try: + process = subprocess.run( + cmd, + check=True, + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + except subprocess.CalledProcessError as error: + err_output = error.stdout.decode() + msg = ( + "Getting symbol table for ELF format file '{}' failed," + " error: {}".format(elf_file, err_output) + ) + raise SymbolTableError(msg) + + symbol_table = process.stdout.decode() + log.debug("Symbol table:\n{}\n".format(symbol_table)) + + return symbol_table + + +class SymbolTableError(Exception): + """An exception for a failure to obtain a symbol table.""" + + +class ArgumentParserWithDefaultHelp(argparse.ArgumentParser): + """Subclass that always shows the help message on invalid arguments.""" + + def error(self, message): + """Error handler.""" + sys.stderr.write("error: {}\n".format(message)) + self.print_help() + + +def set_log_verbosity(increase_verbosity): + """Set the verbosity of the log output.""" + log_level = logging.DEBUG if increase_verbosity else logging.INFO + + log.setLevel(log_level) + logging.basicConfig(level=log_level, format=LOG_FORMAT) + + +def check_float_symbols(elf_file): + """Check for floating point symbols in ELF format file. + + Return the floating point symbols found. + """ + parser = SymbolParser() + symbol_table = parser.get_symbol_table(elf_file) + + float_symbols = parser.get_symbols_from_table( + symbol_table, FLOATING_POINT_SYMBOL_REGEX + ) + + return float_symbols + + +def check_action(args): + """Entry point for checking the ELF file.""" + float_symbols = check_float_symbols(args.elf_file) + if float_symbols: + print("Found float symbols:") + for float_symbol in float_symbols: + print(float_symbol) + else: + print("No float symbols found.") + + +def parse_args(): + """Parse the command line args.""" + parser = ArgumentParserWithDefaultHelp( + description="ELF floats checker", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + parser.add_argument( + "elf_file", + type=str, + help=( + "the Executable and Linkable Format (ELF) file to check" + " for floating point instruction inclusion." + ), + ) + + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="increase verbosity of status information.", + ) + + parser.set_defaults(func=check_action) + + args_namespace = parser.parse_args() + + # We want to fail gracefully, with a consistent + # help message, in the no argument case. + # So here's an obligatory hasattr hack. + if not hasattr(args_namespace, "func"): + parser.error("No arguments given!") + else: + return args_namespace + + +def run_elf_floats_checker(): + """Application main algorithm.""" + args = parse_args() + + set_log_verbosity(args.verbose) + + log.debug("Starting elf-floats-checker") + log.debug("Command line arguments:{}".format(args)) + + args.func(args) + + +if __name__ == "__main__": + """Run elf-floats-checker.""" + try: + run_elf_floats_checker() + except Exception as error: + print(error) diff --git a/tools/executable_analysis_tools/test_elf-float-checker.py b/tools/executable_analysis_tools/test_elf-float-checker.py new file mode 100644 index 0000000000..224dbb2296 --- /dev/null +++ b/tools/executable_analysis_tools/test_elf-float-checker.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 Arm Limited and Contributors. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Pytest for testing elf-float-checker.""" + +import importlib +import mock +import subprocess +from pathlib import Path + + +TARGET = importlib.import_module("elf-float-checker") + +SYMBOL_TABLE_WITHOUT_FLOATS = ( + " Symbol table '.symtab' contains 2723 entries:\n" + "Num: Value Size Type Bind Vis Ndx Name\n" + " 0: 00000000 0 NOTYPE LOCAL DEFAULT UND \n" + " 1: 000045fd 16 FUNC GLOBAL HIDDEN 3 lp_ticker_clear_interrupt\n" + " 2: 00004609 16 FUNC GLOBAL HIDDEN 3 __aeabi_uwrite4\n" + " 3: 00004615 16 FUNC GLOBAL HIDDEN 3 lp_ticker_fire_interrupt\n" + " 4: 00004625 36 FUNC GLOBAL HIDDEN 3 lp_ticker_free\n" + " 5: 00004645 8 FUNC GLOBAL HIDDEN 3 lp_ticker_get_info\n" + " 6: 0000464d 116 FUNC GLOBAL HIDDEN 3 __aeabi_lasr\n" + " 7: 000046bd 20 FUNC GLOBAL HIDDEN 3 lp_ticker_irq_handler\n" + " 8: 000046d1 16 FUNC GLOBAL HIDDEN 3 lp_ticker_read\n" + " 9: 000046e1 52 FUNC GLOBAL HIDDEN 3 __aeabi_lmul\n" +) + +FLOAT_SYMBOLS = [ + "__aeabi_cdcmpeq", + "__aeabi_cfcmpeq", + "__aeabi_f2iz", + "__aeabi_h2f_alt", + "__aeabi_i2d", + "__aeabi_d2iz", + "__aeabi_i2f", +] + +SYMBOL_TABLE_WITH_FLOATS = ( + " Symbol table '.symtab' contains 2723 entries:\n" + "Num: Value Size Type Bind Vis Ndx Name\n" + f" 0: 00000000 0 NOTYPE LOCAL DEFAULT UND \n" + f" 1: 000045fd 16 FUNC GLOBAL HIDDEN 3 {FLOAT_SYMBOLS[0]}\n" + f" 2: 00004609 16 FUNC GLOBAL HIDDEN 3 lp_ticker_disable_interrupt\n" + f" 3: 00004615 16 FUNC GLOBAL HIDDEN 3 {FLOAT_SYMBOLS[1]}\n" + f" 4: 00004625 36 FUNC GLOBAL HIDDEN 3 {FLOAT_SYMBOLS[2]}\n" + f" 5: 00004645 8 FUNC GLOBAL HIDDEN 3 lp_ticker_get_info\n" + f" 6: 0000464d 116 FUNC GLOBAL HIDDEN 3 {FLOAT_SYMBOLS[3]}\n" + f" 7: 000046bd 20 FUNC GLOBAL HIDDEN 3 lp_ticker_irq_handler\n" + f" 8: 000046d1 16 FUNC GLOBAL HIDDEN 3 {FLOAT_SYMBOLS[4]}\n" + f" 9: 000046e1 52 FUNC GLOBAL HIDDEN 3 {FLOAT_SYMBOLS[5]}\n" + f" 10: 000046f1 52 FUNC GLOBAL HIDDEN 3 {FLOAT_SYMBOLS[6]}\n" +) + + +ELF_FORMAT_FILE = "mbed-os-example.elf" +OBJECT_FILE_ANALYSIS_CMD = [*(TARGET.OBJECT_FILE_ANALYSIS_CMD), f"{ELF_FORMAT_FILE}"] + +class TestElfFloatChecker: + """Test class""" + + @classmethod + def setup_class(cls): + # Create a dummy ELF format file + Path(ELF_FORMAT_FILE).touch() + + @classmethod + def teardown_class(cls): + # Remove the dummy ELF format file + Path(ELF_FORMAT_FILE).unlink() + + @mock.patch("subprocess.run") + def test_correctly_detect_absence_of_float_symbols( + self, mock_subprocess_run + ): + """Test that no false positive occur.""" + mock_subprocess_run.return_value = subprocess.CompletedProcess( + args=( + f"{OBJECT_FILE_ANALYSIS_CMD}" + " check=True, stderr=-2, stdin=None, stdout=-1" + ), + returncode=0, + stdout=SYMBOL_TABLE_WITHOUT_FLOATS.encode(), + stderr=None, + ) + assert [] == TARGET.check_float_symbols(ELF_FORMAT_FILE) + mock_subprocess_run.assert_called_with( + OBJECT_FILE_ANALYSIS_CMD, + check=True, + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + @mock.patch("subprocess.run") + def test_correctly_detect_presence_of_float_symbols( + self, mock_subprocess_run + ): + """Test that float symbols can be discovered in a symbol table.""" + mock_subprocess_run.return_value = subprocess.CompletedProcess( + args=( + f"{OBJECT_FILE_ANALYSIS_CMD}" + " check=True, stderr=-2, stdin=None, stdout=-1" + ), + returncode=0, + stdout=SYMBOL_TABLE_WITH_FLOATS.encode(), + stderr=None, + ) + assert FLOAT_SYMBOLS == TARGET.check_float_symbols( + ELF_FORMAT_FILE + ) + mock_subprocess_run.assert_called_with( + OBJECT_FILE_ANALYSIS_CMD, + check=True, + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + )