This document discusses how to implement your own instructions.
For this first step, you will be editing the virtual CPU definition in
cHardwareCPU.h and cHardwareCPU.cc, both of which are
found in the directory source/cpu/. Start by going to the
final section of the class definition in the header file and writing
the declaration for the new method that will be called whenever the
instruction is executed. For example, if you were going to add the
instruction minus-17
(which performs the oh-so-useful behavior
of subtracting 17 from the ?BX? register), you would add the line:
bool Inst_Minus17(cAvidaContext& ctx);
If possible, place it near other instructions of the same type. There are about a hundred methods cHardwareCPU. This instruction would likely fit best with the group of instruction described as "Single-Argument Math". That is, all those instructions that perform mathematical operation that only affect a single register.
All methods associated with instructions return a
bool value that determines if it was
successfully executed. Most instructions will always return true since they
have now way to fail. The convention that we use to designate a method
explicitly associated with an instruction is placing a prefix of
Inst_
in front of it.
Next, you have to write the function body in the code file (cHardwareCPU.cc). The method bodies will be listed at the end of this file in the same order that they were declared in the header. You would find the proper position, and write something to the effect of:
void cHardwareCPU::Inst_Minus17(cAvidaContext& ctx) { const int reg_used = FindModifiedRegister(nHardwareCPU::REG_BX); GetRegister(reg_used) -= 17; return true; }
The first line of this method uses a helper function called
FindModifiedRegister() to identify the register
that should be affected (it scans the next instruction to test if it is a
nop
), with a default value of REG_BX
passed in.
The second line then subtracts 17 from the value in that register. The
constant values and available helper functions will be described in more
detail below, as will a guide to accessing the components in the virtual
CPU. For the moment, you have finished implementing the method!
Note that this would be a good time to recompile if you want to test how well your implementation is going so far.
cInstEntryCPU("minus-17", &cHardwareCPU::Inst_Minus17);
in the same order that it was defined in the class definition.
Since we want to use a pointer to the appropriate method, that is what we must pass into the dictionary. To obtain said pointer, we must list the class the function is part of (cHardwareCPU) follow it by a double colon (::) and then give the method name (Inst_Minus17) without the normal parentheses following it. The parentheses indicate that we should execute the method. Without them, it is just the data that represents the method, and by preceding this whole mess with an ampersand ('&') we get the pointer to the location in memory that the method resides.
Compile again, and you should have your instruction ready for use.
This last part should be the easiest. If you want the new instruction you just created to be loaded on startup, you must add a line in the instruction set you are using (specified in the configuration file) to indicate its inclusion:
minus-17 1
And there you have it! Now the real trick is to test if its working
properly. I'd recommend using as a framework the creature
default-classic.org and modifying some of the long series of
nop-C
instructions inside of it to perform some math using the
new instruction (only the very first nop-C
cannot be changed). You
can then either go into zoom mode in the viewer and step through the creature,
or else use analyze mode trace its execution. If you are going to use zoom
mode, setup your modified creature as the START_CREATURE in configuration file.
If you want to use analyze mode, put the following lines into the
analyze.cfg
file in your work/ directory:
LOAD_ORGANISM inst_test.org TRACE
Where you have to replace inst_test.org with the name of the organism you want to trace. The new file will appear in the data/archive/ directory, with the same name as the one you loaded in, but a .trace appended to the end.
Various CPU components are often manipulated by instructions, and we need a standard way of doing this. We have settled on each component being associated with a method to access it, to provide a single location that can control that access. This has already been useful -- in a multi-threaded CPU (i.e., a CPU that has multiple positions in its genome being executed at the same time) each thread has its own registers and heads, so we need to always be sure we are manipulating the component of the active thread. If you simply use the following methods, they will always find the correct component for you.
void StackPush(int value); int StackPop(); void SwitchStack();
cCPUHead& GetHead(int head_id); cCPUHead& IP();
HEAD_IP
, HEAD_READ
,
HEAD_WRITE
, and HEAD_FLOW
. These heads each point
to a position in memory, and all have their own purpose. A head can be
accessed by passing the appropriate constant into the GetHead() method. The
extra method IP() was added to more easily obtain just the instruction pointer.
The IP is a very special head since it designates what instruction is going to
be executed next, and often it will make code clearer if you obtain it by
calling IP(). (It will show that you need to make sure of the special
qualities of the instruction pointer.)
int& Register(int reg_id);
REG_AX
, REG_BX
, and REG_CX
. If the
Register() method is called, an integer reference will be returned associated
with that register. Any change to this integer will make a corresponding
change to the register in question.
cCPUMemory& GetMemory();
These are only a sampling of the available methods of interacting with the components of the CPU, but they give you a good cross-section without overwhelming you with all of the possibilities. You should look through the source files if you want to see the other options that are available to you.
There are several very common tasks that are performed during the execution of many of the instructions. For each of these tasks we have created a helper function to ease the creation of new instructions.
void ReadLabel(); cCodeLabel& GetLabel(); cCPUHead FindLabel(int direction);
int FindModifiedRegister(int default_register); int FindModifiedHead(int default_head);
int FindComplementRegister(int base_reg);
To test your understanding of adding instruction into Avida, try writing two
new instructions. The first one is the mathematical instruction
cube
that will take the ?BX? register, and put
its value to the third power. If you look in the actual source files, you
will see that there is already a square
instruction that you can
model this on.
Next, you will implement the instruction if-twice
that will execute the next instruction if-and-only-if the value in the ?BX?
register is twice that of the value in its complement. In other words by
default, if would test of BX was twice CX, but if it is followed by a
nop-C
it will test if CX is twice AX.
For both of these instruction make sure to craft an organism to test that they are working properly!