binutils-gdb/gdb/contrib/excheck.py

297 lines
11 KiB
Python
Raw Normal View History

# Copyright 2011-2016 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()