TIGCC Assembly Programming Lessons
Lesson 4: Integrating C and Assembly
Step 4 - C Calling Conventions
We have seen plenty of examples in the first three lessons on how to
call external functions in assembly. We have used the AMS jump table to
call the system functions, and have used some TIGCC functions from the
TIGCC library as well. Up till now however, we have not discussed in
detail how we knew to use these functions.
We have used the phrase 'calling conventions' a lot, but what does it
mean? Simply put, a call means to use a function. The calling
conventions determine how we call that function. For our purposes, this
means, how do we find the address of the function, and how do we pass
arguments to this function.
Each system defines its own calling conventions. There are a lot of
common threads, but no real standards. When using TIGCC, we even get
multiple calling conventions. So we need to discuss how to identify a
functions calling conventions, and how to use them.
We have already seen examples using all the various calling conventions.
The AMS functions have their arguments pushed on the stack in reverse
order, and the address is located in the system jump table. The TIGCC
library functions written in C use regparm(4) conventions. This means
they get their arguments in the registers, d0-d3 and a0-a3 for data and
addresses, respectively. The TIGCC library functions written in assembly
pass their arguments in registers, but not necessarily according to
regparm(4) conventions. The final one, functions you write in C will use
stkparm. This means the arguments are pushed on the stack in reverse
order. This is the same as the AMS functions, and is the default calling
convention of gcc.
So, how do we identify the various calling conventions? There are
several clues in the TIGCC docs and header files. In the TIGCC docs for
a function, saw DrawStr for example, the upper right corner of the page
says ROM Call. This means it is an AMS function. So to call DrawStr, we
push its arguments on the stack in reverse order, and use the jump table
to get the address.
Another function, atoi, is part of the TIGCC library. If you look in the
TIGCC docs, we see at the top-right that is says tigcc.a. This means it
is a TIGCC library function. But this does not tell us its calling
conventions. For that, we need to look in the header file that has the
prototype. Also near the top-right is the header file, in this case
stdlib.h. Unless you changed the path, in Windows, this header file is
located in C:\Program Files\TIGCC\Includes\C. Open the stdlib.h file
and search for atoi. You'll note it has an __ATTR_LIB_C__ at the end.
This means it uses regparm(4) conventions.
Let's try another one, the atexit function. It's easy enouh to find
because it's also defined in stdlib.h. At the end of this function, we
see it has __ATTR_LIB_ASM__. We will cover the exceptions in a moment,
but this usually signifies stkparm, i.e. that the parameters are pushed
on the stack in reverse order.
Let's see one of those exceptions now. Another TIGCC library function
is the _rowread function, which we have used before. It is defined in
the kbd.h header file. If you look for _rowread, you will see it has
__ATTR_LIB_ASM__ defined. But, we do not pass the parameters on the
stack. If you look in the prototype, after the short it says asm("d0").
This means the first (and only in this case) argument is passed in the
d0 register.
I don't have an example here, because we have already seen examples for
all of these in our previous lessons. Sprite16 is an example of an
__ATTR_LIB_C__ function we have used. DrawStr is an AMS function.
_rowread is an __ATTR_LIB_ASM__ function that specifies the registers
explicitly. Maybe we haven't seen a stkparm __ATTR_LIB_ASM__, but it's
just like the AMS functions.
Now, what about the addresses? We've already seen how to use the jump
table. What about the TIGCC library functions? Well, that's easy. We can
use bsr [name of function] for any TIGCC library function. This is
because it will become part of our program.
There is one more note I want to make. If you look up a 'function' in
the TIGCC docs, and it says macro at the top-right, this means it is not
a function at all. You cannot call macros.
Step 5 - The Inline Assembler
The final method for integrating C and assembly is the inline assembler.
You can actually embed assembly within your C code. To do this, simply
use put asm("opcodes") into your program. You can separate multiple
opcodes with semi-colons, or newlines. You can even use C variables.
Let's take a quick look at a couple examples. Start TIGCC and create a
new project. Make a new C source file called romcall. We will not need
to make an assembly file this time.
romcall.c
#include <tigcclib.h>
#define NGETCHX 0x51
static void clear_screen(void) {
asm("movea.l 0xc8,%a0
movea.l 1656(%a0),%a0
jsr (%a0)");
}
static void romcall(short int number) {
number <<= 2;
asm("movea.l 0xc8,%%a0
adda.l %[number],%%a0
movea.l (%%a0),%%a0
jsr (%%a0)"
: // no output
: [number] "g" (number)
: "a0"
);
}
void _main(void) {
clear_screen();
romcall(NGETCHX);
}
Step 5a - Program Analysis
Build the program and sent it to TiEmu. I didn't make a screenshot,
because the program simply clears the screen and waits for a key press.
The clear_screen function is pretty simple. We use three opcodes to call
the ClrScr AMS function. In case you were wondering where the number
came from, ClrScr is ROM Call 0x19E, and 4 * 0x19E = 1656 decimal.
The romcall function is a little more complex. The assembly is easy
enough. The only caveat is that we can't simply put number beside the
movea function, because number is a variable, not a constant. This means
we can't use it as a displacement value. number(%a0) has no meaning in
assembly. So we'll just add it to the address of the jump table, then
dereference to get the function. Because we gave the romcall function
the ngetchx ROM call number, this is the AMS function we are calling.
Now you may be wondering about the double-percent signs in front of the
registers. This is required to differentiate it from the C variables we
are going to use, which also require you to prepend a %. We can use
small numbers, like %0, %1, or we can give them names. We didn't have to
use number as our name in the inline assembly, but it seemed
appropriate.
At the end of our assembly instructions, we have three fields separated
by a : (colon). The first field marks our output variables, i.e., those
fields that will have their value changed by the assembly. The second
field is for our input variables. In this case, we are going to use the
number function argument in the assembly, so we need to give it as an
input. The third field is for our clobber registers. If we are going to
change a register, we need to tell gcc about it. This is because it may
be using that register. It won't matter in this example, but there will
come a time when it does.
The [number] specifies the name. We didn't have to use [number] here, as
I've already said. The (number) is for the variable. The middle
parameter is how this variable should be used. GCC will have to generate
assembly in order to address our C variables. "g" stands for general,
which means it can be a data register, address register, or memory
operand. You may have to play with this value if gcc generates invalid
code. Take a look at the Inline Assembler section in the TIGCC docs for
more information on what kinds of values are possible.
Step 6 - Conclusions
I hope you have taken something useful from this lesson. There is a lot
more to learn about the inline assembler, but understanding the basics
is the first step. The TIGCC docs has a lot more information, and you
can google a ton on this as well.
In this lesson, you learned about calling conventions, how to integrate
C and assembly functions, and how to use the inline assembler. You
should now have the tools you need to optimize any C program where
needed.
Lesson 1: Introduction to TIGCC Assembly Programming
Questions or Comments? Feel free to
contact us.
|