Embedding GNU: Newlib, Part 2 - Embedded.com

Embedding GNU: Newlib, Part 2

In the first part of this article (January 2002), I introduced newlib, a C runtime library for some embedded software built using GNU tools. I discussed some of its features, the issue of reentrancy, portability, and started to explain how to build newlib. This time, I'll show you how to integrate newlib into a multithreaded runtime environment that features Jean Labrosse's μC/OS.

Porting newlib
The newlib C standard library supports more than a dozen target CPU architectures, but it doesn't come with stubs to connect it to very many operating systems. As a result, the odds are almost certain that you'll need to do some porting work to get newlib running in your system. Fortunately, the process is both straightforward and relatively painless.

As I mentioned, all of newlib's functionality builds on a set of 17 stubs that supply capabilities newlib cannot provide itself-low-level filesystem access, requests to enlarge its memory heap, getting the time of day, and various types of context management like process forking and killing. These stubs are fully documented in newlib's libc.info file, in the section called “Syscalls.” The key to a successfully ported newlib is finding the functionality in your system to hook these stubs to.

What's that you say? Your embedded system runs a custom RTOS that lacks a filesystem or memory management API, or maybe you don't have an RTOS at all? No problem-newlib doesn't really care, as long as the stubs you end up using can supply the needed functionality.

To demonstrate this, I'll show how to use newlib in an embedded system that uses μC/OS (www.ucos-ii.com), a small pragmatic RTOS that features a reentrant memory pool implementation, but lacks any concept of a device driver or filesystem API. Despite this operating systems's spartan feature set, the results will be both practical and educational-the combination of μC/OS and newlib yields a perfectly usable system, and offers insights into how you would use newlib in both larger and smaller settings.

Reentrant vs. nonreentrant stubs
Before we get started, one observation is in order. In many places, newlib offers two types of stubs: reentrant and nonreentrant. The only difference between the two is that the reentrant stubs include a _reent structure pointer in their signatures, which allows the implementer to carry context-specific information between the library and the target operating environment, which, among other things, lets you cleanly communicate errno values to the proper execution thread when errors occur.

In order for newlib to use your reentrant stubs, you have to add

-DREENTRANT_SYSCALLS_PROVIDED to the CFLAGS_FOR_TARGET variable in the makefile before building newlib. I always do this, and the following examples will assume that you have done so too. If you choose to use the nonreentrant stubs instead, then eliminate the _r from each stub's name (_fork_r becomes _fork ), and eliminate the portions of the stubs that relate to the _reent structure.

Let's get started now, shall we?

_fork_r
Newlib calls upon this stub to do the work for the fork() system call, which, in POSIX environments, is used to create a clone of the current processing context.

A hardcore POSIX enthusiast could implement this stub with help from μC/OS's OSTaskCreate() function, but that is a challenging exercise. (The semantics of the conventional fork() do not coexist peacefully with μC/OS's way of managing task creation.) In fact, trying to implement fork() in μC/OS is probably a bad idea, because it raises task priority and synchronization issues that μC/OS already addresses quite well on its own. So we'll save joining the two for another day, and leave this stub essentially unimplemented.

int_fork_r ( struct _reent *ptr )
{
   /* return “not supported” */
    ptr->errno = ENOTSUP;
  return -1;
}

If the application wants to create a new thread of execution, it will have to do so explicitly with μC/OS's OSTaskCreate() system call, and not via fork(). We'll take this approach for several other context management-related stubs, including _execve, _kill, _wait_r , and _getpid_r . That takes care of the first five stubs.

_write_r and _read_r
These stubs are a bit more interesting to implement, because μC/OS does not provide any type of device driver or filesystem model.

In a nutshell, newlib calls _write_r any time it wants to send data to a device, be it due to a write() call, printf() or fprintf(), or anything similar. The _reent parameter provides a place for the stub to communicate errors should they occur, and file descriptor parameter, fd, tells the stub which device newlib is trying to talk to. The remaining arguments supply a source data buffer and number of bytes to write.

The tricky part here is the semantics. For example, you don't need to write all the bytes that newlib asks you to, but if you don't, newlib will simply call you again with the remaining data. So if the return value never eventually equals the number of bytes requested, newlib will misbehave.

Furthermore, newlib doesn't call open() for file descriptors 0, 1, or 2, which means that the _write_r call is the first activity you will see on those streams. Stream 0 is defined by convention to be the “standard input” stream, which newlib uses for the getc() and similar functions that don't otherwise specify an input stream. Stream 1 is “standard output,” the destination for printf() and puts() . Stream 2 refers to “standard error,” the destination conventionally reserved for messages of grave importance.

You may use any other positive integers for file descriptors, as we'll see when we discuss the _open_r stub.

To implement _write_r , I'll start by defining a simple “device operations” table, with function pointers for all the kinds of activities you would expect a stream-like device driver to support, as shown in Listing 1. Each device driver will supply its own device operations table, as shown in Listing 2.

Listing 1: Device operations table

typedef struct {
   const char *name;
   int (*open_r )( struct _reent *r, const char *path, int flags, int mode );
   int (*close_r )( struct _reent *r, int fd );
   long (*write_r ) ( struct _reent *r, int fd, const char *ptr, int len );
   long (*read_r )( struct _reent *r, int fd, char *ptr, int len );
} devoptab_t;

Listing 2: Each driver provides a devoptab_t

/* dtab for an example stream device called “com1” */
const devoptab_t devoptab_com1 = { “com1”, com1_open_r, com1_close_r,
com1_write_r, com1_read_r };

Each driver will also provide its own open_r , close_r , write_r , and read_r functions to handle device initialization and shutdown, and data movement to and from the physical device.

Somewhere in the application, we'll gather up all the application's devoptab_t declarations into a master array, as in Listing 3.

Listing 3: The full set of devices

const devoptab_t *devoptab_list[] = {
   &dotab_com1, /* standard input */
   &dotab_com1, /* standard output */
   &dotab_com1, /* standard error */
   &dotab_com2, /* another device */
   … , /* and so on… */
   0 /* terminates the list */
};

With all of that in place, the _write_r stub is simple to implement, because all it has to do is map a file descriptor to the proper set of device operations. Listing 4 shows one way to do it.

Listing 4: Stub for device writes

long
_write_r ( struct _reent *ptr, int fd, const void *buf, size_t cnt )
{
   return devoptab_list[fd].write_r( ptr, fd, buf, cnt );
}

_read_r is identical, except that it calls the driver's read_r method, instead of write_r .

With this approach, device drivers are free to use whatever μC/OS services they need in order to manage reentrancy and mutual exclusion issues. For example, a driver's write_r function could use a mutex to prevent two simultaneous write requests, or it could atomically push transmissions to a message queue that was simultaneously emptied by a waiting task. Such details are well beyond newlib's concern, of course, but they illustrate the flexibility that is possible.

_open_r
This stub's primary purpose is to translate a string device or file “name” to a file descriptor. With the exception of the standard input, standard output, and standard error devices, this function can also be used to provide advance notice of an impending write() or read() request.

Continuing with our approach of utilizing device operation tables, the _open_r stub can be very simple, as shown in Listing 5. You can safely ignore the _open_r stub's flags and mode parameters, because they're only of interest when talking to a true filesystem.

Listing 5: Stub for device opens.

int
_open_r ( struct _reent *ptr, const char *file, int flags, int mode )
{
   int which_devoptab = 0;
   int fd = -1;

/* search for “file” in dotab_list[].name */
do {
      if( strcmp( devoptab_list[which_devoptab].name, file ) == 0 )
   {
      fd = which_devoptab;
      break;
   }
} while( devoptab_list[which_devoptab++] );

/* if we found the requested file/device, invoke the device's open_r() */
if( fd != -1 ) devoptab_list[fd].open_r( ptr, file, flags, mode );

/* it doesn't exist! */
else ptr->errno = ENODEV;
return fd;
}

_close_r
Man, this one's easy. It's basically a clone of _write_r and _read_r , but with slightly modified semantics, as shown in Listing 6. And now we've finished nine stubs.

Listing 6: Stub for device close

long
_close_r ( struct _reent *ptr, int fd )
{
   return devoptab_list[fd].close_r( ptr, fd );
}

_sbrk_r

Newlib calls this stub whenever malloc() runs out of heap space and wants more. As it turns out, this happens frequently-newlib's malloc() will only ask for incremental chunks of memory, a benign artifact of its Unix heritage.

Assuming you reserved a heap memory area using a character array called _heap , a simple _sbrk_r would look like Listing 7.

Listing 7: Stub for growing the heap space

unsigned char _heap[HEAPSIZE];

caddr_t _sbrk_r ( int incr )
{
      static unsigned char *heap_end;
      unsigned char *prev_heap_end;

/* initialize */
      if( heap_end == 0 ) heap_end = heap;

prev_heap_end = heap_end;

      if( heap_end + incr – heap > HEAPSIZE ) {

/* heap overflow – announce on stderr */
      write( 2, “Heap overflow!n”, 15 );
      abort();
}

   heap_end += incr;

   etirm (caddr_t) prev_heap_end;
}

In this example, each time newlib calls _sbrk_r , the heap end grows by incr bytes. If it reaches the end of the maximum heap space (which hopefully never occurs), it sends an error message to the standard error stream, and then forcibly terminates the program. An alternative approach to a heap overflow would be to return NULL, and let the application find a way to muddle through on its own.

__malloc_lock and __malloc_unlock
Newlib's memory management routines, like malloc(), call these functions when they need to lock the memory heap. By implementing some sort of mutual exclusion in these functions, you can make newlib's memory management code reentrant, or at least thread safe.

Bits of newlib's memory management code are recursive, and so you will likely see the following sequence of invocations in response to a malloc function call:

__malloc_lock, __malloc_lock, __malloc_unlock, __malloc_unlock

The tricky part here is that, if you aren't careful, the second __malloc_lock will cause itself to wait for a lock that it already holds, due to the first __malloc_lock .

There are two ways to solve this problem. The first is to simply punt and reimplement malloc() in its entirety using μC/OS's reentrant memory pool API. The second option is to really implement a working __malloc_lock and __malloc_unlock . Both approaches have their advantages, and which one you choose will depend on how your application needs to use dynamic memory.

Listing 8 is an example of how to use μC/OS memory pools to implement malloc. In this code, each allocation request consumes one block in the memory pool, whether the allocation needs that much space or not. Furthermore, if the allocation size exceeds the block size, the request fails, because μC/OS's memory block manager does not permit this.

Listing 8: Implementing malloc() via μC/OS

/* number of bytes per allocation */
#define HEAPBLKSIZE 64

/* number of allocations available */
#define HEAPBLKS 1024

/* our heap */
   OS_MEM *heap;
   unsigned char heapmem[HEAPBLKS * HEAPBLKSIZE];

void *malloc ( size_t size )
{
   INT8U err = OS_NO_ERR;
   void *alloc = 0;

/* initialize, if necessary */
OS_ENTER_CRITICAL();
   if( !heap )
   heap = OSMemCreate( heapmem, HEAPBLKS, HEAPBLKSIZE, &err );
OS_EXIT_CRITICAL();

if( heap && err == OS_NO_ERR ) {

/* if the request fits the heap block length, make the allocation
   from the heap */
   if( size <= HEAPBLKLEN )
alloc = OSMemGet( heap, &err );

/* otherwise, we're sunk */
   else err = OS_MEM_NO_FREE_BLKS;
}

/* deny the allocation on errors */
   if( err != OS_NO_ERR )
   alloc = 0;

return alloc;}

Using memory pools eliminates fragmentation worries and makes malloc() reentrant, but it wastes memory if the pool's block sizes don't match up with the typical allocation request. You can reduce some of the waste by providing buffer pools of several different sizes (perhaps corresponding to the sizes of data structures you know you'll be allocating memory for), but this approach is hardly generic-particularly when a distribution of sizes is needed.

For situations where a range of allocation sizes is needed, or the size of the largest potential allocation request is unknown, you have to use newlib's memory allocator, and implement __malloc_lock and __malloc_unlock functions. Listing 9 shows an example of how to do that. And then there were 12 stubs complete.

Listing 9: Implementing malloc() via lock and unlock

/* semaphore to prtect the heap */
static OS_EVENT *heapsem;

/* id of the task that is currently manipulating the heap */
static int lockid;

/* number of times __Malloc_lock has recursed */
static int locks;

void__malloc_lock ( struct _reent *_r )
   {
      OS_PCB tcb;
      OS_SEM_DATA semdata;
      INT8U err;
      int id;
/* use our priority as a task id */
      OSTaskQuery( OS_PRIO_SELF, &tcb );
      id = tcb.OSTCBPrio;

/* see if we own the heap already */
OSSemQuery( heapsem, &semdata );
      if( semdata.OSEventGrp && id == lockid ) {

/* we do; just count the recursion */
      locks++;
   }

else {
/* wait on the other task to yield the heap, then claim ownership of it */
      OSSemPend( heapsem, 0, &err );
      Lockid = id;
   }

void
__malloc_unlock ( struct _reent *_r )
   {
/* release the heap once the number of locks == the number of unlocks */
      if( ( locks) == 0 ) {
      lockid = -1;
      OSSemPost( heapsem );
   }
}

__env_lock and __env_unlock
These functions protect the application's environment memory space, similar to what __malloc_lock and __malloc_unlock do for heap space. These stubs are involved with the setenv() and getenv() functions, so you can ignore them if you don't use environment variables, or you can duplicate the strategy used for heap memory protection shown above.

_exit
This stub forcibly terminates the application in response to the exit() or system() functions. There are several possiblities here, from simply allowing a watchdog timeout to passing control to some kind of secondary application to simulating a powerup reset in software.

The Hitachi SH-2 CPU reads its initial program counter and stack pointer from the first eight bytes of memory, so this snippet of code can be used to simulate a powerup reset:

; disable interrupts
mov #-1, r0
ldc r0, sr

; reset the stack pointer
mov #4, r0
mov.l @r0, r15

; reset the program counter
mov #0, r0
mov.l @r0, r0
jmp @r0
nop

The same approach can be used for most other processors, but you have to be careful here: this technique does not restore all of the CPU's registers and peripherals to their powerup states, so your application code cannot depend on initial values for proper operation.

_stat_r, _fstat_r, _link_r, _unlink_r, and _lseek_r
These stubs are used to implement newlib's stat(), fstat(), link(), unlink() , and lseek() functions. These functions all involve files, so they're of little importance when the target environment lacks a filesystem. We only worry about them here so that if the application or a bit of newlib's internal implementation calls these functions unexpectedly, we get a sane result.

For _stat_r and _fstat_r , we'll just tell the caller that the requested file or descriptor is a character device, as shown in Listing 10. For _link_r and _unlink_r , we'll claim that the operation always fails, as shown in Listing 11. For _lseek_r , we'll just pretend that the request is always successful, as shown in Listing 12.

Listing 10: All devices are of type character

int
_stat_r ( struct _reent *_r, const char *file, struct stat *pstat )
{
   pstat->st_mode = S_IFCHR;
   return 0;
}

int
_fstat_r ( struct _reent *_r, int fd, struct stat *pstat )
{
   pstat->st_mode = S_IFCHR;
   return 0;
}

Listing 11: Operation fails for _link_r and _unlink_r.

int _link_r ( struct _reent *_r, const char *oldname, const char *newname )
{
   r->errno = EMLINK;
   return -1;
}

int _unlink_r ( struct _reent *_r, const char *name )
{
   r->errno = EMLINK;
   return -1;
}

Listing 12: Request is successful for _lseek_r

off_t _lseek_r( struct _reent *_r, int fd, off_t pos, int whence )
{
   return 0;
}

_getpid_r
This function returns the context's unique process ID, which we can emulate using μC/OS's OSTaskQuery() function.

_times_r
This stub returns various times for the current context. μC/OS doesn't keep statistics on a task's run time, so we leave this unimplemented:

int _times_r ( struct _reent *r,
struct tms *tmsbuf )
{
   return -1;
}

Onward!
The code I have provided here is just the minimum set needed to get newlib up and running on your system. As you grow into newlib, you are likely to find places where it makes sense to replace newlib's implementations with your own, as I often do for malloc() . I won't claim that newlib was designed with this in mind, but its clean implementation makes many kinds of modifications simple and easy.

Bill Gatliff is an independent embedded systems consultant and a contributing editor to ESP. He welcomes questions and comments at .

Return to January 2002 Table of Contents

1 thought on “Embedding GNU: Newlib, Part 2

  1. “To provide custom malloc, it seems that the correct API to replace is malloc_r and not malloc? See:nhttps://sourceware.org/ml/newlib/2000/msg00143.html”

    Log in to Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.