* contrib/excheck.py: New file.
authorTom Tromey <tromey@redhat.com>
Thu, 28 Feb 2013 19:28:25 +0000 (19:28 +0000)
committerTom Tromey <tromey@redhat.com>
Thu, 28 Feb 2013 19:28:25 +0000 (19:28 +0000)
* contrib/exsummary.py: New file.
* contrib/gcc-with-excheck: New file.

gdb/ChangeLog
gdb/contrib/excheck.py [new file with mode: 0644]
gdb/contrib/exsummary.py [new file with mode: 0644]
gdb/contrib/gcc-with-excheck [new file with mode: 0755]

index 8194eeae192159d20ffb971f5ab8452670322281..c79359371850636e1f4b4cf51be63593018f79a0 100644 (file)
@@ -1,3 +1,9 @@
+2013-02-28  Tom Tromey  <tromey@redhat.com>
+
+       * contrib/excheck.py: New file.
+       * contrib/exsummary.py: New file.
+       * contrib/gcc-with-excheck: New file.
+
 2013-02-28  Tom Tromey  <tromey@redhat.com>
 
        * python/python.c (gdbpy_print_stack): Call begin_line and
diff --git a/gdb/contrib/excheck.py b/gdb/contrib/excheck.py
new file mode 100644 (file)
index 0000000..f8f917f
--- /dev/null
@@ -0,0 +1,296 @@
+#   Copyright 2011, 2013 Free Software Foundation, Inc.
+#
+#   This is free software: you can redistribute it and/or modify it
+#   under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program 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
+#   General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see
+#   <http://www.gnu.org/licenses/>.
+
+# This is a GCC plugin that computes some exception-handling data for
+# gdb.  This data can then be summarized and checked by the
+# exsummary.py script.
+
+# To use:
+# * First, install the GCC Python plugin.  See
+#   https://fedorahosted.org/gcc-python-plugin/
+# * export PYTHON_PLUGIN=/full/path/to/plugin/directory
+#   This should be the directory holding "python.so".
+# * cd build/gdb; make mostlyclean
+# * make CC=.../gcc-with-excheck
+#   This will write a number of .py files in the build directory.
+# * python .../exsummary.py
+#   This will show the violations.
+
+import gcc
+import gccutils
+import sys
+
+# Where our output goes.
+output_file = None
+
+# Cleanup functions require special treatment, because they take a
+# function argument, but in theory the function must be nothrow.
+cleanup_functions = {
+    'make_cleanup': 1,
+    'make_cleanup_dtor': 1,
+    'make_final_cleanup': 1,
+    'make_my_cleanup2': 1,
+    'make_my_cleanup': 1
+}
+
+# Functions which may throw but which we want to ignore.
+ignore_functions = {
+    # This one is super special.
+    'exceptions_state_mc': 1,
+    # gdb generally pretends that internal_error cannot throw, even
+    # though it can.
+    'internal_error': 1,
+    # do_cleanups and friends are supposedly nothrow but we don't want
+    # to run afoul of the indirect function call logic.
+    'do_cleanups': 1,
+    'do_final_cleanups': 1
+}
+
+# Functions which take a function argument, but which are not
+# interesting, usually because the argument is not called in the
+# current context.
+non_passthrough_functions = {
+    'signal': 1,
+    'add_internal_function': 1
+}
+
+# Return True if the type is from Python.
+def type_is_pythonic(t):
+    if isinstance(t, gcc.ArrayType):
+        t = t.type
+    if not isinstance(t, gcc.RecordType):
+        return False
+    # Hack.
+    return str(t).find('struct Py') == 0
+
+# Examine all the fields of a struct.  We don't currently need any
+# sort of recursion, so this is simple for now.
+def examine_struct_fields(initializer):
+    global output_file
+    for idx2, value2 in initializer.elements:
+        if isinstance(idx2, gcc.Declaration):
+            if isinstance(value2, gcc.AddrExpr):
+                value2 = value2.operand
+                if isinstance(value2, gcc.FunctionDecl):
+                    output_file.write("declare_nothrow(%s)\n"
+                                      % repr(str(value2.name)))
+
+# Examine all global variables looking for pointers to functions in
+# structures whose types were defined by Python.
+def examine_globals():
+    global output_file
+    vars = gcc.get_variables()
+    for var in vars:
+        if not isinstance(var.decl, gcc.VarDecl):
+            continue
+        output_file.write("################\n")
+        output_file.write("# Analysis for %s\n" % var.decl.name)
+        if not var.decl.initial:
+            continue
+        if not type_is_pythonic(var.decl.type):
+            continue
+
+        if isinstance(var.decl.type, gcc.ArrayType):
+            for idx, value in var.decl.initial.elements:
+                examine_struct_fields(value)
+        else:
+            gccutils.check_isinstance(var.decl.type, gcc.RecordType)
+            examine_struct_fields(var.decl.initial)
+
+# Called at the end of compilation to write out some data derived from
+# globals and to close the output.
+def close_output(*args):
+    global output_file
+    examine_globals()
+    output_file.close()
+
+# The pass which derives some exception-checking information.  We take
+# a two-step approach: first we get a call graph from the compiler.
+# This is emitted by the plugin as Python code.  Then, we run a second
+# program that reads all the generated Python and uses it to get a
+# global view of exception routes in gdb.
+class GdbExceptionChecker(gcc.GimplePass):
+    def __init__(self, output_file):
+        gcc.GimplePass.__init__(self, 'gdb_exception_checker')
+        self.output_file = output_file
+
+    def log(self, obj):
+        self.output_file.write("# %s\n" % str(obj))
+
+    # Return true if FN is a call to a method on a Python object.
+    # We know these cannot throw in the gdb sense.
+    def fn_is_python_ignorable(self, fn):
+        if not isinstance(fn, gcc.SsaName):
+            return False
+        stmt = fn.def_stmt
+        if not isinstance(stmt, gcc.GimpleAssign):
+            return False
+        if stmt.exprcode is not gcc.ComponentRef:
+            return False
+        rhs = stmt.rhs[0]
+        if not isinstance(rhs, gcc.ComponentRef):
+            return False
+        if not isinstance(rhs.field, gcc.FieldDecl):
+            return False
+        return rhs.field.name == 'tp_dealloc' or rhs.field.name == 'tp_free'
+
+    # Decode a function call and write something to the output.
+    # THIS_FUN is the enclosing function that we are processing.
+    # FNDECL is the call to process; it might not actually be a DECL
+    # node.
+    # LOC is the location of the call.
+    def handle_one_fndecl(self, this_fun, fndecl, loc):
+        callee_name = ''
+        if isinstance(fndecl, gcc.AddrExpr):
+            fndecl = fndecl.operand
+        if isinstance(fndecl, gcc.FunctionDecl):
+            # Ordinary call to a named function.
+            callee_name = str(fndecl.name)
+            self.output_file.write("function_call(%s, %s, %s)\n"
+                                   % (repr(callee_name),
+                                      repr(this_fun.decl.name),
+                                      repr(str(loc))))
+        elif self.fn_is_python_ignorable(fndecl):
+            # Call to tp_dealloc.
+            pass
+        elif (isinstance(fndecl, gcc.SsaName)
+              and isinstance(fndecl.var, gcc.ParmDecl)):
+            # We can ignore an indirect call via a parameter to the
+            # current function, because this is handled via the rule
+            # for passthrough functions.
+            pass
+        else:
+            # Any other indirect call.
+            self.output_file.write("has_indirect_call(%s, %s)\n"
+                                   % (repr(this_fun.decl.name),
+                                      repr(str(loc))))
+        return callee_name
+
+    # This does most of the work for examine_one_bb.
+    # THIS_FUN is the enclosing function.
+    # BB is the basic block to process.
+    # Returns True if this block is the header of a TRY_CATCH, False
+    # otherwise.
+    def examine_one_bb_inner(self, this_fun, bb):
+        if not bb.gimple:
+            return False
+        try_catch = False
+        for stmt in bb.gimple:
+            loc = stmt.loc
+            if not loc:
+                loc = this_fun.decl.location
+            if not isinstance(stmt, gcc.GimpleCall):
+                continue
+            callee_name = self.handle_one_fndecl(this_fun, stmt.fn, loc)
+
+            if callee_name == 'exceptions_state_mc_action_iter':
+                try_catch = True
+
+            global non_passthrough_functions
+            if callee_name in non_passthrough_functions:
+                continue
+
+            # We have to specially handle calls where an argument to
+            # the call is itself a function, e.g., qsort.  In general
+            # we model these as "passthrough" -- we assume that in
+            # addition to the call the qsort there is also a call to
+            # the argument function.
+            for arg in stmt.args:
+                # We are only interested in arguments which are functions.
+                t = arg.type
+                if isinstance(t, gcc.PointerType):
+                    t = t.dereference
+                if not isinstance(t, gcc.FunctionType):
+                    continue
+
+                if isinstance(arg, gcc.AddrExpr):
+                    arg = arg.operand
+
+                global cleanup_functions
+                if callee_name in cleanup_functions:
+                    if not isinstance(arg, gcc.FunctionDecl):
+                        gcc.inform(loc, 'cleanup argument not a DECL: %s' % repr(arg))
+                    else:
+                        # Cleanups must be nothrow.
+                        self.output_file.write("declare_cleanup(%s)\n"
+                                               % repr(str(arg.name)))
+                else:
+                    # Assume we have a passthrough function, like
+                    # qsort or an iterator.  We model this by
+                    # pretending there is an ordinary call at this
+                    # point.
+                    self.handle_one_fndecl(this_fun, arg, loc)
+        return try_catch
+
+    # Examine all the calls in a basic block and generate output for
+    # them.
+    # THIS_FUN is the enclosing function.
+    # BB is the basic block to examine.
+    # BB_WORKLIST is a list of basic blocks to work on; we add the
+    # appropriate successor blocks to this.
+    # SEEN_BBS is a map whose keys are basic blocks we have already
+    # processed.  We use this to ensure that we only visit a given
+    # block once.
+    def examine_one_bb(self, this_fun, bb, bb_worklist, seen_bbs):
+        try_catch = self.examine_one_bb_inner(this_fun, bb)
+        for edge in bb.succs:
+            if edge.dest in seen_bbs:
+                continue
+            seen_bbs[edge.dest] = 1
+            if try_catch:
+                # This is bogus, but we magically know the right
+                # answer.
+                if edge.false_value:
+                    bb_worklist.append(edge.dest)
+            else:
+                bb_worklist.append(edge.dest)
+
+    # Iterate over all basic blocks in THIS_FUN.
+    def iterate_bbs(self, this_fun):
+        # Iteration must be in control-flow order, because if we see a
+        # TRY_CATCH construct we need to drop all the contained blocks.
+        bb_worklist = [this_fun.cfg.entry]
+        seen_bbs = {}
+        seen_bbs[this_fun.cfg.entry] = 1
+        for bb in bb_worklist:
+            self.examine_one_bb(this_fun, bb, bb_worklist, seen_bbs)
+
+    def execute(self, fun):
+        if fun and fun.cfg and fun.decl:
+            self.output_file.write("################\n")
+            self.output_file.write("# Analysis for %s\n" % fun.decl.name)
+            self.output_file.write("define_function(%s, %s)\n"
+                                   % (repr(fun.decl.name),
+                                      repr(str(fun.decl.location))))
+
+            global ignore_functions
+            if fun.decl.name not in ignore_functions:
+                self.iterate_bbs(fun)
+
+def main(**kwargs):
+    global output_file
+    output_file = open(gcc.get_dump_base_name() + '.gdb_exc.py', 'w')
+    # We used to use attributes here, but there didn't seem to be a
+    # big benefit over hard-coding.
+    output_file.write('declare_throw("throw_exception")\n')
+    output_file.write('declare_throw("throw_verror")\n')
+    output_file.write('declare_throw("throw_vfatal")\n')
+    output_file.write('declare_throw("throw_error")\n')
+    gcc.register_callback(gcc.PLUGIN_FINISH_UNIT, close_output)
+    ps = GdbExceptionChecker(output_file)
+    ps.register_after('ssa')
+
+main()
diff --git a/gdb/contrib/exsummary.py b/gdb/contrib/exsummary.py
new file mode 100644 (file)
index 0000000..5c9d8c4
--- /dev/null
@@ -0,0 +1,185 @@
+#   Copyright 2011, 2013 Free Software Foundation, Inc.
+#
+#   This is free software: you can redistribute it and/or modify it
+#   under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program 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
+#   General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see
+#   <http://www.gnu.org/licenses/>.
+
+import sys
+import glob
+
+# Compute the summary information from the files created by
+# excheck.py.  Run in the build directory where you used the
+# excheck.py plugin.
+
+class Function:
+    def __init__(self, name):
+        self.name = name
+        self.location = None
+        self.callers = []
+        self.can_throw = False
+        self.marked_nothrow = False
+        self.reason = None
+
+    def log(self, message):
+        print "%s: note: %s" % (self.location, message)
+
+    def set_location(self, location):
+        self.location = location
+
+    # CALLER is an Edge.
+    def add_caller(self, caller):
+        # self.log("adding call from %s" % caller.from_fn.name)
+        self.callers.append(caller)
+        # self.log("len = %d" % len(self.callers))
+
+    def consistency_check(self):
+        if self.marked_nothrow and self.can_throw:
+            print ("%s: error: %s marked as both 'throw' and 'nothrow'"
+                   % (self.location, self.name))
+
+    def declare_nothrow(self):
+        self.marked_nothrow = True
+        self.consistency_check()
+
+    def declare_throw(self):
+        result = not self.can_throw # Return True the first time
+        self.can_throw = True
+        self.consistency_check()
+        return result
+
+    def print_stack(self, is_indirect):
+        if is_indirect:
+            print ("%s: error: function %s is marked nothrow but is assumed to throw due to indirect call"
+                   % (self.location, self.name))
+        else:
+            print ("%s: error: function %s is marked nothrow but can throw"
+                   % (self.location, self.name))
+
+        edge = self.reason
+        while edge is not None:
+            print ("%s: info: via call to %s"
+                   % (edge.location, edge.to_fn.name))
+            edge = edge.to_fn.reason
+
+    def mark_throw(self, edge, work_list, is_indirect):
+        if not self.can_throw:
+            # self.log("can throw")
+            self.can_throw = True
+            self.reason = edge
+            if self.marked_nothrow:
+                self.print_stack(is_indirect)
+            else:
+                # Do this in the 'else' to avoid extra error
+                # propagation.
+                work_list.append(self)
+
+class Edge:
+    def __init__(self, from_fn, to_fn, location):
+        self.from_fn = from_fn
+        self.to_fn = to_fn
+        self.location = location
+
+# Work list of known-throwing functions.
+work_list = []
+# Map from function name to Function object.
+function_map = {}
+# Work list of indirect calls.
+indirect_functions = []
+# Whether we should process cleanup functions as well.
+process_cleanups = False
+# Whether we should process indirect function calls.
+process_indirect = False
+
+def declare(fn_name):
+    global function_map
+    if fn_name not in function_map:
+        function_map[fn_name] = Function(fn_name)
+    return function_map[fn_name]
+
+def define_function(fn_name, location):
+    fn = declare(fn_name)
+    fn.set_location(location)
+
+def declare_throw(fn_name):
+    global work_list
+    fn = declare(fn_name)
+    if fn.declare_throw():
+        work_list.append(fn)
+
+def declare_nothrow(fn_name):
+    fn = declare(fn_name)
+    fn.declare_nothrow()
+
+def declare_cleanup(fn_name):
+    global process_cleanups
+    fn = declare(fn_name)
+    if process_cleanups:
+        fn.declare_nothrow()
+
+def function_call(to, frm, location):
+    to_fn = declare(to)
+    frm_fn = declare(frm)
+    to_fn.add_caller(Edge(frm_fn, to_fn, location))
+
+def has_indirect_call(fn_name, location):
+    global indirect_functions
+    fn = declare(fn_name)
+    phony = Function("<indirect call>")
+    phony.add_caller(Edge(fn, phony, location))
+    indirect_functions.append(phony)
+
+def mark_functions(worklist, is_indirect):
+    for callee in worklist:
+        for edge in callee.callers:
+            edge.from_fn.mark_throw(edge, worklist, is_indirect)
+
+def help_and_exit():
+    print "Usage: exsummary [OPTION]..."
+    print ""
+    print "Read the .py files from the exception checker plugin and"
+    print "generate an error summary."
+    print ""
+    print "  --cleanups     Include invalid behavior in cleanups"
+    print "  --indirect     Include assumed errors due to indirect function calls"
+    sys.exit(0)
+
+def main():
+    global work_list
+    global indirect_functions
+    global process_cleanups
+    global process_indirect
+
+    for arg in sys.argv:
+        if arg == '--cleanups':
+            process_cleanups = True
+        elif arg == '--indirect':
+            process_indirect = True
+        elif arg == '--help':
+            help_and_exit()
+
+    for fname in sorted(glob.glob('*.c.gdb_exc.py')):
+        execfile(fname)
+    print "================"
+    print "= Ordinary marking"
+    print "================"
+    mark_functions(work_list, False)
+    if process_indirect:
+        print "================"
+        print "= Indirect marking"
+        print "================"
+        mark_functions(indirect_functions, True)
+    return 0
+
+if __name__ == '__main__':
+    status = main()
+    sys.exit(status)
diff --git a/gdb/contrib/gcc-with-excheck b/gdb/contrib/gcc-with-excheck
new file mode 100755 (executable)
index 0000000..b810878
--- /dev/null
@@ -0,0 +1,28 @@
+#!/bin/sh
+#   Copyright 2011, 2013 Free Software Foundation, Inc.
+#
+#   This is free software: you can redistribute it and/or modify it
+#   under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   This program 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
+#   General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program.  If not, see
+#   <http://www.gnu.org/licenses/>.
+
+# You must set PYTHON_PLUGIN in the environment.
+# It should be the directory holding the "python.so" file.
+
+exdir=`dirname $0`
+
+# Recent versions of the Python plugin build two .so files in
+# different directories, so we have to set this.  This will be fixed
+# upstream at some point.
+export LD_LIBRARY_PATH=$PYTHON_PLUGIN:$PYTHON_PLUGIN/gcc-c-api
+
+gcc -fplugin=$PYTHON_PLUGIN/python.so -fplugin-arg-python-script=$exdir/excheck.py "$@"