To make a program that has been compiled with GCC to run, you need to write some startup code. The initial piece of startup code is called a crt0. (C RunTime 0) This is usually written in assembler, and it's object gets linked in first, and bootstraps the rest of the application when executed. This file needs to do the following things.
portable between all the m68k based boards we have here. section crt0.S - The startup file.
/* ANSI concatenation macros. */ #define CONCAT1(a, b) CONCAT2(a, b) #define CONCAT2(a, b) a ## b
These we'll use later.
/* These are predefined by new versions of GNU cpp. */ #ifndef __USER_LABEL_PREFIX__ #define __USER_LABEL_PREFIX__ _ #endif /* Use the right prefix for global labels. */ #define SYM(x) CONCAT1 (__USER_LABEL_PREFIX__, x)
These macros are to make this code portable between both COFF and a.out. COFF always has an _ (underline) prepended on the front of all global symbol names. a.out has none.
#ifndef __REGISTER_PREFIX__ #define __REGISTER_PREFIX__ #endif /* Use the right prefix for registers. */ #define REG(x) CONCAT1 (__REGISTER_PREFIX__, x) #define d0 REG (d0) #define d1 REG (d1) #define d2 REG (d2) #define d3 REG (d3) #define d4 REG (d4) #define d5 REG (d5) #define d6 REG (d6) #define d7 REG (d7) #define a0 REG (a0) #define a1 REG (a1) #define a2 REG (a2) #define a3 REG (a3) #define a4 REG (a4) #define a5 REG (a5) #define a6 REG (a6) #define fp REG (fp) #define sp REG (sp)
This is for portability between assemblers. Some register names have a % or $ prepended to the register name.
/* * Set up some room for a stack. We just grab a chunk of memory. */ .set stack_size, 0x2000 .comm SYM (stack), stack_size
Set up space for the stack. This can also be done in the linker script, but it typically gets done here.
/* * Define an empty environment. */ .data .align 2 SYM (environ): .long 0
Set up an empty space for the environment. This is bogus on any most ROM monitor, but we setup a valid address for it, and pass it to main. At least that way if an application checks for it, it won't crash.
.align 2 .text .global SYM (stack) .global SYM (main) .global SYM (exit) /* * This really should be __bss_start, not SYM (__bss_start). */ .global __bss_start
Setup a few global symbols that get used elsewhere. __bss_start needs to be unchanged, as it's setup by the linker script.
/* * start -- set things up so the application will run. */ SYM (start): link a6, #-8 moveal #SYM (stack) + stack_size, sp /* * zerobss -- zero out the bss section */ moveal #__bss_start, a0 moveal #SYM (end), a1 1: movel #0, (a0) leal 4(a0), a0 cmpal a0, a1 bne 1b
The global symbol start
is used by the linker as the default
address to use for the .text
section. then it zeros the
.bss
section so the uninitialized data will all be cleared. Some
programs have wild side effects from having the .bss section let
uncleared. Particularly it causes problems with some implementations of
malloc
.
/* * Call the main routine from the application to get it going. * main (argc, argv, environ) * We pass argv as a pointer to NULL. */ pea 0 pea SYM (environ) pea sp@(4) pea 0 jsr SYM (main) movel d0, sp@-
Setup the environment pointer and jump to main()
. When
main()
returns, it drops down to the exit
routine below.
/* * _exit -- Exit from the application. Normally we cause a user trap * to return to the ROM monitor for another run. */ SYM (exit): trap #0
Implementing exit
here is easy. Both the rom68k
and bug
can handle a user caused exception of zero
with no side effects.
Although the bug
monitor has a user caused trap that will return
control to the ROM monitor, this solution has been more portable.
The linker script sets up the memory map of an application. It also
sets up default values for variables used elsewhere by sbrk() and the
crt0. These default variables are typically called _bss_start
and
_end
.
For G++, the constructor and destructor tables must also be setup here.
The actual section names vary depending on the object file format. For
a.out
and coff
, the three main sections are .text
,
.data
, and .bss
.
Now that you have an image, you can test to make sure it got the
memory map right. You can do this by having the linker create a memory
map (by using the -Map
option), or afterwards by using nm
to
check a few critical addresses like start
, bss_end
, and
_etext
.
Here's a breakdown of a linker script for a m68k based target board.
See the file libgloss/m68k/idp.ld
, or go to the appendixes in
the end of the manual. section Linker script for the IDP board.
STARTUP(crt0.o) OUTPUT_ARCH(m68k) INPUT(idp.o) SEARCH_DIR(.) __DYNAMIC = 0;
The STARTUP
command loads the file specified so that it's
first. In this case it also doubles to load the file as well, because
the m68k-coff configuration defaults to not linking in the crt0.o by
default. It assumes that the developer probably has their own crt0.o.
This behavior is controlled in the config file for each architecture.
It's a macro called STARTFILE_SPEC
, and if it's set to
null
, then when gcc
formats it's command line, it doesn't
add crto.o
. Any file name can be specified here, but the default
is always crt0.o
.
Course if you only use ld
to link, then the control of whether or
not to link in crt0.o
is done on the command line. If you have
multiple crto files, then you can leave this out all together, and link
in the crt0.o
in the makefile, or by having different linker
scripts. Sometimes this is done for initializing floating point
optionally, or to add device support.
The OUTPUT_ARCH
sets architecture the output file is for.
INPUT
loads in the file specified. In this case, it's a relocated
library that contains the definitions for the low-level functions need
by libc.a. This could have also been specified on the command line, but
as it's always needed, it might as well be here as a default.
SEARCH_DIR
specifies the path to look for files, and
_DYNAMIC
means in this case there are no shared libraries.
/* * Setup the memory map of the MC68ec0x0 Board (IDP) * stack grows up towards high memory. This works for * both the rom68k and the mon68k monitors. */ MEMORY { ram : ORIGIN = 0x10000, LENGTH = 2M }
This specifies a name for a section that can be referred to later in the script. In this case, it's only a pointer to the beginning of free RAM space, with an upper limit at 2M. If the output file exceeds the upper limit, it will produce an error message.
/* * stick everything in ram (of course) */ SECTIONS { .text : { CREATE_OBJECT_SYMBOLS *(.text) etext = .; __CTOR_LIST__ = .; LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2) *(.ctors) LONG(0) __CTOR_END__ = .; __DTOR_LIST__ = .; LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2) *(.dtors) LONG(0) __DTOR_END__ = .; *(.lit) *(.shdata) } > ram .shbss SIZEOF(.text) + ADDR(.text) : { *(.shbss) }
Set up the .text
section. In a COFF
file, .text is where
all the actual instructions are. This also sets up the CONTRUCTOR
and the DESTRUCTOR tables for G++
. Notice that the section
description redirects itself to the ram variable setup earlier.
.talias : { } > ram .data : { *(.data) CONSTRUCTORS _edata = .; } > ram
Setup the .data
section. In a coff
file, this is where all
he initialized data goes. CONSTRUCTORS
is a special command used
by ld
.
.bss SIZEOF(.data) + ADDR(.data) : { __bss_start = ALIGN(0x8); *(.bss) *(COMMON) end = ALIGN(0x8); _end = ALIGN(0x8); __end = ALIGN(0x8); } .mstack : { } > ram .rstack : { } > ram .stab . (NOLOAD) : { [ .stab ] } .stabstr . (NOLOAD) : { [ .stabstr ] } }
Setup the .bss
section. In a COFF
file, this is where
unitialized data goes. The symbols _bss_start
and _end
are setup here for use by the crt0.o
when it zero's the
.bss
section.
A few ROM monitors load binary images, typically a.out
, but most all
will load an srecord
. An srecord is an ASCII representation of a binary
image. At it's simplest, an srecord is an address, followed by a byte
count, followed by the bytes, and a 2's compliment checksum. A whole
srecord file has an optional start record, and a required end
record. To make an srecord from a binary image, the GNU objcopy
program
is used. This will read the image and make an srecord from it. To do
this, invoke objcopy like this: objcopy -O srec infile outfile
. Most
PROM burners also read srecords or a similar format. Use objdump -i
to
get a list of support object files types for your architecture.
This describes newlib
, a freely available libc replacement. Most
applications use calls in the standard C library. When initially linking
in libc.a, several I/O functions are undefined. If you don't plan on
doing any I/O, then you're OK, otherwise they need to be created. These
routines are read, write, open, close. sbrk, and kill. Open & close
don't need to be fully supported unless you have a filesystems, so
typically they are stubbed out. Kill is also a stub, since you can't do
process control on an embedded system.
Sbrk() is only needed by applications that do dynamic memory
allocation. It's uses the symbol _end
that is setup in the linker
script. It also requires a compile time option to set the upper size
limit on the heap space. This leaves us with read and write, which are
required for serial I/O. Usually these two routines are written in C,
and call a lower level function for the actual I/O operation. These two
lowest level I/O primitives are inbyte() and outbyte(), and are also
used by GDB back ends if you've written an exception handler. Some
systems also implement a havebyte() for input as well.
Other commonly included functions are routines for manipulating LED's on the target (if they exist) or low level debug help. Typically a putnum() for printing words and bytes as a hex number is helpful, as well as a low-level print() to output simple strings.
As libg++ uses the I/O routines in libc.a, if read and write work, then libg++ will also work with no additional changes.
To support using any of the memory functions, you need to implement
sbrk(). malloc()
, calloc()
, and realloc()
all call
sbrk()
at there lowest level. caddr_t
is defined elsewhere
as char *
. RAMSIZE
is presently a compile time option. All
this does is move a pointer to heap memory and check for the upper
limit. section C based "glue" code.. sbrk()
returns a
pointer to the previous value before more memory was allocated.
/* _end is set in the linker command file * extern caddr_t _end;/ /* just in case, most boards have at least some memory */ #ifndef RAMSIZE # define RAMSIZE (caddr_t)0x100000 #endif /* * sbrk -- changes heap size size. Get nbytes more * RAM. We just increment a pointer in what's * left of memory on the board. */ caddr_t sbrk(nbytes) int nbytes; { static caddr_t heap_ptr = NULL; caddr_t base; if (heap_ptr == NULL) { heap_ptr = (caddr_t)&_end; } if ((RAMSIZE - heap_ptr) >= 0) { base = heap_ptr; heap_ptr += nbytes; return (base); } else { errno = ENOMEM; return ((caddr_t)-1); } }
These are called by newlib
but don't apply to the embedded
environment. isatty()
is self explanatory. kill()
doesn't
apply either in an environment withno process control, so it justs
exits, which is a similar enough behavior. getpid()
can safely
return any value greater than 1. The value doesn't effect anything in
newlib
because once again there is no process control.
/* * isatty -- returns 1 if connected to a terminal device, * returns 0 if not. Since we're hooked up to a * serial port, we'll say yes and return a 1. */ int isatty(fd) int fd; { return (1); } /* * getpid -- only one process, so just return 1. */ #define __MYPID 1 int getpid() { return __MYPID; } /* * kill -- go out via exit... */ int kill(pid, sig) int pid; int sig; { if(pid == __MYPID) _exit(sig); return 0; }
There are always a few useful functions for debugging your project in
progress. I typically implement a simple print()
routine that
runs standalone in liblgoss, with no newlib
support. The I/O
function outbyte()
can also be used for low level debugging. Many
times print will work when there are problems that cause printf()
to
cause an exception. putnum()
is just to print out values in hex
so they are easier to read.
/* * print -- do a raw print of a string */ int print(ptr) char *ptr; { while (*ptr) { outbyte (*ptr++); } } /* * putnum -- print a 32 bit number in hex */ int putnum (num) unsigned int num; { char buffer[9]; int count; char *bufptr = buffer; int digit; for (count = 7 ; count >= 0 ; count--) { digit = (num >> (count * 4)) & 0xf; if (digit <= 9) *bufptr++ = (char) ('0' + digit); else *bufptr++ = (char) ('a' - 10 + digit); } *bufptr = (char) 0; print (buffer); return; }
If there are LEDs on the board, they can also be put to use for
debugging when the serial I/O code is being written. I usually implement
a zylons()
function, which strobes the LEDS (if there is more
than one) in sequence, creating a rotating effect. This is convenient
between I/O to see if the target is still alive. Another useful LED
function is led_putnum()
, which takes a digit and displays it as
a bit pattern or number. These usually have to be written in assembler
for each target board. Here are a number of C based routines that may be
useful.
led_putnum()
puts a number on a single digit segmented
LED display. This LED is set by setting a bit mask to an address, where
1 turns the segment off, and 0 turns it on. There is also a little
decimal point on the LED display, so it gets the leftmost bit. The other
bits specify the segment location. The bits look like:
[d.p | g | f | e | d | c | b | a ] is the byte.
The locations are set up as:
a ----- f | | b | g | ----- | | e | | c ----- d
This takes a number that's already been converted to a string, and prints it.
#define LED_ADDR 0xd00003 void led_putnum ( num ) char num; { static unsigned char *leds = (unsigned char *)LED_ADDR; static unsigned char num_bits [18] = { 0xff, /* clear all */ 0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x98, /* numbers 0-9 */ 0x98, 0x20, 0x3, 0x27, 0x21, 0x4, 0xe /* letters a-f */ }; if (num >= '0' && num <= '9') num = (num - '0') + 1; if (num >= 'a' && num <= 'f') num = (num - 'a') + 12; if (num == ' ') num = 0; *leds = num_bits[num]; } /* * zylons -- draw a rotating pattern. NOTE: this function never returns. */ void zylons() { unsigned char *leds = (unsigned char *)LED_ADDR; unsigned char curled = 0xfe; while (1) { *leds = curled; curled = (curled >> 1) | (curled << 7); delay ( 200 ); } }
Go to the first, previous, next, last section, table of contents.