22C:18, Lecture 12, Fall 1996

Douglas W. Jones
University of Iowa Department of Computer Science

Relative Addressing

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:
     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.
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.

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)+16
Note 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!

Some Handy Macros

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
	ENDMAC
This 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
	ENDMAC
The 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
        ENDMAC
The 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.

Jump and Call

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,disp
Which 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	faraway
The 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	; return
When 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.

Calling Sequences

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:

The Calling Sequence
The sequence of instructions used to call a procedure. On the Hawk machine, the minimal calling sequence is "JSR Rx,P", but even if such issues as recursion are ignored, more complex sequences must be considered. For example, consider the following calling sequence, appropriate for a procedure more than 32768 bytes from the point of call:
		LOAD	Rx,procref
		JSRS	Rx,Rx
		...
	procref:W	proc
	
The Receiving Sequence
The sequence of instructions at the entry point to a procedure, used to receive control from the calling sequence. In the simplest case, this sequence can be an empty one, but where recursion or more than a few local variables are involved, some complexity is common.

The Return Sequence
The sequence of instructions at the end of a procedure, used to return control to the calling sequence. For both calling sequences mentioned above, the return sequence is one instruction, "JUMPS Rx", but if recursion or more than a few local variables are involved, more complex sequences are needed.
Whatever sequences are used, it is important to remember that the calling sequence, receiving sequence, and return sequence largely determine each other. Given any two, most of the details of the third are almost completely pinned down. Because of this, it is common to speak only of the calling sequence, and assume that the receiving and return sequences are coded to match.

The key problems that a calling sequence must solve are:

  1. Where does the called procedure find its return address?
  2. Where does the called procedure find its parameters?
  3. How does the calling procedure find any results?
  4. How are registers that need saving saved?
  5. Where are local variables allocated?

A Standard Calling Sequence

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:

  1. R1 is used for the return address. Therefore, "JSR R1,DST" sits at the center of the calling sequence.

  2. R3 is used for the first parameter, and R4, R5, etc. are available for parameters.

  3. R3 is used to return results from functions, and R4 and up are available for more complex results.

  4. The author of a procedure or function is obligated to document what registers that procedure or function does and does not not disturb. The calling sequence for that procedure or function must be prepared to save and restore any register the procedure or function disturbs, but in any particular context, only those registers that contain useful data need to be saved. This way, the only registers that get saved are those that the caller needs and that the called routine might disturb.

  5. At the point of call, R2 points to the lowest unused word in a stack area, and R2 is restored to this value when the procedure or function exits. Thus, the procedure or function may allocate local variables and space to save registers relative to R2.
There is nothing sacred about this set of conventions, and in fact, there are many possible variations. This does not specify the Hawk calling sequence, but rather, one among many families of compatable calling sequences that can be used on the Hawk machine.