Working Without Standard Library
There are times when you do not want to use the standard C++ library and also want to remove the dependency of your binary on the standard C++ library. Some of the binary dependencies are automatically created by the compiler when you compile your code without using any library explicitly. For example write this code-
int main()
{
return 0;
}
Now compile this code with g++ compiler as:
g++ a.cpp
This will generate a.out Then to see what libraries on which there is a dependency created, run this command:
ldd a.out
It will show you a bunch of libraries on which the a.out depends. To remove this dependency, compile the code as:
g++ -nostdlib -fno-exceptions -fno-rtti a.cpp
This will generate a.out which will have no dependency on any other library.
Here are the meanings of the compiler options used above:
nostdlib: do not use standard libraries.
fno-exceptions: disable the exception handling.
fno-rtti: disable runtime type identification.
Ideally you can compile only with the nostdlib option. You will need to implement exception handling ABI. But we will not go through that route. The technique here described is with all three compiler options.
When do you need to compile without the standard library?
There are many cases. Here are a few:-
- You are writing an OS kernel in C++. You need to write a little amount of code in assembly language and the rest of the things you can write in C++. In this case, you don't have the standard library available to you. You need to build your own library to use with your code.
- You are writing code for an embedded system. Depending upon your framework, the support for C++ library may or may not be there. If there is no support for any C++ library, you can still write code in C++ for such systems using the technique described here.
- You are simply a crazy person(like me!) and don't want to use the standard C++ library.
Things to take care if there is no support for C++ in your system
- Initializing static and global variables. This includes calls to constructors of global and static objects before the execution starts at the entry point (typically main function) in your code.
- Memory allocation implementation. C++ generated code calls the implementation of new the operator. The new operator is defined in the standard library. If you are not using the standard library, then you will have to implement this. In the implementation of the new operator, you will call the memory allocation function that is available in your framework.
- g++ needs the implementation(definition) for pure virtual function. Not sure why this is needed. You need to provide a definition of pure virtual functions. You can write error handling code in this function. I guess, the pure virtual function can only be called if there is some bug in the compiler. There are few more non-sense that you need to implement. We will see those in the code.
The 1st issue can be solved by calling the constructors of all global variables yourself. You have to do what stdlib does for calling constructors of global and static variables. Here is code to do that:
void callConstructors()
{
void (**constructor)() = &__CTOR_LIST__ ;
int total = *(int *)constructor ;
constructor++ ;
while(total){
(*constructor)() ;
total-- ;
constructor++ ;
}
}
void callDestructors()
{
void (**deconstructor)() = &__DTOR_LIST__ ;
int total = *(int *)deconstructor ;
deconstructor++ ;
while(total){
(*deconstructor)() ;
total-- ;
deconstructor++ ;
}
}
The __CTOR_LIST__ is the list which contains the function pointers of functions which call the constructors of global and static variables. The first entry of the list contains the number of such pointers then all function pointers.
Similarly the __DTOR_LIST__ contains the function pointers of function which calls the destructors of static and global variables.
The functions which call constructors and destructors are generated by the compiler and their address are stored in sections called .ctors and .dtors respectively. The __CTOR_LIST__ and __DTOR_LIST__ is created using these sections and with the help of a linker script.
ENTRY(entry)
LOAD_VIR = 0x100000;
LOAD_PHYS = 0x100000;
SECTIONS
{
.text LOAD_VIR : AT(LOAD_PHYS)
{
_linker_code = .;
*(.text)
*(.text.*)
*(.rodata*)
*(.gnu.linkonce.t*)
*(.gnu.linkonce.r*)
. = ALIGN(4096);
}
.data :
{
_linker_data_start = .;
__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__ = .;
_linker_data = .;
*(.data)
*(.data.*)
*(.gnu.linkonce.d*)
. = ALIGN(4096);
}
.bss :
{
_linker_bss = .;
*(.bss)
*(.bss.*)
*(.gnu.linkonce.b.*)
*(.COMMON)
. = ALIGN(4096);
}
_linker_end = .;
}
This linker script does these things:
- Defines the load address (physical and virtual) of the binary which will be the output of the linker. In this script it is 0x100000. You can set this address according to the address your system will load your binary.
- Merges different sections.
- Create __CTOR_LIST__ and __DTOR_LIST__
Now lets see the code, compiling and linking steps:
extern "C" {
extern void (*__CTOR_LIST__)() ;
extern void (*__DTOR_LIST__)() ;
}
void callConstructors()
{
void (**constructor)() = &__CTOR_LIST__ ;
int total = *(int *)constructor ;
constructor++ ;
while(total){
(*constructor)() ;
total-- ;
constructor++ ;
}
}
void callDestructors()
{
void (**deconstructor)() = &__DTOR_LIST__ ;
int total = *(int *)deconstructor ;
deconstructor++ ;
while(total){
(*deconstructor)() ;
total-- ;
deconstructor++ ;
}
}
int main()
{
callConstructors();
// Call you code
callDestructors();
}
void* operator new (unsigned size)
{
// call your memory allocation routine
}
void * operator new[] (unsigned size)
{
// call your memory allocation routine
}
void operator delete (void*)
{
// call your memory free routine
}
void operator delete[] (void*)
{
// call your memory free routine
}
extern "C" void __cxa_pure_virtual (){}
void * __dso_handle=0;
extern "C" void __cxa_atexit(){}
Save this code a.cpp. Then use these commands:
g++ -c -nostdlib -fno-exceptions -fno-rtti a.cpp
ld -T ldscriptFile a.o -o a.out
These commands will produce a.out binary from the file a.cpp. If you have more then one file, then compile each file separately and use the linker to link all object files into an executable file.