The Instruction Model is the main abstraction to define what is the behavior that a given approximation has in the target application. When the architecture simulator, while running an application, decodes an instruction, it checks for the activated Instruction Models to look for what it should do. There are two types of instruction models: implementation modifiers and data modifiers.
Implementation modifiers
Implementation modifiers directly affect the operation an instruction executes. These models may replace or augment the original instruction behavior (defined by the CPU Model).
An implementation modifier is a C++ method that returns nothing (void) and may receive ordinary parameters, including words, as defined by the architecture. They are identified by the IM keyword as type idendifier. When a method uses a word as parameter, it is by default passed as a reference, which means that any changes to the word are reflected in the original structure it refers to.
The example below show a simple implementation modifier that performs an addition operation. Naturally, it should receive three words as parameters, two as the operands and one to store the result.
IM MyAdd(word r, word a, word b) {
r = a + b;
}
This simple example intends to replace the behavior of a given instruction. In ADeLe, this is called an “alternate behavior”, and is declared in the ADeLe Description File using the keyword alt_behavior. ADeLe can also define an implementation modifier to be called before or after an instruction execution. In the example below, a designer wants to trace all addition operations performed by an application for later offline analysis. So, they define two methods, one to execute before the instruction and the other after, to capture the data and print to a file handler add_dump.
IM BeforeAdd (word a, word b) {
add_dump << static_cast<unsigned int>(a) << " + " << static_cast<unsigned int>(b) << " = ";
}
IM AfterAdd (word r) {
add_dump << static_cast<unsigned int>(r) << std::endl;
}
These models that execute before and after an instruction operation are called, in the ADeLe Description File, pre_behavior and post_behavior, respectively. For more information on how to instantiate the implementation modifiers, see Instantiating implementation modifiers.
Data Modifiers
Data modifiers diverge from implementation modifiers because they do not explicitly change the operation performed by an instruction. Instead, they affect just the data, without any knownledge of what application is being executed.
Fow example, assume one wants do model a 16-bit ALU in a 32-bit architecture. Since the word size in the architecture is 32-bit, the idea is to truncate all data read and written by logical and arithmetical operations to 16-bit. Using a data modifier, this is a transparent change to the instruction implementation. The example below used bitwise operations to truncate the data while maintaining, when possible, the value as a signed operand.
DM Truncate32to16(source_t source, uint64_t& data) {
if (data & 0x80000000 != 0x0) {
data = (data & 0x7FFF) | 0xFFFFFFFFFFFF8000;
}
else {
data = data & 0x7FFF;
}
}
A data modifier automatically receives, from its caller, two parameters: a data structure containing information about the caller routine and source of the data (source_t) and a reference to the data itself, encoded as an unsigned 64-bit value (currently the largest data word length supported).
The source structure
The source structure encondes information on where that triggered a data modifier call come from. This is a C++ structure of the form:
struct source_t {
typedef enum {MEM, REGBANK, REG} type_t;
typedef enum {READ, WRITE} op_t;
type_t type;
const std::string& name;
uint32_t address;
op_t op;
size_t width;
};
Where type indicates whether the data resides in memory (MEM), in a register bank (REGBANK), or in an individual register (REG); name stores the user-defined name of the storage unit (some common names are “RB” for a register bank, “RBF” for floating-point registers, and “MEM” for the default memory port); address contains the location being accessed in the storage unit, which is the memory address or register number (individual registers do not use this field); op indicates the operation being performed (read or write); and width contains the data word width, in bytes.
The source structure is intended provide additional information on the data handling operation. For example, the following code snippet randomly flips one single bit when reading from the memory device called “MEM”. Supposing that the modeled architecture implements an “upside-down” stack and stores the stack pointer in register $2 of a register bank called “RB”, such as MIPS, this modifier isolates the stack from any approximation. Moreover, it uses the information of width to select a random bit from the data regardless of its length.
DM RandomBitFlip(source_t source, uint64_t& data) {
if (
source.type == source_t::MEM &&
source.name == "MEM" &&
source.address < RB[2] && // Stack pointer
source.op == source_t::READ
) {
unsigned int bit = rand() % (source.width * 8);
data ^= (0x1ULL << bit);
}
}