[otbn] Refactor how constants are represented for OTBN information flow.
Adjust the OTBN information-flow script with nicer handling of the
current constant values.
Signed-off-by: Jade Philipoom <jadep@google.com>
diff --git a/hw/ip/otbn/util/analyze_information_flow.py b/hw/ip/otbn/util/analyze_information_flow.py
index d2e89a6..854f661 100755
--- a/hw/ip/otbn/util/analyze_information_flow.py
+++ b/hw/ip/otbn/util/analyze_information_flow.py
@@ -64,14 +64,14 @@
# If no secrets were given or the --verbose flag is set, then print the
# full information-flow graphs.
if (args.verbose or args.secrets is None):
- if ret_iflow is not None:
+ if ret_iflow.exists:
print(
'Information flow for paths ending in a return to the caller:')
print(ret_iflow.pretty(indent=2))
- if end_iflow is not None:
+ if end_iflow.exists:
print('--------')
- if end_iflow is not None:
+ if end_iflow.exists:
print('Information flow for paths ending the program:')
print(end_iflow.pretty(indent=2))
@@ -101,14 +101,14 @@
# Print final secrets (if initial secrets were provided).
if args.secrets is not None:
- if ret_iflow is not None:
+ if ret_iflow.exists:
final_secrets = {
sink
for node in args.secrets for sink in ret_iflow.sinks(node)
}
print('Final secrets for paths ending in a return to the caller:',
', '.join(sorted(final_secrets)))
- if end_iflow is not None:
+ if end_iflow.exists:
final_secrets = {
sink
for node in args.secrets for sink in end_iflow.sinks(node)
diff --git a/hw/ip/otbn/util/shared/cache.py b/hw/ip/otbn/util/shared/cache.py
index 8976697..957cb70 100644
--- a/hw/ip/otbn/util/shared/cache.py
+++ b/hw/ip/otbn/util/shared/cache.py
@@ -2,7 +2,7 @@
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
-from typing import Generic, Optional, TypeVar
+from typing import Dict, Generic, Optional, List, TypeVar
K = TypeVar('K')
V = TypeVar('V')
diff --git a/hw/ip/otbn/util/shared/constants.py b/hw/ip/otbn/util/shared/constants.py
new file mode 100644
index 0000000..108a97d
--- /dev/null
+++ b/hw/ip/otbn/util/shared/constants.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+from typing import Dict, Optional
+
+from .insn_yaml import Insn
+from .operand import RegOperandType
+
+def get_op_val_str(insn: Insn, op_vals: Dict[str, int], opname: str) -> str:
+ '''Get the value of the given (register) operand as a string.'''
+ op = insn.name_to_operand[opname]
+ assert isinstance(op.op_type, RegOperandType)
+ return op.op_type.op_val_to_str(op_vals[opname], None)
+
+class ConstantContext:
+ '''Represents known-constant GPRs.
+
+ This datatype is used to track and evaluate GPR pointers for indirect
+ references.
+ '''
+ def __init__(self, values: Dict[str,int]):
+ # The x0 register needs to always be 0
+ assert values.get('x0', None) == 0
+ self.values = values.copy()
+
+ @staticmethod
+ def empty() -> 'ConstantContext':
+ '''Represents a context with no known constants.'''
+ return ConstantContext({'x0': 0})
+
+ def set(self, gpr: str, value: int) -> None:
+ '''Set the value of a GPR in the context.'''
+ if gpr == 'x0':
+ # Ignore writes to x0; it's read-only.
+ return
+ self.values[gpr] = value
+
+ def get(self, gpr: str) -> Optional[int]:
+ '''Get the value of a GPR in the context.'''
+ return self.values.get(gpr, None)
+
+ def __contains__(self, gpr: str) -> bool:
+ return gpr in self.values
+
+ def copy(self) -> 'ConstantContext':
+ return ConstantContext(self.values)
+
+ def intersect(self, other: 'ConstantContext') -> 'ConstantContext':
+ '''Returns a new context with only values on which self/other agree.
+
+ Does not modify self or other.
+ '''
+ out = {}
+ for k,v in self.values.items():
+ if other.get(k) == v:
+ out[k] = v
+ return ConstantContext(out)
+
+ def update_insn(self, insn: Insn, op_vals: Dict[str, int]) -> None:
+ '''Updates to new known constant values GPRs after the instruction.
+
+ Currently, this procedure supports only a limited set of instructions.
+ Since constant values only need to be known in order to decode indirect
+ references to WDRs and loop counts, this set is chosen based on operations
+ likely to happen to those registers: `addi`, `lui`, and bignum instructions
+ containing `_inc` op_vals.
+ '''
+ new_values = {}
+ if insn.mnemonic == 'addi':
+ grs1_name = get_op_val_str(insn, op_vals, 'grs1')
+ if grs1_name in self.values:
+ grd_name = get_op_val_str(insn, op_vals, 'grd')
+ # Operand is a constant; add/update grd
+ new_values[grd_name]= self.values[grs1_name] + op_vals['imm']
+ elif insn.mnemonic == 'lui':
+ grd_name = get_op_val_str(insn, op_vals, 'grd')
+ new_values[grd_name] = op_vals['imm'] << 12
+ else:
+ # If the instruction has any op_vals ending in _inc,
+ # assume we're incrementing the corresponding register
+ for op in insn.operands:
+ if op.name.endswith('_inc'):
+ # If reg to be incremented is a constant, increment it
+ inc_op = op.name[:-(len('_inc'))]
+ inc_name = get_op_val_str(insn, op_vals, inc_op)
+ if inc_name in self.values:
+ new_values[inc_name] = self.values[inc_name] + 1
+
+ # If the instruction's information-flow graph indicates that we updated any
+ # constant register other than the ones handled above, the value of that
+ # register can no longer be determined; remove it from the constants
+ # dictionary.
+ iflow = insn.iflow.evaluate(op_vals, self.values)
+ for sink in iflow.all_sinks():
+ # Remove from self.values if key exists
+ self.values.pop(sink, None)
+
+ self.values.update(new_values)
diff --git a/hw/ip/otbn/util/shared/information_flow.py b/hw/ip/otbn/util/shared/information_flow.py
index 0ba2b79..cb5fbdf 100644
--- a/hw/ip/otbn/util/shared/information_flow.py
+++ b/hw/ip/otbn/util/shared/information_flow.py
@@ -2,7 +2,7 @@
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
-from typing import Dict, List, Optional, Sequence, Set
+from typing import Dict, Iterator, List, Optional, Sequence, Set
from serialize.parse_helpers import check_keys, check_list, check_str
@@ -25,7 +25,7 @@
means the sink is overwritten with a constant value, and information is not
flowing to it from any nodes (including its own previous value).
'''
- def __init__(self, flow: Dict[str, Set[str]], exists=True):
+ def __init__(self, flow: Dict[str, Set[str]], exists:bool=True):
self.flow = flow
# Should not be modified directly. See the nonexistent() method
@@ -81,6 +81,25 @@
out.add(source)
return out
+ def all_sources(self) -> Set[str]:
+ '''Returns all sources in the graph.'''
+ out : Set[str] = set()
+ return out.union(*self.flow.values())
+
+ def all_sinks(self) -> Set[str]:
+ '''Returns all sinks in the graph.'''
+ return set(self.flow.keys())
+
+ def sources_for_any(self, sinks: Iterator[str]) -> Set[str]:
+ '''Returns all nodes that are a source for any of the given sinks.'''
+ out : Set[str] = set()
+ return out.union(*(self.sources(s) for s in sinks))
+
+ def sinks_for_any(self, sinks: Iterator[str]) -> Set[str]:
+ '''Returns all nodes that are a sink for any of the given sources.'''
+ out : Set[str] = set()
+ return out.union(*(self.sinks(s) for s in sinks))
+
def update(self, other: 'InformationFlowGraph') -> None:
'''Updates self to include the information flow from other.
@@ -504,11 +523,11 @@
sources.add(node.evaluate(op_vals, constant_regs))
flow = {}
for node in self.flows_to:
- if node in READONLY:
+ dest = node.evaluate(op_vals, constant_regs)
+ if dest in READONLY:
# No information will actually flow to this node, because it is
# not writeable; skip.
continue
- dest = node.evaluate(op_vals, constant_regs)
flow[dest] = sources.copy()
return InformationFlowGraph(flow)
diff --git a/hw/ip/otbn/util/shared/information_flow_analysis.py b/hw/ip/otbn/util/shared/information_flow_analysis.py
index 8d17720..7625b72 100755
--- a/hw/ip/otbn/util/shared/information_flow_analysis.py
+++ b/hw/ip/otbn/util/shared/information_flow_analysis.py
@@ -3,14 +3,14 @@
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
-from typing import Dict, List, Optional, Set, Tuple
+from typing import Callable, Dict, List, Optional, Set, Tuple
-from .control_flow import *
from .cache import CacheEntry, Cache
+from .constants import ConstantContext, get_op_val_str
+from .control_flow import *
from .decode import OTBNProgram
from .information_flow import InformationFlowGraph, InsnInformationFlow
from .insn_yaml import Insn
-from .operand import RegOperandType
from .section import CodeSection
# Calls to _get_iflow return results in the form of a tuple with entries:
@@ -23,18 +23,18 @@
# end iflow: an information-flow graph for any paths that lead to the
# end of the entire program (i.e. ECALL instruction or end of
# IMEM)
-# new constants: values of constants that are shared between all paths
-# ending in RET (i.e. the constants a caller can rely on
-# regardless of input)
+# constants: known constants that are in common between all "return"
+# iflow paths (i.e. the constants that a caller can always
+# rely on)
# control deps: a dictionary whose keys are the information-flow nodes
# whose values at the start PC influence the control flow of
# the program; the value is a set of PCs of control-flow
# instructions through which the node influences the control
# flow
-IFlowResult = Tuple[Set[str], InformationFlowGraph, InformationFlowGraph, Dict[str, int],
+IFlowResult = Tuple[Set[str], InformationFlowGraph, InformationFlowGraph, ConstantContext,
Dict[str, Set[int]]]
-class IFlowCacheEntry(CacheEntry[Dict[str,int], IFlowResult]):
+class IFlowCacheEntry(CacheEntry[ConstantContext, IFlowResult]):
'''Represents an entry in the cache for _get_iflow.
The key for the cache entry is the values of certain constants in the input
@@ -44,13 +44,13 @@
same values for those constants but different values for others, the result
should not change.
'''
- def is_match(self, constants: Dict[str,int]) -> bool:
- for k,v in self.key.items():
- if constants.get(k, None) != v:
+ def is_match(self, constants: ConstantContext) -> bool:
+ for k,v in self.key.values.items():
+ if constants.get(k) != v:
return False
return True
-class IFlowCache(Cache[int,Dict[str,int], IFlowResult]):
+class IFlowCache(Cache[int,ConstantContext, IFlowResult]):
'''Represents the cache for _get_iflow.
The index of the cache is the start PC for the call to _get_iflow. If this
@@ -69,64 +69,9 @@
# at the start, we're not expecting any return paths!
ProgramIFlow = Tuple[InformationFlowGraph, Dict[str, Set[int]]]
-
-def _get_op_val_str(insn: Insn, op_vals: Dict[str, int], opname: str) -> str:
- '''Get the value of the given operand as a string.'''
- op = insn.name_to_operand[opname]
- return op.op_type.op_val_to_str(op_vals[opname], None)
-
-
-def _update_constants_insn(insn: Insn, op_vals: Dict[str, int],
- constants: Dict[str, int]) -> None:
- '''Determines the values of constant registers after the instruction.
-
- Currently, this procedure supports only a limited set of instructions.
- Since constant values only need to be known in order to decode indirect
- references to WDRs and loop counts, this set is chosen based on operations
- likely to happen to those registers: `addi`, `lui`, and bignum instructions
- containing `_inc` op_vals.
-
- Modifies the input `constants` dictionary.
- '''
- new_constants = {'x0': 0}
- if insn.mnemonic == 'addi':
- grs1_name = _get_op_val_str(insn, op_vals, 'grs1')
- if grs1_name in constants:
- grd_name = _get_op_val_str(insn, op_vals, 'grd')
- # Operand is a constant; add/update grd
- new_constants[grd_name] = constants[grs1_name] + op_vals['imm']
- elif insn.mnemonic == 'lui':
- grd_name = _get_op_val_str(insn, op_vals, 'grd')
- new_constants[grd_name] = op_vals['imm'] << 12
- else:
- # If the instruction has any op_vals ending in _inc that are nonzero,
- # assume we're incrementing the corresponding register
- for op in insn.operands:
- if op.name.endswith('_inc'):
- if op_vals[op.name] != 0:
- # If reg to be incremented is a constant, increment it!
- inc_op = op.name[:-(len('_inc'))]
- inc_name = _get_op_val_str(insn, op_vals, inc_op)
- if inc_name in constants:
- new_constants[inc_name] = constants[inc_name] + 1
-
- # If the instruction's information-flow graph indicates that we updated any
- # constant register other than the ones handled above, the value of that
- # register can no longer be determined; remove it from the constants
- # dictionary.
- iflow_graph = insn.iflow.evaluate(op_vals, constants)
- for sink, sources in iflow_graph.flow.items():
- if sink in constants and sink not in new_constants:
- del constants[sink]
-
- constants.update(new_constants)
-
- return
-
-
def _build_iflow_insn(
insn: Insn, op_vals: Dict[str, int], pc: int,
- constants: Dict[str, int]) -> Tuple[Set[str], InformationFlowGraph]:
+ constants: ConstantContext) -> Tuple[Set[str], InformationFlowGraph]:
'''Constructs the information-flow graph for a single instruction.
Raises a ValueError if the information-flow graph cannot be constructed
@@ -149,9 +94,9 @@
'need to add constant-tracking support for more '
'instructions.'.format(const, pc,
insn.disassemble(pc, op_vals),
- list(constants.keys())))
+ list(constants.values.keys())))
- return constant_deps, insn.iflow.evaluate(op_vals, constants)
+ return constant_deps, insn.iflow.evaluate(op_vals, constants.values)
def _get_insn_control_deps(insn: Insn, op_vals: Dict[str, int]) -> Set[str]:
@@ -164,18 +109,18 @@
return set()
elif insn.mnemonic in ['beq', 'bne']:
# both compared values influence control flow
- grs1_name = _get_op_val_str(insn, op_vals, 'grs1')
- grs2_name = _get_op_val_str(insn, op_vals, 'grs2')
+ grs1_name = get_op_val_str(insn, op_vals, 'grs1')
+ grs2_name = get_op_val_str(insn, op_vals, 'grs2')
return {grs1_name, grs2_name}
elif insn.mnemonic == 'jalr':
if op_vals['grs1'] == 1:
return set()
# jump destination register influences control flow
- grs1_name = _get_op_val_str(insn, op_vals, 'grs1')
+ grs1_name = get_op_val_str(insn, op_vals, 'grs1')
return {grs1_name}
elif insn.mnemonic == 'loop':
# loop #iterations influences control flow
- grs_name = _get_op_val_str(insn, op_vals, 'grs')
+ grs_name = get_op_val_str(insn, op_vals, 'grs')
return {grs_name}
elif insn.mnemonic in ['ecall', 'jal', 'loopi']:
# these all rely only on immediates
@@ -187,7 +132,7 @@
def _build_iflow_straightline(
program: OTBNProgram, start_pc: int, end_pc: int,
- constants: Dict[str, int]) -> Tuple[Set[str], InformationFlowGraph]:
+ constants: ConstantContext) -> Tuple[Set[str], InformationFlowGraph]:
'''Constructs the information-flow graph for a straightline code section.
Returns two values:
@@ -217,19 +162,20 @@
iflow = iflow.seq(insn_iflow)
# Update constants to their values after the instruction
- _update_constants_insn(insn, op_vals, constants)
+ constants.update_insn(insn, op_vals)
# Update used constants to include constants that were used to compute the
# new constants
- for const in constants:
- const_sources = iflow.flow[const] if const in iflow.flow else {const}
- constant_deps.update(const_sources)
+ # TODO: results in unnecessary re-computations for updated constants that
+ # we don't end up using; see if we can improve performance here?
+ const_sources = iflow.sources_for_any(iter(constants.values.keys()))
+ constant_deps.update(const_sources)
return constant_deps, iflow
def _get_constant_loop_iterations(insn: Insn, op_vals: Dict[str, int],
- constants: Dict[str, int]) -> Optional[int]:
+ constants: ConstantContext) -> Optional[int]:
'''Given a loop instruction, returns the number of iterations if constant.
If the number of iterations is not constant, returns None.
@@ -238,23 +184,23 @@
if insn.mnemonic == 'loopi':
return op_vals['iterations']
elif insn.mnemonic == 'loop':
- reg_name = _get_op_val_str(insn, op_vals, 'grs')
- return constants.get(reg_name, None)
+ reg_name = get_op_val_str(insn, op_vals, 'grs')
+ return constants.get(reg_name)
# Should not get here!
assert False
-def _get_iflow_cache_update(pc: int, constants: Dict[str, int],
+def _get_iflow_cache_update(pc: int, constants: ConstantContext,
result: IFlowResult, cache: IFlowCache) -> None:
'''Updates the cache for _get_iflow.'''
used_constants = result[0]
used_constant_values = {}
for name in used_constants:
assert name in constants
- used_constant_values[name] = constants[name]
+ used_constant_values[name] = constants.values[name]
- cache.add(pc, IFlowCacheEntry(used_constant_values, result))
+ cache.add(pc, IFlowCacheEntry(ConstantContext(used_constant_values), result))
return
@@ -306,7 +252,7 @@
def _get_iflow(program: OTBNProgram, graph: ControlGraph, start_pc: int,
- start_constants: Dict[str, int], loop_end_pc: Optional[int],
+ start_constants: ConstantContext, loop_end_pc: Optional[int],
cache: IFlowCache) -> IFlowResult:
'''Gets the information-flow graphs for paths starting at start_pc.
@@ -337,7 +283,7 @@
program_end_iflow = InformationFlowGraph.nonexistent()
# The control-flow nodes whose values at the start PC influence control
- # flow
+ # flow (and the PCs of the control-flow instructions they influence)
control_deps: Dict[str, Set[int]] = {}
section = graph.get_section(start_pc)
@@ -358,7 +304,8 @@
section.end - 4,
constants)
- # Get the instruction/operands at the very end of the block for special handling
+ # Get the instruction/operands at the very end of the block (i.e. the
+ # control-flow instruction) for special handling
last_insn = program.get_insn(section.end)
last_op_vals = program.get_operands(section.end)
@@ -390,18 +337,15 @@
'known constant at PC {:#x} (known constants: {}). If '
'the register is in fact a constant, you may need to '
'add constant-tracking support for more instructions.'.format(
- section.end, constants.keys()))
+ section.end, constants.values.keys()))
- if len(edges) != 1 or not isinstance(edges[0], LoopStart):
- raise RuntimeError(
- 'Control graph section ends in {} at PC {:#x} but the '
- 'next control-flow locations are: {} instead of 1 '
- 'LoopStart instance as expected'.format(
- last_insn.mnemonic, section.end, edges))
+ # A loop instruction should result in exactly one edge of type
+ # LoopStart; check that assumption before we rely on it
+ assert len(edges) == 1 and isinstance(edges[0], LoopStart)
body_loc = edges[0]
# Update the constants to include the loop instruction
- _update_constants_insn(last_insn, last_op_vals, constants)
+ constants.update_insn(last_insn, last_op_vals)
# Recursive calls for each iteration
for _ in range(iterations):
@@ -426,13 +370,10 @@
elif last_insn.mnemonic == 'jal' and last_op_vals['grd'] == 1:
# Special handling for jumps; recursive call for jump destination, then
# continue at pc+4
- if len(edges) != 1 or edges[0].is_special():
- raise RuntimeError(
- 'Control graph section ends in {} at PC {:#x} but the '
- 'next control-flow locations are: {} instead of 1 '
- 'non-special ControlLoc instance as expected'.format(
- last_insn.mnemonic, section.end, edges))
+ # Jumps should produce exactly one non-special edge; check that
+ # assumption before we rely on it
+ assert len(edges) == 1 and not edges[0].is_special()
jump_loc = edges[0]
jump_result = _get_iflow(program, graph, jump_loc.pc, constants, None,
cache)
@@ -454,37 +395,28 @@
edges = [ControlLoc(section.end + 4)]
else:
# Update the constants to include the last instruction
- _update_constants_insn(last_insn, last_op_vals, constants)
+ constants.update_insn(last_insn, last_op_vals)
# We're only returning constants that are the same in all RET branches
common_constants = None
for loc in edges:
if isinstance(loc, Ecall) or isinstance(loc, ImemEnd):
- if len(edges) != 1:
- raise RuntimeError(
- 'Control graph section at PC {:#x} has edges {}; if '
- 'edges contain an Ecall or ImemEnd, it is expected to '
- 'be the only edge.'.format(section.end, edges))
-
+ # Ecall or ImemEnd nodes are expected to be the only edge
+ assert len(edges) == 1
# Clear common constants; there are no return branches here
- common_constants = {'x0': 0}
+ common_constants = ConstantContext.empty()
program_end_iflow.update(iflow)
elif isinstance(loc, Ret):
if loop_end_pc is not None:
raise RuntimeError(
'RET before end of loop at PC {:#x} (loop end PC: '
'{:#x})'.format(section.end, loop_end_pc))
- if len(edges) != 1:
- raise RuntimeError(
- 'Control graph section at PC {:#x} has edges {}; if '
- 'edges contain a Ret, it is expected to be the only '
- 'edge.'.format(section.end, edges))
-
+ # Ret nodes are expected to be the only edge
+ assert len(edges) == 1
# Since this is the only edge, common_constants must be unset
common_constants = constants
return_iflow.update(iflow)
-
elif isinstance(loc, LoopStart):
# We shouldn't hit a LoopStart here; those cases (a loop
# instruction or the end of a loop) are all handled earlier
@@ -502,14 +434,11 @@
# Get information flow for return paths and new constants
_, rec_return_iflow, _, rec_constants, _ = result
- # Update common constants
+ # Take values on which existing and recursive constants agree
if common_constants is None:
common_constants = rec_constants
else:
- for const, value in rec_constants.items():
- if common_constants.get(const, None) != value:
- # Remove from common_constants if key exists
- common_constants.pop(const, None)
+ common_constants.intersect(rec_constants)
# Update return_iflow with the current iflow composed with return
# paths
@@ -519,8 +448,9 @@
'Unexpected next control location type at PC {:#x}: {}'.format(
section.end, type(loc)))
- if common_constants is None:
- common_constants = constants
+ # There should be at least one edge, and all edges should set
+ # common_constants to some non-None value
+ assert common_constants is not None
# Update the cache and return
out = (used_constants, return_iflow, program_end_iflow, common_constants,
@@ -565,7 +495,7 @@
check_acyclic(graph)
start_pc = program.get_pc_at_symbol(subroutine_name)
_, ret_iflow, end_iflow, _, control_deps = _get_iflow(
- program, graph, start_pc, {'x0': 0}, None, IFlowCache())
+ program, graph, start_pc, ConstantContext.empty(), None, IFlowCache())
if not (ret_iflow.exists or end_iflow.exists):
raise ValueError('Could not find any complete control-flow paths when '
'analyzing subroutine.')
@@ -584,7 +514,7 @@
'''
check_acyclic(graph)
_, ret_iflow, end_iflow, _, control_deps = _get_iflow(
- program, graph, program.min_pc(), {'x0': 0}, None, IFlowCache())
+ program, graph, program.min_pc(), ConstantContext.empty(), None, IFlowCache())
if ret_iflow.exists:
# No paths from imem_start should end in RET
raise ValueError('Unexpected information flow for paths ending in RET '
diff --git a/hw/ip/otbn/util/shared/section.py b/hw/ip/otbn/util/shared/section.py
index 1bc2ac2..1b3c300 100644
--- a/hw/ip/otbn/util/shared/section.py
+++ b/hw/ip/otbn/util/shared/section.py
@@ -3,7 +3,7 @@
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
-from typing import List
+from typing import Iterator, List
from shared.decode import OTBNProgram
from shared.insn_yaml import Insn
@@ -19,12 +19,14 @@
self.end = end
def get_insn_sequence(self, program: OTBNProgram) -> List[Insn]:
- return [
- program.get_insn(pc) for pc in range(self.start, self.end + 4, 4)
- ]
+ return [ program.get_insn(pc) for pc in self.__iter__() ]
def pretty(self) -> str:
return '0x{:x}-0x{:x}'.format(self.start, self.end)
+ def __iter__(self) -> Iterator[int]:
+ '''Iterates through PCs in the section.'''
+ return iter(range(self.start, self.end + 4, 4))
+
def __contains__(self, pc: int) -> bool:
return self.start <= pc and pc <= self.end