18. Runtime library

This chapter describes the supplied C runtime library, adapted from the Apache NuttX library.

18.1. Design considerations

The original Apache NuttX library is a collection of routines from the C standard library, POSIX standard, and other components common in real-time operating systems.

It is a scalable and highly configurable runtime library that works in both 8-bits and 32-bits environments.

The adapted version becomes a Standard C library, with POSIX and RTOS-style components either removed or disabled.

Apart from that, the following are the key changes:

  • Replaced header files closely related to the compiler (e.g., stddef.h, stdarg.h, setjmp.h, stdint.h).

  • Internal names are changed to start with either double underscores or a single underscore followed by a capital letter. This avoids namespace pollution, as such identifiers are reserved for the compiler vendor.

  • The stub interface uses names prefixed by _Stub instead of up_ for clarity and to avoid namespace pollution.

Note

The Apache NuttX library’s configuration utility, designed for environmental tailoring, is not used here; the build system is different. This allows the library to be used without extensive configuration, as the compiler is known and target-board-specific matters are excluded. Some library functions have different versions to reduce memory footprint by making certain capabilities optional (e.g., formatters; see Library on a diet).

18.2. Using the library

The C library consists of header files that are immediately available to use by the compiler and can be included:

#include <stdio.h>

int main () {
  printf("Hello World!\n");
  return 0;
}

When linking, the C library does not need to be explicitly added on the command line, as the linker automatically finds the correct C runtime library by examining the settings used during C object file compilation:

$ ln6502 main.o linker-rules.scm

C library variants matching different compiler settings are provided in the installation directory; the appropriate one is automatically selected.

The linker detects mixing object files compiled with different settings or incompatible third-party libraries. This relies on matching runtime attributes in object files. A mismatch results in a descriptive linker error.

18.3. Provided library files

The following table lists the provided ready to use library files:

Table 18.1 Library variants

Library name

Processor core

Size of double

Code model

Target system

clib-6502.a

6502

32 bits

Plain

Generic

clib-6502-double64.a

6502

64 bits

Plain

Generic

clib-65b02.a

65B02

32 bits

Plain

Generic

clib-65b02-double64.a

65B02

64 bits

Plain

Generic

clib-65c02.a

65C02

32 bits

Plain

Generic

clib-65c02-double64.a

65C02

64 bits

Plain

Generic

clib-65cnr02.a

65CNR02

32 bits

Plain

Generic

clib-65cnr02-double64.a

65CNR02

64 bits

Plain

Generic

clib-6502-c64.a

6502

32 bits

Plain

Commodore 64

clib-6502-double64-c64.a

6502

64 bits

Plain

Commodore 64

clib-45gs02-mega65.a

45GS02

32 bits

Plain

MEGA65

clib-45gs02-double64-mega65.a

45GS02

64 bits

Plain

MEGA65

Note

There is normally no need to specify the C library to use when linking. The linker is able to automatically pick the correct C library by inspecting the C object files.

18.4. Stubs interface

How do you provide functions such as fopen(), fprintf(), or assert() when using a cross-compiler that may not know the final execution environment of an application, or even have a file system?

Functions like fprintf() pass through several library layers, handling formatting, buffered I/O, and stream interfaces. Ultimately, they output characters to a display or file. This is managed by a simple stubs API defined in the calypsi/stubs.h header file.

Semi-hosting

Semi-hosting is the concept where the debugger implements operations, such as I/O, on behalf of the target. This involves using a single breakpoint where the target temporarily stops to exchange data with the debugger, which performs the actual operation on the host. After the operation, any return value is passed back, and execution resumes on the target.

The Calypsi C compiler tool chain includes a semi-hosted debug implementation of the stubs API within the standard C library. To enable semi-hosting, add the --semi-hosted command-line option to the linker.

File operations are performed in the host filesystem by the debugger in a directory specified by the --semi-hosted-root command-line option, which defaults to the current directory.

The standard streams stdin, stdout and stderr are fully supported. Depending on your IDE you may have a console window for these streams.

Semi-hosting can be used during development for tracing or prototyping before a real I/O system is implemented in the application. In a final application, you will most likely use target-specific stub functions, not semi-hosted variants.

Note

Even if you implement some stub actions, you can still link with --semi-hosted for partial semi-hosted support. This allows you to implement a file system while retaining assert() through the semi-hosted mechanism. The C library’s semi-hosted stubs are compiled with --weak-symbols, so your implementations take precedence. Unimplemented stubs will use their weak semi-hosted variants.

Stubs header

The calypsi/stubs.h file looks as follows:

/****************************************************************************
 *
 * Copyright Håkan Thörngren
 *
 * This file is part of the Calypsi C library.
 * Permission to use with the Calypsi tool chain is hereby granted.
 *
 ****************************************************************************/

#ifndef __INCLUDE_CALYPSI_STUBS_H
#define __INCLUDE_CALYPSI_STUBS_H

#include <calypsi/config.h>
#include <stddef.h>

/* Currently not implemented. */
#define __noreturn_function

/****************************************************************************
 * Public Data
 ****************************************************************************/

#if defined(__cplusplus)
extern "C"
{
#endif

/****************************************************************************
 * Debug interfaces exported by the architecture-specific logic
 ****************************************************************************/

/****************************************************************************
 * Name: _Stub_open
 *
 * Description:
 *   Open a file.
 *   The oflag argument are POSIX style mode flags, e.g O_RDONLY which
 *   are defined in fcntl.h.
 *   This function is variadic as it optionally can take a mode_t that
 *   are permissions, e.g 0666. If the file system does not handle
 *   permissions you can ignore that this function is variadic.
 *   The return file descriptor shall be a positive number, larger
 *   than 2 (as 0-2 are used for stdin, stdout and stderr).
 *   The actual number does not matter and they need not to be
 *   consequtive, multiple numeric series with gaps between can be used.
 *
 * Return the obtained file descriptor or the desired errno value negated
 * if there is an error.
 *
 ****************************************************************************/

int _Stub_open(const char *path, int oflag, ...);

/****************************************************************************
 * Name: _Stub_close
 *
 * Description:
 *   Close a file
 *
 * Return 0 if operation was OK or the desired errno value negated
 * if there is an error.
 * Note: This will only be invoked for streams opened by _Stub_open(),
 *       there is no need to check for the standard descriptor 0-2.
 *
 ****************************************************************************/

int _Stub_close(int fd);

/****************************************************************************
 * Name: _Stub_lseek
 *
 * Description:
 *   Change position in a file
 *
 * Returns the new position in the file in bytes from the beginning of the
 * file, or the desired errno value negated if there is an error.
 *
 ****************************************************************************/

long _Stub_lseek(int fd, long offset, int whence);

/****************************************************************************
 * Name: _Stub_fgetpos
 *
 * Description:
 *   Get current position in a file
 *
 * Returns 0 on success, or the desired errno value negated if there is an error.
 *
 ****************************************************************************/

int _Stub_fgetpos(int fd, fpos_t *pos);

/****************************************************************************
 * Name: _Stub_fsetpos
 *
 * Description:
 *   Change position in a file
 *
 * Returns 0 on success, or the desired errno value negated if there is an error.
 *
 ****************************************************************************/

int _Stub_fsetpos(int fd, const fpos_t *pos);

/****************************************************************************
 * Name: _Stub_read
 *
 * Description:
 *   Read from a file
 *
 * Returns the number of characters read or the desired errno value negated
 * if there is an error.
 *
 ****************************************************************************/

size_t _Stub_read(int fd, void *buf, size_t count);

/****************************************************************************
 * Name: _Stub_write
 *
 * Description:
 *   Write to a file
 *
 * Returns the number of characters actually written or the desired errno
 * value negated if there is an error.
 *
 ****************************************************************************/

size_t _Stub_write(int fd, const void *buf, size_t count);

/****************************************************************************
 * Name: _Stub_rename
 *
 * Description:
 *   Rename a file or directory
 *
 * Return 0 on success or the desired errno value negated if there is an error.
 *
 ****************************************************************************/

int _Stub_rename(const char *oldpath, const char *newpath);

/****************************************************************************
 * Name: _Stub_remove
 *
 * Description:
 *   Remove a file or directory
 *
 * Return 0 on success  or the desired errno value negated if there is an error.
 *
 ****************************************************************************/

int _Stub_remove(const char *path);

/****************************************************************************
 * Name: _Stub_exit
 *
 * Description:
 *   Terminate the program with an exit code, exit clean-ups are done
 *   before this function is (finally) called.
 *
 ****************************************************************************/

void _Stub_exit(int exitCode) __noreturn_function;

/****************************************************************************
 * Name: _Stub_environ
 *
 * Description:
 *   Get the environment. On UNIX this is typically a global variable
 *   'environ', but in order to make it more flexible and avoid having
 *   such global variable (which is not part of the C standard) it is
 *   obtained using the stub interface.
 *
 * Note:
 *   This stub function is not implemented by the semi-hosted debug stub
 *   interface.
 *
 ****************************************************************************/

char** _Stub_environ(void);

/****************************************************************************
 * Name: _Stub_assert
 *
 * Description:
 *   Handle an assertion
 *
 ****************************************************************************/

void _Stub_assert(const char *filename, int linenum) __noreturn_function;

#if defined(__cplusplus)
}
#endif


#endif /* __INCLUDE_CALYPSI_STUBS_H */

Custom I/O

You can provide your own low-level I/O stubs. File descriptors are integers indexing an internal lookup table (typically an array). If the I/O system is used, the library statically allocates the first three streams: stdin, stdout, and stderr. Additional file streams are dynamically allocated from the heap.

Error handling

If a stub action encounters an error, it should return the negated errno value. The calling C library function then performs appropriate error handling, setting errno to the corresponding positive error code.

18.5. Examine use of library

When working with a memory-constrained system, the risk of running out of memory is always present. To help you understand memory usage, the linker can generate a list file with cross-reference information. This valuable tool details what is placed in memory, its location, and why it occupies that space.

You can tell the linker to provide a list file in the following way:

$ ln6502 main.o clib-6502.a linker-rules.scm --list-file=project.lst --cross-reference

This will result in a list file names project.lst which contains several parts showing things such as:

  • A summary of the memories

  • An overview of sections and where they take space in the memories

  • The object files used and from which library they have been extracted from

  • Complete cross reference, showing where every section fragment is located with its size, together with:

    • What symbols are defined

    • What symbols are referenced

    • Who is referencing me

  • An overall summary of total memory size

18.6. Library on a diet

If you use the C library in a memory-constrained system, you may find it rapidly consumes code space. This is because the C library provides substantial functionality, with many functions acting as simple facades to complex underlying implementations.

There are ways to tune things to reduce the memory footprint. However, first understand the current memory usage before making changes.

Formatters

The C library provides various printf() and scanf() variants, each with different capabilities and memory requirements.

While you can specify a format function, the compiler inspects used format strings and outputs this information to the object file. The linker then automatically selects the smallest variant compatible with those format strings.

Note

Automatic format function selection requires using string literals in calls. This is good practice, as it also allows the compiler to validate argument list consistency with the format string.

If you for some reason have to override the formatter used and dictate a specific one, you can do so using the --rtattr attribute which for printf() would be:

$ ln6502 --rtattr printf=nofloat files...

The default print formatter as mentioned above, is selected automatically to match your needs. The following table describes the available print formatters:

Table 18.2 Print formatters

Capability

printf=reduced

printf=medium

printf=nofloat

printf=float

basics, c d i o p s u X x %

yes

yes

yes

yes

format flag, 0 + - #

no

yes

yes

yes

field width

no

yes

yes

yes

long long

no

no

yes

yes

float, e f g E F G

no

no

no

yes

The scanf() function exits in three variants, mainly depending on if you want support for floating point numbers or not:

Table 18.3 Read formatters

Capability

scanf=medium

scanf=nofloat

scanf=float

basics, c d i o p s u X x %

yes

yes

yes

long long

no

yes

yes

float, e f g E F G

no

no

yes

Note

An appropriate default formatter for scanf() is automatically picked by the linker based on the needs of your application.

Reduced exit

Terminating an application involves closing open files and executing atexit() functions. For embedded applications, which may never exit, termination code can be redundant; closing files also adds related code. An implicit call to exit() occurs when main() returns. If proper exit code is undesirable, you can exclude it.

The simplest way to reduce exit code overhead is to add --rtattr exit=simplified to the linker command line. This prevents closing files or calling atexit() handlers. Instead, exit() will immediately jump to _Stub_exit(), the lowest-level termination routine.

Consider alternatives

If the library still occupies too much space, consider replacing certain functions with smaller, less flexible alternatives.

18.7. Override library functions

You may sometimes need to override a library function. Consider using an alternative function with a different name rather than replacing a standard one. Replacing a library function is appropriate if you have a more suitable application-specific implementation, especially when other library functions call it.

To replace a library function, add a new source file to your project using the same function name and prototype, then build your application normally.

Note

Function replacement works because library functions use weak symbols. Your replacement functions are non-weak by default, taking precedence over library functions during linking.

18.8. C startup

The initial system configuration is performed by the C startup object which is included in the standard C library.

The C startup is responsible for setting up the execution environment before giving control to the main() function. This typically includes setting up the stack pointer and providing initial values to static variables.

The C startup object also contains a couple of small optional sections that are only included if needed. They are responsible for initializing optional parts of the runtime system, such as the heap and the file streams.

If you do not use functions such as malloc(), the heap is not needed and the code to initialize the heap memory system is omitted. The same happens for file streams. If there are no file streams used, the code related to initializing and terminating them are omitted. This is done in order to reduce the memory footprint of the final application.

In case you want to study the source code of the C startup it can be found at src/lib/lowlevel/cstartup.s under the installation directory.

Customizing the startup

In many cases the default C startup will work fine without any changes. However, there are a couple of typical situations when some custom configuration is needed. One example is that the memory system needs to be configured immediately at power on, or if your application is going to run under an operating system that may impose special rules on initialization and termination.

If you only need to run some code to perform early initialization of the hardware you can provide your own __low_level_init() function which is called early in the C startup object, before any static C variables are given their start values.

If you need to make actual changes to the C startup object the easiest way is to copy the existing one to your project, make changes as required and include it in your build system.

To be able to properly suppress the C startup object in the C library you need to provide a different value for the cstartup runtime attribute in your own cstartup.s. The cstartup.s assembly source file starts with:

;;; C startup variant, change attribute value if you make your own
        .rtmodel cstartup,"normal"

You need to change the value normal to something else. What value you use does not matter so much, but using a descriptive value is probably a good idea:

;;; C startup variant, change attribute value if you make your own
        .rtmodel cstartup,"mycustom"

The modified C startup source file needs to be included in your build system. The linker also needs to be informed that it should use a specific C startup object, which is done using the --cstartup command line option:

% ln6502 <object-files> <your-startup.o> clib-6502.a --cstartup=mycustom

Note

The C startup object has been carefully crafted to have a small footprint and to properly initialize the C runtime system. Even though it makes calls to handle the file system, initialize data areas and the heap, it is done in such way that the actual code is only included in the final application if these subsystems are used.

A good understanding of the tools and the provided C runtime is needed in order to make non-trivial changes to the C startup object. Incorrectly made changes may cause linker errors, unused subsystem being pulled in, or result in a runtime that is not properly initialized.

18.9. Time and date

The provided C library implements the functions and definitions in the time.h system header file. The time related types are defined as 64-bit types to avoid the year 2038 problem and reduce issues with overflow.

Providing a time

C defines two functions clock() and time() that provides a concept of a time. The library provides a default implementation for them that returns a value with all bits set, which means that the time is not available.

To use actual times you will need to provide your own implementation of clock() or time() and include it in your project.

The CLOCKS_PER_SEC macro is set to 1000 by the stdint.h header file.

18.10. Rebuilding the C library

The C library is designed to allow for a great deal of configuration without resorting to rebuild it. Situations where you may need to rebuild the C library are when you need a specific variant build, or to enable debug information to debug the library itself.

For the most part, building the library is straightforward, but there are certain files that needs to be built in certain ways, e.g. variants of printf(). To make it easier to build a variant C library there are build scripts provided in the library-build folder in the installation directory.

There is one build file for each library variant that are included in the installation. Select one that has a similar configuration as a starting point. It is a good idea to make copy of the build script and modify it to suit your needs.