feat: overhaul userspace heap and virtual memory
This commit is contained in:
+189
-65
@@ -89,6 +89,7 @@ static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) {
|
||||
|
||||
/* Syscall numbers */
|
||||
#define SYS_EXIT 0
|
||||
#define SYS_YIELD 1
|
||||
#define SYS_SLEEP_MS 2
|
||||
#define SYS_PRINT 4
|
||||
#define SYS_PUTCHAR 5
|
||||
@@ -119,6 +120,9 @@ static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) {
|
||||
#define SYS_KILL 62
|
||||
#define SYS_STAT 152
|
||||
#define SYS_UTIME 167
|
||||
#define SYS_MMAP_ANON 168
|
||||
#define SYS_MUNMAP 169
|
||||
#define SYS_MPROTECT 170
|
||||
|
||||
/* ========================================================================
|
||||
errno
|
||||
@@ -626,44 +630,74 @@ int tolower(int c) { return (c >= 'A' && c <= 'Z') ? c + 32 : c; }
|
||||
Heap allocator (free-list, backed by SYS_ALLOC)
|
||||
======================================================================== */
|
||||
|
||||
#define HEAP_MAGIC 0x5A484541ULL /* "ZHEA" */
|
||||
#define FREED_MAGIC 0xDEADFEEEULL
|
||||
#define HEAP_MAGIC 0x4D544B4845415041ULL /* "MTKHEAPA" */
|
||||
#define DIRECT_MAGIC 0x4D544B4449524543ULL /* "MTKDIREC" */
|
||||
#define FREED_MAGIC 0x4D544B4652454544ULL /* "MTKFREED" */
|
||||
#define HEAP_ALIGN 16ULL
|
||||
#define DIRECT_THRESHOLD (256ULL * 1024ULL)
|
||||
|
||||
struct HeapHeader {
|
||||
uint64_t magic;
|
||||
uint64_t size;
|
||||
} __attribute__((packed));
|
||||
uint64_t requested_size;
|
||||
uint64_t block_size;
|
||||
uint64_t cookie;
|
||||
};
|
||||
|
||||
struct FreeNode {
|
||||
uint64_t magic;
|
||||
uint64_t size;
|
||||
struct FreeNode *next;
|
||||
};
|
||||
|
||||
/* Segregated free lists: power-of-2 size classes for blocks <= 4096 bytes */
|
||||
#define NUM_BUCKETS 8
|
||||
/* Segregated free lists for small and medium blocks. The minimum class must
|
||||
hold both an allocated header and a free-list node. */
|
||||
#define NUM_BUCKETS 7
|
||||
static const uint64_t BUCKET_SIZES[NUM_BUCKETS] = {
|
||||
32, 64, 128, 256, 512, 1024, 2048, 4096
|
||||
64, 128, 256, 512, 1024, 2048, 4096
|
||||
};
|
||||
|
||||
static struct FreeNode *g_buckets[NUM_BUCKETS] = {};
|
||||
static struct FreeNode g_overflow = { 0, NULL };
|
||||
static struct FreeNode g_overflow = { FREED_MAGIC, 0, NULL };
|
||||
static int g_heapInit = 0;
|
||||
static volatile uint32_t g_heapLock = 0;
|
||||
|
||||
static void heap_lock(void) {
|
||||
while (__atomic_exchange_n(&g_heapLock, 1, __ATOMIC_ACQUIRE) != 0)
|
||||
_zos_syscall0(SYS_YIELD);
|
||||
}
|
||||
|
||||
static void heap_unlock(void) {
|
||||
__atomic_store_n(&g_heapLock, 0, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
static int heap_total_size(size_t requested, uint64_t *out) {
|
||||
uint64_t value = (uint64_t)requested;
|
||||
if (value > UINT64_MAX - sizeof(struct HeapHeader) - (HEAP_ALIGN - 1))
|
||||
return 0;
|
||||
*out = (value + sizeof(struct HeapHeader) + (HEAP_ALIGN - 1))
|
||||
& ~(HEAP_ALIGN - 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint64_t heap_cookie(const struct HeapHeader *hdr, uint64_t block_size) {
|
||||
return ((uint64_t)(uintptr_t)hdr >> 4) ^ block_size ^ hdr->magic;
|
||||
}
|
||||
|
||||
static int heap_bucket_index(uint64_t blockSize) {
|
||||
if (blockSize <= 32) return 0;
|
||||
if (blockSize <= 64) return 1;
|
||||
if (blockSize <= 128) return 2;
|
||||
if (blockSize <= 256) return 3;
|
||||
if (blockSize <= 512) return 4;
|
||||
if (blockSize <= 1024) return 5;
|
||||
if (blockSize <= 2048) return 6;
|
||||
if (blockSize <= 4096) return 7;
|
||||
if (blockSize <= 64) return 0;
|
||||
if (blockSize <= 128) return 1;
|
||||
if (blockSize <= 256) return 2;
|
||||
if (blockSize <= 512) return 3;
|
||||
if (blockSize <= 1024) return 4;
|
||||
if (blockSize <= 2048) return 5;
|
||||
if (blockSize <= 4096) return 6;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Insert into overflow list (sorted by address, with coalescing) */
|
||||
static void heap_insert_overflow(void *ptr, uint64_t size) {
|
||||
struct FreeNode *node = (struct FreeNode *)ptr;
|
||||
node->magic = FREED_MAGIC;
|
||||
node->size = size;
|
||||
|
||||
struct FreeNode *prev = &g_overflow;
|
||||
@@ -693,7 +727,7 @@ static void heap_insert_overflow(void *ptr, uint64_t size) {
|
||||
}
|
||||
|
||||
/* Take a block >= needed from overflow. Splits remainder back. */
|
||||
static void *heap_take_overflow(uint64_t needed) {
|
||||
static void *heap_take_overflow(uint64_t needed, uint64_t *actual_size) {
|
||||
struct FreeNode *prev = &g_overflow;
|
||||
struct FreeNode *cur = g_overflow.next;
|
||||
|
||||
@@ -702,9 +736,13 @@ static void *heap_take_overflow(uint64_t needed) {
|
||||
uint64_t blockSize = cur->size;
|
||||
prev->next = cur->next;
|
||||
|
||||
if (blockSize > needed + sizeof(struct FreeNode) + 16) {
|
||||
uint64_t min_free = (sizeof(struct FreeNode) + HEAP_ALIGN - 1)
|
||||
& ~(HEAP_ALIGN - 1);
|
||||
if (blockSize >= needed + min_free) {
|
||||
heap_insert_overflow((uint8_t *)cur + needed, blockSize - needed);
|
||||
blockSize = needed;
|
||||
}
|
||||
*actual_size = blockSize;
|
||||
return (void *)cur;
|
||||
}
|
||||
prev = cur;
|
||||
@@ -713,11 +751,8 @@ static void *heap_take_overflow(uint64_t needed) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Next slab size for heap growth. The kernel tracks a finite number of
|
||||
SYS_ALLOC records per process (MaxHeapAllocs), so growing once per
|
||||
large allocation exhausts them: ld ran out mid-link and BFD reported
|
||||
the resulting NULL mallocs as "file format not recognized". Doubling
|
||||
slabs keep the syscall count logarithmic in total heap size. */
|
||||
/* Next slab size for heap growth. Doubling slabs keeps syscall and VMA
|
||||
metadata traffic logarithmic in total heap size. */
|
||||
static uint64_t g_heap_slab = 16 * 0x1000;
|
||||
|
||||
static void heap_grow(uint64_t bytes) {
|
||||
@@ -742,16 +777,24 @@ static int heap_refill_bucket(int idx) {
|
||||
uint64_t bsize = BUCKET_SIZES[idx];
|
||||
uint64_t chunk = (bsize < 4096) ? 4096 : bsize;
|
||||
|
||||
void *block = heap_take_overflow(chunk);
|
||||
uint64_t actual = 0;
|
||||
void *block = heap_take_overflow(chunk, &actual);
|
||||
if (block == NULL) {
|
||||
heap_grow(chunk);
|
||||
block = heap_take_overflow(chunk);
|
||||
block = heap_take_overflow(chunk, &actual);
|
||||
if (block == NULL) return 0;
|
||||
}
|
||||
|
||||
/* A small unsplittable tail belongs to the overflow list, rather than
|
||||
disappearing when the main chunk is carved into bucket blocks. */
|
||||
if (actual - chunk >= ((sizeof(struct FreeNode) + HEAP_ALIGN - 1)
|
||||
& ~(HEAP_ALIGN - 1)))
|
||||
heap_insert_overflow((uint8_t *)block + chunk, actual - chunk);
|
||||
|
||||
uint64_t count = chunk / bsize;
|
||||
for (uint64_t i = 0; i < count; i++) {
|
||||
struct FreeNode *node = (struct FreeNode *)((uint8_t *)block + i * bsize);
|
||||
node->magic = FREED_MAGIC;
|
||||
node->size = bsize;
|
||||
node->next = g_buckets[idx];
|
||||
g_buckets[idx] = node;
|
||||
@@ -759,19 +802,31 @@ static int heap_refill_bucket(int idx) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
void *malloc(size_t size) {
|
||||
static void *heap_malloc_locked(size_t size) {
|
||||
uint64_t needed;
|
||||
if (!heap_total_size(size, &needed))
|
||||
return NULL;
|
||||
|
||||
/* Large objects get their own page mapping, so free can promptly return
|
||||
both physical memory and virtual space to the kernel. */
|
||||
if (needed >= DIRECT_THRESHOLD) {
|
||||
if (needed > UINT64_MAX - 0xFFFULL) return NULL;
|
||||
uint64_t mapping_size = (needed + 0xFFFULL) & ~0xFFFULL;
|
||||
struct HeapHeader *hdr = (struct HeapHeader *)
|
||||
_zos_syscall1(SYS_ALLOC, (long)mapping_size);
|
||||
if (hdr == NULL) return NULL;
|
||||
hdr->magic = DIRECT_MAGIC;
|
||||
hdr->requested_size = size;
|
||||
hdr->block_size = mapping_size;
|
||||
hdr->cookie = heap_cookie(hdr, mapping_size);
|
||||
return (uint8_t *)hdr + sizeof(*hdr);
|
||||
}
|
||||
|
||||
if (!g_heapInit) {
|
||||
heap_grow(16 * 0x1000);
|
||||
g_heapInit = 1;
|
||||
}
|
||||
|
||||
/* Guard against overflow: size + Header must not wrap */
|
||||
if (size > (uint64_t)-1 - sizeof(struct HeapHeader) - 15)
|
||||
return NULL;
|
||||
|
||||
uint64_t needed = size + sizeof(struct HeapHeader);
|
||||
needed = (needed + 15) & ~15ULL;
|
||||
|
||||
int idx = heap_bucket_index(needed);
|
||||
|
||||
if (idx >= 0) {
|
||||
@@ -781,44 +836,63 @@ void *malloc(size_t size) {
|
||||
|
||||
struct FreeNode *node = g_buckets[idx];
|
||||
g_buckets[idx] = node->next;
|
||||
uint64_t block_size = node->size;
|
||||
|
||||
struct HeapHeader *hdr = (struct HeapHeader *)node;
|
||||
hdr->magic = HEAP_MAGIC;
|
||||
hdr->size = size;
|
||||
hdr->requested_size = size;
|
||||
hdr->block_size = block_size;
|
||||
hdr->cookie = heap_cookie(hdr, hdr->block_size);
|
||||
return (void *)((uint8_t *)hdr + sizeof(struct HeapHeader));
|
||||
}
|
||||
|
||||
/* Large allocation — search overflow list */
|
||||
void *block = heap_take_overflow(needed);
|
||||
uint64_t actual = 0;
|
||||
void *block = heap_take_overflow(needed, &actual);
|
||||
if (block == NULL) {
|
||||
heap_grow(needed);
|
||||
block = heap_take_overflow(needed);
|
||||
block = heap_take_overflow(needed, &actual);
|
||||
if (block == NULL) return NULL;
|
||||
}
|
||||
|
||||
struct HeapHeader *hdr = (struct HeapHeader *)block;
|
||||
hdr->magic = HEAP_MAGIC;
|
||||
hdr->size = size;
|
||||
hdr->requested_size = size;
|
||||
hdr->block_size = actual;
|
||||
hdr->cookie = heap_cookie(hdr, actual);
|
||||
return (void *)((uint8_t *)hdr + sizeof(struct HeapHeader));
|
||||
}
|
||||
|
||||
void free(void *ptr) {
|
||||
static int heap_header_valid(const struct HeapHeader *hdr) {
|
||||
return (hdr->magic == HEAP_MAGIC || hdr->magic == DIRECT_MAGIC) &&
|
||||
hdr->block_size >= sizeof(struct HeapHeader) &&
|
||||
(hdr->block_size & (HEAP_ALIGN - 1)) == 0 &&
|
||||
hdr->cookie == heap_cookie(hdr, hdr->block_size) &&
|
||||
hdr->requested_size <= hdr->block_size - sizeof(struct HeapHeader);
|
||||
}
|
||||
|
||||
static void heap_free_locked(void *ptr) {
|
||||
if (ptr == NULL) return;
|
||||
|
||||
struct HeapHeader *hdr = (struct HeapHeader *)((uint8_t *)ptr - sizeof(struct HeapHeader));
|
||||
|
||||
if (hdr->magic == FREED_MAGIC) return; /* double-free */
|
||||
if (hdr->magic != HEAP_MAGIC) return; /* corrupt */
|
||||
if (!heap_header_valid(hdr)) return;
|
||||
|
||||
uint64_t blockSize = hdr->block_size;
|
||||
int direct = hdr->magic == DIRECT_MAGIC;
|
||||
hdr->magic = FREED_MAGIC;
|
||||
|
||||
uint64_t blockSize = hdr->size + sizeof(struct HeapHeader);
|
||||
blockSize = (blockSize + 15) & ~15ULL;
|
||||
if (direct) {
|
||||
_zos_syscall1(SYS_FREE, (long)hdr);
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = heap_bucket_index(blockSize);
|
||||
|
||||
if (idx >= 0) {
|
||||
/* Small block — push onto bucket (O(1)) */
|
||||
struct FreeNode *node = (struct FreeNode *)hdr;
|
||||
node->magic = FREED_MAGIC;
|
||||
node->size = BUCKET_SIZES[idx];
|
||||
node->next = g_buckets[idx];
|
||||
g_buckets[idx] = node;
|
||||
@@ -828,6 +902,21 @@ void free(void *ptr) {
|
||||
}
|
||||
}
|
||||
|
||||
void *malloc(size_t size) {
|
||||
void *result;
|
||||
heap_lock();
|
||||
result = heap_malloc_locked(size);
|
||||
heap_unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
void free(void *ptr) {
|
||||
if (ptr == NULL) return;
|
||||
heap_lock();
|
||||
heap_free_locked(ptr);
|
||||
heap_unlock();
|
||||
}
|
||||
|
||||
void *calloc(size_t nmemb, size_t size) {
|
||||
/* Check for multiplication overflow */
|
||||
if (nmemb != 0 && size > (size_t)-1 / nmemb)
|
||||
@@ -842,26 +931,39 @@ void *realloc(void *ptr, size_t size) {
|
||||
if (ptr == NULL) return malloc(size);
|
||||
if (size == 0) { free(ptr); return NULL; }
|
||||
|
||||
uint64_t newNeed;
|
||||
if (!heap_total_size(size, &newNeed))
|
||||
return NULL;
|
||||
|
||||
heap_lock();
|
||||
|
||||
struct HeapHeader *hdr = (struct HeapHeader *)((uint8_t *)ptr - sizeof(struct HeapHeader));
|
||||
uint64_t old = hdr->size;
|
||||
if (!heap_header_valid(hdr)) {
|
||||
heap_unlock();
|
||||
return NULL;
|
||||
}
|
||||
uint64_t old = hdr->requested_size;
|
||||
uint64_t oldBlock = hdr->block_size;
|
||||
|
||||
/* Compute actual block size (accounting for bucket rounding) */
|
||||
uint64_t oldBlock = (old + sizeof(struct HeapHeader) + 15) & ~15ULL;
|
||||
int idx = heap_bucket_index(oldBlock);
|
||||
if (idx >= 0) oldBlock = BUCKET_SIZES[idx];
|
||||
|
||||
uint64_t newNeed = (size + sizeof(struct HeapHeader) + 15) & ~15ULL;
|
||||
if (newNeed <= oldBlock) {
|
||||
hdr->size = size;
|
||||
/* Retain the actual extent. Losing it here makes the tail impossible
|
||||
to recover when this block is later freed. */
|
||||
hdr->requested_size = size;
|
||||
hdr->cookie = heap_cookie(hdr, oldBlock);
|
||||
heap_unlock();
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void *newp = malloc(size);
|
||||
if (newp == NULL) return NULL;
|
||||
void *newp = heap_malloc_locked(size);
|
||||
if (newp == NULL) {
|
||||
heap_unlock();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t copySize = old < size ? old : size;
|
||||
memcpy(newp, ptr, copySize);
|
||||
free(ptr);
|
||||
heap_free_locked(ptr);
|
||||
heap_unlock();
|
||||
return newp;
|
||||
}
|
||||
|
||||
@@ -3874,17 +3976,26 @@ long sysconf(int name) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Anonymous mappings only: SYS_ALLOC hands back zeroed page-aligned
|
||||
memory and SYS_FREE releases it. Length is remembered by the kernel
|
||||
per allocation, so munmap ignores its length argument. */
|
||||
/* Anonymous private mappings with kernel-enforced page permissions. */
|
||||
void *mmap(void *addr, size_t length, int prot, int flags, int fd,
|
||||
long offset) {
|
||||
(void)addr; (void)prot; (void)offset;
|
||||
if (length == 0 || fd != -1 || !(flags & MAP_ANONYMOUS)) {
|
||||
errno = ENODEV;
|
||||
int supported_flags = MAP_PRIVATE | MAP_ANONYMOUS;
|
||||
if (length == 0 || fd != -1 || offset != 0 ||
|
||||
(flags & supported_flags) != supported_flags ||
|
||||
(flags & ~supported_flags) != 0) {
|
||||
errno = EINVAL;
|
||||
return MAP_FAILED;
|
||||
}
|
||||
void *p = (void *)_zos_syscall1(SYS_ALLOC, (long)length);
|
||||
/* Address hints may be ignored; fixed placement is intentionally rejected
|
||||
by the flag validation above. */
|
||||
(void)addr;
|
||||
if ((prot & ~(PROT_READ | PROT_WRITE | PROT_EXEC)) != 0 ||
|
||||
!(prot & PROT_READ) ||
|
||||
((prot & PROT_WRITE) && (prot & PROT_EXEC))) {
|
||||
errno = EINVAL;
|
||||
return MAP_FAILED;
|
||||
}
|
||||
void *p = (void *)_zos_syscall2(SYS_MMAP_ANON, (long)length, (long)prot);
|
||||
if (p == NULL) {
|
||||
errno = ENOMEM;
|
||||
return MAP_FAILED;
|
||||
@@ -3893,18 +4004,31 @@ void *mmap(void *addr, size_t length, int prot, int flags, int fd,
|
||||
}
|
||||
|
||||
int munmap(void *addr, size_t length) {
|
||||
(void)length;
|
||||
if (addr == NULL || addr == MAP_FAILED) {
|
||||
if (addr == NULL || addr == MAP_FAILED || length == 0 ||
|
||||
((uintptr_t)addr & 0xFFFULL) != 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (_zos_syscall2(SYS_MUNMAP, (long)addr, (long)length) < 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
_zos_syscall1(SYS_FREE, (long)addr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mprotect(void *addr, size_t length, int prot) {
|
||||
(void)addr; (void)length; (void)prot;
|
||||
return 0; /* page protections are not adjustable from userspace */
|
||||
if (addr == NULL || length == 0 || ((uintptr_t)addr & 0xFFFULL) != 0 ||
|
||||
(prot & ~(PROT_READ | PROT_WRITE | PROT_EXEC)) != 0 ||
|
||||
!(prot & PROT_READ) ||
|
||||
((prot & PROT_WRITE) && (prot & PROT_EXEC))) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (_zos_syscall3(SYS_MPROTECT, (long)addr, (long)length, (long)prot) < 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
long pathconf(const char *path, int name) {
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user