1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
//! Creating and computing backward interprocedural fixpoint problems.
//!
//! # General notes
//!
//! This module supports computation of fixpoint problems on the control flow graphs generated by the `graph` module.
//!
//!
//! To compute a generalized fixpoint problem,
//! first construct a context object implementing the `Context`trait.
//! Use it to construct a `Computation` object.
//! The `Computation` object provides the necessary methods for the actual fixpoint computation.
use super::fixpoint::Context as GeneralFPContext;
use super::forward_interprocedural_fixpoint;
use super::graph::*;
use super::interprocedural_fixpoint_generic::*;
use crate::intermediate_representation::*;
use petgraph::graph::EdgeIndex;
use std::marker::PhantomData;
/// The context for an backward interprocedural fixpoint computation.
///
/// Basically, a `Context` object needs to contain a reference to the actual graph,
/// a method for merging node values,
/// and methods for computing the edge transitions for each different edge type.
///
/// All trait methods have access to the FixpointProblem structure, so that context informations are accessible through it.
///
/// All edge transition functions can return `None` to indicate that no information flows through the edge.
/// For example, this can be used to indicate edges that can never been taken.
pub trait Context<'a> {
/// The type of the values that are assigned to nodes during the fixpoint computation.
type Value: PartialEq + Eq + Clone;
/// Get a reference to the graph that the fixpoint is computed on.
/// The return value is expected to be the reversed CFG.
fn get_graph(&self) -> &Graph<'a>;
/// Merge two node values.
fn merge(&self, value1: &Self::Value, value2: &Self::Value) -> Self::Value;
/// Transition function for `Def` terms.
/// The transition function for a basic block is computed
/// by iteratively applying this function to the starting value for each `Def` term in the basic block.
/// The iteration short-circuits and returns `None` if `update_def` returns `None` at any point.
fn update_def(&self, value: &Self::Value, def: &Term<Def>) -> Option<Self::Value>;
/// Transition function for (conditional and unconditional) `Jmp` terms.
fn update_jumpsite(
&self,
value_after_jump: &Self::Value,
jump: &Term<Jmp>,
untaken_conditional: Option<&Term<Jmp>>,
jumpsite: &Term<Blk>,
) -> Option<Self::Value>;
/// Transition function for in-program calls.
/// The target value is coming in via the call edge from the BlkStart node of the called subroutine and
/// the return_value is coming in via the call stub edge from the returned-to node of the caller
fn update_callsite(
&self,
target_value: Option<&Self::Value>,
return_value: Option<&Self::Value>,
caller_sub: &Term<Sub>,
call: &Term<Jmp>,
return_: &Term<Jmp>,
) -> Option<Self::Value>;
/// Transition function for call stub split.
/// Has access to the value at the ReturnCombine node and
/// decides which data is transferred along the Call Stub Edge.
fn split_call_stub(&self, combined_value: &Self::Value) -> Option<Self::Value>;
/// Transition function for return stub split.
/// Has access to the value at the ReturnCombine node and
/// decides which data is transferred along the Return Stub Edge.
fn split_return_stub(
&self,
combined_value: &Self::Value,
returned_from_sub: &Term<Sub>,
) -> Option<Self::Value>;
/// Transition function for calls to functions not contained in the binary.
/// The corresponding edge goes from the callsite to the returned-to block.
fn update_call_stub(
&self,
value_after_call: &Self::Value,
call: &Term<Jmp>,
) -> Option<Self::Value>;
/// This function is used to refine the value using the information on which branch was taken on a conditional jump.
fn specialize_conditional(
&self,
value_after_jump: &Self::Value,
condition: &Expression,
is_true: bool,
) -> Option<Self::Value>;
}
impl<'a, T: Context<'a>> GeneralFPContext for GeneralizedContext<'a, T> {
type EdgeLabel = Edge<'a>;
type NodeLabel = Node<'a>;
type NodeValue = NodeValue<T::Value>;
/// Get a reference to the underlying graph.
fn get_graph(&self) -> &Graph<'a> {
self.context.get_graph()
}
/// Merge two values using the merge function from the interprocedural context object.
fn merge(&self, val1: &Self::NodeValue, val2: &Self::NodeValue) -> Self::NodeValue {
use NodeValue::*;
match (val1, val2) {
(Value(value1), Value(value2)) => Value(self.context.merge(value1, value2)),
(
CallFlowCombinator {
call_stub: call1,
interprocedural_flow: target1,
},
CallFlowCombinator {
call_stub: call2,
interprocedural_flow: target2,
},
) => CallFlowCombinator {
call_stub: merge_option(call1, call2, |v1, v2| self.context.merge(v1, v2)),
interprocedural_flow: merge_option(target1, target2, |v1, v2| {
self.context.merge(v1, v2)
}),
},
_ => panic!("Malformed CFG in fixpoint computation"),
}
}
/// Backward edge transition function.
/// Applies the transition functions from the interprocedural context object
/// corresponding to the type of the provided edge.
fn update_edge(
&self,
node_value: &Self::NodeValue,
edge: EdgeIndex,
) -> Option<Self::NodeValue> {
let graph = self.context.get_graph();
let (start_node, end_node) = graph.edge_endpoints(edge).unwrap();
match graph.edge_weight(edge).unwrap() {
// Added rev() function to iterator to iterate backwards over the definitions
Edge::Block => {
let block_term = graph.node_weight(start_node).unwrap().get_block();
let value = node_value.unwrap_value();
let defs = &block_term.term.defs;
let end_val = defs.iter().rev().try_fold(value.clone(), |accum, def| {
self.context.update_def(&accum, def)
});
end_val.map(NodeValue::Value)
}
Edge::ReturnCombine(_) => {
Some(Self::NodeValue::Value(node_value.unwrap_value().clone()))
}
// The Call Edge value is added to the CallSourceCombinator.
// The end node will be the callsite node and the node_value parameter is the value at the
// called subroutine's BlkStart node
Edge::Call(_) => Some(NodeValue::CallFlowCombinator {
call_stub: None,
interprocedural_flow: Some(node_value.unwrap_value().clone()),
}),
// The CallStub Edge value is added to the CallSourceCombinator
// The user has the ability to split the node value at the BlkStart return to node
// to only send specific data along the CallStub Edge to the callsite
Edge::CrCallStub => Some(NodeValue::CallFlowCombinator {
call_stub: self.context.split_call_stub(node_value.unwrap_value()),
interprocedural_flow: None,
}),
// The user has the ability to split the node value at the BlkStart return node
// to only send specific data along the ReturnStub Edge to the last BlkEnd node called subroutine
Edge::CrReturnStub => {
// The subroutine term from which the program returns
let returned_from_sub = match graph.node_weight(end_node) {
Some(Node::BlkEnd { 0: _, 1: sub_term }) => sub_term,
_ => panic!("Malformed Control flow graph"),
};
self.context
.split_return_stub(node_value.unwrap_value(), returned_from_sub)
.map(NodeValue::Value)
}
// The CallCombine Edge merges the values coming in from the CallStub Edge and Call Edge
// It also gives the user access to the call and return term.
Edge::CallCombine(return_term) => match node_value {
NodeValue::Value(_) => panic!("Unexpected interprocedural fixpoint graph state"),
NodeValue::CallFlowCombinator {
call_stub,
interprocedural_flow,
} => {
let (call_block, caller_sub) = match graph.node_weight(start_node) {
Some(Node::CallSource {
source: (call_block, call_sub),
target: _,
}) => (call_block, call_sub),
_ => panic!("Malformed Control flow graph"),
};
let call_term = &call_block.term.jmps[0];
self.context
.update_callsite(
interprocedural_flow.as_ref(),
call_stub.as_ref(),
caller_sub,
call_term,
return_term,
)
.map(NodeValue::Value)
}
},
Edge::ExternCallStub(call) => self
.context
.update_call_stub(node_value.unwrap_value(), call)
.map(NodeValue::Value),
Edge::Jump(jump, untaken_conditional) => self
.context
.update_jumpsite(
node_value.unwrap_value(),
jump,
*untaken_conditional,
graph[end_node].get_block(),
)
.map(NodeValue::Value),
}
}
}
/// This struct is a wrapper to create a general fixpoint context out of an interprocedural fixpoint context.
pub struct GeneralizedContext<'a, T: Context<'a>> {
context: T,
_phantom_graph_reference: PhantomData<Graph<'a>>,
}
impl<'a, T: Context<'a>> GeneralizedContext<'a, T> {
/// Create a new generalized context out of an interprocedural context object.
pub fn new(context: T) -> Self {
GeneralizedContext {
context,
_phantom_graph_reference: PhantomData,
}
}
/// Get the inner context object.
pub fn get_context(&self) -> &T {
&self.context
}
}
/// Generate a new computation from the corresponding context and an optional default value for nodes.
pub fn create_computation<'a, T: Context<'a>>(
problem: T,
default_value: Option<T::Value>,
) -> super::fixpoint::Computation<GeneralizedContext<'a, T>> {
let generalized_problem = GeneralizedContext::new(problem);
super::fixpoint::Computation::new(generalized_problem, default_value.map(NodeValue::Value))
}
/// Generate a new computation from the corresponding context and an optional default value for nodes.
/// Uses a bottom up worklist order when computing the fixpoint.
///
/// The worklist order prefers callee nodes before caller nodes.
pub fn create_computation_with_bottom_up_worklist_order<'a, T: Context<'a>>(
problem: T,
default_value: Option<T::Value>,
) -> super::fixpoint::Computation<GeneralizedContext<'a, T>> {
let priority_sorted_nodes =
forward_interprocedural_fixpoint::create_bottom_up_worklist(problem.get_graph());
let generalized_problem = GeneralizedContext::new(problem);
super::fixpoint::Computation::from_node_priority_list(
generalized_problem,
default_value.map(NodeValue::Value),
priority_sorted_nodes,
)
}
/// Generate a new computation from the corresponding context and an optional default value for nodes.
/// Uses a top down worklist order when computing the fixpoint.
///
/// The worklist order prefers caller nodes before callee nodes.
pub fn create_computation_with_top_down_worklist_order<'a, T: Context<'a>>(
problem: T,
default_value: Option<T::Value>,
) -> super::fixpoint::Computation<GeneralizedContext<'a, T>> {
let priority_sorted_nodes =
forward_interprocedural_fixpoint::create_top_down_worklist(problem.get_graph());
let generalized_problem = GeneralizedContext::new(problem);
super::fixpoint::Computation::from_node_priority_list(
generalized_problem,
default_value.map(NodeValue::Value),
priority_sorted_nodes,
)
}
#[cfg(test)]
pub mod tests;
#[cfg(test)]
pub mod mock_context;