/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2025 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include "xenia/base/memory.h" #include #include #include #include #include #include #include #include #include #include "xenia/base/math.h" #include "xenia/base/platform.h" #include "xenia/base/string.h" #if XE_PLATFORM_ANDROID #include #include #include #include #include "xenia/base/main_android.h" #endif namespace xe { namespace memory { #if XE_PLATFORM_ANDROID // May be null if no dynamically loaded functions are required. static void* libandroid_; // API 26+. static int (*android_ASharedMemory_create_)(const char* name, size_t size); void AndroidInitialize() { if (xe::GetAndroidApiLevel() >= 26) { libandroid_ = dlopen("libandroid.so", RTLD_NOW); assert_not_null(libandroid_); if (libandroid_) { android_ASharedMemory_create_ = reinterpret_cast( dlsym(libandroid_, "ASharedMemory_create")); assert_not_null(android_ASharedMemory_create_); } } } void AndroidShutdown() { android_ASharedMemory_create_ = nullptr; if (libandroid_) { dlclose(libandroid_); libandroid_ = nullptr; } } #endif size_t page_size() { return getpagesize(); } size_t allocation_granularity() { return page_size(); } uint32_t ToPosixProtectFlags(PageAccess access) { switch (access) { case PageAccess::kNoAccess: return PROT_NONE; case PageAccess::kReadOnly: return PROT_READ; case PageAccess::kReadWrite: return PROT_READ | PROT_WRITE; case PageAccess::kExecuteReadOnly: return PROT_READ | PROT_EXEC; case PageAccess::kExecuteReadWrite: return PROT_READ | PROT_WRITE | PROT_EXEC; default: assert_unhandled_case(access); return PROT_NONE; } } PageAccess ToXeniaProtectFlags(const char* protection) { if (protection[0] == 'r' && protection[1] == 'w' && protection[2] == 'x') { return PageAccess::kExecuteReadWrite; } if (protection[0] == 'r' && protection[1] == '-' && protection[2] == 'x') { return PageAccess::kExecuteReadOnly; } if (protection[0] == 'r' && protection[1] == 'w' && protection[2] == '-') { return PageAccess::kReadWrite; } if (protection[0] == 'r' && protection[1] == '-' && protection[2] == '-') { return PageAccess::kReadOnly; } return PageAccess::kNoAccess; } bool IsWritableExecutableMemorySupported() { return true; } struct MappedFileRange { uintptr_t region_begin; uintptr_t region_end; }; std::vector mapped_file_ranges; std::mutex g_mapped_file_ranges_mutex; // Track shm file names for cleanup on exit std::vector g_shm_file_names; std::mutex g_shm_file_names_mutex; static bool g_cleanup_handlers_installed = false; #if !XE_PLATFORM_ANDROID static void CleanupAtExit() { for (const auto& name : g_shm_file_names) { shm_unlink(name.c_str()); } } static void InstallCleanupHandlers() { if (g_cleanup_handlers_installed) { return; } g_cleanup_handlers_installed = true; std::atexit(CleanupAtExit); std::at_quick_exit(CleanupAtExit); } #endif // !XE_PLATFORM_ANDROID void* AllocFixed(void* base_address, size_t length, AllocationType allocation_type, PageAccess access) { // mmap does not support reserve / commit, so ignore allocation_type. uint32_t prot = ToPosixProtectFlags(access); int flags = MAP_PRIVATE | MAP_ANONYMOUS; if (base_address != nullptr) { if (allocation_type == AllocationType::kCommit) { if (Protect(base_address, length, access)) { return base_address; } return nullptr; } flags |= MAP_FIXED_NOREPLACE; } void* result = mmap(base_address, length, prot, flags, -1, 0); if (result != MAP_FAILED) { return result; } return nullptr; } bool DeallocFixed(void* base_address, size_t length, DeallocationType deallocation_type) { const auto region_begin = reinterpret_cast(base_address); const uintptr_t region_end = reinterpret_cast(base_address) + length; std::lock_guard guard(g_mapped_file_ranges_mutex); for (const auto& mapped_range : mapped_file_ranges) { if (region_begin >= mapped_range.region_begin && region_end <= mapped_range.region_end) { switch (deallocation_type) { case DeallocationType::kDecommit: return Protect(base_address, length, PageAccess::kNoAccess); case DeallocationType::kRelease: return false; default: assert_unhandled_case(deallocation_type); } } } switch (deallocation_type) { case DeallocationType::kDecommit: return Protect(base_address, length, PageAccess::kNoAccess); case DeallocationType::kRelease: return munmap(base_address, length) == 0; default: assert_unhandled_case(deallocation_type); } } bool Protect(void* base_address, size_t length, PageAccess access, PageAccess* out_old_access) { if (out_old_access) { size_t length_copy = length; QueryProtect(base_address, length_copy, *out_old_access); } uint32_t prot = ToPosixProtectFlags(access); return mprotect(base_address, length, prot) == 0; } bool QueryProtect(void* base_address, size_t& length, PageAccess& access_out) { // No generic POSIX solution exists. The Linux solution should work on all // Linux kernel based OS, including Android. std::ifstream memory_maps; memory_maps.open("/proc/self/maps", std::ios_base::in); std::string maps_entry_string; while (std::getline(memory_maps, maps_entry_string)) { std::stringstream entry_stream(maps_entry_string); uintptr_t map_region_begin, map_region_end; char separator; char protection[5]; // 4 chars (e.g., "r-xp") + null terminator entry_stream >> std::hex >> map_region_begin >> separator >> map_region_end >> protection; if (map_region_begin <= reinterpret_cast(base_address) && map_region_end > reinterpret_cast(base_address)) { length = map_region_end - reinterpret_cast(base_address); access_out = ToXeniaProtectFlags(protection); // Look at the next consecutive mappings while (std::getline(memory_maps, maps_entry_string)) { std::stringstream next_entry_stream(maps_entry_string); uintptr_t next_map_region_begin, next_map_region_end; char next_protection[5]; // 4 chars (e.g., "r-xp") + null terminator next_entry_stream >> std::hex >> next_map_region_begin >> separator >> next_map_region_end >> next_protection; if (map_region_end == next_map_region_begin && access_out == ToXeniaProtectFlags(next_protection)) { length = next_map_region_end - reinterpret_cast(base_address); continue; } break; } memory_maps.close(); return true; } } memory_maps.close(); return false; } FileMappingHandle CreateFileMappingHandle(const std::filesystem::path& path, size_t length, PageAccess access, bool commit) { #if XE_PLATFORM_ANDROID // TODO(Triang3l): Check if memfd can be used instead on API 30+. if (android_ASharedMemory_create_) { int sharedmem_fd = android_ASharedMemory_create_(path.c_str(), length); return sharedmem_fd >= 0 ? sharedmem_fd : kFileMappingHandleInvalid; } // Use /dev/ashmem on API versions below 26, which added ASharedMemory. // /dev/ashmem was disabled on API 29 for apps targeting it. // https://chromium.googlesource.com/chromium/src/+/master/third_party/ashmem/ashmem-dev.c int ashmem_fd = open("/" ASHMEM_NAME_DEF, O_RDWR); if (ashmem_fd < 0) { return kFileMappingHandleInvalid; } char ashmem_name[ASHMEM_NAME_LEN]; strlcpy(ashmem_name, path.c_str(), xe::countof(ashmem_name)); if (ioctl(ashmem_fd, ASHMEM_SET_NAME, ashmem_name) < 0 || ioctl(ashmem_fd, ASHMEM_SET_SIZE, length) < 0) { close(ashmem_fd); return kFileMappingHandleInvalid; } return ashmem_fd; #else int oflag; switch (access) { case PageAccess::kNoAccess: oflag = 0; break; case PageAccess::kReadOnly: case PageAccess::kExecuteReadOnly: oflag = O_RDONLY; break; case PageAccess::kReadWrite: case PageAccess::kExecuteReadWrite: oflag = O_RDWR; break; default: assert_always(); return kFileMappingHandleInvalid; } oflag |= O_CREAT; auto full_path = "/" / path; int ret = shm_open(full_path.c_str(), oflag, 0777); if (ret < 0) { return kFileMappingHandleInvalid; } if (ftruncate64(ret, length) < 0) { close(ret); shm_unlink(full_path.c_str()); return kFileMappingHandleInvalid; } // Track for cleanup on abnormal exit and install cleanup handlers { std::lock_guard guard(g_shm_file_names_mutex); g_shm_file_names.push_back(full_path.string()); } InstallCleanupHandlers(); return ret; #endif } void CloseFileMappingHandle(FileMappingHandle handle, const std::filesystem::path& path) { close(handle); #if !XE_PLATFORM_ANDROID auto full_path = "/" / path; shm_unlink(full_path.c_str()); // Remove from tracking { std::lock_guard guard(g_shm_file_names_mutex); auto it = std::find(g_shm_file_names.begin(), g_shm_file_names.end(), full_path.string()); if (it != g_shm_file_names.end()) { g_shm_file_names.erase(it); } } #endif } void* MapFileView(FileMappingHandle handle, void* base_address, size_t length, PageAccess access, size_t file_offset) { uint32_t prot = ToPosixProtectFlags(access); int flags = MAP_SHARED; if (base_address != nullptr) { flags |= MAP_FIXED_NOREPLACE; } void* result = mmap(base_address, length, prot, flags, handle, file_offset); if (result != MAP_FAILED) { std::lock_guard guard(g_mapped_file_ranges_mutex); mapped_file_ranges.push_back( {reinterpret_cast(result), reinterpret_cast(result) + length}); return result; } return nullptr; } bool UnmapFileView(FileMappingHandle handle, void* base_address, size_t length) { std::lock_guard guard(g_mapped_file_ranges_mutex); for (auto mapped_range = mapped_file_ranges.begin(); mapped_range != mapped_file_ranges.end();) { if (mapped_range->region_begin == reinterpret_cast(base_address) && mapped_range->region_end == reinterpret_cast(base_address) + length) { mapped_file_ranges.erase(mapped_range); return munmap(base_address, length) == 0; } ++mapped_range; } // TODO: Implement partial file unmapping. assert_always("Error: Partial unmapping of files not yet supported."); return munmap(base_address, length) == 0; } } // namespace memory } // namespace xe