Go to the first, previous, next, last section, table of contents.


Porting newlib

Crt0, the main startup file

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.

  1. Initialize anything that needs it. This init section varies. If you are developing an application that gets download to a ROM monitor, then there is usually no need for any special initialization. The ROM monitor handles it for you. If you plan to burn your code in a ROM, then the crt0 typically has to do all the hardware initialization that is required to run an application. This can include things like initializing serial ports or run a memory check. It all depends on the hardware.
  2. Zero the BSS section. This is for uninitialized data. All the addresses in this section need to be initialized to zero so that programs that forget to check new variables default value will get unpredictable results.
  3. Call main() This is what basically starts things running. If your ROM monitor supports it, then first setup argc and argv for command line arguments and an environment pointer. Then branch to main(). For G++ the the main routine gets a branch to __main inserted by the code generator at the very top. __main() is used by G++ to initialize it's internal tables. __main() then returns back to your original main() and your code gets executed.
  4. Call exit() After main() has returned, you need to cleanup things and return control of the hardware from the application. On some hardware, there is nothing to return to, especially if your program is in ROM. Sometimes the best thing to do in this case is do a hardware reset, or branch back to the start address all over again. When there is a ROM monitor present, usually a user trap can be called and then the ROM takes over. Pick a safe vector with no side effects. Some ROMs have a builtin trap handler just for this case.

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.

Linker scripts for memory management

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.

What to do when you have a binary image

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.

Libraries

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.

Making I/O work

Routines for dynamic memory allocation

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);
  }
}

Misc support routines

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;
}

Useful debugging functions

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.