The memory reference instructions of the Hawk instruction set support an odd addressing mode. To quote the Hawk manual:
The effective memory address referenced by such an instruction is formed by sign-extending the 16 bit displacement to 32 bits and adding it to the specified index register:Thus, "LOAD R1,0,16" does not reference memory location 16, it references location (.+4)+16 -- that is, the location 16 bytes beyond the LOAD instruction, noting that the LOAD instruction itself is 4 bytes long.ea = r[x] + sxt(disp)In this special case, when x = 0, r[x] refers to the program counter. As a result, indexed addressing using r[0] is relative to the address of the immediately following instruction.
This addressing mode is called PC relative addressing or just relative addressing, for short. The branch instructions use a very similar addressing mode, but using an 8 bit branch displacement where the memory addressing instructions use a 16 bit displacement. Thus, where the branch instructions could only reference things within a 256 byte range, the LOAD, STORE and related instructions can do PC relative references to anywhere within a 65536 word range.
To simplify use of the PC relative addressing, the SMAL Hawk assembly language allows a two address form of the memory address instructions. Thus, the following two SMAL Hawk instructions are equivalent:
LOAD R1,0,16 LOAD R1,(.+4)+16Note that, if only two operands are given on a memory reference instruction, the assembler automatically assumes that the x (index) field should be zero and the assembler interprets the final operand as the address to be referenced. Usually, the third operand would be a label, for example:
LOAD R1,data ... BR somewhere ALIGN 4 data: W "XXXX"In this example, the constant "XXXX" is loaded into R1 using PC relative addressing. The assembler automatically computes the difference between the label data and the location of the LOAD instruction and stores that difference as the displacement on the PC relative LOAD.
It is always safe to embed constants in your program, so long as the constants are not executed. Thus, it is safe to put constants after any unconditional branch, and in fact, it is recommended to put constants needed by a procedure or function either immediately before the entry point or immediately after the return that marks the end of that procedure or function. Don't do both; adopt a convention and use it consistantly.
Remember that instructions are composed of strings of halfwords, but that one-word constants must be aligned -- stored so that they do not straddle a word boundary. The use of the ALIGN directive above assures this!
The Hawk machine requires that programmers use radically different styles of programming to reference bytes and words, but this difference can be hidden by use of appropriate macros. Consider:
MACRO LOADBS =r,=x LOADS r,x EXTB r,r,x ENDMACThis loads one byte, using a form that looks, to the assembly language programmer, like the Hawk short memory reference instruction.
MACRO STORBS =r,=x LOADS RF,x STUFFB RF,r,x STORES RF,x ENDMACThe above macro stores one byte from r into the memory location referenced by register x. It needs a temporary register, and uses r[15] for this purpose. Programmers using this macro would typically avoid using r[15] for anything, and having made this decision, other macros that need a temporary register could also use r[15].
MACRO LOADB =r,=x,=a LEA RF,x,a LOADS r,RF EXTB r,r,RF ENDMACThe above macro appears, to the assembly language programmer, to be the load byte instruction that corresponds to the simple LOAD instruction.
The corresponding STOREB, LOADH and STOREH macros are left as exercises.
For control structures that involve control transfers within a 256 byte range, the BR and conditional branch instructions are adequate. For those structures that require longer transfers, the Hawk machine provides a JUMP instruction. Typically, this is coded as
JUMP where ... where:In fact, this is a PC relative memory addressing instruction, and it is legal to use:
JUMP R5,dispWhich adds R5 to disp, a 16-bit constant, in order to compute the destination address.
If a program must jump outside the range of a 16 bit PC relative displacement, the usual thing to do is to load the destination address into a register and then use the short form, JUMPS, to jump to the address in that register. The recommended way to do this is as follows:
LOAD R4,farref JUMPS R4 ALIGN 4 farref: W farawayThe reason for loading a word from memory instead of using something like a load immediate word macro (LIL followed by ORIS) will become apparent when we discuss separate assembly.
The JUMP instructions are actually special cases of the JSR (jump to subroutine) command. JSR commands transfer control to an effective address, but first, they store the return address in a register. The JUMP instructions store the return address in the nonexistant register r[0].
The following little program fragment illustrates a minimal use of the JSR instruction:
JSR R1,mul10 ; multiply R3 by 100 JSR R1,mul10 ... mul10: ; procedure to multiply R3 by 10 ; link through R1 ADDSL R3,R3,2 ADDSL R3,0,1 JUMPS R1 ; returnWhen writing procedures in a high level language, the language itself forces you to document the existence of parameters and it hides how the return address is saved while the procedure is running. In assembly language, the programmer must explicitly document all of this. Thus, in the above, the procedure entry point includes a comment documenting the use of R3 as a parameter, and it includes a comment explaining how to call the procedure, and the calls include a comment indicating what register is modified.
When talking about procedure calling at the assembly and machine language level, it is useful to have names for the various parts. The key parts with which we are dealing are:
LOAD Rx,procref JSRS Rx,Rx ... procref:W proc
The key problems that a calling sequence must solve are:
On many machines, particularly those designed in the CISC (Complex Instruction Set Computer) tradition, the procedure calling instructions are designed with one particular calling sequence in mind. This is clearly true of the DEC VAX, and it is also well illustrated by the Intel 80x86 family and the Motorola 68000 family. With RISC machines, on the other hand, the instructions used to transfer control to and from the called routine frequently give few hints about what sequence is preferred.
Here is a suggested standard calling sequence for the HAWK machine: