Passing Variables from C to Assembly under GCC

A project log for 68K CPU with Frame Buffer on FPGA

Working with AMR's build and improvements to the TG68 FPGA project 01/23/2021 at 13:210 Comments

I've loved the (on-line GodBolt) Compiler Explorer project for a while now. It lets you type code in one window and see it compiled to assembly language in another window. There's a Compiler Explorer site which does 68k cross compiling. This also lets you play with compiler options like optimization.

GodBolt Compiler Explorer GCC versions

The GodBolt tool supports various GCC versions. 

My GCC compiler toolchain uses version:

I tried various GCC versions and the results in the below simple case were the same.

Passing variables from C to assembly functions

One of the more painful things to grapple with is passing variables from C to assembly language. To understand the passing process, let's look at a simple example. Here's C code which passes two variables to an add function and returns the sum of the two numbers:

// test passing variables using GCC on the 68K CPU

int addTwoNums(int a,int b)
    return (a+b);

    int i =- 2;
    int j = 3;

Here's where the GodBolt Compiler Explorer comes in handy. Let's look at how the GCC compiler handles passing variables between C functions to see what we'd need to do if we write assembly that gets called from C.

        link.w a5,#0
        move.l (8,a5),d0
        add.l (12,a5),d0
        unlk a5
        link.w a5,#-8
        moveq #-2,d0
        move.l d0,(-4,a5)
        moveq #3,d0
        move.l d0,(-8,a5)
        move.l (-8,a5),-(sp)
        move.l (-4,a5),-(sp)
        jbsr __Z10addTwoNumsii
        addq.l #8,sp
        moveq #0,d0
        unlk a5

Values which are passed from the C function to the assembly language routine relative to the stack. In this example, the first variable, i, is passed at an offset of -4 from the a5 register. The second variable is passed at an offset of -8 from the a5 register.

The return value from the add function is found in d0.

The routine is bounded by the link at the start and unlk *unlink" at the end of the routines. These are described in this 68000 Programmers Reference document.

If we want to write assembly language routines that can be called from C we can mimic this functionality. But there's a rub with the AMR code that adds a tiny bit of complexity (or is it simplification?).

AMR's C/Assembly Code

Let's take a look at one of the AMR pieces of assembly code to see what was done for that code.  This code (spi_readsector.s) reads a sector from the SD card and loads it to an address range specified by register a0:

SPI_PUMP    equ    $81000100

    XDEF spi_readsector
    move.l    4(a7),a0
    movem.l    d2-d7/a2,-(a7)

    moveq    #15,d7
    lea    SPI_PUMP,a1
    move.w    (a1),d0
    movem.l    (a1),d0-d6/a2
    movem.l    d0-d6/a2,(a0)
    add.l    #32,a0
    dbf    d7,.loop

    movem.l    (a7)+,d2-d7/a2

The code pushes the registers used by the routine at the start onto the stack and restores (via pull) the registers before the return from subroutine. This preserves the registers in the calling routine.

Take note, though. This code isn't using the link and unlink. It's not using the a5 register as the stack frame for the routine. The reason that the code can do this is found in the gcc options found in the makefile. The option used is: -fomit-frame-pointer. This causes the compiler to not use the frame pointer. Instead it uses the a7 stack pointer directly. Obviously this is more efficient both in speed and memory usage.

That's also why the assembly language routine looks for the first passed variable at 4 from the a7 register instead of 8 in the GCC example. Adding the -fomit-frame-pointer option to the Godbolt compiler produces the following assembly code:

        move.l (4,sp),d0
        add.l (8,sp),d0
        subq.l #8,sp
        moveq #-2,d0
        move.l d0,(4,sp)
        moveq #3,d0
        move.l d0,(sp)
        move.l (sp),-(sp)
        move.l (8,sp),-(sp)
        jbsr __Z10addTwoNumsii
        addq.l #8,sp
        moveq #0,d0
        addq.l #8,sp

This code has the same form as AMR's code. The first line in _main reserves 2 longs (8 bytes) for the stacked variables used by the routine.

Compiler Options

-fomit-frame-pointer option instructs the compiler to not store stack frame pointers if the function does not need it. You can use this option to reduce the code image size.

-fno-common specifies that the compiler places uninitialized global variables in the BSS section of the object file. This inhibits the merging of tentative definitions by the linker so you get a multiple-definition error if the same variable is accidentally defined in more than one compilation unit.