diff --git a/bin/frama-c-script b/bin/frama-c-script index 0179f39113f1bc5b953a841c2fc23a41f0d5fc72..022f9b140965db5b7d07cff1ccfba1ff712386b2 100755 --- a/bin/frama-c-script +++ b/bin/frama-c-script @@ -31,12 +31,44 @@ usage() { echo "" echo " where cmd is:" echo "" + echo " - configure machdep" + echo " Runs an existing configure script to only consider files" + echo " in Frama-C's libc; this will hopefully disable non-essential" + echo " and non-POSIX external libraries." + echo " (run 'frama-c -machdep help' to get the list of machdeps)." + echo "" + echo " - estimate-difficulty file..." + echo " Applies several heuristics to try and estimate the difficulty" + echo " of analyzing the specified files with Frama-C." + echo "" + echo " - find-fun function-name [dir...]" + echo " Lists files in dir... declaring or defining function-name" + echo " [default: PWD /usr/include]." + echo " Heuristics-based: neither correct nor complete." + echo "" + echo " - flamegraph flamegraph.txt [dir]" + echo " Generates flamegraph.svg and flamegraph.html in dir" + echo " [default: FRAMAC_SESSION]." + echo " Also opens it in a browser, unless variable NOGUI is set." + echo "" echo " - help" - echo " Display this help message and exit." + echo " Displays this help message and exits." echo "" - echo " - make-template [dir]" - echo " Interactively prepares a template for analyses," - echo " writing it to dir/GNUmakefile [default: .frama-c]." + echo " - heuristic-detect-recursion file..." + echo " Uses a heuristic, syntactic-based, callgraph to detect recursive" + echo " calls. Results are guaranteed neither correct nor complete." + echo "" + echo " - heuristic-list-functions want_defs want_decls file..." + echo " Uses heuristics to find possible function definitions and declarations." + echo " If [want_defs] is true, lists definitions." + echo " If [want_decls] is true, lists declarations." + echo " Results are guaranteed neither correct nor complete." + echo "" + echo " - heuristic-print-callgraph [--dot outfile] file..." + echo " Prints a heuristic, syntactic-based, callgraph for the" + echo " specified files. Use --dot outfile to print it in DOT" + echo " (Graphviz) format, to [outfile]. If [outfile] is '-'," + echo " prints to stdout." echo "" echo " - list-files [path/to/compile_commands.json]" echo " Lists all sources in the given compile_commands.json" @@ -49,26 +81,9 @@ usage() { echo " definitions, with source location and number of statements." echo " Accepts Frama-C options (e.g. -cpp-extra-args for parsing)." echo "" - echo " - flamegraph flamegraph.txt [dir]" - echo " Generates flamegraph.svg and flamegraph.html in dir" - echo " [default: FRAMAC_SESSION]." - echo " Also opens it in a browser, unless variable NOGUI is set." - echo "" - echo " - find-fun function-name [dir...]" - echo " Lists files in dir... declaring or defining function-name" - echo " [default: PWD /usr/include]." - echo " Heuristics-based: neither correct nor complete." - echo "" - echo " - summary [options]" - echo " Monitors and summarizes multiple analyses dispatched by a Makefile" - echo " in the current PWD." - echo " Use $0 summary --help for more informations." - echo "" - echo " - configure machdep" - echo " Runs an existing configure script to only consider files" - echo " in Frama-C's libc; this will hopefully disable non-essential" - echo " and non-POSIX external libraries." - echo " (run 'frama-c -machdep help' to get the list of machdeps)." + echo " - make-template [dir]" + echo " Interactively prepares a template for analyses," + echo " writing it to dir/GNUmakefile [default: .frama-c]." echo "" echo " - make-wrapper target arg..." echo " Runs 'make target arg...', parsing the output to suggest" @@ -78,6 +93,11 @@ usage() { echo " Applies some transformations to an existing compile_commands.json" echo " (such as relativizing paths) to improve portability." echo " [default: ./compile_commands.json]" + echo "" + echo " - summary [options]" + echo " Monitors and summarizes multiple analyses dispatched by a Makefile" + echo " in the current PWD." + echo " Use $0 summary --help for more informations." exit "$1" } @@ -215,6 +235,22 @@ case "$command" in shift; configure_for_frama_c "$@"; ;; + "heuristic-print-callgraph") + shift; + ${FRAMAC_SHARE}/analysis-scripts/print_callgraph.py "$@"; + ;; + "heuristic-detect-recursion") + shift; + ${FRAMAC_SHARE}/analysis-scripts/detect_recursion.py "$@"; + ;; + "heuristic-list-functions") + shift; + ${FRAMAC_SHARE}/analysis-scripts/heuristic_list_functions.py "$@"; + ;; + "estimate-difficulty") + shift; + ${FRAMAC_SHARE}/analysis-scripts/estimate_difficulty.py "$@"; + ;; "make-wrapper") shift; "${FRAMAC_SHARE}"/analysis-scripts/make_wrapper.py "$0" "$@"; diff --git a/headers/header_spec.txt b/headers/header_spec.txt index 4c33aba539b7f8afe6bf137ad3e8d23e07830666..fefab89f71c3fecec85cc19c091c4b28258040b3 100644 --- a/headers/header_spec.txt +++ b/headers/header_spec.txt @@ -204,6 +204,7 @@ share/libc/__fc_define_wint_t.h: CEA_LGPL share/libc/__fc_gcc_builtins.h: CEA_LGPL share/libc/__fc_inet.h: CEA_LGPL share/libc/__fc_integer.h: CEA_LGPL +share/libc/__fc_libc.h: CEA_LGPL share/libc/__fc_machdep.h: CEA_LGPL share/libc/__fc_machdep_linux_shared.h: CEA_LGPL share/libc/__fc_runtime.c: CEA_LGPL diff --git a/share/analysis-scripts/build_callgraph.py b/share/analysis-scripts/build_callgraph.py new file mode 100755 index 0000000000000000000000000000000000000000..b53e0d33bb133f15413b096c368f0cc8701d3df2 --- /dev/null +++ b/share/analysis-scripts/build_callgraph.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +#-*- coding: utf-8 -*- +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2021 # +# CEA (Commissariat à l'énergie atomique et aux énergies # +# alternatives) # +# # +# you can redistribute it and/or modify it under the terms of the GNU # +# Lesser General Public License as published by the Free Software # +# Foundation, version 2.1. # +# # +# It is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU Lesser General Public License for more details. # +# # +# See the GNU Lesser General Public License version 2.1 # +# for more details (enclosed in the file licenses/LGPLv2.1). # +# # +########################################################################## + +# This script finds files containing likely declarations and definitions +# for a given function name, via heuristic syntactic matching. + +import sys +import os +import re +import function_finder + +MIN_PYTHON = (3, 5) +if sys.version_info < MIN_PYTHON: + sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) + +arg = "" +if len(sys.argv) < 2: + print(f"usage: {sys.argv[0]} file...") + print(" builds a heuristic callgraph for the specified files.") + sys.exit(1) +else: + files = sys.argv[1:] + +debug = os.getenv("DEBUG") + +class Callgraph: + """ + Heuristics-based callgraphs. + Nodes are function names. Edges (caller, callee, locations) contain the source + and target nodes, plus a list of locations (file, line) where calls from + [caller] to [callee] occur. + """ + + # maps each caller to the list of its callees + succs = {} + + # maps (caller, callee) to the list of call locations + edges = {} + + def add_edge(self, caller, callee, loc): + if (caller, callee) in self.edges: + # edge already exists + self.edges[(caller, callee)].append(loc) + else: + # new edge: check if caller exists + if not caller in self.succs: + self.succs[caller] = [] + # add callee as successor of caller + self.succs[caller].append(callee) + # add call location to edge information + self.edges[(caller, callee)] = [loc] + + def nodes(self): + return self.succs.keys() + + def __repr__(self): + return f"Callgraph({self.succs}, {self.edges})" + +def compute(files): + #print(f"Computing callgraph for {len(files)} file(s)...") + cg = Callgraph() + for f in files: + #print(f"Processing {os.path.relpath(f)}...") + newlines = function_finder.compute_newline_offsets(f) + defs = function_finder.find_definitions_and_declarations(True, False, f, newlines) + calls = function_finder.find_calls(f, newlines) + for call in calls: + caller = function_finder.find_caller(defs, call) + if caller: + called, line, _ = call + loc = (f, line) + if debug: + print(f"build_callgraph: {f}:{line}: {caller} -> {called}") + cg.add_edge(caller, called, loc) + #print(f"Callgraph computed ({len(cg.succs)} node(s), {len(cg.edges)} edge(s))") + return cg + +def print_edge(cg, caller, called, padding="", end="\n"): + locs = cg.edges[(caller, called)] + for (filename, line) in locs: + print(f"{padding}{os.path.relpath(filename)}:{line}: {caller} -> {called}", end=end) + +def print_cg(cg): + for (caller, called) in cg.edges: + print_edge(cg, caller, called) + +# note: out _must_ exist (the caller must create it if needed) +def print_cg_dot(cg, out=sys.stdout): + print("digraph callgraph {", file=out) + for (caller, called) in cg.edges: + print(f" {caller} -> {called};", file=out) + print("}", file=out) + +# succs: dict, input, not modified +# visited: set, input-output, modified +# just_visited: set, input-output, modified +# n: input, not modified +# +# The difference between visited and just_visited is that the latter refers +# to the current bfs; nodes visited in previous bfs already had their cycles +# reported, so we do not report them multiple times. +def cycle_bfs(cg, visited, just_visited, n): + if debug: + print(f"cycle_bfs: visited = {visited}, just_visited = {just_visited}, n = {n}") + just_visited.add(n) + if n not in cg.succs: + return [] + fifo = [] + for succ in cg.succs[n]: + if succ in just_visited: + return [(n, succ)] + elif succ in visited: + # already reported in a previous iteration + continue + fifo.append(succ) + # no cycle found with direct successors, go to next level + for succ in fifo: + my_just_visited = just_visited.copy() + res = cycle_bfs(cg, visited, my_just_visited, succ) + if res: + # note that other cycles may not be reported + caller = res[0][0] + return [(n, caller)] + res + return [] + +def compute_recursive_cycles(cg, acc): + to_visit = set(cg.nodes()) + if not to_visit: # empty graph -> no recursion + return + visited = set() + while to_visit: + just_visited = set() + n = sorted(list(to_visit))[0] + cycle = cycle_bfs(cg, visited, just_visited, n) + visited = visited.union(just_visited) + if cycle: + (fst, snd) = cycle[0] + cycle_start_loc = cg.edges[(fst, snd)][0] + acc.append((cycle_start_loc, cycle)) + to_visit -= visited + +def detect_recursion(cg): + to_visit = set(cg.nodes()) + if not to_visit: # empty graph -> no recursion + return False + visited = set() + has_cycle = False + while to_visit: + just_visited = set() + n = sorted(list(to_visit))[0] + cycle = cycle_bfs(cg, visited, just_visited, n) + visited = visited.union(just_visited) + if cycle: + has_cycle = True + print(f"recursive cycle detected: ") + for (caller, called) in cycle: + print_edge(cg, caller, called, padding=" ") + to_visit -= visited + if not has_cycle: + print(f"no recursive calls detected") diff --git a/share/analysis-scripts/detect_recursion.py b/share/analysis-scripts/detect_recursion.py new file mode 100755 index 0000000000000000000000000000000000000000..332d80f3b8ca50eb555c5100b68d96dc7c880e4f --- /dev/null +++ b/share/analysis-scripts/detect_recursion.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +#-*- coding: utf-8 -*- +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2021 # +# CEA (Commissariat à l'énergie atomique et aux énergies # +# alternatives) # +# # +# you can redistribute it and/or modify it under the terms of the GNU # +# Lesser General Public License as published by the Free Software # +# Foundation, version 2.1. # +# # +# It is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU Lesser General Public License for more details. # +# # +# See the GNU Lesser General Public License version 2.1 # +# for more details (enclosed in the file licenses/LGPLv2.1). # +# # +########################################################################## + +# This script finds files containing likely declarations and definitions +# for a given function name, via heuristic syntactic matching. + +import sys +import build_callgraph + +MIN_PYTHON = (3, 5) # for glob(recursive) +if sys.version_info < MIN_PYTHON: + sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) + +arg = "" +if len(sys.argv) < 2: + print(f"usage: {sys.argv[0]} [file1 file2 ...]") + print(" prints a heuristic callgraph for the specified files.") + sys.exit(1) +else: + files = sys.argv[1:] + +cg = build_callgraph.compute(files) +build_callgraph.detect_recursion(cg) diff --git a/share/analysis-scripts/estimate_difficulty.py b/share/analysis-scripts/estimate_difficulty.py new file mode 100755 index 0000000000000000000000000000000000000000..dab975aabac5e7bf03d0de6a7f20bb4adac0a367 --- /dev/null +++ b/share/analysis-scripts/estimate_difficulty.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +#-*- coding: utf-8 -*- +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2021 # +# CEA (Commissariat à l'énergie atomique et aux énergies # +# alternatives) # +# # +# you can redistribute it and/or modify it under the terms of the GNU # +# Lesser General Public License as published by the Free Software # +# Foundation, version 2.1. # +# # +# It is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU Lesser General Public License for more details. # +# # +# See the GNU Lesser General Public License version 2.1 # +# for more details (enclosed in the file licenses/LGPLv2.1). # +# # +########################################################################## + +# This script uses several heuristics to try and estimate the difficulty +# of analyzing a new code base with Frama-C. + +import argparse +import build_callgraph +import function_finder +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile + +#TODO : avoid relativizing paths when introducing too many ".." ; +#TODO : accept directory as argument (--full-tree), and then do glob **/*.{c,i} inside +#TODO : try to check the presence of compiler builtins +#TODO : try to check for pragmas + +MIN_PYTHON = (3, 5) +if sys.version_info < MIN_PYTHON: + sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) + +parser = argparse.ArgumentParser(description=""" +Estimates the difficulty of analyzing a given code base""") +parser.add_argument("--header-dirs", "-d", metavar='DIR', nargs='+', + help='directories containing headers (default: .frama-c)') +parser.add_argument("files", nargs='+', help='source files') +args = vars(parser.parse_args()) + +header_dirs = args["header_dirs"] +if not header_dirs: + header_dirs = [] +files = args["files"] + +# gather information from several sources + +def extract_keys(l): + return [list(key.keys())[0] for key in l] + +def get_framac_libc_function_statuses(framac, framac_share): + (_metrics_handler, metrics_tmpfile) = tempfile.mkstemp(prefix="fc_script_est_diff", suffix=".json") + if debug: + print(f"metrics_tmpfile: {metrics_tmpfile}") + fc_runtime = framac_share / "libc" / "__fc_runtime.c" + fc_libc_headers = framac_share / "libc" / "__fc_libc.h" + subprocess.run([framac, "-no-autoload-plugins", fc_runtime, fc_libc_headers, + "-load-module", "metrics", "-metrics", "-metrics-libc", + "-metrics-output", metrics_tmpfile], stdout=subprocess.DEVNULL, check=True) + with open(metrics_tmpfile) as f: + metrics_json = json.load(f) + os.remove(metrics_tmpfile) + defined = extract_keys(metrics_json["defined-functions"]) + spec_only = extract_keys(metrics_json["specified-only-functions"]) + return (defined, spec_only) + +re_include = re.compile(r'\s*#\s*include\s*("|<)([^">]+)("|>)') +def grep_includes_in_file(file): + with open(file, "r", encoding="utf-8", errors='ignore') as f: + i = 0 + for line in f.readlines(): + i += 1 + m = re_include.match(line) + if m: + kind = m.group(1) + header = m.group(2) + yield((i,kind,header)) + +def get_includes(files): + quote_includes = {} + chevron_includes = {} + for filename in files: + for line, kind, header in grep_includes_in_file(filename): + if kind == '<': + includes = chevron_includes[header] if header in chevron_includes else [] + else: + includes = quote_includes[header] if header in quote_includes else [] + includes.append((filename, line)) + if kind == '<': + chevron_includes[header] = includes + else: + quote_includes[header] = includes + return chevron_includes, quote_includes + +debug = os.getenv("DEBUG") +verbose = False + +print("Building callgraph...") +cg = build_callgraph.compute(files) + +framac_bin = os.getenv('FRAMAC_BIN') +if not framac_bin: + sys.exit("error: FRAMAC_BIN not in environment") +framac = Path(framac_bin) / "frama-c" + +framac_share = Path(subprocess.check_output([framac, '-no-autoload-plugins', '-print-share-path']).decode()) + +print("Computing data about libc/POSIX functions...") +libc_defined_functions, libc_specified_functions = get_framac_libc_function_statuses(framac, framac_share) + +with open(framac_share / "compliance" / "c11_functions.json") as f: + c11_functions = json.load(f)["data"] + +with open(framac_share / "compliance" / "c11_headers.json") as f: + c11_headers = json.load(f)["data"] + +with open(framac_share / "compliance" / "posix_identifiers.json") as f: + all_data = json.load(f) + posix_identifiers = all_data["data"] + posix_headers = all_data["headers"] + +recursive_cycles = [] +reported_recursive_pairs = set() +build_callgraph.compute_recursive_cycles(cg, recursive_cycles) +for (cycle_start_loc, cycle) in recursive_cycles: + # Note: in larger code bases, many cycles are reported for the same final + # function (e.g. for the calls 'g -> g', we may have 'f -> g -> g', + # 'h -> g -> g', etc; to minimize this, we print just the first one. + # This does not prevent 3-cycle repetitions, such as 'f -> g -> f', + # but these are less common. + if cycle[-1] in reported_recursive_pairs: + continue + reported_recursive_pairs.add(cycle[-1]) + (filename, line) = cycle_start_loc + (x, y) = cycle[0] + pretty_cycle = f"{x} -> {y}" + for (x, y) in cycle[1:]: + pretty_cycle += f" -> {y}" + print(f"[recursion] found recursive cycle near {filename}:{line}: {pretty_cycle}") + +callees = [callee for (_, callee) in list(cg.edges.keys())] +callees = set(callees) +used_headers = set() +print(f"Estimating difficulty for {len(callees)} function calls...") +warnings = 0 + +for callee in sorted(callees): + def callee_status(status, standard, reason): + global warnings + if status == "warning": + warnings += 1 + if verbose or debug or status == "warning": + print(f"- {status}: {callee} ({standard}) {reason}") + #print(f"callee: {callee}") + if callee in posix_identifiers: + used_headers.add(posix_identifiers[callee]["header"]) + if callee in c11_functions: + standard = "C11" + # check that the callee is not a macro or type (e.g. va_arg); + # a few functions, such as strcpy_s, are in C11 but not in POSIX, + # so we must test membership before checking the POSIX type + if callee in posix_identifiers and posix_identifiers[callee]["id_type"] != "function": + continue + #print(f"C11 function: {callee}") + if callee in libc_specified_functions: + callee_status("good", standard, "is specified in Frama-C's libc") + elif callee in libc_defined_functions: + callee_status("ok", standard, "is defined in Frama-C's libc") + else: + # Some functions without specification are actually variadic + # (and possibly handled by the Variadic plug-in) + if callee in posix_identifiers and "notes" in posix_identifiers[callee] and "variadic-plugin" in posix_identifiers[callee]["notes"]: + callee_status("ok", standard, "is handled by the Variadic plug-in") + else: + callee_status("warning", standard, "has neither code nor spec in Frama-C's libc") + elif callee in posix_identifiers: + standard = "POSIX" + # check that the callee is not a macro or type (e.g. va_arg) + if posix_identifiers[callee]["id_type"] != "function": + continue + #print(f"Non-C11, POSIX function: {callee}") + if callee in libc_specified_functions: + callee_status("good", standard, "specified in Frama-C's libc") + elif callee in libc_defined_functions: + callee_status("ok", standard, "defined in Frama-C's libc") + else: + callee_status("warning", standard, "has neither code nor spec in Frama-C's libc") +print(f"Function-related warnings: {warnings}") + +if (verbose or debug) and used_headers: + print(f"used headers:") + for header in sorted(used_headers): + print(f" <{header}>") + +(chevron_includes, quote_includes) = get_includes(files) + +def is_local_header(header_dirs, header): + for d in header_dirs: + path = Path(d) + if Path(path / header).exists(): + return True + return False + +print(f"Estimating difficulty for {len(chevron_includes)} '#include <header>' directives...") +non_posix_headers = [] +for header in sorted(chevron_includes, key=str.casefold): + if header in posix_headers: + fc_support = posix_headers[header]["fc-support"] + if fc_support == "unsupported": + print(f"- WARNING: included header <{header}> is explicitly unsupported by Frama-C") + elif fc_support == "none": + print(f"- warning: included header <{header}> not currently included in Frama-C's libc") + else: + if verbose or debug: + c11_or_posix = "C11" if header in c11_headers else "POSIX" + print(f"- note: included {c11_or_posix} header ") + else: + if is_local_header(header_dirs, header): + if verbose or debug: + print(f"- ok: included header <{header}> seems to be available locally") + else: + non_posix_headers.append(header) + print(f"- warning: included non-POSIX header <{header}>") +print(f"Header-related warnings: {len(non_posix_headers)}") + + +# dynamic allocation + +dynalloc_functions = set(["malloc", "calloc", "free", "realloc", "alloca", "mmap"]) +dyncallees = dynalloc_functions.intersection(callees) +if dyncallees: + print(f"- note: calls to dynamic allocation functions: {', '.join(sorted(dyncallees))}") + + +# unsupported C11-specific features + +c11_unsupported = ["_Alignas", "_Alignof", "_Generic", "_Static_assert"] + +for keyword in c11_unsupported: + out = subprocess.Popen(["grep", "-n", '\\b' + keyword + '\\b'] + files + ["/dev/null"], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + lines = out.communicate()[0].decode('utf-8').splitlines() + if lines: + n = len(lines) + print(f"- warning: found {n} line{'s' if n > 1 else ''} with occurrences of unsupported C11 construct '{keyword}'") + +# assembly code + +if "asm" in callees or "__asm" in callees or "__asm__" in callees: + print(f"- warning: code seems to contain inline assembly ('asm(...)')") + +# TODO: +# - detect absence of 'main' function (library) diff --git a/share/analysis-scripts/find_fun.py b/share/analysis-scripts/find_fun.py index 18588b293bd0452317f4ae72f90e8b396775c3a1..e50d3d3f287e7caa95998cc614ee3a82e8e9ff5b 100755 --- a/share/analysis-scripts/find_fun.py +++ b/share/analysis-scripts/find_fun.py @@ -73,9 +73,9 @@ print("Looking for '%s' inside %d file(s)..." % (fname, len(files))) possible_declarators = [] possible_definers = [] -re_fun = function_finder.prepare(fname) +re_fun = function_finder.prepare_re_specific_name(fname) for f in files: - found = function_finder.find(re_fun, f) + found = function_finder.find_specific_name(re_fun, f) if found: if found == 1: possible_declarators.append(f) @@ -95,7 +95,7 @@ else: relative_path = relative_path_to(reldir) if possible_declarators != []: print(f"Possible declarations for function '{fname}' in the following file(s){reldir_msg}:") - print(" " + "\n ".join([os.path.relpath(path, start=reldir) for path in possible_declarators])) + print(" " + "\n ".join(sorted([os.path.relpath(path, start=reldir) for path in possible_declarators]))) if possible_definers != []: print(f"Possible definitions for function '{fname}' in the following file(s){reldir_msg}:") - print(" " + "\n ".join([os.path.relpath(path, start=reldir) for path in possible_definers])) + print(" " + "\n ".join(sorted([os.path.relpath(path, start=reldir) for path in possible_definers]))) diff --git a/share/analysis-scripts/function_finder.py b/share/analysis-scripts/function_finder.py old mode 100644 new mode 100755 index ee43f29adfb7ac6b75544622045a75cbceb5b003..e68f15ee479361b58be8bbea9284b2538b0130e5 --- a/share/analysis-scripts/function_finder.py +++ b/share/analysis-scripts/function_finder.py @@ -4,7 +4,7 @@ # # # This file is part of Frama-C. # # # -# Copyright (C) 2007-2020 # +# Copyright (C) 2007-2021 # # CEA (Commissariat à l'énergie atomique et aux énergies # # alternatives) # # # @@ -24,6 +24,8 @@ # Exports find_function_in_file, to be used by other scripts +import bisect +import os import re # To minimize the amount of false positives, we try to match the following: @@ -37,18 +39,27 @@ import re # - identifiers are allowed after the parentheses, to allow for some macros/ # modifiers +# auxiliary regexes +c_identifier = "[a-zA-Z_][a-zA-Z0-9_]*" +c_id_maybe_pointer = c_identifier + "\**" +optional_c_id = "(?:" + c_identifier + ")?" +non_empty_whitespace = "[ \t\r\n]+" # includes newline/CR +whitespace = "[ \t\r\n]*" # includes newline/CR +type_prefix = c_id_maybe_pointer + "(?:\s+\**" + c_id_maybe_pointer + ")*" + non_empty_whitespace + "\**" +optional_type_prefix = "(?:" + type_prefix + whitespace + ")?" +argument_list = "\([^)]*\)" + +debug = bool(os.getenv("DEBUG", False)) + # Precomputes the regex for 'fname' -def prepare(fname): - c_identifier = "[a-zA-Z_][a-zA-Z0-9_]*" - c_id_maybe_pointer = c_identifier + "\**" - type_prefix = c_id_maybe_pointer + "(?:\s+\**" + c_id_maybe_pointer + ")*\s+\**" - parentheses_suffix = "\s*\([^)]*\)" - re_fun = re.compile("^(?:" + type_prefix + "\s*)?" + fname + parentheses_suffix - + "\s*(?:" + c_identifier + ")?\s*(;|{)", flags=re.MULTILINE) +def prepare_re_specific_name(fname): + re_fun = re.compile("^" + optional_type_prefix + fname + whitespace + + argument_list + whitespace + + optional_c_id + whitespace + "(;|{)", flags=re.DOTALL | re.MULTILINE) return re_fun # Returns 0 if not found, 1 if declaration, 2 if definition -def find(prepared_re, f): +def find_specific_name(prepared_re, f): with open(f, encoding="ascii", errors='ignore') as content_file: content = content_file.read() has_decl_or_def = prepared_re.search(content) @@ -57,3 +68,145 @@ def find(prepared_re, f): else: is_decl = has_decl_or_def.group(1) == ";" return 1 if is_decl else 2 + + +# matches function definitions +re_fundef_or_decl = re.compile("^" + optional_type_prefix + + "(" + c_identifier + ")" + whitespace + + argument_list + whitespace + + optional_c_id + whitespace + "(;|{)", + flags=re.DOTALL | re.MULTILINE) + +# matches function calls +re_funcall = re.compile("(" + c_identifier + ")" + whitespace + "\(") + +# Computes the offset (in bytes) of each '\n' in the file, +# returning them as a list +def compute_newline_offsets(filename): + offsets = [] + current = 0 + with open(filename, encoding="ascii", errors='ignore') as data: + for line in data: + current += len(line) + offsets.append(current) + return offsets + +# Returns the line number (starting at 1) containing the character +# of offset [offset]. +# [offsets] is the sorted list of offsets for newline characters in the file. +def line_of_offset(offsets, offset): + i = bisect.bisect_right(offsets, offset) + return i+1 + +# Returns the line number (starting at 1) of each line starting with '}' +# as its first character. +# +# This is a heuristic to attempt to detect function closing braces: +# it assumes that the first '}' (without preceding whitespace) after a +# function definition denotes its closing brace. +def compute_closing_braces(filename): + braces = [] + with open(filename, encoding="ascii", errors='ignore') as data: + for i, line in enumerate(data, start=1): + if line.startswith("}"): + braces.append(i) + return braces + +# Returns the first element of [line_numbers] greater than [n], or [None] +# if all numbers are smaller than [n] (this may happen e.g. when no +# closing braces were found). +# +# [line_numbers] must be sorted in ascending order. +def get_first_line_after(line_numbers, n): + for line in line_numbers: + if line > n: + return line + return None + +# Returns a list of tuples (fname, is_def, line_start, line_end, terminator_offset) +# for each function definition or declaration. +# If [want_defs] is True, definitions are included. +# If [want_decls] is True, declarations are included. +# [terminator_offset] is the byte offset of the `{` or `;`. +# The list is sorted w.r.t. line numbers (in ascending order). +# +# [terminator_offset] is used by the caller to filter the function prototype +# itself and avoid considering it as a call. For function definitions, +# this is the opening brace; for function declarations, this is the semicolon. +def find_definitions_and_declarations(want_defs, want_decls, filename, newlines): + braces = compute_closing_braces(filename) + with open(filename, encoding="ascii", errors='ignore') as data: + content = data.read() + res = [] + for match in re.finditer(re_fundef_or_decl, content): + funcname = match.group(1) + is_def = match.group(2) == "{" + is_decl = match.group(2) == ";" + assert is_def or is_decl + start = line_of_offset(newlines, match.start(1)) + if is_decl: + if not want_decls: + continue + end = line_of_offset(newlines, match.start(2)) + else: + if not want_defs: + continue + definition = content[match.start(1):newlines[start-1]] + # try "single-line function heuristic": + # assume the function is defined as 'type f(...) { code; }', + # in a single line + if definition.strip().endswith("}"): + end = line_of_offset(newlines, match.start(2)) + else: + end = get_first_line_after(braces, start) + if not end: + # no closing braces found; try again the "single-line function heuristic" + def_start_newline_offset = newlines[start-1] + line_of_opening_brace = line_of_offset(newlines, match.start(2)) + if start == line_of_opening_brace and definition.rstrip().endswith("}"): + # assume the '}' is closing the '{' from the same line + end = line_of_opening_brace + else: + # no opening brace; assume a false positive and skip definition + print(f"{os.path.relpath(filename)}:{start}:closing brace not found, " + + f"skipping potential definition of '{funcname}'") + continue + terminator_offset = match.start(2) + if debug: + print(f"function_finder: {'def' if is_def else 'decl'} of {funcname} between {start} and {end}") + res.append((funcname, is_def, start, end, terminator_offset)) + return res + +# list of identifiers which are never function calls +calls_blacklist = ["if", "while", "for", "return", "sizeof", "switch", "_Alignas"] + +# Returns a list of tuples (fname, line, offset) for each function call. +# +# Note: this may include the function prototype itself; +# it must be filtered by the caller. +def find_calls(filename, newlines): + with open(filename, encoding="ascii", errors='ignore') as data: + content = data.read() + # create a list of Match objects that fit "pattern" regex + res = [] + for match in re.finditer(re_funcall, content): + funcname = match.group(1) + offset = match.start(1) + line = line_of_offset(newlines, offset) + if funcname not in calls_blacklist: + res.append((funcname, line, offset)) + return res + +# Returns the caller of [call], that is, the function whose definition +# contains the line where [call] happens. +# Returns [None] if there is no function at such line (i.e. a false positive). +# +# [defs] must be sorted in ascending order. +def find_caller(defs, call): + (called, line, offset) = call + for (fname, _is_def, start, end, brace_offset) in defs: + if line >= start and line <= end and offset > brace_offset: + return fname + elif start > line: + return None + return None diff --git a/share/analysis-scripts/heuristic_list_functions.py b/share/analysis-scripts/heuristic_list_functions.py new file mode 100755 index 0000000000000000000000000000000000000000..ceee3fd5a3cf2892f1ed5c8fc8de5b8e76ce3c82 --- /dev/null +++ b/share/analysis-scripts/heuristic_list_functions.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +#-*- coding: utf-8 -*- +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2021 # +# CEA (Commissariat à l'énergie atomique et aux énergies # +# alternatives) # +# # +# you can redistribute it and/or modify it under the terms of the GNU # +# Lesser General Public License as published by the Free Software # +# Foundation, version 2.1. # +# # +# It is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU Lesser General Public License for more details. # +# # +# See the GNU Lesser General Public License version 2.1 # +# for more details (enclosed in the file licenses/LGPLv2.1). # +# # +########################################################################## + +# This script uses heuristics to list all function definitions and +# declarations in a set of files. + +import sys +import os +import re +import function_finder + +MIN_PYTHON = (3, 5) # for glob(recursive) +if sys.version_info < MIN_PYTHON: + sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) + +debug = bool(os.getenv("DEBUG", False)) + +arg = "" +if len(sys.argv) < 4: + print("usage: %s want_defs want_decls file..." % sys.argv[0]) + print(" looks for likely function definitions and/or declarations") + print(" in the specified files.") + sys.exit(1) + +def boolish_string(s): + if s.lower() == "true" or s == "1": + return True + if s.lower() == "false" or s == "0": + return False + sys.exit(f"error: expected 'true', 'false', 0 or 1; got: {s}") + +want_defs = boolish_string(sys.argv[1]) +want_decls = boolish_string(sys.argv[2]) +files = sys.argv[3:] + +for f in files: + newlines = function_finder.compute_newline_offsets(f) + defs_and_decls = function_finder.find_definitions_and_declarations(want_defs, want_decls, f, newlines) + for (funcname, is_def, start, end, _offset) in defs_and_decls: + if is_def: + print(f"{os.path.relpath(f)}:{start}:{end}: {funcname} (definition)") + else: + print(f"{os.path.relpath(f)}:{start}:{end}: {funcname} (declaration)") diff --git a/share/analysis-scripts/make_template.py b/share/analysis-scripts/make_template.py index c986e4551c446b47caff148e869d65ef1713c58d..6e47b60c60993be74bbc2d40a32e9cd45150f880 100755 --- a/share/analysis-scripts/make_template.py +++ b/share/analysis-scripts/make_template.py @@ -98,11 +98,11 @@ main = input("Main target name: ") if not re.match("^[a-zA-Z_0-9-]+$", main): sys.exit("error: invalid main target name (can only contain letters, digits, dash or underscore)") -main_fun_finder_re = function_finder.prepare("main") +main_fun_finder_re = function_finder.prepare_re_specific_name("main") # returns 0 if none, 1 if declaration, 2 if definition def defines_or_declares_main(f): - return function_finder.find(main_fun_finder_re, f) + return function_finder.find_specific_name(main_fun_finder_re, f) def expand_and_normalize_sources(expression, relprefix): subexps = shlex.split(expression) diff --git a/share/analysis-scripts/print_callgraph.py b/share/analysis-scripts/print_callgraph.py new file mode 100755 index 0000000000000000000000000000000000000000..9c5451669dc0955757608d50db117bb2249b7923 --- /dev/null +++ b/share/analysis-scripts/print_callgraph.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +#-*- coding: utf-8 -*- +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2021 # +# CEA (Commissariat à l'énergie atomique et aux énergies # +# alternatives) # +# # +# you can redistribute it and/or modify it under the terms of the GNU # +# Lesser General Public License as published by the Free Software # +# Foundation, version 2.1. # +# # +# It is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU Lesser General Public License for more details. # +# # +# See the GNU Lesser General Public License version 2.1 # +# for more details (enclosed in the file licenses/LGPLv2.1). # +# # +########################################################################## + +# This script finds files containing likely declarations and definitions +# for a given function name, via heuristic syntactic matching. + +import sys +import build_callgraph + +MIN_PYTHON = (3, 5) # for glob(recursive) +if sys.version_info < MIN_PYTHON: + sys.exit("Python %s.%s or later is required.\n" % MIN_PYTHON) + +dotfile = None +args = sys.argv[1:] +if "--dot" in args: + dotarg = args.index("--dot") + dotfile = args[dotarg+1] + args_before = args[:dotarg-1] if dotarg > 0 else [] + args_after = args[dotarg+2:] + args = args_before + args_after +if not args: + print(f"usage: {sys.argv[0]} [--dot outfile] file1 file2 ...") + print(" prints a heuristic callgraph for the specified files.") + print(" If --dot is specified, print in DOT (Graphviz) format") + print(" to file outfile, or to stdout if outfile is '-'.") + sys.exit(1) + +cg = build_callgraph.compute(args) +if dotfile: + if dotfile == "-": + out = sys.stdout + else: + out = open(dotfile, 'w') + build_callgraph.print_cg_dot(cg, out) + if dotfile != "-": + print(f"wrote {dotfile}") +else: + build_callgraph.print_cg(cg) diff --git a/share/libc/__fc_libc.h b/share/libc/__fc_libc.h new file mode 100644 index 0000000000000000000000000000000000000000..f9677ee2920415c3d8636d49de0dcd153fac5844 --- /dev/null +++ b/share/libc/__fc_libc.h @@ -0,0 +1,109 @@ +/**************************************************************************/ +/* */ +/* This file is part of Frama-C. */ +/* */ +/* Copyright (C) 2007-2021 */ +/* CEA (Commissariat à l'énergie atomique et aux énergies */ +/* alternatives) */ +/* */ +/* you can redistribute it and/or modify it under the terms of the GNU */ +/* Lesser General Public License as published by the Free Software */ +/* Foundation, version 2.1. */ +/* */ +/* It is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU Lesser General Public License for more details. */ +/* */ +/* See the GNU Lesser General Public License version 2.1 */ +/* for more details (enclosed in the file licenses/LGPLv2.1). */ +/* */ +/**************************************************************************/ + +// This file includes all compatible libc/POSIX/BSD headers known by the +// Frama-C standard library. It is used by some Frama-C scripts. + +#define _XOPEN_SOURCE 600 +#define _POSIX_C_SOURCE 200112L +#define _GNU_SOURCE 1 + +#include "alloca.h" +#include "arpa/inet.h" +#include "assert.h" +#include "byteswap.h" +#include "ctype.h" +#include "dirent.h" +#include "dlfcn.h" +#include "endian.h" +#include "errno.h" +#include "fcntl.h" +#include "features.h" +#include "fenv.h" +#include "float.h" +#include "fnmatch.h" +#include "ftw.h" +#include "getopt.h" +#include "glob.h" +#include "grp.h" +#include "iconv.h" +#include "ifaddrs.h" +#include "inttypes.h" +#include "iso646.h" +#include "libgen.h" +#include "limits.h" +#include "locale.h" +#include "malloc.h" +#include "math.h" +#include "memory.h" +#include "netdb.h" +#include "net/if.h" +#include "netinet/in.h" +#include "netinet/ip.h" +#include "netinet/tcp.h" +#include "nl_types.h" +#include "poll.h" +#include "pthread.h" +#include "pwd.h" +#include "regex.h" +#include "resolv.h" +#include "sched.h" +#include "semaphore.h" +#include "setjmp.h" +#include "signal.h" +#include "stdarg.h" +#include "stdbool.h" +#include "stddef.h" +#include "stdint.h" +#include "stdio.h" +#include "stdlib.h" +#include "string.h" +#include "strings.h" +#include "stropts.h" +#include "sys/file.h" +#include "sys/ioctl.h" +#include "sys/ipc.h" +#include "syslog.h" +#include "sys/mman.h" +#include "sys/param.h" +#include "sys/random.h" +#include "sys/resource.h" +#include "sys/select.h" +#include "sys/shm.h" +#include "sys/signal.h" +#include "sys/socket.h" +#include "sys/stat.h" +#include "sys/time.h" +#include "sys/times.h" +#include "sys/timex.h" +#include "sys/types.h" +#include "sys/uio.h" +#include "sys/un.h" +#include "sys/utsname.h" +#include "sys/wait.h" +#include "termios.h" +#include "time.h" +#include "unistd.h" +#include "utime.h" +#include "utmpx.h" +#include "wchar.h" +#include "wctype.h" diff --git a/tests/fc_script/build-callgraph.i b/tests/fc_script/build-callgraph.i new file mode 100644 index 0000000000000000000000000000000000000000..b8105502931616ce2861af2459303855112b449a --- /dev/null +++ b/tests/fc_script/build-callgraph.i @@ -0,0 +1,69 @@ +/* run.config + NOFRAMAC: testing frama-c-script, not frama-c itself + EXECNOW: LOG build-callgraph.res LOG build-callgraph.err bin/frama-c-script heuristic-print-callgraph @PTEST_DIR@/@PTEST_NAME@.i > @PTEST_DIR@/result/build-callgraph.res 2> @PTEST_DIR@/result/build-callgraph.err + */ + +#include <stdio.h> +void main() { + strlen(""); +} +struct s { + int a; int b; +} s; + +volatile int v; + +int fn2(int, int); + +int fn1(int x, int y) +{ + Frama_C_show_each_1(x); + Frama_C_show_each_2(y); + return x + y; +} + +int X, Y; +int main1 () { + R1 = fn1(G, G|0); + R2 = fn2(G, G|0); + Frama_C_show_each_d(G); + pv = (int *) &X; + return Y; +} + +int * main2() { + return 0; +} + +#define not_a_function_call(v) \ + yes_a_function_call(v) + +#define yet_another_not_a_call(v) do { \ + yes_again(); \ + } while (0) + +/* call_inside_comment(evaluation); */ +void main3 () { + //@ not_a_function_call(v); + yet_another_not_a_call(v); +} + +/* Tests the initialization of local variables. */ +void main4 () { + f(g(h(i, j), k (l, m ( n( + o, p( + q) + ) + ) + ) + ) + ); + f(); + g(); +} +void main() { + main1(); + main2(); + main3(); + main4(); +} diff --git a/tests/fc_script/list_functions.i b/tests/fc_script/list_functions.i new file mode 100644 index 0000000000000000000000000000000000000000..aab65784552e38fdecd78fed6fd1ead8744f654d --- /dev/null +++ b/tests/fc_script/list_functions.i @@ -0,0 +1,4 @@ +/* run.config + NOFRAMAC: testing frama-c-script, not frama-c itself + EXECNOW: LOG heuristic_list_functions.res LOG heuristic_list_functions.err bin/frama-c-script heuristic-list-functions true true @PTEST_DIR@/*.c @PTEST_DIR@/*.i > @PTEST_DIR@/result/heuristic_list_functions.res 2> @PTEST_DIR@/result/heuristic_list_functions.err + */ diff --git a/tests/fc_script/oracle/build-callgraph.err b/tests/fc_script/oracle/build-callgraph.err new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/fc_script/oracle/build-callgraph.res b/tests/fc_script/oracle/build-callgraph.res new file mode 100644 index 0000000000000000000000000000000000000000..145f1bccaf869fe75bcfed8dbd2834e13396ee33 --- /dev/null +++ b/tests/fc_script/oracle/build-callgraph.res @@ -0,0 +1,21 @@ +tests/fc_script/build-callgraph.i:8: main -> strlen +tests/fc_script/build-callgraph.i:20: fn1 -> Frama_C_show_each_1 +tests/fc_script/build-callgraph.i:21: fn1 -> Frama_C_show_each_2 +tests/fc_script/build-callgraph.i:27: main1 -> fn1 +tests/fc_script/build-callgraph.i:28: main1 -> fn2 +tests/fc_script/build-callgraph.i:29: main1 -> Frama_C_show_each_d +tests/fc_script/build-callgraph.i:47: main3 -> not_a_function_call +tests/fc_script/build-callgraph.i:48: main3 -> yet_another_not_a_call +tests/fc_script/build-callgraph.i:53: main4 -> f +tests/fc_script/build-callgraph.i:61: main4 -> f +tests/fc_script/build-callgraph.i:53: main4 -> g +tests/fc_script/build-callgraph.i:62: main4 -> g +tests/fc_script/build-callgraph.i:53: main4 -> h +tests/fc_script/build-callgraph.i:53: main4 -> k +tests/fc_script/build-callgraph.i:53: main4 -> m +tests/fc_script/build-callgraph.i:53: main4 -> n +tests/fc_script/build-callgraph.i:54: main4 -> p +tests/fc_script/build-callgraph.i:65: main -> main1 +tests/fc_script/build-callgraph.i:66: main -> main2 +tests/fc_script/build-callgraph.i:67: main -> main3 +tests/fc_script/build-callgraph.i:68: main -> main4 diff --git a/tests/fc_script/oracle/find_fun1.res b/tests/fc_script/oracle/find_fun1.res index 0ceb3c8c68b411868c7fd156287a3c668e09466c..7d423014f36cda2c6e9e7f405bb272dd0f42d8e3 100644 --- a/tests/fc_script/oracle/find_fun1.res +++ b/tests/fc_script/oracle/find_fun1.res @@ -1,5 +1,6 @@ -Looking for 'main2' inside 11 file(s)... +Looking for 'main2' inside 14 file(s)... Possible declarations for function 'main2' in the following file(s): tests/fc_script/for-find-fun.c Possible definitions for function 'main2' in the following file(s): + tests/fc_script/build-callgraph.i tests/fc_script/main2.c diff --git a/tests/fc_script/oracle/find_fun2.res b/tests/fc_script/oracle/find_fun2.res index e59924a72f49f8278f2ff8fcec02062b9c68b2e3..ded016c9272e4dccb55c80cbb45e4fe3115c214b 100644 --- a/tests/fc_script/oracle/find_fun2.res +++ b/tests/fc_script/oracle/find_fun2.res @@ -1,5 +1,6 @@ -Looking for 'main3' inside 11 file(s)... +Looking for 'main3' inside 14 file(s)... Possible declarations for function 'main3' in the following file(s): tests/fc_script/for-find-fun2.c Possible definitions for function 'main3' in the following file(s): + tests/fc_script/build-callgraph.i tests/fc_script/for-find-fun.c diff --git a/tests/fc_script/oracle/find_fun3.res b/tests/fc_script/oracle/find_fun3.res index 6d151d463efe8d8df1a1ac874653880564ee4eb1..4a42d9117089960eb086c47a8e4b07b44795e3c3 100644 --- a/tests/fc_script/oracle/find_fun3.res +++ b/tests/fc_script/oracle/find_fun3.res @@ -1,2 +1,2 @@ -Looking for 'false_positive' inside 11 file(s)... +Looking for 'false_positive' inside 14 file(s)... No declaration/definition found for function 'false_positive' diff --git a/tests/fc_script/oracle/heuristic_list_functions.err b/tests/fc_script/oracle/heuristic_list_functions.err new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/fc_script/oracle/heuristic_list_functions.res b/tests/fc_script/oracle/heuristic_list_functions.res new file mode 100644 index 0000000000000000000000000000000000000000..fe8495493cd286842024661ea2ed28407625a805 --- /dev/null +++ b/tests/fc_script/oracle/heuristic_list_functions.res @@ -0,0 +1,50 @@ +tests/fc_script/for-find-fun.c:6:7: main2 (declaration) +tests/fc_script/for-find-fun.c:13:17: main3 (definition) +tests/fc_script/for-find-fun2.c:5:7: main3 (declaration) +tests/fc_script/for-find-fun2.c:10:12: f (definition) +tests/fc_script/for-find-fun2.c:14:17: g (definition) +tests/fc_script/for-find-fun2.c:19:21: h (definition) +tests/fc_script/for-find-fun2.c:28:31: static_fun (definition) +tests/fc_script/for-list-functions.c:8:15: static_fun (definition) +tests/fc_script/for-list-functions.c:17:22: k (definition) +tests/fc_script/main.c:12:14: main (definition) +tests/fc_script/main2.c:6:8: fake_main (definition) +tests/fc_script/main2.c:10:12: domain (definition) +tests/fc_script/main2.c:14:16: main2 (definition) +tests/fc_script/main3.c:6:9: main (definition) +tests/fc_script/make-wrapper.c:7:7: defined (declaration) +tests/fc_script/make-wrapper.c:9:9: specified (declaration) +tests/fc_script/make-wrapper.c:11:11: external (declaration) +tests/fc_script/make-wrapper.c:13:18: main (definition) +tests/fc_script/make-wrapper2.c:5:7: defined (definition) +tests/fc_script/make-wrapper2.c:13:13: specified (declaration) +tests/fc_script/make-wrapper2.c:16:16: external (declaration) +tests/fc_script/make-wrapper3.c:7:9: external (definition) +tests/fc_script/build-callgraph.i:7:9: main (definition) +tests/fc_script/build-callgraph.i:16:16: fn2 (declaration) +tests/fc_script/build-callgraph.i:18:23: fn1 (definition) +tests/fc_script/build-callgraph.i:26:32: main1 (definition) +tests/fc_script/build-callgraph.i:34:36: main2 (definition) +tests/fc_script/build-callgraph.i:46:49: main3 (definition) +tests/fc_script/build-callgraph.i:52:63: main4 (definition) +tests/fc_script/build-callgraph.i:64:69: main (definition) +tests/fc_script/recursions.i:8:10: g (definition) +tests/fc_script/recursions.i:12:15: f (definition) +tests/fc_script/recursions.i:17:20: h (definition) +tests/fc_script/recursions.i:22:24: i (definition) +tests/fc_script/recursions.i:26:28: j (definition) +tests/fc_script/recursions.i:30:30: l (declaration) +tests/fc_script/recursions.i:31:31: m (declaration) +tests/fc_script/recursions.i:33:35: k (definition) +tests/fc_script/recursions.i:37:39: l (definition) +tests/fc_script/recursions.i:41:43: m (definition) +tests/fc_script/recursions.i:45:46: norec (definition) +tests/fc_script/recursions.i:48:50: direct_rec (definition) +tests/fc_script/recursions.i:52:52: indirect_rec1 (declaration) +tests/fc_script/recursions.i:54:56: indirect_rec2 (definition) +tests/fc_script/recursions.i:58:60: indirect_rec1 (definition) +tests/fc_script/recursions.i:62:62: decl_only (declaration) +tests/fc_script/recursions.i:64:64: one_liner_function (definition) +tests/fc_script/recursions.i:66:66: multiple_indirect1 (declaration) +tests/fc_script/recursions.i:68:71: multiple_indirect2 (definition) +tests/fc_script/recursions.i:73:76: multiple_indirect1 (definition) diff --git a/tests/fc_script/oracle/recursions.err b/tests/fc_script/oracle/recursions.err new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/fc_script/oracle/recursions.res b/tests/fc_script/oracle/recursions.res new file mode 100644 index 0000000000000000000000000000000000000000..af11f5fead4d3586b970c25f0d6301a1ad632348 --- /dev/null +++ b/tests/fc_script/oracle/recursions.res @@ -0,0 +1,18 @@ +recursive cycle detected: + tests/fc_script/recursions.i:49: direct_rec -> direct_rec +recursive cycle detected: + tests/fc_script/recursions.i:13: f -> f +recursive cycle detected: + tests/fc_script/recursions.i:18: h -> h +recursive cycle detected: + tests/fc_script/recursions.i:59: indirect_rec1 -> indirect_rec2 + tests/fc_script/recursions.i:55: indirect_rec2 -> indirect_rec1 +recursive cycle detected: + tests/fc_script/recursions.i:34: k -> l + tests/fc_script/recursions.i:38: l -> m + tests/fc_script/recursions.i:42: m -> k +recursive cycle detected: + tests/fc_script/recursions.i:74: multiple_indirect1 -> multiple_indirect2 + tests/fc_script/recursions.i:75: multiple_indirect1 -> multiple_indirect2 + tests/fc_script/recursions.i:69: multiple_indirect2 -> multiple_indirect1 + tests/fc_script/recursions.i:70: multiple_indirect2 -> multiple_indirect1 diff --git a/tests/fc_script/recursions.i b/tests/fc_script/recursions.i new file mode 100644 index 0000000000000000000000000000000000000000..841d609939737908e7378823b9642e55af7fb2bb --- /dev/null +++ b/tests/fc_script/recursions.i @@ -0,0 +1,76 @@ +/* run.config + NOFRAMAC: testing frama-c-script, not frama-c itself + EXECNOW: LOG recursions.res LOG recursions.err bin/frama-c-script heuristic-detect-recursion @PTEST_FILE@ > @PTEST_DIR@/result/recursions.res 2> @PTEST_DIR@/result/recursions.err +*/ + +volatile int v; + +void g() { + int g = 42; +} + +void f() { + if (v) f(); + else g(); +} + +void h() { + if (v) h(); + else g(); +} + +void i() { + g(); +} + +void j() { + f(); +} + +void l(void); +void m(void); + +void k() { + if (v) l(); +} + +void l() { + if (v) m(); +} + +void m() { + if (v) k(); +} + +void norec() { +} + +int direct_rec() { + return direct_rec(); +} + +void indirect_rec1(); + +void indirect_rec2() { + indirect_rec1(); +} + +void indirect_rec1() { + indirect_rec2(); +} + +void decl_only(); + +void one_liner_function() { decl_only(); } + +void multiple_indirect1(); + +void multiple_indirect2() { + multiple_indirect1(); + multiple_indirect1(); +} + +void multiple_indirect1() { + multiple_indirect2(); + multiple_indirect2(); +} diff --git a/tests/libc/oracle/fc_libc.2.res.oracle b/tests/libc/oracle/fc_libc.2.res.oracle index 06372c7bffc52bdd4752aa4f83bb1b3fc3a3277e..3061c00a62693b3c162c1ee0902ec050b76d8b3a 100644 --- a/tests/libc/oracle/fc_libc.2.res.oracle +++ b/tests/libc/oracle/fc_libc.2.res.oracle @@ -38,6 +38,7 @@ [kernel] Parsing share/libc/__fc_gcc_builtins.h (with preprocessing) [kernel] Parsing share/libc/__fc_inet.h (with preprocessing) [kernel] Parsing share/libc/__fc_integer.h (with preprocessing) +[kernel] Parsing share/libc/__fc_libc.h (with preprocessing) [kernel] Parsing share/libc/__fc_machdep.h (with preprocessing) skipping share/libc/__fc_machdep_linux_shared.h [kernel] Parsing share/libc/__fc_select.h (with preprocessing) diff --git a/tests/libc/oracle/fc_libc.5.res.oracle b/tests/libc/oracle/fc_libc.5.res.oracle index 73dac90425a73c971fb285476320f30bc36be7d1..ee4ef42c99932124f83b4c3b138d25035db3e95b 100644 --- a/tests/libc/oracle/fc_libc.5.res.oracle +++ b/tests/libc/oracle/fc_libc.5.res.oracle @@ -1 +1,2 @@ #include "__fc_integer.h" +#include "__fc_libc.h"