14

What sort of code could be used to substitute a high level function with a return value to a 6502 subroutine? Take this C-like function for example.

byte func(byte a, byte b, byte c) {
    d = (a + b) * c;
    return d;
}

How could that be done on 6502, or is it impossible? I am new to 6502 assembly and I can't find any possible equivalents similar at all to return values or subroutines with arguments. If 6502 programmers would need to do something like the above, using a subroutine, how would they do it?

  • Kind of broad but... a simple param (like one byte) can be passed in a register, e.g. LDA then JSR and use the value in the accumulator. More complex parameter passing usually involves a block, and passing the address of the block in (lsb/msb) in a pair of registers (or using a fixed memory location, if you aren't worried about reentrancy). Same with return values. – Joe Jan 26 '17 at 20:48
  • Another alternative is to have a fixed location for the block @Joe mentioned; then the location doesn't have to be passed. A more complex approach than any of this is to pass arguments and get return values on the 6502's stack (or on a separate programmer-defined and programmer-implemented stack if you're going to need more than 256 bytes total). – Mitchell Spector Jan 26 '17 at 21:38
  • Also have a look at the UCSD p-Code that was used as an interpreted intermediary language to compile Pascal to on 6502 machines like the Apple II. It's much better suited to compile high level languages (even C, though that wasn't done AFAIK), because if you do it directly, code size becomes an issue quickly, with only 64K available. Interpreted languages on the 6502 are common, think Infocom, or Woz's Sweet 16. – dirkt Jan 27 '17 at 8:56
16

There is a C compiler for the 6502 CPU (cc65). The wiki for this compiler has a section about the internals of the compiler itself, in which the argument passing and result return is discussed.

As the wiki has a CC by 3.0 license, I can copy and redistribute the documentation inside that wiki, for the sake of completeness, provided that appropiate credit is given, a link to the license is made available, and an indication about possible changes is added.

The following documentation belongs to the CC65 project. It is an exceprt of the "Compiler Internals" section. Most precisely, the documentation is a verbatim copy of the three first points under that section.

The main URL, where the original document is available from, is: http://wiki.cc65.org/doku.php?id=cc65:mainpage

To the best of my knowledge, no changes to the content have been performed, and if there is anyone, it has been made unintentionally.


The primary register

The 6502 CPU has no single register that is able to hold any C data besides char. And, for arithmetic operations, only the A register (the accumulator) can be used. Obviously, the 6502 architecture is not very well suited for C and its larger data types.

For this reason, operands are placed in an artificial “primary register”. This register consists of the A and X CPU registers if the operand is 16 bit wide (low byte in A, high byte in X). For 32 bit operands, a 16 bit zeropage location named sreg is used in addition to these CPU registers. So the high word goes into sreg and the low word goes into A/X.

If a runtime function uses more than one operand (a multiplication for example), the first operand is passed on the parameter stack, while the second one is passed in the primary register.

Parameter and return stacks

The 6502 has a fixed stack with a size of 256 bytes in page 1 (at address $100). On several platforms, the stack is even smaller, because the operating system uses part of it for other purposes. This means that the stack is too small to use it as storage for local variables in C programs. For this reason, cc65 compiled programs utilize separate parameter and return stacks.

The return stack

The “return stack” is actually the 6502 hardware stack. It is called “return stack”, because it contains mostly return address of subroutines. It can be (and is) used also as temporary data storage (for example saving a register). It is not used for parameters and variables.

The parameter stack

General use

The parameter stack is a software stack that usually resides at the highest program data address and grows downwards. It is addressed using a two byte zeropage variable named sp and the Y register. The parameter stack is also used for local auto variables.

Functions for use with the parameter stack

The runtime library contains functions to access the stack, push and pop values, and reserve or drop space on the stack. Here are a few examples:

function    purpose
pusha       Push byte in A onto the parameter stack
pushax      Push word in A/X onto the parameter stack
popa        Pop byte on TOS into A
popax       Pop word on TOS into A/X
decsp2      Decrement the stack pointer by 2
incsp2      Increment the stack pointer by 2

Accessing data on the parameter stack

Data on the parameter stack is accessed using the sp zeropage variable using “indirect Y” addressing mode. The last byte pushed is always at offset zero. So if we pushed the value $42 onto the runtime stack using

    lda     #$42
    jsr     pusha

we can access it using

    ldy     #$00
    lda     (sp),y

Moving the stack

The stack isn't put in a segment; it's put into a separate memory area. That area isn't named; it's implied by two expressions. The first one is in the ld65 configuration file. For example, “c64.cfg” has “size = $C7F3 - STACKSIZE”. The second one is in the start-up code. For example, “c64/crt0.s” has ”(RAM_START + RAM_SIZE + STACKSIZE)”.

If it were named explicitly (it should be!), then they would look like this:

HEADER: file=%O, define=yes, start=$0801, size=$000C;
STACK: file="", define=yes, start=$d000-__STACKSIZE__, size=__STACKSIZE__;
RAM: file=%O, define=yes, start=__HEADER_LAST__, size=__STACK_START__-__RAM_START__;

And, sp would be set to ”(STACK_START + STACK_SIZE)”.

If you do want to put your stack into a segment, then you simply assemble a big buffer, put a label at the end of it, and store that label in sp.

Parameter passing and calling conventions

Parameters are passed to functions on the parameter stack. In general, parameters are pushed from left to right, so the rightmost parameter is the last one pushed (and therefore the one at the lowest position on the parameter stack). Provided that there are no local variables, the last parameter is at offset zero on the parameter stack. Parameter passing in presence of a prototype

In presence of a prototype, parameters are pushed as their respective types. This does especially mean that characters are pushed as such and are not promoted to integers.

Parameter passing without a prototype

If no prototype is available, the default promotions are applied before pushing parameters. This means that characters are promoted to integer before pushing them.

Variable argument lists

Parameters in variable argument lists (ellipsis, …) are treated the same as if there were no prototype.

The ''fastcall'' calling convention

If a function is declared as fastcall (or fastcall), the last (rightmost) parameter is not passed on the stack, but passed in the primary register to the called function. This is A in case of an eight bit value, A/X in case of a 16 bit value, and A/X/sreg in case of a 32 bit value.

If the called function is a C function, its first instruction will be a call to one of the push functions to push the passed value onto the stack. This means that for C functions fastcall doesn't make the code really faster. Assembler functions however, can take advantage of the values passed in registers.

Although fastcall doesn't help making C functions faster it usually helps making the whole program somewhat smaller as all the callers of a fastcall function can omit one call to a push function. Callee cleans up the stack

Contrary to most other C compilers, the callee is responsible for cleaning up the stack (dropping stack space used for parameters) before returning. This is done in order to generate smaller code, since dropping parameters can in many places be combined with dropping local variables. Examples

The following example assume that there are no local variables. Presence of local variables would change the stack offset of the parameters in the function.

Prototype:

void foo (unsigned bar, unsigned char baz);

Stack layout within the function:

           +------------------+
           | High byte of bar |
Offset 2 ->+------------------+
           | Low byte of bar  |
Offset 1 ->+------------------+
           | baz              |
Offset 0 ->+------------------+

Example code for accessing bar. The variable is in A/X after the code snippet:

    ldy     #2              ; Offset of high byte of bar
    lda     (sp),y          ; High byte now in A
    tax                     ; High byte now in X
    dey                     ; Offset of low byte of bar
    lda     (sp),y          ; Low byte now in A
  • Does the callee-pops convention only apply with "fastcall"? If so it might help to clarify that, since variadic arguments need to be cleaned up by the caller. – supercat Jan 30 '17 at 16:22
10

The short answer is, you do it however you like. It's your machine and you're the programmer, so do whatever works for you. On an 8-bit CPU like the 6502, there is no one 'right' way to do it. The processor is too limited for any single solution to work in all cases.

One obvious option is to do something similar to what's done on more modern processors: push arguments onto the stack, and return values in A if they fit in one byte, or a pair of registers (like A and X) if they require 2 bytes. This is probably not a good option because the 6502 has such a small stack, and it lacks instructions that make it easy to access arguments on the stack.

Another option is to have the arguments follow the JSR instruction. The function can then use the return address on the stack to locate arguments, updating the return address as it reads each argument. When the function is done reading arguments the return address will be left pointing to the byte after the argument list, where your executable code would resume execution after the function returns.

Or, you could allocate a page of memory somewhere for arguments, store your arguments somewhere in that page, and pass the address of the first argument in the X or the Y register.

Or you could store your arguments somewhere in zero page.

There are lots of options for how you do arguments. Likewise, there are lots of options for how to return values. For example if you don't want to return values in A, how about returning them in memory location 0? If your function just returns a true/false value, don't forget you can use the condition codes -- for example you can set the carry flag to indicate true, and clear the carry flag to indicate false, or you can use the overflow flag or any other flag.

  • 1
    I wasn't really meaning to ask for a "right way" to do it per se, but just a way to do it, something to get me started, since I'm really new to 6502 assembly and had tried but couldn't find any obvious ways to do it. – Accumulator Jan 26 '17 at 22:41
  • 2
    To make it short, people don't use functions in 6502 assembly that often. Functions aren't more than building blocks and you hold and pass your data through global variables. Only when you need recursion you put more thought into it. – Janka Jan 27 '17 at 1:29
  • 2
    @Accumulator: If you're writing programs for a real 6502 that's part of a real computer (i.e. something with some kind of operating system), have a look at how OS subroutines are called, and how parameters are passed in and return values passed back. – Ken Gober Jan 27 '17 at 14:09
  • That parameter idea is quite good; at present I've been using register values but I often need to use those for logic, especially when there are lots of parameters. – wizzwizz4 Mar 14 '17 at 21:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.