To HLL and Back

Applying High Level Language constructs to 6502 Assembly

An occasional series by Michael Martin

Part 5: Call Stacks

All our previous work has been assuming FORTRAN-style calling conventions. In this, all procedure-local variables are actually secretly globals. This means that a function that calls itself will end up stomping on its previous values, and everything will be hideously scrambled. Various workarounds for this are covered in the Structured Programming article. Here, we solve the problem fully.

Recursion

A procedure in C or other similar languages declares a chunk of storage that's unique to that invocation. This chunk is just large enough to hold the return address and all the local variables, and is called the stack frame. Stack frames are arranged on a call stack; when a function is called, the stack grows with the new frame, and when that function returns, its frame is destroyed. Once the main function returns, the stack is empty.

Most modern architectures are designed to let you implement variable access like this directly, without touching the registers at all. The x86 architecture even dedicates a register to function explicitly as the stack pointer, and then one could read, say, the fifth 16-bit variable into the register AX with the command:

MOV AX, [SP+10]

As we saw in the Indirection article, the 6502 isn't nearly as convenient. We'd need to keep the stack pointer somewhere on the zero page, then load the Y register with 10, then load the accumulator with an indexed-indirect call. This is verbose, keeps trashing our registers, and it's very, very slow.

So, in the spirit of programmers everywhere, we'll cheat.

Our Goals

The system we develop should have all of the following characteristics.

Our Solution

Here is a system that meets all these properties.

This meets our design criteria neatly:

The necessary support code is pretty straightforward. The stack modification routines take the size of the frame in the accumulator, and while saving the local area, it copies over the corresponding values from the scratch space. (This is because most functions will be wanting to keep their arguments around across calls.)

.scope
; Stack routines
.data zp
.space _sp 	$02
.space _counter	$01
.space fun'args $10
.space fun'vars $40

.text
init'stack:
	lda	#$00
	sta	_sp
	lda	#$A0
	sta	_sp+1
	rts

save'stack:
	sta	_counter
	sec
	lda	_sp
	sbc	_counter
	sta	_sp
	lda	_sp+1
	sbc	#$00
	sta	_sp+1
	ldy	#$00
*	lda	fun'vars, y
	sta	(_sp), y
	lda	fun'args, y
	sta	fun'vars, y
	iny
	dec	_counter
	bne -
	rts

restore'stack:
	pha
	sta	_counter
	ldy	#$00
*	lda	(_sp), y
	sta	fun'vars, y
	iny
	dec	_counter
	bne -
	pla
	clc
	adc	_sp
	sta	_sp
	lda	_sp+1
	adc	#$00
	sta	_sp+1
	rts
.scend

Example: Fibonnacci Numbers

About the simplest "interesting" recursive function is the Fibonacci numbers. The function fib(x) is defined as being 1 if x is 0 or 1, and being fib(x-2)+fib(x-1) otherwise.

Actually expressing it like that directly produces a very inefficient implementation, but it's a simple demonstration of the system. Here's P65-assemblable code for expressing the fib function:

.scope
; Uint16 fib (Uint8 x): compute Xth fibonnaci number.
; fib(0) = fib(1) = 1.
; Stack usage: 3.

fib:	lda	#$03
	jsr	save'stack
	lda	fun'vars
	cmp	#$02
	bcc	_base

	dec	fun'args
	jsr	fib
	lda	fun'args
	sta	fun'vars+1
	lda	fun'args+1
	sta	fun'vars+2
	lda	fun'vars
	sec
	sbc	#$02
	sta	fun'args
	jsr	fib
	clc
	lda	fun'args
	adc	fun'vars+1
	sta	fun'args
	lda	fun'args+1
	adc	fun'vars+2
	sta	fun'args+1
	jmp	_done

_base:	ldy	#$01
	sty	fun'args
	dey
	sty	fun'args+1

_done:	lda	#$03
	jsr	restore'stack
	rts
.scend

The complete application

The full application, which deals with interfacing with CBM BASIC and handles console I/O and such, is here. The binary, which most C64 emulators can run directly, is here.


Back to series index