Construct the data dependence graph.

        We track the following types of dependence:
        - (Intra-IRSB) temporary variable dependencies
        - Register dependencies
        - Memory dependencies, although it's very limited. See below.

        We track the following types of memory access:
        - (Intra-functional) Stack read/write.
            Trace changes of stack pointers inside a function, and the dereferences of stack pointers.
        - (Inter-functional) Stack read/write.
        - (Global) Static memory positions.
            Keep a map of all accessible memory positions to their source statements per function. After that, we
            traverse the CFG and link each pair of reads/writes together in the order of control-flow.

        We do not track the following types of memory access
        - Symbolic memory access
            Well, they cannot be tracked under fastpath mode (which is the mode we are generating the CTF) anyways.

def _construct(self):
        """
        Construct the data dependence graph.

        We track the following types of dependence:
        - (Intra-IRSB) temporary variable dependencies
        - Register dependencies
        - Memory dependencies, although it's very limited. See below.

        We track the following types of memory access:
        - (Intra-functional) Stack read/write.
            Trace changes of stack pointers inside a function, and the dereferences of stack pointers.
        - (Inter-functional) Stack read/write.
        - (Global) Static memory positions.
            Keep a map of all accessible memory positions to their source statements per function. After that, we
            traverse the CFG and link each pair of reads/writes together in the order of control-flow.

        We do not track the following types of memory access
        - Symbolic memory access
            Well, they cannot be tracked under fastpath mode (which is the mode we are generating the CTF) anyways.
        """

        worklist = []
        worklist_set = set()

        # Initialize the worklist
        if self._start is None:
            # initial nodes are those nodes in CFG that has no in-degrees
            for n in self._cfg.graph.nodes():
                if self._cfg.graph.in_degree(n) == 0:
                    # Put it into the worklist
                    job = DDGJob(n, 0)
                    self._worklist_append(job, worklist, worklist_set)
        else:
            for n in self._cfg.get_all_nodes(self._start):
                job = DDGJob(n, 0)
                self._worklist_append(job, worklist, worklist_set)

        # A dict storing defs set
        # DDGJob -> LiveDefinition
        live_defs_per_node = {}

        while worklist:
            # Pop out a node
            ddg_job = worklist[0]
            l.debug("Processing %s.", ddg_job)
            node, call_depth = ddg_job.cfg_node, ddg_job.call_depth
            worklist = worklist[ 1 : ]
            worklist_set.remove(node)

            # Grab all final states. There are usually more than one (one state for each successor), and we gotta
            # process all of them
            final_states = node.final_states

            if node in live_defs_per_node:
                live_defs = live_defs_per_node[node]
            else:
                live_defs = LiveDefinitions()
                live_defs_per_node[node] = live_defs

            successing_nodes = list(self._cfg.graph.successors(node))

            # try to assign every final state to a successor and vice versa
            match_suc = defaultdict(bool)
            match_state = defaultdict(set)

            for suc in successing_nodes:
                matched = False
                for state in final_states:
                    try:
                        if state.solver.eval(state.ip) == suc.addr:
                            match_suc[suc.addr] = True
                            match_state[state].add(suc)
                            matched = True
                    except (SimUnsatError, SimSolverModeError, ZeroDivisionError):
                        # ignore
                        matched = matched
                if not matched:
                    break

            # whether all final states could be matched to a successor and vice versa
            matches = len(match_suc) == len(successing_nodes) and len(match_state) == len(final_states)

            for state in final_states:
                if not matches and state.history.jumpkind == 'Ijk_FakeRet' and len(final_states) > 1:
                    # Skip fakerets if there are other control flow transitions available
                    continue

                new_call_depth = call_depth
                if state.history.jumpkind == 'Ijk_Call':
                    new_call_depth += 1
                elif state.history.jumpkind == 'Ijk_Ret':
                    new_call_depth -= 1

                if self._call_depth is not None and call_depth > self._call_depth:
                    l.debug('Do not trace into %s due to the call depth limit', state.ip)
                    continue

                new_defs = self._track(state, live_defs, node.irsb.statements if node.irsb is not None else None)

                #corresponding_successors = [n for n in successing_nodes if
                #                            not state.ip.symbolic and n.addr == state.solver.eval(state.ip)]
                #if not corresponding_successors:
                #    continue

                changed = False

                # if every successor can be matched with one or more final states (by IP address),
                # only take over the LiveDefinition of matching states
                if matches:
                    add_state_to_sucs = match_state[state]
                else:
                    add_state_to_sucs = successing_nodes

                for successing_node in add_state_to_sucs:

                    if (state.history.jumpkind == 'Ijk_Call' or state.history.jumpkind.startswith('Ijk_Sys')) and \
                            (state.ip.symbolic or successing_node.addr != state.solver.eval(state.ip)):
                        suc_new_defs = self._filter_defs_at_call_sites(new_defs)
                    else:
                        suc_new_defs = new_defs

                    if successing_node in live_defs_per_node:
                        defs_for_next_node = live_defs_per_node[successing_node]
                    else:
                        defs_for_next_node = LiveDefinitions()
                        live_defs_per_node[successing_node] = defs_for_next_node

                    for var, code_loc_set in suc_new_defs.items():
                        # l.debug("Adding %d new definitions for variable %s.", len(code_loc_set), var)
                        changed |= defs_for_next_node.add_defs(var, code_loc_set)

                if changed:
                    if (self._call_depth is None) or \
                            (self._call_depth is not None and 0 <= new_call_depth <= self._call_depth):
                        # Put all reachable successors back to our work-list again
                        for successor in self._cfg.get_all_successors(node):
                            nw = DDGJob(successor, new_call_depth)
                            self._worklist_append(nw, worklist, worklist_set)