From 3a23ec06a42c28781fb451d598e2a178ea55182c Mon Sep 17 00:00:00 2001 From: Billy Laws Date: Thu, 25 Jun 2020 16:51:05 +0100 Subject: [PATCH] Implement NSO loader The NSO format is used by all retail games and some homebrew. It supports compressing sections with lz4 and dynamic linking through the use of rtld. --- .gitmodules | 3 + .idea/vcs.xml | 3 +- app/CMakeLists.txt | 5 +- app/src/main/cpp/skyline/kernel/memory.h | 2 + app/src/main/cpp/skyline/loader/loader.h | 1 + app/src/main/cpp/skyline/loader/nso.cpp | 65 +++++++++++++ app/src/main/cpp/skyline/loader/nso.h | 92 +++++++++++++++++++ app/src/main/cpp/skyline/os.cpp | 6 +- app/src/main/java/emu/skyline/MainActivity.kt | 7 +- .../main/java/emu/skyline/loader/RomFile.kt | 7 +- 10 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 app/src/main/cpp/skyline/loader/nso.cpp create mode 100644 app/src/main/cpp/skyline/loader/nso.h diff --git a/.gitmodules b/.gitmodules index 4b7ec23e..d07594aa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,3 +11,6 @@ [submodule "app/libraries/vkhpp"] path = app/libraries/vkhpp url = https://github.com/skyline-emu/vkhpp +[submodule "app/libraries/lz4"] + path = app/libraries/lz4 + url = https://github.com/lz4/lz4.git diff --git a/.idea/vcs.xml b/.idea/vcs.xml index c458539a..8bfaf805 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -3,8 +3,9 @@ + - \ No newline at end of file + diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 2a8c4bc6..0d2a00ab 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -18,6 +18,8 @@ set(CMAKE_POLICY_DEFAULT_CMP0048 OLD) add_subdirectory("libraries/tinyxml2") add_subdirectory("libraries/fmt") add_subdirectory("libraries/oboe") +add_subdirectory("libraries/lz4/contrib/cmake_unofficial") +include_directories("libraries/lz4/lib") include_directories("libraries/oboe/include") include_directories("libraries/vkhpp/include") set(CMAKE_POLICY_DEFAULT_CMP0048 NEW) @@ -40,6 +42,7 @@ add_library(skyline SHARED ${source_DIR}/skyline/os.cpp ${source_DIR}/skyline/loader/loader.cpp ${source_DIR}/skyline/loader/nro.cpp + ${source_DIR}/skyline/loader/nso.cpp ${source_DIR}/skyline/kernel/memory.cpp ${source_DIR}/skyline/kernel/ipc.cpp ${source_DIR}/skyline/kernel/svc.cpp @@ -101,5 +104,5 @@ add_library(skyline SHARED ${source_DIR}/skyline/vfs/nacp.cpp ) -target_link_libraries(skyline vulkan android fmt tinyxml2 oboe) +target_link_libraries(skyline vulkan android fmt tinyxml2 oboe lz4_static) target_compile_options(skyline PRIVATE -Wno-c++17-extensions) diff --git a/app/src/main/cpp/skyline/kernel/memory.h b/app/src/main/cpp/skyline/kernel/memory.h index 229fd087..3fb091cd 100644 --- a/app/src/main/cpp/skyline/kernel/memory.h +++ b/app/src/main/cpp/skyline/kernel/memory.h @@ -212,6 +212,7 @@ namespace skyline { namespace loader { class NroLoader; + class NsoLoader; } namespace kernel { @@ -322,6 +323,7 @@ namespace skyline { friend class type::KTransferMemory; friend class type::KProcess; friend class loader::NroLoader; + friend class loader::NsoLoader; friend void svc::SetMemoryAttribute(DeviceState &state); diff --git a/app/src/main/cpp/skyline/loader/loader.h b/app/src/main/cpp/skyline/loader/loader.h index 27b5bb81..7c3cb08c 100644 --- a/app/src/main/cpp/skyline/loader/loader.h +++ b/app/src/main/cpp/skyline/loader/loader.h @@ -14,6 +14,7 @@ namespace skyline::loader { */ enum class RomFormat { NRO, //!< The NRO format: https://switchbrew.org/wiki/NRO + NSO, //!< The NSO format: https://switchbrew.org/wiki/NSO XCI, //!< The XCI format: https://switchbrew.org/wiki/XCI NSP, //!< The NSP format from "nspwn" exploit: https://switchbrew.org/wiki/Switch_System_Flaws }; diff --git a/app/src/main/cpp/skyline/loader/nso.cpp b/app/src/main/cpp/skyline/loader/nso.cpp new file mode 100644 index 00000000..3f74c1f1 --- /dev/null +++ b/app/src/main/cpp/skyline/loader/nso.cpp @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#include +#include +#include +#include +#include "nso.h" + +namespace skyline::loader { + NsoLoader::NsoLoader(const std::shared_ptr &backing) : Loader(backing) { + u32 magic{}; + backing->Read(&magic); + + if (magic != util::MakeMagic("NSO0")) + throw exception("Invalid NSO magic! 0x{0:X}", magic); + } + + std::vector NsoLoader::GetSegment(const std::shared_ptr &backing, const NsoSegmentHeader &segment, u32 compressedSize) { + std::vector outputBuffer(segment.decompressedSize); + + if (compressedSize) { + std::vector compressedBuffer(compressedSize); + backing->Read(compressedBuffer.data(), segment.fileOffset, compressedSize); + + LZ4_decompress_safe(reinterpret_cast(compressedBuffer.data()), reinterpret_cast(outputBuffer.data()), compressedSize, segment.decompressedSize); + } else { + backing->Read(outputBuffer.data(), segment.fileOffset, segment.decompressedSize); + } + + return outputBuffer; + } + + Loader::ExecutableLoadInfo NsoLoader::LoadNso(const std::shared_ptr &backing, const std::shared_ptr process, const DeviceState &state, size_t offset) { + NsoHeader header{}; + backing->Read(&header); + + if (header.magic != util::MakeMagic("NSO0")) + throw exception("Invalid NSO magic! 0x{0:X}", header.magic); + + Executable nsoExecutable{}; + + nsoExecutable.text.contents = GetSegment(backing, header.text, header.flags.textCompressed ? header.textCompressedSize : 0); + nsoExecutable.text.contents.resize(util::AlignUp(nsoExecutable.text.contents.size(), PAGE_SIZE)); + nsoExecutable.text.offset = header.text.memoryOffset; + + nsoExecutable.ro.contents = GetSegment(backing, header.ro, header.flags.textCompressed ? header.textCompressedSize : 0); + nsoExecutable.ro.contents.resize(util::AlignUp(nsoExecutable.ro.contents.size(), PAGE_SIZE)); + nsoExecutable.ro.offset = header.ro.memoryOffset; + + nsoExecutable.data.contents = GetSegment(backing, header.data, header.flags.textCompressed ? header.textCompressedSize : 0); + nsoExecutable.data.contents.resize(util::AlignUp(nsoExecutable.data.contents.size(), PAGE_SIZE)); + nsoExecutable.data.offset = header.data.memoryOffset; + + nsoExecutable.bssSize = util::AlignUp(header.bssSize, PAGE_SIZE); + + return LoadExecutable(process, state, nsoExecutable, offset); + } + + void NsoLoader::LoadProcessData(const std::shared_ptr process, const DeviceState &state) { + auto loadInfo = LoadNso(backing, process, state); + + state.os->memory.InitializeRegions(loadInfo.base, loadInfo.size, memory::AddressSpaceType::AddressSpace39Bit); + } +} diff --git a/app/src/main/cpp/skyline/loader/nso.h b/app/src/main/cpp/skyline/loader/nso.h new file mode 100644 index 00000000..66f283b0 --- /dev/null +++ b/app/src/main/cpp/skyline/loader/nso.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#pragma once + +#include +#include "loader.h" + +namespace skyline::loader { + /** + * @brief The NsoLoader class abstracts access to an NSO file through the Loader interface (https://switchbrew.org/wiki/NSO) + */ + class NsoLoader : public Loader { + private: + union NsoFlags { + struct { + bool textCompressed : 1; //!< .text is compressed + bool roCompressed : 1; //!< .rodata is compressed + bool dataCompressed : 1; //!< .data is compressed + bool textHash : 1; //!< .text hash should be checked before loading + bool roHash : 1; //!< .rodata hash should be checked before loading + bool dataHash : 1; //!< .data hash should be checked before loading + }; + u32 raw; //!< The raw value of the flags + }; + static_assert(sizeof(NsoFlags) == 0x4); + + /** + * @brief This holds a single data segment's offset, loading offset and size + */ + struct NsoSegmentHeader { + u32 fileOffset; //!< The offset of the segment in the NSO + u32 memoryOffset; //!< The memory offset where the region should be loaded + u32 decompressedSize; //!< Size of the region after decompression + }; + static_assert(sizeof(NsoSegmentHeader) == 0xc); + + /** + * @brief This holds the header of an NSO file + */ + struct NsoHeader { + u32 magic; //!< The NSO magic "NSO0" + u32 version; //!< The version of the application + u32 _pad0_; + NsoFlags flags; //!< The flags used with the NSO + + NsoSegmentHeader text; //!< The .text segment header + u32 modOffset; //!< The offset of the MOD metadata + NsoSegmentHeader ro; //!< The .rodata segment header + u32 modSize; //!< The size of the MOD metadata + NsoSegmentHeader data; //!< The .data segment header + u32 bssSize; //!< The size of the .bss segment + + u64 buildId[4]; //!< The build ID of the NSO + + u32 textCompressedSize; //!< The size of the compressed .text segment + u32 roCompressedSize; //!< The size of the compressed .rodata segment + u32 dataCompressedSize; //!< The size of the compressed .data segment + + u32 _pad1_[7]; + + u64 apiInfo; //!< The .rodata-relative offset of .apiInfo + u64 dynstr; //!< The .rodata-relative offset of .dynstr + u64 dynsym; //!< The .rodata-relative offset of .dynsym + + u64 segmentHashes[3][4]; //!< The SHA256 checksums of the .text, .rodata and .data segments + }; + static_assert(sizeof(NsoHeader) == 0x100); + + /** + * @brief This reads the specified segment from the backing and decompresses it if needed + * @param segment The header of the segment to read + * @param compressedSize The compressed size of the segment, 0 if the segment is not compressed + * @return A buffer containing the data of the requested segment + */ + static std::vector GetSegment(const std::shared_ptr &backing, const NsoSegmentHeader &segment, u32 compressedSize); + + public: + NsoLoader(const std::shared_ptr &backing); + + /** + * @brief This loads an NSO into memory, offset by the given amount + * @param backing The backing of the NSO + * @param process The process to load the NSO into + * @param offset The offset from the base address to place the NSO + * @return An ExecutableLoadInfo struct containing the load base and size + */ + static ExecutableLoadInfo LoadNso(const std::shared_ptr &backing, const std::shared_ptr process, const DeviceState &state, size_t offset = 0); + + void LoadProcessData(const std::shared_ptr process, const DeviceState &state); + }; +} diff --git a/app/src/main/cpp/skyline/os.cpp b/app/src/main/cpp/skyline/os.cpp index 045a768b..d6916a5c 100644 --- a/app/src/main/cpp/skyline/os.cpp +++ b/app/src/main/cpp/skyline/os.cpp @@ -3,6 +3,7 @@ #include "vfs/os_backing.h" #include "loader/nro.h" +#include "loader/nso.h" #include "nce/guest.h" #include "os.h" @@ -14,8 +15,11 @@ namespace skyline::kernel { if (romType == loader::RomFormat::NRO) { state.loader = std::make_shared(romFile); - } else + } else if (romType == loader::RomFormat::NSO) { + state.loader = std::make_shared(romFile); + } else { throw exception("Unsupported ROM extension."); + } auto process = CreateProcess(constant::BaseAddress, 0, constant::DefStackSize); state.loader->LoadProcessData(process, state); diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt index d19d19e5..0784da0c 100644 --- a/app/src/main/java/emu/skyline/MainActivity.kt +++ b/app/src/main/java/emu/skyline/MainActivity.kt @@ -78,7 +78,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick } } - romFd.close(); + romFd.close() } } } @@ -111,10 +111,11 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick try { runOnUiThread { adapter.clear() } - val foundNros = addEntries("nro", RomFormat.NRO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) + var foundRoms = addEntries("nro", RomFormat.NRO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) + foundRoms = foundRoms or addEntries("nso", RomFormat.NSO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) runOnUiThread { - if (!foundNros) + if (!foundRoms) adapter.addHeader(getString(R.string.no_rom)) try { diff --git a/app/src/main/java/emu/skyline/loader/RomFile.kt b/app/src/main/java/emu/skyline/loader/RomFile.kt index d27189b5..7ad9d920 100644 --- a/app/src/main/java/emu/skyline/loader/RomFile.kt +++ b/app/src/main/java/emu/skyline/loader/RomFile.kt @@ -24,8 +24,9 @@ import java.util.* */ enum class RomFormat(val format: Int){ NRO(0), - XCI(1), - NSP(2), + NSO(1), + XCI(2), + NSP(3), } /** @@ -172,7 +173,7 @@ internal class RomFile(val context : Context, val format : RomFormat, val file : private external fun destroy(instance : Long) /** - * This is used to get the [AppEntry] for the specified NRO + * This is used to get the [AppEntry] for the specified ROM */ fun getAppEntry(uri : Uri) : AppEntry { return if (hasAssets(instance)) {