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
_Stubinstead ofup_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:
Library name |
Processor core |
Size of double |
Code model |
Target system |
|---|---|---|---|---|
|
6502 |
32 bits |
Plain |
Generic |
|
6502 |
64 bits |
Plain |
Generic |
|
65B02 |
32 bits |
Plain |
Generic |
|
65B02 |
64 bits |
Plain |
Generic |
|
65C02 |
32 bits |
Plain |
Generic |
|
65C02 |
64 bits |
Plain |
Generic |
|
65CNR02 |
32 bits |
Plain |
Generic |
|
65CNR02 |
64 bits |
Plain |
Generic |
|
6502 |
32 bits |
Plain |
Commodore 64 |
|
6502 |
64 bits |
Plain |
Commodore 64 |
|
45GS02 |
32 bits |
Plain |
MEGA65 |
|
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:
Capability |
printf=reduced |
printf=medium |
printf=nofloat |
printf=float |
|---|---|---|---|---|
basics, |
yes |
yes |
yes |
yes |
format flag, |
no |
yes |
yes |
yes |
field width |
no |
yes |
yes |
yes |
long long |
no |
no |
yes |
yes |
float, |
no |
no |
no |
yes |
The scanf() function exits in three variants, mainly depending on if
you want support for floating point numbers or not:
Capability |
scanf=medium |
scanf=nofloat |
scanf=float |
|---|---|---|---|
basics, |
yes |
yes |
yes |
long long |
no |
yes |
yes |
float, |
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.