# Copyright 2011 Tom Tromey # Copyright 2011 Red Hat, 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 # . import gcc import gccutils import sys from libcpychecker import get_all_PyMethodDef_methods # 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() # Called when __attribute__((gdb_throw)) is seen. def attribute_throw_callback(*args): global output_file gccutils.check_isinstance(args[0], gcc.FunctionDecl) output_file.write("declare_throw(%s)\n" % repr(args[0].name)) # Called when __attribute__((gdb_nothrow)) is seen. def attribute_nothrow_callback(*args): global output_file gccutils.check_isinstance(args[0], gcc.FunctionDecl) output_file.write("declare_nothrow(%s)\n" % repr(args[0].name)) # Register our attributes and define the corresponding macro. def register_attributes(): gcc.register_attribute('gdb_throw', 0, 0, True, False, False, attribute_throw_callback) gcc.register_attribute('gdb_nothrow', 0, 0, True, False, False, attribute_nothrow_callback) gcc.define_macro('WITH_GDB_CHECK_EXCEPTION') # 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 == '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_nothrow(%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') gcc.register_callback(gcc.PLUGIN_ATTRIBUTES, register_attributes) gcc.register_callback(gcc.PLUGIN_FINISH_UNIT, close_output) ps = GdbExceptionChecker(output_file) ps.register_after('ssa')