From 8d98acc6f74dea0733f76886b5a174b62c1a72b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Thu, 23 Apr 2026 21:05:11 -0700 Subject: [PATCH] Added more functionality to memoryMap/Unmap. (#384) --- include/bx/os.h | 120 ++++++++++++++++++++- src/os.cpp | 262 +++++++++++++++++++++++++++++++++++++++++----- tests/os_test.cpp | 101 +++++++++++++++++- 3 files changed, 449 insertions(+), 34 deletions(-) diff --git a/include/bx/os.h b/include/bx/os.h index e385660..15d45b2 100644 --- a/include/bx/os.h +++ b/include/bx/os.h @@ -21,54 +21,168 @@ BX_ERROR_RESULT(kErrorMemoryUnmapFailed, BX_MAKEFOURCC('b', 'x', '8', '1') ); namespace bx { + /// Virtual memory flags used by `memoryMap`, `memoryUnmap`, and `memoryPageSize`. + /// Flags are grouped into mutually exclusive fields (State, Protection, Page, Advise). + /// Zero in a field means "no change" to that aspect of an existing region. + struct Memory + { + enum Enum : uint32_t + { + // State (bits 0..3) - zero = no change to existing region. + Reserve = 0x00000001, //!< Reserve address range (no backing). + Commit = 0x00000003, //!< Reserve + back with physical pages. + Decommit = 0x00000004, //!< Drop physical pages (keep reservation). + StateMask = 0x0000000f, + + // Protection (bits 4..7) - zero = no change to existing region. Mutually exclusive values. + ProtectNone = 0x00000010, //!< No access. + ProtectRead = 0x00000020, //!< Read-only. + ProtectReadWrite = 0x00000030, //!< Read/Write. + ProtectReadExec = 0x00000040, //!< Read/Execute. + ProtectMask = 0x000000f0, + + // Page size (bits 8..11) - zero = default system page size. Applied only on fresh reservation. + PageLarge = 0x00000100, //!< Large/huge pages (~2 MiB). + PageHuge = 0x00000200, //!< Gigantic pages (~1 GiB) where supported. + PageMask = 0x00000f00, + + // Access advice (bits 12..15) - zero = no change. Mutually exclusive values. + Normal = 0x00001000, //!< Reset access pattern to default. + SequentialAccess = 0x00002000, //!< Hint: memory is accessed sequentially. + RandomAccess = 0x00003000, //!< Hint: memory is accessed randomly. + Prefetch = 0x00004000, //!< Fault-in / prefault pages. + DontNeed = 0x00005000, //!< Can drop pages opportunistically. + AdviseMask = 0x0000f000, + + ReadWrite = Commit | ProtectReadWrite, //!< Reserve + commit + read/write. + }; + }; + + /// Suspend calling thread for at least `_ms` milliseconds. + /// + /// @param[in] _ms Minimum sleep duration, in milliseconds. /// void sleep(uint32_t _ms); - /// + /// Yield remainder of the current thread's time slice to another runnable thread. void yield(); + /// Get the OS-level identifier of the calling thread. + /// + /// @returns Platform-specific thread id. /// uint32_t getTid(); + /// Get resident memory used by the current process, in bytes. + /// + /// @returns Process resident memory footprint in bytes, or 0 if unavailable. /// size_t getProcessMemoryUsed(); + /// Open a dynamic library. + /// + /// @param[in] _filePath Path to the library (extension may be platform-specific, see `BX_DL_EXT`). + /// @returns Handle to the loaded library, or NULL on failure. /// void* dlopen(const FilePath& _filePath); + /// Close a dynamic library previously opened with `dlopen`. + /// + /// @param[in] _handle Handle returned by `dlopen`. /// void dlclose(void* _handle); + /// Look up a symbol in a dynamic library. + /// + /// @param[in] _handle Handle returned by `dlopen`. + /// @param[in] _symbol Symbol name to resolve. + /// @returns Address of the symbol, or NULL if not found. /// void* dlsym(void* _handle, const StringView& _symbol); + /// Look up a typed symbol in a dynamic library. + /// + /// @param[in] _handle Handle returned by `dlopen`. + /// @param[in] _symbol Symbol name to resolve. + /// @returns Address of the symbol cast to `ProtoT`, or NULL if not found. /// template ProtoT dlsym(void* _handle, const StringView& _symbol); + /// Read an environment variable. + /// + /// @param[out] _out Buffer that receives the value (may be NULL to query required size). + /// @param[in,out] _inOutSize On input the capacity of `_out`, on output the required size including terminator. + /// @param[in] _name Name of the environment variable. + /// @returns true if the variable exists and its value fits in `_out`. /// bool getEnv(char* _out, uint32_t* _inOutSize, const StringView& _name); + /// Set an environment variable. + /// + /// @param[in] _name Name of the environment variable. + /// @param[in] _value Value to assign. /// void setEnv(const StringView& _name, const StringView& _value); + /// Change current working directory of the process. + /// + /// @param[in] _path Target directory path. + /// @returns 0 on success, non-zero on failure. /// int chdir(const char* _path); + /// Execute a program in a child process. + /// + /// @param[in] _argv NULL-terminated argument vector; `_argv[0]` is the executable path. + /// @returns Platform-specific process handle, or NULL on failure. /// void* exec(const char* const* _argv); + /// Terminate the current process. + /// + /// @param[in] _exitCode Exit code returned to the OS. + /// @param[in] _cleanup When true, run cleanup (atexit handlers, etc.); when false, exit immediately. /// [[noreturn]] void exit(int32_t _exitCode, bool _cleanup = true); + /// Map, reconfigure, or reshape a virtual memory region. /// - void* memoryMap(void* _address, size_t _size, Error* _err); + /// - If `_address` is NULL: reserves a new region of `_size`, aligned to `_alignment` + /// (0 = page size), and applies the requested state/protect/advise flags. Flags + /// must include `Memory::Reserve`. + /// - If `_address` is non-NULL: reconfigures the existing region according to the + /// flags present. Each flag group (State / Protection / Advise) is only touched + /// when its bits are non-zero, allowing targeted changes. + /// + /// Page-size bits select which page size to use for a fresh reservation; they + /// are ignored for in-place reconfiguration of an existing region. + /// + /// @param[in] _address Existing base, or NULL to allocate a new region. + /// @param[in] _size Size in bytes, rounded up to the selected page size. + /// @param[in] _alignment Base alignment in bytes, 0 = page size. + /// @param[in] _flags OR-combination of `Memory::Enum` values. + /// @param[out] _err Error state. + /// @returns Base address of the region (same as `_address` for reconfiguration), or NULL on failure. + /// + void* memoryMap(void* _address, size_t _size, size_t _alignment, uint32_t _flags, Error* _err); + /// Release a region previously returned by `memoryMap`. Pages are decommitted and + /// the reservation is returned to the OS. + /// + /// @param[in] _address Base address returned by `memoryMap`. + /// @param[in] _size Size in bytes of the region to release. + /// @param[out] _err Error state. /// void memoryUnmap(void* _address, size_t _size, Error* _err); + /// Query virtual memory page size. /// - size_t memoryPageSize(); + /// @param[in] _flags Pass `Memory::PageLarge` / `Memory::PageHuge` to query large/huge + /// page sizes; 0 returns the default system page size. + /// @returns Page size in bytes, or 0 when the requested page kind is unsupported. + /// + size_t memoryPageSize(uint32_t _flags = 0); } // namespace bx diff --git a/src/os.cpp b/src/os.cpp index 917bd68..9ccaf92 100644 --- a/src/os.cpp +++ b/src/os.cpp @@ -375,37 +375,228 @@ namespace bx #endif // BX_PLATFORM_* } - void* memoryMap(void* _address, size_t _size, Error* _err) + namespace + { +#if BX_PLATFORM_LINUX || BX_PLATFORM_OSX + static int32_t toPosixProt(uint32_t _protect) + { + switch (_protect) + { + case Memory::ProtectRead: return PROT_READ; + case Memory::ProtectReadWrite: return PROT_READ | PROT_WRITE; + case Memory::ProtectReadExec: return PROT_READ | PROT_EXEC; + case Memory::ProtectNone: // fall through + default: return PROT_NONE; + } + } + + static int32_t toPosixAdvice(uint32_t _advise) + { + switch (_advise) + { + case Memory::Normal: return MADV_NORMAL; + case Memory::SequentialAccess: return MADV_SEQUENTIAL; + case Memory::RandomAccess: return MADV_RANDOM; + case Memory::Prefetch: return MADV_WILLNEED; + case Memory::DontNeed: return MADV_DONTNEED; + default: return -1; + } + } +#elif BX_PLATFORM_WINDOWS + static DWORD toWinProtect(uint32_t _protect) + { + switch (_protect) + { + case Memory::ProtectRead: return PAGE_READONLY; + case Memory::ProtectReadWrite: return PAGE_READWRITE; + case Memory::ProtectReadExec: return PAGE_EXECUTE_READ; + case Memory::ProtectNone: // fall through + default: return PAGE_NOACCESS; + } + } +#endif // BX_PLATFORM_* + } // namespace + + void* memoryMap(void* _address, size_t _size, size_t _alignment, uint32_t _flags, Error* _err) { BX_ERROR_SCOPE(_err); + const uint32_t state = _flags & Memory::StateMask; + const uint32_t protect = _flags & Memory::ProtectMask; + const uint32_t advise = _flags & Memory::AdviseMask; + const uint32_t page = _flags & Memory::PageMask; + + const size_t pageSize = memoryPageSize(); + BX_ASSERT(_alignment <= pageSize, "Alignments greater than the page size are not implemented (requested %zu, page %zu).", _alignment, pageSize); + BX_UNUSED(_alignment, pageSize); + #if BX_PLATFORM_LINUX || BX_PLATFORM_OSX - constexpr int32_t flags = 0 - | MAP_ANON - | MAP_PRIVATE - ; - - void* result = mmap(_address, _size, PROT_READ | PROT_WRITE, flags, -1 /*fd*/, 0 /*offset*/); - - if (MAP_FAILED == result) + if (NULL == _address) { - BX_ERROR_SET( - _err - , kErrorMemoryMapFailed - , "kErrorMemoryMapFailed" - ); + if (0 == (state & Memory::Reserve) ) + { + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: Memory::Reserve required for fresh allocation."); + return NULL; + } - return NULL; + uint32_t effectiveProtect = protect; + if (0 == effectiveProtect) + { + effectiveProtect = (Memory::Commit == (state & Memory::Commit) ) ? Memory::ProtectReadWrite : Memory::ProtectNone; + } + + int32_t mmapFlags = MAP_ANON | MAP_PRIVATE; +# if BX_PLATFORM_LINUX + if (0 != (page & Memory::PageHuge) ) + { + mmapFlags |= MAP_HUGETLB; +# ifdef MAP_HUGE_1GB + mmapFlags |= MAP_HUGE_1GB; +# endif // MAP_HUGE_1GB + } + else if (0 != (page & Memory::PageLarge) ) + { + mmapFlags |= MAP_HUGETLB; +# ifdef MAP_HUGE_2MB + mmapFlags |= MAP_HUGE_2MB; +# endif // MAP_HUGE_2MB + } +# else + BX_UNUSED(page); +# endif // BX_PLATFORM_LINUX + + void* result = mmap(_address, _size, toPosixProt(effectiveProtect), mmapFlags, -1 /*fd*/, 0 /*offset*/); + + if (MAP_FAILED == result) + { + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: mmap failed."); + return NULL; + } + + if (0 != advise) + { + const int32_t adv = toPosixAdvice(advise); + if (-1 != adv) + { + madvise(result, _size, adv); + } + } + + return result; } - return result; -#elif BX_PLATFORM_WINDOWS - void* result = VirtualAlloc(_address, _size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + // Reconfigure existing region. + if (0 != (state & Memory::Decommit) ) + { + // Drop physical pages, then revoke access so use-after-free faults. + madvise(_address, _size, MADV_DONTNEED); + const int32_t prot = (0 != protect) ? toPosixProt(protect) : PROT_NONE; + if (0 != mprotect(_address, _size, prot) ) + { + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: mprotect (decommit) failed."); + return NULL; + } + } + else if (Memory::Commit == (state & Memory::Commit) ) + { + const uint32_t effectiveProtect = (0 != protect) ? protect : Memory::ProtectReadWrite; + if (0 != mprotect(_address, _size, toPosixProt(effectiveProtect) ) ) + { + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: mprotect (commit) failed."); + return NULL; + } + } + else if (0 != protect) + { + if (0 != mprotect(_address, _size, toPosixProt(protect) ) ) + { + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: mprotect failed."); + return NULL; + } + } - return result; + if (0 != advise) + { + const int32_t adv = toPosixAdvice(advise); + if (-1 != adv) + { + madvise(_address, _size, adv); + } + } + + return _address; +#elif BX_PLATFORM_WINDOWS + if (NULL == _address) + { + if (0 == (state & Memory::Reserve) ) + { + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: Memory::Reserve required for fresh allocation."); + return NULL; + } + + DWORD allocType = MEM_RESERVE; + DWORD winProtect = PAGE_NOACCESS; + + if (Memory::Commit == (state & Memory::Commit) ) + { + allocType |= MEM_COMMIT; + winProtect = toWinProtect( (0 != protect) ? protect : Memory::ProtectReadWrite); + } + else if (0 != protect) + { + winProtect = toWinProtect(protect); + } + + if (0 != (page & (Memory::PageLarge | Memory::PageHuge) ) ) + { + allocType |= MEM_LARGE_PAGES; + } + + void* result = VirtualAlloc(_address, _size, allocType, winProtect); + + if (NULL == result) + { + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: VirtualAlloc failed."); + return NULL; + } + + BX_UNUSED(advise); + return result; + } + + // Reconfigure existing region. + if (0 != (state & Memory::Decommit) ) + { + if (!VirtualFree(_address, _size, MEM_DECOMMIT) ) + { + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: VirtualFree(MEM_DECOMMIT) failed."); + return NULL; + } + } + else if (Memory::Commit == (state & Memory::Commit) ) + { + const DWORD winProtect = toWinProtect( (0 != protect) ? protect : Memory::ProtectReadWrite); + if (NULL == VirtualAlloc(_address, _size, MEM_COMMIT, winProtect) ) + { + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: VirtualAlloc(MEM_COMMIT) failed."); + return NULL; + } + } + else if (0 != protect) + { + DWORD oldProtect = 0; + if (!VirtualProtect(_address, _size, toWinProtect(protect), &oldProtect) ) + { + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: VirtualProtect failed."); + return NULL; + } + } + + BX_UNUSED(advise); + return _address; #else - BX_UNUSED(_address, _size); - BX_ERROR_SET(_err, kErrorMemoryMapFailed, "Not implemented!"); + BX_UNUSED(_address, _size, _alignment, _flags, state, protect, advise, page, pageSize); + BX_ERROR_SET(_err, kErrorMemoryMapFailed, "memoryMap: Not implemented!"); return NULL; #endif // BX_PLATFORM_* } @@ -426,7 +617,8 @@ namespace bx ); } #elif BX_PLATFORM_WINDOWS - if (!VirtualFree(_address, _size, MEM_RELEASE) ) + BX_UNUSED(_size); + if (!VirtualFree(_address, 0, MEM_RELEASE) ) { BX_ERROR_SET( _err @@ -440,21 +632,33 @@ namespace bx #endif // BX_PLATFORM_* } - size_t memoryPageSize() + size_t memoryPageSize(uint32_t _flags) { - size_t pageSize; + const uint32_t page = _flags & Memory::PageMask; + #if BX_PLATFORM_LINUX || BX_PLATFORM_OSX - pageSize = sysconf(_SC_PAGESIZE); + if (0 != (page & Memory::PageHuge) ) + { + return 1ull << 30; // 1 GiB nominal + } + if (0 != (page & Memory::PageLarge) ) + { + return 2ull << 20; // 2 MiB nominal + } + return sysconf(_SC_PAGESIZE); #elif BX_PLATFORM_WINDOWS + if (0 != (page & (Memory::PageLarge | Memory::PageHuge) ) ) + { + return ::GetLargePageMinimum(); + } SYSTEM_INFO si; memSet(&si, 0, sizeof(si) ); ::GetSystemInfo(&si); - pageSize = si.dwAllocationGranularity; + return si.dwAllocationGranularity; #else - pageSize = 16<<10; + BX_UNUSED(page); + return 16<<10; #endif // BX_PLATFORM_WINDOWS - - return pageSize; } } // namespace bx diff --git a/tests/os_test.cpp b/tests/os_test.cpp index 0e0a2d2..b6c913a 100644 --- a/tests/os_test.cpp +++ b/tests/os_test.cpp @@ -8,7 +8,7 @@ #include #include -TEST_CASE("getProcessMemoryUsed", "") +TEST_CASE("getProcessMemoryUsed", "[os]") { if (BX_ENABLED(BX_PLATFORM_EMSCRIPTEN) ) { @@ -20,7 +20,7 @@ TEST_CASE("getProcessMemoryUsed", "") #if BX_CONFIG_SUPPORTS_THREADING -TEST_CASE("semaphore_timeout", "") +TEST_CASE("semaphore_timeout", "[os]") { bx::Semaphore sem; @@ -34,3 +34,100 @@ TEST_CASE("semaphore_timeout", "") } #endif // BX_CONFIG_SUPPORTS_THREADING + +TEST_CASE("memoryPageSize", "[os]") +{ + const size_t pageSize = bx::memoryPageSize(); + REQUIRE(0 != pageSize); + // Page sizes are always powers of two on supported platforms. + REQUIRE(0 == (pageSize & (pageSize - 1) ) ); + + // Large/huge page queries must not return a value smaller than the base page size. + const size_t large = bx::memoryPageSize(bx::Memory::PageLarge); + const size_t huge = bx::memoryPageSize(bx::Memory::PageHuge); + REQUIRE( (0 == large || large >= pageSize) ); + REQUIRE( (0 == huge || huge >= pageSize) ); +} + +TEST_CASE("memoryMap-readWrite-roundtrip", "[os]") +{ + const size_t size = 4 * bx::memoryPageSize(); + + bx::Error err; + void* addr = bx::memoryMap(NULL, size, 0, bx::Memory::ReadWrite, &err); + REQUIRE(err.isOk() ); + REQUIRE(nullptr != addr); + + uint8_t* bytes = (uint8_t*)addr; + for (size_t ii = 0; ii < size; ++ii) + { + bytes[ii] = uint8_t(ii & 0xff); + } + for (size_t ii = 0; ii < size; ++ii) + { + REQUIRE(uint8_t(ii & 0xff) == bytes[ii]); + } + + bx::memoryUnmap(addr, size, &err); + REQUIRE(err.isOk() ); +} + +TEST_CASE("memoryMap-reserve-then-commit", "[os]") +{ + const size_t pageSize = bx::memoryPageSize(); + const size_t size = 8 * pageSize; + + bx::Error err; + void* addr = bx::memoryMap(NULL, size, 0, bx::Memory::Reserve | bx::Memory::ProtectNone, &err); + REQUIRE(err.isOk() ); + REQUIRE(nullptr != addr); + + // Commit one page inside the reservation and write to it. + uint8_t* bytes = (uint8_t*)addr; + bx::memoryMap(bytes + pageSize, pageSize, 0, bx::Memory::Commit | bx::Memory::ProtectReadWrite, &err); + REQUIRE(err.isOk() ); + + for (size_t ii = 0; ii < pageSize; ++ii) + { + bytes[pageSize + ii] = uint8_t( (ii * 7) & 0xff); + } + for (size_t ii = 0; ii < pageSize; ++ii) + { + REQUIRE(uint8_t( (ii * 7) & 0xff) == bytes[pageSize + ii]); + } + + // Decommit the page; reservation stays, but physical backing is dropped. + bx::memoryMap(bytes + pageSize, pageSize, 0, bx::Memory::Decommit | bx::Memory::ProtectNone, &err); + REQUIRE(err.isOk() ); + + // Release the whole reservation. + bx::memoryUnmap(addr, size, &err); + REQUIRE(err.isOk() ); +} + +TEST_CASE("memoryMap-protection-change", "[os]") +{ + const size_t size = bx::memoryPageSize(); + + bx::Error err; + void* addr = bx::memoryMap(NULL, size, 0, bx::Memory::ReadWrite, &err); + REQUIRE(err.isOk() ); + REQUIRE(nullptr != addr); + + uint8_t* bytes = (uint8_t*)addr; + bytes[0] = 0xab; + REQUIRE(0xab == bytes[0]); + + // Flip to read-only, then back to read/write. Neither call should fail. + bx::memoryMap(addr, size, 0, bx::Memory::ProtectRead, &err); + REQUIRE(err.isOk() ); + REQUIRE(0xab == bytes[0]); + + bx::memoryMap(addr, size, 0, bx::Memory::ProtectReadWrite, &err); + REQUIRE(err.isOk() ); + bytes[0] = 0xcd; + REQUIRE(0xcd == bytes[0]); + + bx::memoryUnmap(addr, size, &err); + REQUIRE(err.isOk() ); +}