|
Content
|
Style Guide for MMIX Assembler Programming
This is an evolving collection of hints how to write readable
programs in MMIX assembly language.
Names
- You can use all lower case names. For example i,
count, or xptr. This is the preferred way
to name registers. Keep these names short but not too short.
- You can use all UPPER case to name constants. For example
NEXT, LINK, or DATA. This is the
prefer way of naming constant offsets to a base address.
- You can use CamelCase. For example StdOut,
BinaryRead, NextChar or Main.
CamelCase is the preferred way to write program labels.
- You can use underscores to make long names more readable. For example
Data_Segment or lring_size. This is the preferred
way of naming entities that are not used very often and therefore
require longer, more descriptive names.
- You should use register numbers like $0 or $1 only
for short program fragments. For complete procedures, assign names to
the registers using the IS operator. As an exception to this
rule, you may use $255 to hold the parameter value for a
TRAP instruction or $0 to receive the return value
of a subroutine.
Subroutines
Comments
All subroutines should start with a short comment
explaining the code to follow. After that, use comments
only if the comment adds information, that is not
yet obvious from the code. It is better to have readable
code without comments than unreadable code with comments.
The Stack Frame
A subroutine starts with setting up its stack frame.
The stack frame contains parameters and local variables.
For most subroutines, it is sufficient to name the appropriate
registers.
Therefore, the code should start naming the parameters and
local register variables.
Often you need a temporary variable, just to move data from one
instruction to the next. One temporary variable is almost always
enough. If you need more than one, think about naming them appropriately.
It is good practice to make the temporary variable the last of the
local variables (see Nested Subroutines below).
You may use $0 just before a POP 1,0 to receive
the function return value.
For example:
% converting a number to an ASCII digit
d IS $0 parameter
ToAscii ADD $0,d,'0'
POP 1,0
or
% computing the sum of an array a of n tetras
a IS $0 parameters
n IS $1
s IS $2 local variables
tmp IS $3
Sum SET s,0
JMP 2F
1H LDT tmp,a,0
ADD s,s,tmp
ADD a,a,4
SUB n,n,1
2H BP n,1B
SET $0,s
POP 1,0
Local Namespace
If you use multiple subroutines, name conflicts will be
inevitable, unless you use the PREFIX pseudo instruction.
In most cases you have a separate namespace for each subroutine.
It can have the same name as the subroutine itself.
The entry point of the subroutine is usually the only "global"
name defined by a subroutine.
Example:
PREFIX :Sum:
% computing the sum of an array a of n tetras
a IS $0 parameters
n IS $1
s IS $2 local variables
tmp IS $3
:Sum SET s,0
JMP 2F
1H LDT tmp,a,0
ADD s,s,tmp
ADD a,a,4
SUB n,n,1
2H BP n,1B
SET $0,s
POP 1,0
In the example above, the subroutine is referenced in the rest of the
program by :Sum.
Calling Subroutines
The most convenient way to pass parameters to a subroutine is the following:
- Define a local variable tmp as the last local variable
(the one with the highest register number) and
call the subroutine using a PUSHJ tmp,YZ instruction.
This will ensure that none of the other local variables or parameters
will be modified by the subroutine.
- Use tmp+1, tmp+2 to name the registers containing
the arguments for the subroutine call. This naming of the arguments
remains invariant when reordering or renumbering the local registers
in the enclosing subroutine.
- Use the names tmp, tmp+1, tmp+2 to access
the return values.
Example: Calling the above function :Sum, we can write:
LDA tmp+1,my_array
SET tmp+2,my_size
PUSHJ tmp,:Sum
Note that assigning a different register number to tmp will
automatically renumber the corresponding registers for the argument values.
Further, all local variables and parameters (having local register numbers
smaller than tmp) will keep their values during the subroutine call.
Nested Subroutines
If one subroutine calls another subroutine, this is called
nested subroutines. Two things need to be done: saving the return address
and passing parameters.
The most common error when doing so is not saving and restoring
the rJ register, which contains the return address
for the POP instruction.
There are two preferred places to save and restore rJ
- Start the subroutine with a GET instruction,
saving rJ in a local register, and end the
subroutine with a PUT instruction, restoring rJ,
immediately preceding the POP instruction that
terminates the subroutine.
- If the subroutine contains only a single PUSHJ instruction,
you can save rJ immediately before the PUSHJ
and restore it immediately after the PUSHJ.
Example: Using the above function :Sum, we can write the
function :Average.
PREFIX :Average:
% compute the average of an array a of n tetras
a IS $0 parameters
n IS $1
return IS $2 local variables
tmp IS $3
:Sum GET return,:rJ
SET tmp+1,a
SET tmp+2,n
PUSHJ tmp,:Sum
FLOT tmp,tmp
FLOT n,n
FDIV $0,tmp,n
PUT :rJ,return
POP 1,0
or
PREFIX :Average:
% compute the average of an array a of n tetras
a IS $0 parameters
n IS $1
return IS $2 local variables
tmp IS $3
:Sum SET tmp+1,a
SET tmp+2,n
GET return,:rJ
PUSHJ tmp,:Sum
PUT :rJ,return
FLOT tmp,tmp
FLOT n,n
FDIV $0,tmp,n
POP 1,0
|