When writing assembly, there are no rules about how to pass data to and from subroutines. Each subroutine can do it in a different way, for maximum efficiency. If there are just a few parameters, it probably makes sense to pass them in registers. Extra ones can go on the stack, although this can require a bit of juggling if the same stack is used for return addresses. Alternatively, data can be passed via a "parameter block" in-line with the code - check out this useful resource to see how that was done on the 6502.
Likewise, return values (and there can be more than one) can go in registers, on the stack, in fixed memory locations, or even in the form of the carry and zero status flags in the case of boolean values.
Here's an example of a hand-written subroutine in assembly. As the comment says, the inputs are the x and y registers, and the output is returned in b. It modifies the c register, so the caller would need to save its value on the stack, if it was important.
The "call" instruction is actually a macro that pushes the return address (the one after the call) and jumps to the given address. So it's really two instructions - 6 bytes in total. The "ret" instruction is a real instruction that pops a 16-bit value into the program counter.
The routine uses a "local" variable, mulbit, which is stored in a fixed location in memory.
mov x, #56 mov y, #39 call mulxy ; b == 2184 ;mulitply (8bit x 8bit = 16bit result) ; xy inputs ; b output ; c clobbered mulxy mov b, #0 mov cl, x mov ch, #0 stl #1, mulbit mloop mov x, y ; add if y & bit and x, [mulbit] jz mnoadd add bl, cl ; b += c adc bh, ch mnoadd add cl, cl ; c *= 2 adc ch, ch adr mulbit ; bit *= 2 ldx add x, x stx jcc mloop ret mulbit .byte 1
From assembly to a higher level
Eventually I'd like to implement a high-level language though (at least higher-level than assembly), and then we do need some rules - a calling convention. This will make heavy use of the stack, for arguments, return values, and each function's local variables. A stack-relative addressing mode, with which we can read and write values on the stack without pushing and popping them, is key, and that's why I have sp+imm8 and sp+imm16 addressing modes in the ISA.
Because the modes can only add a positive offset to the stack pointer, it makes more sense for the stack to grow down. If it grew up, you would need a negative offset to access a function's parameters.
So here's what the output might look like from a high-level compiler:
; call myfunc with 2 1-byte parameters push #12 push #34 push #return_addr ; call jmp myfunc ; call ... myfunc push x ; callee saves x and y push y sub sp, #32 ; allocate space for locals ... ldx sp+37 ; load parameter stx sp+3 ; store local variable ... call func2 ; call child function ... add sp, #32 ; deallocate locals pop y ; restore x/y pop x pop b add sp, #2 ; deallocate parameters jmp b ; return
This is all very preliminary, but it shows that a high level language could be implemented fairly efficiently.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.