It's not possible to answer that question without a specific toolchain / binary format in mind. Therefore I am answering the question in the context of the GNU tools for AVR, which is in widespread use and freely available. (For Clang/LLVM it's basically the same since they use the same ABI like avr-gcc, what's different though will be the command line options that have to be used).
Combining Assembly with C/C++
The route to get from source code to binary (skipping features like LTO) is:
- Compile a C/C++ source to assemlby code.
- Assemble that assembly code to object code (ELF).
- Link all the object files and suuport libraries to final executable (ELF).
In the case of assembly code, you can start at 2. So suppose we have an assembly code in main.asm
and C code in calc.c
. So the commands you will be using are, respectively:
> avr-gcc -mmcu=atmega16 -c calc.c -Os
> avr-gcc -mmcu=atmega16 -c -x assembler-with-cpp main.asm
> avr-gcc -mmcu=atmega16 calc.o main.o -o main.elf
Some notes:
avr-gcc
is a driver program that compiler / assembles the provided files and determines what to do with them according to their file extension. It recognizes extensions .sx
and .S
as "assembly with C preprocessor". .asm
is not a recognized file extension, hence you have to tell avr-gcc that it is "assembly with C preprocessor".
You can do in in one command line, like:
> avr-gcc -mmcu=atmega16 -x assembler-with-cpp file1.asm ... -x none module1.c ... -Os -o main.elf
calc.c
This is straight forward C code:
#include <stdint.h>
uint8_t calc (uint16_t *pvar)
{
uint16_t var16 = *pvar;
return var16 <= 255 ? var16 : 255;
}
main.asm
The code below shows some basic functionalities (defining global functions, defining variables, defining ISR). You may also find these links helpful:
;; Define __SFR_OFFSET to 0x0 so we can use SFR addresses like PORTB
;; in SBI PORTB, ... etc.
#define __SFR_OFFSET 0x0
;; Define stuff like PORTB.
#include <avr/io.h>
;; Convenience macros for start and end of a global function.
.macro DEFUN name
.global \name
.type \name, @function
\name:
.endm
.macro ENDF name
.size \name, . - \name
.endm
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.text
;; No need for explicit vector table. The CRT brings it all.
;; Implement some ISR
DEFUN INT0_vect
;; Code for INT0 ISR
sbi PORTB, 0
reti
ENDF INT0_vect
;; No preparation / explicit startup code need. The CRT will setup
;; the device according to the ABI and then call main.
DEFUN main
ldi R24, lo8(pvar)
ldi R25, hi8(pvar)
call calc
;; Do something with R24
rjmp main
ENDF main
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.bss
.type var16, @object
var16:
.zero 2
.size var16, 2
;; Pull in the part of the CRT that clears .bss.
.global __do_clear_bss
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.data
.type pvar, @object
pvar:
.word var16
.size pvar, 2
;; Pull in the part of the CRT that initialized .data.
.global __do_copy_data
The result
> avr-objdump -d main.elf
Disassembly of section .text:
00000000 <__vectors>:
0: 0c 94 2a 00 jmp 0x54 ; 0x54 <__init>
4: 0c 94 49 00 jmp 0x92 ; 0x92 <__vector_1>
8: 0c 94 47 00 jmp 0x8e ; 0x8e <__bad_interrupt>
...
00000054 <__init>:
54: 11 24 clr r1
56: 1f be out 0x3f, r1 ; 63
58: cf e5 ldi r28, 0x5F ; 95
5a: d4 e0 ldi r29, 0x04 ; 4
5c: de bf out 0x3e, r29 ; 62
5e: cd bf out 0x3d, r28 ; 61
00000060 <__do_copy_data>:
...
00000076 <__do_clear_bss>:
...
00000086 <.init9>:
86: 0e 94 4b 00 call 0x96 ; 0x96 <main>
8a: 0c 94 56 00 jmp 0xac ; 0xac <_exit>
0000008e <__bad_interrupt>:
8e: 0c 94 00 00 jmp 0 ; 0x0 <__vectors>
00000092 <__vector_1>:
92: c0 9a sbi 0x18, 0
94: 18 95 reti
00000096 <main>:
96: 80 e6 ldi r24, 0x60 ; 96
98: 90 e0 ldi r25, 0x00 ; 0
9a: 0e 94 50 00 call 0xa0 ; 0xa0 <calc>
9e: fb cf rjmp .-10 ; 0x96 <main>
000000a0 <calc>:
a0: fc 01 movw r30, r24
a2: 80 81 ld r24, Z
a4: 91 81 ldd r25, Z+1
a6: 91 11 cpse r25, r1
a8: 8f ef ldi r24, 0xFF
aa: 08 95 ret
000000ac <_exit>:
ac: f8 94 cli
000000ae <__stop_program>:
ae: ff cf rjmp .-2 ; 0xae <__stop_program>
rstack or cstack?
avr-gcc only uses ONE stack as initialized by the CRT. The startup code sets SP
to point to the end of the internal SRAM (more precidely, it initialized SP
with the value of symbol __stack
, which defaults to RAMEND
).
This stack is used for function return addresses, local variables that didn't get a hard register, function arguments that are passed on the stack, and hard registers that have to be spilled.