Codementor Events

The glibc malloc (3) API Family

Published Jan 28, 2019

Learn about the glibc malloc(3) API family in this article by Kaiwan N Billimoria who has worked on many aspects of the Linux system programming stack, including Bash scripting, system programming in C, kernel internals, and embedded Linux work.

There are regions or segments meant for the use of dynamic memory-allocation within the process of Virtual Address Space (VAS). The heap segment is one such dynamic region—a free gift of memory made available to the process for its runtime consumption.

How exactly does the developer exploit this gift of memory? Not just that, the developer has to be extremely careful with matching memory allocations to subsequent memory frees; otherwise, the system isn't going to like it!

The GNU C library (glibc) provides a small but powerful set of APIs to enable the developer to manage dynamic memory, and this article will cover the basic glibc APIs used to allocate and free memory dynamically.

As you will come to see, the memory-management APIs are literally a handful: malloc(3), calloc, realloc, and free. Still, using them correctly remains a challenge!

The malloc(3) API

Perhaps one of the most common APIs used by application developers is the renowned malloc(3).

We use malloc(3) to dynamically allocate a chunk of memory at runtime. This is as opposed to static—or compile-time – memory-allocation where we make a statement, such as:

char buf[256];

In the preceding case, the memory has been statically allocated (at compile-time). So, how exactly do you use malloc(3)? Let's check out its signature:

#include <stdlib.h>
void *malloc(size_t size);

The parameter to malloc(3) is the number of bytes to allocate. But what is the size_t data type? Obviously, it's not a C primitive data type; it's a typedef – long unsigned int on your typical 64-bit platform (the exact data type does vary with the platform; the important point is that it's always unsigned – it cannot be negative. On a 32-bit Linux, it will be unsigned int). Ensuring that your code precisely matches the function signature and data types is crucial in writing robust and correct programs. While we're at it, ensure that you include the header file that the man page displays with the API signature.

The return value is a pointer to the zeroth byte of the newly-allocated memory region on success, and NULL on failure.

So, using the API is very straightforward: as an example, allocate 256 bytes of memory dynamically, and store the pointer to that newly allocated region in the ptr variable:

void *ptr;
ptr = malloc(256);

As another typical example, the programmer needs to allocate memory for a data structure; let's call it struct sbar. You could do so like this:

    struct sbar {
        int a[10], b[10];
        char buf[512];
    } *psbar;

    psbar = malloc(sizeof(struct sbar));
    // initialize and work with it
    [...]
    free(psbar);

Hey, what about checking the failure case? It's a key point, so we will rewrite the preceding code like so (and of course it would be the case for the malloc(256) code snippet too):

struct [...] *psbar;
sbar = malloc(sizeof(struct sbar));
if (!sbar) {
    <... handle the error ...>
}

Let's use one of the powerful tracing tools ltrace to check that this works as expected; ltrace is used to display all library APIs in the process-execution path (similarly, use strace to trace all system calls). Let's assume that we compile the preceding code and the resulting binary executable file is called tst:

$ ltrace ./tst
malloc(592)           = 0xd60260
free(0xd60260)        = <void>
exit(0 <no return ...>
+++ exited (status 0) +++
$

We can clearly see malloc(3) (and the fact that the example structure we used took up 592 bytes on an x86_64), and its return value (following the = sign). The free API follows, and then it simply exits.

It's important to understand that the content of the memory chunk allocated by malloc(3) is considered to be random. Thus, it's the programmer's responsibility to initialize the memory before reading from it; if you fail to do so, it results in a bug called Uninitialized Memory Read (UMR).

malloc(3) – some FAQs

The following are some FAQs that will help us to learn more about malloc(3):
• FAQ 1: How much memory can malloc(3) allocate with a single call?

A rather pointless question in practical terms, but one that is often asked!

The parameter to malloc(3) is an integer value of the size_t data type, so, logically, the maximum number we can pass as a parameter to malloc(3) is the maximum value a size_t can take on the platform. Practically speaking, on a 64-bit Linux, size_t will be 8 bytes, which of course, in bits is 8 * 8 = 64. Therefore, the maximum amount of memory that can be allocated in a single malloc(3) call is 2^64!

So, how much is it? Let's be empirical and actually try it out (note that the following code snippet has to be linked with the math library using the -lm switch):

    int szt = sizeof(size_t);
    float max=0;
    max = pow(2, szt*8);
    printf("sizeof size_t = %u; " 
            "max value of the param to malloc = %.0f\n", 
            szt, max);

The output, on an x86_64:

**sizeof size_t = 8; max param to malloc = 18446744073709551616**

Aha! That's a mighty large number; more readably, it's as follows:

2^64 = 18,446,744,073,709,551,616 = 0xffffffffffffffff

That's 16 EB (exabytes, which is 16,384 PB, which is 16 million TB)!

So, on a 64-bit OS, malloc(3) can allocate a maximum of 16 EB in a single call, in theory.

In practice, obviously, this would be impossible because, of course, that's the entire user mode VAS of the process itself. In reality, the amount of memory that can be allocated is limited by the amount of free memory contiguously available on the heap. Actually, there's more to it. Memory for malloc(3) can come from other regions of the VAS, too. Don't forget there's a resource limit on data segment size; the default is usually unlimited, which really means that there's no artificial limit imposed by the OS.

So, in practice, it's best to be sensible, not assume anything and check the return value for NULL.

As an aside, what's the maximum value a size_t can take on a 32-bit OS? Accordingly, we compile on x86_64 for 32-bit by passing the -m32 switch to the compiler:

$ gcc -m32 mallocmax.c -o mallocmax32 -Wall -lm
$ ./mallocmax32
*** max_malloc() ***
sizeof size_t = 4; max value of the param to malloc = 4294967296
[...]
$

Clearly, it's 4 GB (gigabytes) – again, the entire VAS of a 32-bit process.
• FAQ 2: What if I pass malloc(3) a negative argument?

The data type of the parameter to malloc(3), size_t, is an unsigned integer quantity – it cannot be negative. But humans are imperfect, and Integer OverFlow (IOF) bugs do exist! You can imagine a scenario where a program attempts to calculate the number of bytes to allocate, like this:

num = qa * qb;

What if num is declared as a signed integer variable and qa and qb are large enough that the result of the multiplication operation causes an overflow? The num result will then wrap around and become negative! malloc(3) should fail, of course. But hang on: if the num variable is declared as size_t (which should be the case), the negative quantity will turn into some positive quantity!

The mallocmax program has a test case for this.

Here is the output when run on an x86_64 Linux box:

*** negative_malloc() ***
size_t max    = 18446744073709551616
ld_num2alloc  = -288225969623711744
szt_num2alloc = 18158518104085839872
1. long int used:  malloc(-288225969623711744) returns (nil)
2. size_t used:    malloc(18158518104085839872) returns (nil)
3. short int used: malloc(6144) returns 0x136b670
4. short int used: malloc(-4096) returns (nil)
5. size_t used:    malloc(18446744073709547520) returns (nil)

Here are the relevant variable declarations:

const size_t onePB    = 1125899907000000; /* 1 petabyte */
int qa = 28*1000000;
long int ld_num2alloc = qa * onePB;
size_t szt_num2alloc  = qa * onePB;
short int sd_num2alloc;

Now, let's try it with a 32-bit version of the program. Compile it for 32-bit and run it:

$ ./mallocmax32
*** max_malloc() ***
sizeof size_t = 4; max param to malloc = 4294967296
*** negative_malloc() ***
size_t max    = 4294967296
ld_num2alloc  = 0
szt_num2alloc = 1106247680
1. long int used:  malloc(-108445696) returns (nil)
2. size_t used:    malloc(4186521600) returns (nil)
3. short int used: malloc(6144) returns 0x85d1570
4. short int used: malloc(-4096) returns (nil)
5. size_t used:    malloc(4294963200) returns (nil)
$

To be fair, the compiler does warn us:

gcc -Wall   -c -o mallocmax.o mallocmax.c
mallocmax.c: In function ‘negative_malloc’:
mallocmax.c:87:6: warning: argument 1 value ‘18446744073709551615’ exceeds maximum object size 9223372036854775807 [-Walloc-size-larger-than=]
  ptr = malloc(-1UL);
  ~~~~^~~~~~~~~~~~~~
In file included from mallocmax.c:18:0:
/usr/include/stdlib.h:424:14: note: in a call to allocation function ‘malloc’ declared here
 extern void *malloc (size_t __size) __THROW __attribute_malloc__ __wur;
              ^~~~~~ 
[...]

Interesting! The compiler answers our FAQ 1 question now:

 [...] warning: argument 1 value ‘18446744073709551615’ exceeds maximum object size9223372036854775807 [-Walloc-size-larger-than=] [...]

The maximum value you can allocate as per the compiler seems to be 9223372036854775807.

Wow. A little calculator time reveals that this is 8192 PB = 8 EB! So, we must conclude that the correct answer to the previous question: How much memory can malloc allocate with a single call? Answer: 8 exabytes. Again, in theory.

• FAQ 3: What if I use malloc(0)?

Not much; depending on the implementation, malloc(3) will return NULL, or, a non-NULL pointer that can be passed to free. Of course, even if the pointer is non-NULL, there is no memory, so don't attempt to use it.

Let's try it out:

void *ptr;
  ptr = malloc(0);
  free(ptr);
We compile and then run it via ltrace:
$ ltrace ./a.out 
malloc(0)                                  = 0xf50260
free(0xf50260)                                = <void>
exit(0 <no return ...>
+++ exited (status 0) +++
$ 

Here, malloc(0) did indeed return a non-NULL pointer.

• FAQ 4: What if I use malloc(2048) and attempt to read/write beyond 2,048 bytes?

This is a bug of course – an out-of-bounds memory-access bug, further defined as a read or write buffer overflow.

The free API

One of the golden rules of development in this ecosystem is that programmer-allocated memory must be freed.

Failure to do so leads to a bad situation – a bug, really – called memory leakage. Carefully matching your allocations and frees is essential.

Using the free(3) API is straightforward:

void free(void *ptr);

It accepts one parameter: the pointer to the memory chunk to be freed. ptr must be a pointer returned by one of the malloc(3) family routines: malloc(3), calloc, or realloc[array].

free does not return any value; don't even attempt to check whether it worked; if you used it correctly, it worked. Once a memory chunk is freed, you obviously cannot attempt to use any part of that memory chunk again; doing so will result in a bug (or what's called UB – undefined behavior).

A common misconception regarding free() sometimes leads to its being used in a buggy fashion; take a look at this pseudocode snippet:

void *ptr = NULL;
[...] 
while(<some-condition-is-true>) {
    if (!ptr)
        ptr = malloc(n);

    [...
 <use 'ptr' here>
    ...]

    free(ptr);
}

This program will possibly crash in the loop (within the <use 'ptr' here> code) in a few iterations. Why? Because the ptr memory pointer is freed and is attempting to be reused. But how come? Ah, look carefully: the code snippet is only going to malloc(3) the ptr pointer if it is currently NULL, that is, its programmer has assumed that once we free() memory, the pointer we just freed gets set to NULL.

This is not the case!!

Be wary and be defensive in writing code. Don't assume anything; it's a rich source of bugs.

The calloc API

The calloc(3) API is almost identical to malloc(3), differing in two main respects:
• It initializes the memory chunk it allocates to the zero value (that is, ASCII 0 or NULL, not the number 0)
• It accepts two parameters, not one
The calloc(3) function signature is as follows:

 void *calloc(size_t nmemb, size_t size);

The first parameter, nmemb, is n members; the second parameter, size, is the size of each member. In effect, calloc(3) allocates a memory chunk of (nmemb * size) bytes. So, if you want to allocate memory for an array of, say, 1,000 integers, you can do so like this:

    int *ptr;
    ptr = calloc(1000, sizeof(int));

Assuming the size of an integer is 4 bytes, we would have allocated a total of (1000 * 4) = 4000 bytes.

Whenever one requires memory for an array of items (a frequent use case in applications is an array of structures), calloc is a convenient way to both allocate and simultaneously initialize the memory.

The realloc API

The realloc API is used to resize an existing memory chunk—to grow or shrink it. This resizing can only be performed on a piece of memory previously allocated with one of the malloc(3) family of APIs (the usual suspects: malloc(3), calloc, or realloc[array]). Here is its signature:

 void *realloc(void *ptr, size_t size);

The first parameter, ptr, is a pointer to a chunk of memory previously allocated with one of the malloc(3)family of APIs; the second parameter, size, is the new size of the memory chunk—it can be larger or smaller than the original, thus growing or shrinking the memory chunk.

A quick example code snippet will help us understand realloc:

void *ptr, *newptr;
ptr = calloc(100, sizeof(char)); // error checking code not shown here
newptr = realloc(ptr, 150);
if (!newptr) {
    fprintf(stderr, "realloc failed!");
    free(ptr);
    exit(EXIT_FAILURE);
}
< do your stuff >
free(newptr);

The pointer returned by realloc is the pointer to the newly resized chunk of memory; it may or may not be the same address as the original ptr. In effect, you should now completely disregard the original pointer ptr and regard the realloc-returned newptr pointer like the one to work with. If it fails, the return value is NULL (check it!) and the original memory chunk is left untouched.

A key point: the pointer returned by realloc(3), newptr, is the one that must be subsequently freed, not the original pointer (ptr) to the (now resized) memory chunk. Of course, do not attempt to free both pointers, as that too is a bug.

What about the contents of the memory chunk that just got resized? They remain unchanged up to MIN(original_size, new_size). Thus, in the preceding example, MIN(100, 150) = 100, the contents of memory up to 100 bytes will be unchanged.
What about the remainder (50 bytes)? It's considered to be random content (just like malloc(3)).

The realloc(3) – corner cases

Consider the following code snippet:

void *ptr, *newptr;
ptr = calloc(100, sizeof(char)); // error checking code not shown here
newptr = realloc(NULL, 150);

The pointer passed to realloc is NULL? The library treats this as equivalent to a new allocation – malloc(150), and all the implications of the malloc(3) That's it.
Now, consider the following code snippet:

void *ptr, *newptr;
ptr = calloc(100, sizeof(char)); // error checking code not shown here
newptr = realloc(ptr, 0);

The size parameter passed to realloc is 0? The library treats this as equivalent to free(ptr). That's it.

The reallocarray API

A scenario: you allocate memory for an array using calloc(3); later, you want to resize it to be, say, a lot larger. We can do so with realloc(3); for example:

struct sbar *ptr, *newptr;
ptr = calloc(1000, sizeof(struct sbar)); // array of 1000 struct sbar's
[...]
// now we want 500 more!
newptr = realloc(ptr, 500*sizeof(struct sbar));

Fine. There's an easier way, though—using the reallocarray(3) API. Its signature is as follows:

 void *reallocarray(void *ptr, size_t nmemb, size_t size);
With it, the code becomes simpler:
 [...]
// now we want 500 more!
newptr = reallocarray(ptr, 500, sizeof(struct sbar));

The return value of reallocarray is pretty identical to that of the realloc API: the new pointer to the resized memory chunk on success (it may differ from the original), NULL on failure. If it fails, the original memory chunk is left untouched.

reallocarray has one real advantage over realloc– safety. However, unlike that realloc() call, reallocarray() fails safely in the case where the multiplication would overflow. If such an overflow occurs, reallocarray() returns NULL, sets errno to ENOMEM, and leaves the original block of memory unchanged.

Also, realize that the reallocarray API is a GNU extension; it will work on modern Linux but should not be considered portable to other OSes.

Finally, consider this: some projects have strict alignment requirements for their data objects; using calloc (or even allocating said objects via malloc(3)) can result in subtle bugs! The bottom line: be careful. Read the documentation, think, and decide which API would be appropriate given the circumstances.

If you found this article interesting, you can explore Hands-On System Programming with Linux to get up and running with system programming concepts in Linux. Hands-On System Programming with Linux gives you a solid theoretical base and practical industry-relevant descriptions and covers the Linux system programming domain.

Discover and read more posts from PACKT
get started
post commentsBe the first to share your opinion
Show more replies