:param cfg: An angr CFG object
        :returns:    a dictionary of function addresses to tuples of attributes

def _compute_function_attributes(cfg):
        """
        :param cfg: An angr CFG object
        :returns:    a dictionary of function addresses to tuples of attributes
        """
        # the attributes we use are the number of basic blocks, number of edges, and number of subfunction calls
        attributes = dict()
        all_funcs = set(cfg.kb.callgraph.nodes())
        for function_addr in cfg.kb.functions:
            # skip syscalls and functions which are None in the cfg
            if cfg.kb.functions.function(function_addr) is None or cfg.kb.functions.function(function_addr).is_syscall:
                continue
            if cfg.kb.functions.function(function_addr) is not None:
                normalized_funtion = NormalizedFunction(cfg.kb.functions.function(function_addr))
                number_of_basic_blocks = len(normalized_funtion.graph.nodes())
                number_of_edges = len(normalized_funtion.graph.edges())
            else:
                number_of_basic_blocks = 0
                number_of_edges = 0
            if function_addr in all_funcs:
                number_of_subfunction_calls = len(list(cfg.kb.callgraph.successors(function_addr)))
            else:
                number_of_subfunction_calls = 0
            attributes[function_addr] = (number_of_basic_blocks, number_of_edges, number_of_subfunction_calls)

        return attributes