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