Does printf() allocate memory in C?

AdHominem picture AdHominem · Oct 4, 2016 · Viewed 8.5k times · Source

This simple method just creates an array of dynamic size n and initializes it with values 0 ... n-1. It contains a mistake, malloc() allocates just n instead of sizeof(int) * n bytes:

int *make_array(size_t n) {
    int *result = malloc(n);

    for (int i = 0; i < n; ++i) {
        //printf("%d", i);
        result[i] = i;
    }

    return result;
}

int main() {
    int *result = make_array(8);

    for (int i = 0; i < 8; ++i) {
        printf("%d ", result[i]);
    }

    free(result);
}

When you check the output you will see that it will print some numbers as expected but the last ones are gibberish. However, once I inserted the printf() inside the loop, the output was strangely correct, even tho the allocation was still wrong! Is there some kind of memory allocation associated with printf()?

Answer

Braden Best picture Braden Best · Oct 20, 2016

Strictly, to answer the question in the title, the answer would be that it depends on the implementation. Some implementations might allocate memory, while others might not.

Though there are other problems inherent in your code, which I will elaborate on below.


Note: this was originally a series of comments I made on the question. I decided that it was too much for a comment, and moved them to this answer.


When you check the output you will see that it will print some numbers as expected but the last ones are gibberish.

I believe on systems using a segmented memory model, allocations are "rounded up" to a certain size. I.e. if you allocate X bytes, your program will indeed own those X bytes, however, you'll also be able to (incorrectly) run past those X bytes for a while before the CPU notices that you're violating bounds and sends a SIGSEGV.

This is most likely why your program isn't crashing in your particular configuration. Note that the 8 bytes you allocated will only cover two ints on systems where sizeof (int) is 4. The other 24 bytes needed for the other 6 ints do not belong to your array, so anything can write to that space, and when you read from that space, you are going to get garbage, if your program doesn't crash first, that is.

The number 6 is important. Remember it for later!

The magic part is that the resulting array will then have the correct numbers inside, the printf actually just prints each number another time. But this does change the array.

Note: The following is speculation, and I'm also assuming you're using glibc on a 64-bit system. I'm going to add this because I feel it might help you understand possible reasons why something might appear to work correctly, while actually being incorrect.

The reason it's "magically correct" most likely has to do with printf receiving those numbers through va_args. printf is probably populating the memory area just past the array's physical boundary (because vprintf is allocating memory to perform the "itoa" operation needed to print i). In other words, those "correct" results are actually just garbage that "appears to be correct", but in reality, that's just what happens to be in RAM. If you try changing int to long while keeping the 8 byte allocation, your program will be more likely to crash because long is longer than int.

The glibc implementation of malloc has an optimization where it allocates a whole page from the kernel every time it runs out of heap. This makes it faster because rather than ask the kernel for more memory on every allocation, it can just grab available memory from the "pool" and make another "pool" when the first one fills up.

That said, like the stack, malloc's heap pointers, coming from a memory pool, tend to be contiguous (or at least very close together). Meaning that printf's calls to malloc will likely appear just after the 8 bytes you allocated for your int array. No matter how it works, though, the point is that no matter how "correct" the results may seem, they are actually just garbage and you're invoking undefined behavior, so there's no way of knowing what's going to happen, or whether the program will do something else under different circumstances, like crash or produce unexpected behavior.


So I tried running your program with and without the printf, and both times, the results were wrong.

# without printf
$ ./a.out 
0 1 2 3 4 5 1041 0 

For whatever reason, nothing interfered with the memory holding 2..5. However, something interfered with the memory holding 6 and 7. My guess is that this is vprintf's buffer used to create a string representation of the numbers. 1041 would be the text, and 0 would be the null terminator, '\0'. Even if it's not a result of vprintf, something is writing to that address between the population and the printing of the array.

# with printf
$ ./a.out
*** Error in `./a.out': free(): invalid next size (fast): 0x0000000000be4010 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x77725)[0x7f9e5a720725]
/lib/x86_64-linux-gnu/libc.so.6(+0x7ff4a)[0x7f9e5a728f4a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f9e5a72cabc]
./a.out[0x400679]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f9e5a6c9830]
./a.out[0x4004e9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:02 1573060                            /tmp/a.out
00600000-00601000 r--p 00000000 08:02 1573060                            /tmp/a.out
00601000-00602000 rw-p 00001000 08:02 1573060                            /tmp/a.out
00be4000-00c05000 rw-p 00000000 00:00 0                                  [heap]
7f9e54000000-7f9e54021000 rw-p 00000000 00:00 0 
7f9e54021000-7f9e58000000 ---p 00000000 00:00 0 
7f9e5a493000-7f9e5a4a9000 r-xp 00000000 08:02 7995396                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9e5a4a9000-7f9e5a6a8000 ---p 00016000 08:02 7995396                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9e5a6a8000-7f9e5a6a9000 rw-p 00015000 08:02 7995396                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9e5a6a9000-7f9e5a869000 r-xp 00000000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5a869000-7f9e5aa68000 ---p 001c0000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5aa68000-7f9e5aa6c000 r--p 001bf000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5aa6c000-7f9e5aa6e000 rw-p 001c3000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5aa6e000-7f9e5aa72000 rw-p 00000000 00:00 0 
7f9e5aa72000-7f9e5aa98000 r-xp 00000000 08:02 7999123                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9e5ac5e000-7f9e5ac61000 rw-p 00000000 00:00 0 
7f9e5ac94000-7f9e5ac97000 rw-p 00000000 00:00 0 
7f9e5ac97000-7f9e5ac98000 r--p 00025000 08:02 7999123                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9e5ac98000-7f9e5ac99000 rw-p 00026000 08:02 7999123                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9e5ac99000-7f9e5ac9a000 rw-p 00000000 00:00 0 
7ffc30384000-7ffc303a5000 rw-p 00000000 00:00 0                          [stack]
7ffc303c9000-7ffc303cb000 r--p 00000000 00:00 0                          [vvar]
7ffc303cb000-7ffc303cd000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
012345670 1 2 3 4 5 6 7 Aborted

This is the interesting part. You didn't mention in your question whether your program crashed. But when I ran it, it crashed. Hard.

It's also a good idea to check with valgrind, if you have it available. Valgrind is a helpful program that reports how you're using your memory. Here is valgrind's output:

$ valgrind ./a.out
==5991== Memcheck, a memory error detector
==5991== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==5991== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==5991== Command: ./a.out
==5991== 
==5991== Invalid write of size 4
==5991==    at 0x4005F2: make_array (in /tmp/a.out)
==5991==    by 0x40061A: main (in /tmp/a.out)
==5991==  Address 0x5203048 is 0 bytes after a block of size 8 alloc'd
==5991==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5991==    by 0x4005CD: make_array (in /tmp/a.out)
==5991==    by 0x40061A: main (in /tmp/a.out)
==5991== 
==5991== Invalid read of size 4
==5991==    at 0x40063C: main (in /tmp/a.out)
==5991==  Address 0x5203048 is 0 bytes after a block of size 8 alloc'd
==5991==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5991==    by 0x4005CD: make_array (in /tmp/a.out)
==5991==    by 0x40061A: main (in /tmp/a.out)
==5991== 
0 1 2 3 4 5 6 7 ==5991== 
==5991== HEAP SUMMARY:
==5991==     in use at exit: 0 bytes in 0 blocks
==5991==   total heap usage: 2 allocs, 2 frees, 1,032 bytes allocated
==5991== 
==5991== All heap blocks were freed -- no leaks are possible
==5991== 
==5991== For counts of detected and suppressed errors, rerun with: -v
==5991== ERROR SUMMARY: 12 errors from 2 contexts (suppressed: 0 from 0)

As you can see, valgrind reports that you have an invalid write of size 4 and an invalid read of size 4 (4 bytes is the size of an int on my system). It's also mentioning that you're reading a block of size 0 that comes after a block of size 8 (the block that you malloc'd). This tells you that you're going past the array and into garbage land. Another thing you might notice is that it generated 12 errors from 2 contexts. Specifically, that's 6 errors in a writing context and 6 errors in a reading context. Exactly the amount of un-allocated space I mentioned earlier.

Here's the corrected code:

#include <stdio.h>
#include <stdlib.h>

int *make_array(size_t n) {
    int *result = malloc(n * sizeof (int)); // Notice the sizeof (int)

    for (int i = 0; i < n; ++i)
        result[i] = i;

    return result;
}

int main() {
    int *result = make_array(8);

    for (int i = 0; i < 8; ++i)
        printf("%d ", result[i]);

    free(result);
    return 0;
}

And here's valgrind's output:

$ valgrind ./a.out
==9931== Memcheck, a memory error detector
==9931== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==9931== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==9931== Command: ./a.out
==9931== 
0 1 2 3 4 5 6 7 ==9931== 
==9931== HEAP SUMMARY:
==9931==     in use at exit: 0 bytes in 0 blocks
==9931==   total heap usage: 2 allocs, 2 frees, 1,056 bytes allocated
==9931== 
==9931== All heap blocks were freed -- no leaks are possible
==9931== 
==9931== For counts of detected and suppressed errors, rerun with: -v
==9931== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Notice that it reports no errors and that the results are correct.