skip to Main Content

For educational purposes, I was figuring out how much virtual memory I can allocate on linux. On x86_64, it turns out to allocate 128TB of virtual memory as indicated in the documentation. But on arm64, I manage to allocate only 170TB of virtual memory, although the documentation says 256Tb. I want to understand what prevents me from allocating 256TB of virtual memory.

So I wrote a program

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

int main() {
    char *chars;
    size_t nbytes;

    while(chars != MAP_FAILED) {
      nbytes += 0x10000000000; // 1TB
      chars = mmap(
        NULL,
        nbytes,
        PROT_READ | PROT_WRITE,
        MAP_SHARED | MAP_ANONYMOUS,
        -1,
        0
      );

      munmap(chars, nbytes);
    }

    printf("Allocated %ld total TBn", nbytes/1024/1024/1024/1024);
    exit(EXIT_FAILURE);
}

Set the parameters of the overcommit:

echo 1 > /proc/sys/vm/overcommit_memory

And got result:

Allocated 171 total TB

I tried to increase the parameters:

sysctl -w vm.max_map_count=655300000
ulimit -l unlimited

But nothing helps.

My kernel params:

# grep CONFIG_ARM64_VA_BITS /boot/config-$(uname -r)
# CONFIG_ARM64_VA_BITS_39 is not set
CONFIG_ARM64_VA_BITS_48=y
CONFIG_ARM64_VA_BITS=48

# grep CONFIG_ARM64_PA_BITS /boot/config-$(uname -r)
CONFIG_ARM64_PA_BITS_48=y
CONFIG_ARM64_PA_BITS=48

My system:

ARM Cortex A53 (ARMv8) - 1GB RAM
5.15.0-1034-raspi #37-Ubuntu SMP PREEMPT
# free -m
               total        used        free      shared  buff/cache   available
Mem:             905         198         346           3         360         616

2

Answers


  1. you can’t just magically create virtual memory. The amount is virtual memory depends of the amount of phisical memory available. Allocating 256TB of virtual memory on a Raspberry Pi 3 with 1GB of RAM is not going to happend virtual memory relies on your physical RAM, so the usable amount is limited by what’s actually available.

    Login or Signup to reply.
  2. You’re allocating a contiguous chunk of memory, and that’s the biggest hole in the address space.
    (Needless to say, the address space of your program is 48 bits, i.e. 256TiB.)

    You can see this by looking at /proc/self/maps (or /proc/<pid>/maps) while your process is running. Here’s a very lazy test case that just calls out to system():

    #define _GNU_SOURCE
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/mman.h>
    #include <unistd.h>
    
    int main(void)
    {
        char *chars = NULL;
        size_t nbytes = 0;
    
        char *prev_chars = NULL;
        size_t prev_bytes = 0;
    
        while(1)
        {
            prev_chars = chars;
            prev_bytes = nbytes;
    
            nbytes += 0x10000000000; // 1TB
            chars = mmap(
                NULL,
                nbytes,
                PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_ANONYMOUS,
                -1,
                0
            );
    
            if(chars == MAP_FAILED)
            {
                break;
            }
    
            munmap(chars, nbytes);
        }
    
        printf("Largest successful allocation: %p %zxn", prev_chars, prev_bytes);
        printf("/proc/self/maps:n");
        char buf[32];
        snprintf(buf, sizeof(buf), "cat /proc/%u/maps", getpid());
        system(buf);
    
        return EXIT_FAILURE;
    }
    

    If I compile it with cc -o t t.c -Wall -O3 and run it, then I get this output:

    Largest successful allocation: 0xaad16a0000 aa0000000000
    /proc/self/maps:
    aaaad16a0000-aaaad16a1000 r-xp 00000000 103:02 391637                    /tmp/t
    aaaad16bf000-aaaad16c0000 r--p 0000f000 103:02 391637                    /tmp/t
    aaaad16c0000-aaaad16c1000 rw-p 00010000 103:02 391637                    /tmp/t
    aaaae67e1000-aaaae6802000 rw-p 00000000 00:00 0                          [heap]
    ffff88150000-ffff882d7000 r-xp 00000000 103:02 132374                    /usr/lib/aarch64-linux-gnu/libc.so.6
    ffff882d7000-ffff882ec000 ---p 00187000 103:02 132374                    /usr/lib/aarch64-linux-gnu/libc.so.6
    ffff882ec000-ffff882f0000 r--p 0018c000 103:02 132374                    /usr/lib/aarch64-linux-gnu/libc.so.6
    ffff882f0000-ffff882f2000 rw-p 00190000 103:02 132374                    /usr/lib/aarch64-linux-gnu/libc.so.6
    ffff882f2000-ffff882ff000 rw-p 00000000 00:00 0 
    ffff88317000-ffff8833e000 r-xp 00000000 103:02 132368                    /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
    ffff88350000-ffff88352000 rw-p 00000000 00:00 0 
    ffff88352000-ffff88354000 r--p 00000000 00:00 0                          [vvar]
    ffff88354000-ffff88355000 r-xp 00000000 00:00 0                          [vdso]
    ffff88355000-ffff88357000 r--p 0002e000 103:02 132368                    /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
    ffff88357000-ffff88359000 rw-p 00030000 103:02 132368                    /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
    fffff69e0000-fffff6a01000 rw-p 00000000 00:00 0                          [stack]
    

    For reference, I’m running this in an arm64 Debian VM under a macOS host, but that hardly matters here.

    What you can see here is that the lowest address in use is 0xaaaad16a0000, where the segments of the main binary of the process itself are mapped. That is somewhere between 170 and 171TiB from address 0x0. Following that is some heap memory, and then there’s another large gap until 0xffff88150000, where libc is mapped. That gap makes up another 85 to 86TiB.

    If you requested two separate allocations of 170TiB and 85TiB respectively, then both of those calls should succeed, bringing you to a total of 255TiB. But you can’t get a contiguous mapping because there’s stuff sitting in the middle of the address space.

    That, of course, is the fault of ASLR. If you run the above code, the addresses will vary somewhat between runs, but the ones where the main binary is mapped should always start with 0xaaaa followed by another 8 digits. For consistent results, you can turn off ASLR for the test binary by invoking it with setarch $(uname -m) -R ./[binary_name]. Then your binary should always be mapped at 0xaaaaaaaa0000.

    But it’s still gonna sit in the middle of the address space. If you want to change that, you need to compile with -no-pie. If you do that to the above code and run it, you’ll see that now you can get a much bigger contiguous allocation:

    Largest successful allocation: 0xff93880000 ff0000000000
    /proc/self/maps:
    00400000-00401000 r-xp 00000000 103:02 391637                            /tmp/t
    0041f000-00420000 r--p 0000f000 103:02 391637                            /tmp/t
    00420000-00421000 rw-p 00010000 103:02 391637                            /tmp/t
    0a425000-0a446000 rw-p 00000000 00:00 0                                  [heap]
    ffff93880000-ffff93a07000 r-xp 00000000 103:02 132374                    /usr/lib/aarch64-linux-gnu/libc.so.6
    ffff93a07000-ffff93a1c000 ---p 00187000 103:02 132374                    /usr/lib/aarch64-linux-gnu/libc.so.6
    ffff93a1c000-ffff93a20000 r--p 0018c000 103:02 132374                    /usr/lib/aarch64-linux-gnu/libc.so.6
    ffff93a20000-ffff93a22000 rw-p 00190000 103:02 132374                    /usr/lib/aarch64-linux-gnu/libc.so.6
    ffff93a22000-ffff93a2f000 rw-p 00000000 00:00 0 
    ffff93a53000-ffff93a7a000 r-xp 00000000 103:02 132368                    /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
    ffff93a8c000-ffff93a8e000 rw-p 00000000 00:00 0 
    ffff93a8e000-ffff93a90000 r--p 00000000 00:00 0                          [vvar]
    ffff93a90000-ffff93a91000 r-xp 00000000 00:00 0                          [vdso]
    ffff93a91000-ffff93a93000 r--p 0002e000 103:02 132368                    /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
    ffff93a93000-ffff93a95000 rw-p 00030000 103:02 132368                    /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
    fffff6806000-fffff6827000 rw-p 00000000 00:00 0                          [stack]
    

    With 0xff0000000000 being 255TiB. You can again add setarch $(uname -m) -R on top of this to prevent heap address randomisation, but at this point it hardly matters anymore.

    Of course these things depend on arbitrary default values of the kernel and the toolchain used, so it’s most likely that nothing in this post is guaranteed – but at this point in time, this is how it works.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search