harec

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit edf4fbf95375fec8a848c9d137cb79cc485ba323
parent 604219dd6e033416f52bb9c9361e5158e60d5e42
Author: Eyal Sawady <ecs@d2evs.net>
Date:   Fri, 22 Jan 2021 12:34:24 -0500

rt: implement allocations

Diffstat:
Art/+linux/errno.ha | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Art/+linux/segmalloc.ha | 17+++++++++++++++++
Mrt/+linux/syscalls.ha | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrt/Makefile | 2++
Mrt/abort.ha | 3++-
Mrt/configure | 2++
Art/malloc.ha | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Art/memcpy.ha | 6++++++
8 files changed, 401 insertions(+), 1 deletion(-)

diff --git a/rt/+linux/errno.ha b/rt/+linux/errno.ha @@ -0,0 +1,131 @@ +export def EPERM: int = 1; +export def ENOENT: int = 2; +export def ESRCH: int = 3; +export def EINTR: int = 4; +export def EIO: int = 5; +export def ENXIO: int = 6; +export def E2BIG: int = 7; +export def ENOEXEC: int = 8; +export def EBADF: int = 9; +export def ECHILD: int = 10; +export def EAGAIN: int = 11; +export def ENOMEM: int = 12; +export def EACCES: int = 13; +export def EFAULT: int = 14; +export def ENOTBLK: int = 15; +export def EBUSY: int = 16; +export def EEXIST: int = 17; +export def EXDEV: int = 18; +export def ENODEV: int = 19; +export def ENOTDIR: int = 20; +export def EISDIR: int = 21; +export def EINVAL: int = 22; +export def ENFILE: int = 23; +export def EMFILE: int = 24; +export def ENOTTY: int = 25; +export def ETXTBSY: int = 26; +export def EFBIG: int = 27; +export def ENOSPC: int = 28; +export def ESPIPE: int = 29; +export def EROFS: int = 30; +export def EMLINK: int = 31; +export def EPIPE: int = 32; +export def EDOM: int = 33; +export def ERANGE: int = 34; +export def EDEADLK: int = 35; +export def ENAMETOOLONG: int = 36; +export def ENOLCK: int = 37; +export def ENOSYS: int = 38; +export def ENOTEMPTY: int = 39; +export def ELOOP: int = 40; +export def ENOMSG: int = 42; +export def EIDRM: int = 43; +export def ECHRNG: int = 44; +export def EL2NSYNC: int = 45; +export def EL3HLT: int = 46; +export def EL3RST: int = 47; +export def ELNRNG: int = 48; +export def EUNATCH: int = 49; +export def ENOCSI: int = 50; +export def EL2HLT: int = 51; +export def EBADE: int = 52; +export def EBADR: int = 53; +export def EXFULL: int = 54; +export def ENOANO: int = 55; +export def EBADRQC: int = 56; +export def EBADSLT: int = 57; +export def EBFONT: int = 59; +export def ENOSTR: int = 60; +export def ENODATA: int = 61; +export def ETIME: int = 62; +export def ENOSR: int = 63; +export def ENONET: int = 64; +export def ENOPKG: int = 65; +export def EREMOTE: int = 66; +export def ENOLINK: int = 67; +export def EADV: int = 68; +export def ESRMNT: int = 69; +export def ECOMM: int = 70; +export def EPROTO: int = 71; +export def EMULTIHOP: int = 72; +export def EDOTDOT: int = 73; +export def EBADMSG: int = 74; +export def EOVERFLOW: int = 75; +export def ENOTUNIQ: int = 76; +export def EBADFD: int = 77; +export def EREMCHG: int = 78; +export def ELIBACC: int = 79; +export def ELIBBAD: int = 80; +export def ELIBSCN: int = 81; +export def ELIBMAX: int = 82; +export def ELIBEXEC: int = 83; +export def EILSEQ: int = 84; +export def ERESTART: int = 85; +export def ESTRPIPE: int = 86; +export def EUSERS: int = 87; +export def ENOTSOCK: int = 88; +export def EDESTADDRREQ: int = 89; +export def EMSGSIZE: int = 90; +export def EPROTOTYPE: int = 91; +export def ENOPROTOOPT: int = 92; +export def EPROTONOSUPPORT: int = 93; +export def ESOCKTNOSUPPORT: int = 94; +export def EOPNOTSUPP: int = 95; +export def EPFNOSUPPORT: int = 96; +export def EAFNOSUPPORT: int = 97; +export def EADDRINUSE: int = 98; +export def EADDRNOTAVAIL: int = 99; +export def ENETDOWN: int = 100; +export def ENETUNREACH: int = 101; +export def ENETRESET: int = 102; +export def ECONNABORTED: int = 103; +export def ECONNRESET: int = 104; +export def ENOBUFS: int = 105; +export def EISCONN: int = 106; +export def ENOTCONN: int = 107; +export def ESHUTDOWN: int = 108; +export def ETOOMANYREFS: int = 109; +export def ETIMEDOUT: int = 110; +export def ECONNREFUSED: int = 111; +export def EHOSTDOWN: int = 112; +export def EHOSTUNREACH: int = 113; +export def EALREADY: int = 114; +export def EINPROGRESS: int = 115; +export def ESTALE: int = 116; +export def EUCLEAN: int = 117; +export def ENOTNAM: int = 118; +export def ENAVAIL: int = 119; +export def EISNAM: int = 120; +export def EREMOTEIO: int = 121; +export def EDQUOT: int = 122; +export def ENOMEDIUM: int = 123; +export def EMEDIUMTYPE: int = 124; +export def ECANCELED: int = 125; +export def ENOKEY: int = 126; +export def EKEYEXPIRED: int = 127; +export def EKEYREVOKED: int = 128; +export def EKEYREJECTED: int = 129; +export def EOWNERDEAD: int = 130; +export def ENOTRECOVERABLE: int = 131; +export def ERFKILL: int = 132; +export def EHWPOISON: int = 133; diff --git a/rt/+linux/segmalloc.ha b/rt/+linux/segmalloc.ha @@ -0,0 +1,17 @@ +// Allocates a segment. +fn segmalloc(n: size) nullable *void = { + let p: *void = mmap(null, n, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANON, -1, 0z); + // TODO: remove the cast to nullable *void + return if (p: uintptr: int == -ENOMEM) null: nullable *void else p: nullable *void; +}; + +// Frees a segment allocated with segmalloc. +fn segfree(p: *void, s: size) int = munmap(p, s); + +// Marks a segment as writable and drops the execute bit. +fn segwrite(seg: *void, n: size) void = mprotect(seg, n, PROT_READ | PROT_WRITE); + +// Marks a segment as executable and drops the write bit. +fn segexec(seg: *void, n: size) void = mprotect(seg, n, PROT_READ | PROT_EXEC); diff --git a/rt/+linux/syscalls.ha b/rt/+linux/syscalls.ha @@ -41,4 +41,66 @@ export fn kill(pid: int, signal: int) int = export fn pipe2(pipefd: *[2]int, flags: int) int = syscall2(SYS_pipe2, pipefd: uintptr: u64, flags: u64): int; +export def MAP_SHARED: uint = 0x01u; +export def MAP_PRIVATE: uint = 0x02u; +export def MAP_SHARED_VALIDATE: uint = 0x03u; +export def MAP_FIXED: uint = 0x10u; +export def MAP_ANON: uint = 0x20u; +export def MAP_NORESERVE: uint = 0x4000u; +export def MAP_GROWSDOWN: uint = 0x0100u; +export def MAP_DENYWRITE: uint = 0x0800u; +export def MAP_EXECUTABLE: uint = 0x1000u; +export def MAP_LOCKED: uint = 0x2000u; +export def MAP_POPULATE: uint = 0x8000u; +export def MAP_NONBLOCK: uint = 0x10000u; +export def MAP_STACK: uint = 0x20000u; +export def MAP_HUGETLB: uint = 0x40000u; +export def MAP_SYNC: uint = 0x80000u; +export def MAP_FIXED_NOREPLACE: uint = 0x100000u; +export def MAP_FILE: uint = 0u; +export def MAP_HUGE_SHIFT: uint = 26u; +export def MAP_HUGE_MASK: uint = 0x3fu; +export def MAP_HUGE_64KB: uint = 16u << 26u; +export def MAP_HUGE_512KB: uint = 19u << 26u; +export def MAP_HUGE_1MB: uint = 20u << 26u; +export def MAP_HUGE_2MB: uint = 21u << 26u; +export def MAP_HUGE_8MB: uint = 23u << 26u; +export def MAP_HUGE_16MB: uint = 24u << 26u; +export def MAP_HUGE_32MB: uint = 25u << 26u; +export def MAP_HUGE_256MB: uint = 28u << 26u; +export def MAP_HUGE_512MB: uint = 29u << 26u; +export def MAP_HUGE_1GB: uint = 30u << 26u; +export def MAP_HUGE_2GB: uint = 31u << 26u; +export def MAP_HUGE_16GB: uint = 34u << 26u; + +export def PROT_NONE: uint = 0u; +export def PROT_READ: uint = 1u; +export def PROT_WRITE: uint = 2u; +export def PROT_EXEC: uint = 4u; +export def PROT_GROWSDOWN: uint = 0x01000000u; +export def PROT_GROWSUP: uint = 0x02000000u; + +export fn mmap( + addr: nullable *void, + length: size, + prot: uint, + flags: uint, + fd: int, + offs: size +) *void = { + let r: u64 = syscall6(SYS_mmap, addr: uintptr: u64, + length: u64, prot: u64, flags: u64, fd: u64, offs: u64): u64; + // TODO: Type promotion + return if (r: int == -EPERM && addr: uintptr == null: uintptr + && (flags & MAP_ANON) > 0u && (flags & MAP_FIXED) == 0u) { + (-ENOMEM): uintptr: *void; // Fix up incorrect EPERM from kernel + } else r: uintptr: *void; +}; + +export fn munmap(addr: *void, length: size) int = + syscall2(SYS_munmap, addr: uintptr: u64, length: u64): int; + +export fn mprotect(addr: *void, length: size, prot: uint) int = + syscall3(SYS_mprotect, addr: uintptr: u64, length: u64, prot: u64): int; + export def SIGABRT: int = 6; diff --git a/rt/Makefile b/rt/Makefile @@ -1,6 +1,8 @@ libhart_srcs+=\ rt/abort.ha \ rt/compile.ha \ + rt/malloc.ha \ + rt/memcpy.ha \ rt/rtmain.ha libhart.a: harec $(libhart_srcs) $(libhart_objs) $(rtstart) diff --git a/rt/abort.ha b/rt/abort.ha @@ -7,9 +7,10 @@ export @noreturn @symbol("rt.abort") fn _abort(msg: str) void = { }; // See include/gen.h -const reasons: [2]str = [ +const reasons: [3]str = [ "slice or array access out of bounds", // 0 "type assertion failed", // 1 + "allocation failed", // 2 ]; export @noreturn fn abort_fixed(i: int) void = _abort(reasons[i]); diff --git a/rt/configure b/rt/configure @@ -9,6 +9,8 @@ rt() { rtstart=rt/+linux/start.o libhart_srcs=\ + rt/+linux/errno.ha \ + rt/+linux/segmalloc.ha \ rt/+linux/syscallno+x86_64.ha \ rt/+linux/syscalls.ha diff --git a/rt/malloc.ha b/rt/malloc.ha @@ -0,0 +1,179 @@ +// This is a simple memory allocator, based on Appel, Andrew W., and David A. +// Naumann. "Verified sequential malloc/free." It is not thread-safe. +// +// Large allocations are handled with mmap. +// +// For small allocations, we set up 50 bins, where each bin is responsible for +// 16 different allocation sizes (e.g. bin 1 handles allocations from 10 thru 26 +// bytes); except for the first and last bin, which are responsible for fewer +// than 16 allocation sizes. +// +// Each bin is 1MiB (BIGBLOCK) in size. We ceil the allocation size to the +// largest size supported for this bin, then break the bin up into smaller +// blocks. Each block is structured as [{sz: size, data..., link: *void}...]; +// where sz is the size of this (small) block, data is is set aside for the +// user's actual allocation, and link is a pointer to the next bin's data field. +// +// In short, a bin for a particular size is pre-filled with all allocations of +// that size, and the first word of each allocation is set to a pointer to the +// next allocation. As such, malloc becomes: +// +// 1. Look up bin; pre-fill if not already allocated +// 2. Let p = bin; bin = *bin; return p +// +// Then, free is simply: +// 1. Look up bin +// 2. *p = bin; +// 3. bin = p; +// +// Note that over time this can cause the ordering of the allocations in each +// bin to become non-continuous. This has no consequences for performance or +// correctness. + +// TODO: Type promotion +def ALIGN: size = 2z; +def WORD: size = size(size); +def WASTE: size = WORD * ALIGN - WORD; +def BIGBLOCK: size = (2z << 16z) * WORD; + +// TODO: Expandable arrays +let bins: [50]nullable *void = [ + null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, + null, null, +]; + +fn bin2size(b: size) size = ((b + 1z) * ALIGN - 1z) * WORD; + +fn size2bin(s: size) size = { + assert(s < bin2size(len(bins) - 1z), "Size exceeds maximum for bin"); + return (s + (WORD * (ALIGN - 1z) - 1z)) / (WORD * ALIGN); +}; + +// Allocates n bytes of memory and returns a pointer to them, or null if there +// is insufficient memory. +export fn malloc(n: size) nullable *void = { + assert(n > 0z); + let r: nullable *void = + if (n > bin2size(len(bins) - 1z)) malloc_large(n) + else malloc_small(n); + return r; +}; + +fn malloc_large(n: size) nullable *void = { + let p = segmalloc(n + WASTE + WORD); + if (p == null: nullable *void) { + return null; + }; + let bsize = (p: uintptr + WASTE: uintptr): *[1]size; + bsize[0] = n; + return (p: uintptr + WASTE: uintptr + WORD: uintptr): nullable *void; +}; + +fn malloc_small(n: size) nullable *void = { + const b = size2bin(n); + let p = bins[b]; + if (p == null: nullable *void) { + p = fill_bin(b); + if (p != null: nullable *void) { + bins[b] = p; + }; + }; + return if (p != null: nullable *void) { + let q = *(p: **void); + bins[b] = q; + p: nullable *void; + } else null: nullable *void; +}; + +fn fill_bin(b: size) nullable *void = { + const s = bin2size(b); + let p = segmalloc(BIGBLOCK); + return if (p == null: nullable *void) null: nullable *void + else list_from_block(s, p: uintptr); +}; + +fn list_from_block(s: size, p: uintptr) nullable *void = { + const nblocks = (BIGBLOCK - WASTE) / (s + WORD); + + let q = p + WASTE: uintptr; // align q+WORD + for (let j = 0z; j != nblocks - 1z; j += 1z) { + let sz = q: *size; + let useralloc = q + WORD: uintptr; // aligned + let next = (useralloc + s: uintptr + WORD: uintptr): *void; + *sz = s; + *(useralloc: **void) = next; + q += s: uintptr + WORD: uintptr; + }; + + // Terminate last block: + (q: *[1]size)[0] = s; + *((q + 1: uintptr): *nullable *void) = null; + + // Return first block: + return (p + WASTE: uintptr + WORD: uintptr): *void; +}; + +// Frees a pointer previously allocated with [malloc] or [must_malloc]. +export @symbol("rt.free") fn free_(_p: nullable *void) void = { + if (_p != null: nullable *void) { + let p = _p: *void; + let bsize = (p: uintptr - size(size): uintptr): *[1]size; + let s = bsize[0]; + if (s <= bin2size(len(bins) - 1z)) free_small(p, s) + else free_large(p, s); + }; +}; + +fn free_large(_p: *void, s: size) void = { + let p = (_p: uintptr - (WASTE: uintptr + WORD: uintptr)): *void; + let r = segfree(p, s + WASTE + WORD); + assert(r == 0, "free: munmap failed"); +}; + +fn free_small(p: *void, s: size) void = { + let b = size2bin(s); + let q = bins[b]; + *(p: **void) = q; + bins[b] = p: nullable *void; +}; + +// Changes the allocation size of a pointer to n bytes. If n is smaller than +// the prior allocation, it is truncated; otherwise the allocation is expanded +// and the values of the new bytes are undefined. May return a different pointer +// than the one given if there is insufficient space to expand the pointer +// in-place. Returns null if there is insufficient memory to support the +// request. +export fn realloc(_p: nullable *void, n: size) nullable *void = { + assert(n > 0z); + if (_p == null: nullable *void) { + return malloc(n); + }; + + let p = _p: *void; + let bsize = (p: uintptr - size(size): uintptr): *size; + let s = *bsize; + if (s < n) { + return p; + }; + + if (n < bin2size(len(bins) - 1z) && size2bin(n) == size2bin(s)) { + return p; + }; + + let new = malloc(n); + if (new != null: nullable *void) { + memcpy(new: *void, p, s); + }; + + return new; +}; + +// Like [realloc], but aborts the program if there is insufficient memory. +export fn must_realloc(p: nullable *void, n: size) *void = { + let new = realloc(p, n); + assert(new != null: nullable *void, "out of memory"); + return new: *void; +}; diff --git a/rt/memcpy.ha b/rt/memcpy.ha @@ -0,0 +1,6 @@ +export fn memcpy(dest: *void, src: *void, amt: size) void = { + let a = dest: *[*]u8, b = src: *[*]u8; + for (let i = 0z; i < amt; i += 1z) { + a[i] = b[i]; + }; +};