5

I have a C++ application running bare-metal that I want to make as small as possible.

I am not using dynamic memory allocation anywhere. I am using no STL functions. I've also overridden all the varieties of "delete" and "new" with empty functions. Nonetheless, when I look at a sorted list of symbols I see that malloc() is still one of the largest items in my compiled binary. I could shrink my binary by about 25% if I could get rid of it.

Do C++ runtimes generally require malloc() for behind-the-scenes type work?

(I'm using Xilinx's fork of gcc for the Microblaze architecture, if that matters)

Barry Gackle
  • 829
  • 4
  • 17
  • One can reduce the C++ overhead by providing dummy empty definitions of malloc() and free() – CryingHippo May 16 '15 at 04:31
  • That's a good point. Is it possible that this could break the runtime itself, though? – Barry Gackle May 16 '15 at 04:32
  • 2
    If you don't use heap should be ok. Check this article http://www.embedded.com/design/mcus-processors-and-socs/4007134/Building-Bare-Metal-ARM-Systems-with-GNU-Part-4 – CryingHippo May 16 '15 at 04:35
  • Possible aside: have you considered dynamic linking? –  May 16 '15 at 05:21
  • It shouldn't be necessary unless you use some high level libraries like xilfs or lwip (even there, I'm not sure they use malloc). You can always objdump and verify where malloc is used, shouldn't be long for a bare-metal application. – Jonathan Drolet May 16 '15 at 06:03
  • @Hurkyl, That is a logical question, but no to dynamic linking. I have a single 64k block of ram that is both code storage and stack/heap, so dynamic linking would just move code from one place to another, and the dynamic loader itself would add to the total. – Barry Gackle May 16 '15 at 07:12

4 Answers4

4

Reliance of a program on malloc() can occur in both C and C++, even if the program doesn't directly use them. It is a quality of implementation matter for the compiler and standard library rather than a requirement by the standards.

This really depends on how the both the compiler startup code (code that sets things up so main() can be called) works and how standard library code is implemented.

In both C and C++, for example, startup code (in hosted environments) needs to collect information about command line arguments (possibly copy to some allocated buffer), connect to standard files/streams (like std::cout and std::cin in C++, and `stdout and stdin in C). Any of these things can involve dynamic memory allocation (e.g. for buffers associated with standard streams) or execute code that is not actually needed by the program.

Peter
  • 35,646
  • 4
  • 32
  • 74
2

C++ has two kinds of implementations, hosted and freestanding. Hosted implementations do assume that malloc is present and often do use it for internal purposes. Freestanding implementations assume that only the new function is present, because it supports the C++ keyword new, but it is easy to ensure that this function doesn't get called.

The difference between the two is that in a freestanding implementation, you can control program startup and the set of required headers and libraries is limited. Program startup is controlled by setting the entry point.

g++ -ffreestanding -e _entry program.cpp

program.cpp might be:

extern "C" int entry()
{
    return 0;
}

The extern "C" is necessary to prevent C++ name mangling, which might make it difficult to figure out what the name of entry is during linking. Then, don't use new, std::string, stream I/O, STL, or the like, and avoid at_exit.

This should give you control over the startup and cleanup code and limit what the compiler can implicitly rely on being available from the standard library. Note, however, that this can be a challenging environment. Not only will you prevent initialization of heaps, I/O streams, and the like, but you will also prevent setup of exceptions, RTTI, and the calling of static storage object constructors, among other things. You will have to write code or use libraries to manually opt into several features of C++ you might want to use. If you go this route, you may want to peruse this wiki http://wiki.osdev.org/C%2B%2B. You may want to at least call global constructors, which is pretty easy to do.

Explanation

The standard

C++ has the notion of a "freestanding implementation", in which fewer headers are available.

3.6.1 Main function [basic.start.main]

1 [snip] It is implementation-defined whether a program in a freestanding environment is required to define a main function.

17.6.1.3 Freestanding implementations [compliance]

1 Two kinds of implementations are defined: hosted and freestanding (1.4). For a hosted implementation, this International Standard describes the set of available headers.

2 A freestanding implementation has an implementation-defined set of headers. This set shall include at least the headers shown in Table 16.

3 The supplied version of the header <cstdlib> shall declare at least the functions abort, atexit, at_quick_exit, exit, and quick_exit (18.5). [snip]

Table 16 lists ciso646, cstddef, cfloat, limits, climits, cstdint, cstdlib, new, typeinfo, exception, initializer_list, cstdalign, cstdarg, cstdbool, type_traits, and atomic.

Most of the above headers contain simple definitions of types, constants, and templates. The only ones that may seem problematic are typeinfo, exception, cstdlib, and new. The first two support RTTI and exceptions, respectively, which you can ensure are disabled with additional compiler flags. cstdlib and new you can simply ignore by not calling exit/at_exit and not using new expressions. The reason for avoiding at_exit is it might call new internally. Nothing else in the freestanding fragment should call call anything in cstdlib or new.

The options

The most important option above is -e _entry, which gives you control over what runs when your program starts.

-ffreestanding tells the compiler and the standard library (rather, its freestanding fragment) not to assume that the entire standard library is present (even if it still is). It may prevent the generation of surprising code. Note that this option doesn't actually restrict which headers are available to you. You can still use iostream, for instance, though it may be a bad idea if you also changed the entry point. What it does is it prevents the code supporting the freestanding headers from calling anything outside the freestanding headers, and prevents the compiler from generating any such calls implicitly.

antron
  • 3,749
  • 2
  • 17
  • 23
0

If you use any STL like std::lib or std::map. Or even std::cout, there will be dynamic memory allocations behind the scene.

Alexandre Severino
  • 1,563
  • 1
  • 16
  • 38
0

It will always need malloc. Because the binary has to be loaded and also the shared libraries.

Nipun Talukdar
  • 4,975
  • 6
  • 30
  • 42
  • So, there is a copy of `malloc` in the binary, so that it can be used it in the process of loading the binary? –  May 16 '15 at 05:34
  • No. The binary will also have to be loaded first before even main is called. Some structures/map have to be filled with information regarding loaded shared objects and the executable, even before main is called. They will internally call malloc/calloc to create those structures/map. But I am not saying malloc needs to be statically linked to the binary. But the program can never run without malloc. So, C++ runtime will always need malloc/calloc – Nipun Talukdar May 16 '15 at 06:15