Search code examples
cmemoryoperating-systemarm64

Does the address space not use the first bit on macos/arm64?


I'm using an ARM64 system(M1).

$ uname -m
arm64

This is the C program I'm using to find out the virtual address space range on my system.

#include <stdlib.h>
#include <stdio.h>
#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>

int main(void) {
    printf("Page size = %d\n", getpagesize());
    
    struct rlimit x;

    getrlimit(RLIMIT_AS, &x);
    printf("Current maximum size = %llx\n", x.rlim_cur);
    printf("Limit on maximum size = %llx\n", x.rlim_max);

    unsigned long long int value = 0;
    value -= 1;
    printf("Maximum float = %llx", value);
    return 0;
}

Output:

Current maximum size = 7fffffffffffffff // 9223372036854775807
Limit on maximum size = 7fffffffffffffff // 9223372036854775807
Maximum float = ffffffffffffffff // 18446744073709551615

The maximum size seems to be 2^63-1. The last bit is not being used at all.

Why is this the case? On a 64bit system, the virtual memory address range should be until 2^64-1 right?

I am aware that for addresses only 48 bits are used, but that doesn't seem to be related for the range of address space(Why there are two different lengths for addresses in 32 and 64 bit?)


Solution

  • As far as arm64 hardware goes, EL0 and EL1 share a translation regime, with TTBR0_EL1 and TTBR1_EL1 controlling the lower and upper halves of the address space respectively. Usually the lower half is used for userland, and the upper half for the kernel.

    But as a first note, rlimit has nothing to do with the hardware. This is about the operating system.

    XNU has this (which also gets copied into Apple's SDKs):

    #define RLIM_INFINITY   (((__uint64_t)1 << 63) - 1)     /* no limit */
    

    So that's just the "no limit" value.

    As a second note, despite the description of RLIMIT_AS, this has nothing to do with the address space size. This is about the sum of all existing mappings in the process.

    The real maximum address at which arm64 XNU will let you map memory is 0x00007ffffe000000. This value is just hardcoded for macOS. There is also a minimum address, which is set at process initialisation to the base address of the main binary in the process (usually 0x100000000) plus ASLR slide.

    On Apple OSes other than macOS, the rules for the maximum address are more complicated, but here are the relevant code bits:

    osfmk/mach/arm/vm_param.h:

    #if defined(XNU_PLATFORM_MacOSX) || defined(XNU_PLATFORM_DriverKit)
    #define MACH_VM_MAX_ADDRESS_RAW 0x00007FFFFE000000ULL
    #else
    #define MACH_VM_MAX_ADDRESS_RAW 0x0000000FC0000000ULL
    #endif
    

    osfmk/mach/shared_region.h:

    #define SHARED_REGION_BASE_ARM64                0x180000000ULL
    #define SHARED_REGION_SIZE_ARM64                0x100000000ULL
    

    osfmk/arm/pmap/pmap.c:

    /* end of shared region + 512MB for various purposes */
    #define ARM64_MIN_MAX_ADDRESS (SHARED_REGION_BASE_ARM64 + SHARED_REGION_SIZE_ARM64 + 0x20000000)
    
    // Max offset is 13.375GB for devices with "large" memory config
    #define ARM64_MAX_OFFSET_DEVICE_LARGE (ARM64_MIN_MAX_ADDRESS + 0x138000000)
    // Max offset is 9.375GB for devices with "small" memory config
    #define ARM64_MAX_OFFSET_DEVICE_SMALL (ARM64_MIN_MAX_ADDRESS + 0x38000000)
    
    vm_map_offset_t
    pmap_max_64bit_offset(
        __unused unsigned int option)
    {
        vm_map_offset_t max_offset_ret = 0;
    
    #if defined(__arm64__)
        const vm_map_offset_t min_max_offset = ARM64_MIN_MAX_ADDRESS; // end of shared region + 512MB for various purposes
        if (option == ARM_PMAP_MAX_OFFSET_DEFAULT) {
            max_offset_ret = arm64_pmap_max_offset_default;
        } else if (option == ARM_PMAP_MAX_OFFSET_MIN) {
            max_offset_ret = min_max_offset;
        } else if (option == ARM_PMAP_MAX_OFFSET_MAX) {
            max_offset_ret = MACH_VM_MAX_ADDRESS;
        } else if (option == ARM_PMAP_MAX_OFFSET_DEVICE) {
            if (arm64_pmap_max_offset_default) {
                max_offset_ret = arm64_pmap_max_offset_default;
            } else if (max_mem > 0xC0000000) {
                // devices with > 3GB of memory
                max_offset_ret = ARM64_MAX_OFFSET_DEVICE_LARGE;
            } else if (max_mem > 0x40000000) {
                // devices with > 1GB and <= 3GB of memory
                max_offset_ret = ARM64_MAX_OFFSET_DEVICE_SMALL;
            } else {
                // devices with <= 1 GB of memory
                max_offset_ret = min_max_offset;
            }
        } else if (option == ARM_PMAP_MAX_OFFSET_JUMBO) {
            if (arm64_pmap_max_offset_default) {
                // Allow the boot-arg to override jumbo size
                max_offset_ret = arm64_pmap_max_offset_default;
            } else {
                max_offset_ret = MACH_VM_MAX_ADDRESS;     // Max offset is 64GB for pmaps with special "jumbo" blessing
            }
        } else {
            panic("pmap_max_64bit_offset illegal option 0x%x", option);
        }
    
        assert(max_offset_ret <= MACH_VM_MAX_ADDRESS);
        assert(max_offset_ret >= min_max_offset);
    #else
        panic("Can't run pmap_max_64bit_offset on non-64bit architectures");
    #endif
    
        return max_offset_ret;
    }
    

    So on iOS and other non-macOS configs, the address size limit is either 0x0000000fc0000000 if you have "jumbo" maps (which you can get with the com.apple.developer.kernel.extended-virtual-addressing entitlement, or on some devices with com.apple.developer.kernel.increased-memory-limit), and otherwise either 0x00000002a0000000, 0x00000002b8000000 or 0x00000003b8000000, depending on how much physical memory the device has.

    Note that the latter three sizes are subject to change though, because they're calculated from the size of the shared cache region bounds, which is itself subject to change. The "9.375GB" and "13.375GB" comments are also wrong today, because they stem from a time when SHARED_REGION_SIZE_ARM64 was 0xa0000000.