Okay, let's have a stab at this. I'm going to assume that you pass the 64K data bank in question in DBR - so that absolute addressing lands there - and that you will dedicate the *entire* such bank to be managed by these routines. The code will be quite small, so I'll use RTS instead of RTL; you can add four-byte stubs which consist of "JSR xxxx ; RTL" to permit cross-bank calling if you need it.
The very simplest type of memory allocator is one that can't return any memory on free(). All you need to implement this is a pointer to the next free byte, which you can insert at the beginning of the data bank being managed. This also serves as a (two's complement, negative) counter of how much space is still free, and helps to define an invalid NULL pointer to return when there's not enough memory left:
Code:
; Assume both indexes and accumulator are 16-bit
.LongA
.LongX
InitHeap:
LDA ##2 ; reserve first two bytes for freelist pointer
STA |$0000
RTS
; Pass number of bytes required in A (16-bit).
; Returns zero in X on failure, otherwise returns 16-bit pointer to allocated block in X.
AllocHeap:
LDX |$0000
BEQ :+ ; no space left
CLC
ADC |$0000
BCS :+ ; not enough space left
STA |$0000 ; update freelist pointer
RTS
: LDX ##0
RTS
; Return an allocated block to the heap. Should pass pointer to block in X. Actually a no-op.
FreeHeap:
RTS
This should be enough to get simple programs running.
However, most good allocators have the ability to free memory and return it to the heap. This at minimum requires a little extra sophistication, in the form of a second counter which tracks the net number of allocations made. When the number of frees equals the number of allocations, you can reset the heap to the empty state:
Code:
; Assume both indexes and accumulator are 16-bit
.LongA
.LongX
InitHeap:
LDA ##4 ; reserve first four bytes for freelist pointer and allocation counter
STA |$0000
STZ |$0002
RTS
; Pass number of bytes required in A (16-bit).
; Returns zero in X on failure, otherwise returns 16-bit pointer to allocated block in X.
AllocHeap:
LDX |$0000
BEQ :+ ; no space left
CLC
ADC |$0000
BCS :+ ; not enough space left
STA |$0000 ; update freelist pointer
INC |$0002 ; update allocation counter
RTS
: LDX ##0
RTS
; Return an allocated block to the heap. Should pass pointer to block in X. Does not verify validity.
FreeHeap:
DEC |$0002 ; update allocation counter
BEQ InitHeap
RTS
This method is crude in the extreme, but might be useful in limited circumstances due to its simplicity.
Another, more sophisticated approach is to keep all free blocks of memory in a linked list. This means any block can be returned to the heap by adding it to that list. The size of each block must be remembered to make this work, so the overheads become significant; this method should be used to allocate significant chunks of memory, not individual small objects. If the freelist is kept in sorted order, that makes it easier to coalesce consecutive blocks and thus prevent fragmentation, but for simplicity the following code doesn't actually coalesce blocks.
Code:
; Assume both indexes and accumulator are 16-bit
.LongA
.LongX
InitHeap:
; reserve first two bytes for freelist pointer, and initialise it to point at first block
LDA ##2
STA |$0000
; first block occupies the rest of the bank (minus 4 bytes overhead), and has no following free block
LDA ##$FFFC
STA |$0002
STZ |$0004
RTS
; Pass number of bytes required in A (16-bit).
; Returns zero in X on failure, otherwise returns 16-bit pointer to allocated block in X.
AllocHeap:
; First-fit algorithm - traverse freelist until we find a block big enough, or get to the end of the list
LDY ##0
LDX |$0000
BEQ @allocFail
@checkBlock:
CMP |$0000,X
BCS @nextBlock
; Block found, determine if appropriate to split
PHA
LDA |$0000,X
SEC
SBC 0,S
CMP ##4 ; minimum viable size of split-off block
BCC @noSplit
; Split block into a new free block and a new allocated block
; Because single-linked list, easier to put our block at end of this one
PHA
DEC A
DEC A
STA |$0000,X ; usable size of spare block
TXA
CLC
ADC 0,S
TAX ; now points to header of newly allocated block
PLA
STA |$0000,X ; usable size of this block - no next pointer!
INX
INX
RTS
@noSplit:
; Update previous next-pointer in freelist
; Leave block size alone, so we get it all back on free
LDA |$0002,X
STA |$0000,Y
INX
INX
RTS
@nextBlock:
TXY
INY
INY
LDX |$0000,Y ; Y now points to previous next-pointer
BNE checkBlock
@allocFail:
RTS
; Return an allocated block to the heap. Should pass pointer to block in X. Does not verify validity. Does not coalesce.
FreeHeap:
; Adjust pointer to header
TXA
DEC A
DEC A
; sorted insertion on singly linked list
LDY ##0
: CMP |$0000,Y
LDX |$0000,Y
BCS :+ ; next block is after freed one
BEQ :+ ; no more blocks
TXY
INY
INY
BRA :- ; next block
: STA |$0000,Y ; previous pointer to freed block
TAY
STX |$0002,Y ; freed block's next-pointer to following block (or NULL)
RTS
I leave coalescing blocks as an exercise for the reader. You could do it on free, in which case you should remember that consecutive blocks may appear before, after, or both sides of a block being freed, and that the block size in the header is two bytes
less than the offset between blocks. Or you could do it as a hail-mary pass upon allocation failure, which may be easier to implement (you only have to recursively coalesce a block with a following one) but may also end up with more failures due to memory fragmentation.
(Edited to ensure that the Z flag reflects the state of the X register on return of AllocHeap, to simplify calling.)
The above are by no means the only reasonable approaches to an allocating heap, but they have a reasonable chance of working and are simple enough for hobby use.