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.
This commit is contained in:
Billy Laws 2020-06-25 16:51:05 +01:00 committed by ◱ PixelyIon
parent bf46293fc7
commit 3a23ec06a4
10 changed files with 182 additions and 9 deletions

3
.gitmodules vendored
View File

@ -11,3 +11,6 @@
[submodule "app/libraries/vkhpp"] [submodule "app/libraries/vkhpp"]
path = app/libraries/vkhpp path = app/libraries/vkhpp
url = https://github.com/skyline-emu/vkhpp url = https://github.com/skyline-emu/vkhpp
[submodule "app/libraries/lz4"]
path = app/libraries/lz4
url = https://github.com/lz4/lz4.git

View File

@ -3,6 +3,7 @@
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/libraries/fmt" vcs="Git" /> <mapping directory="$PROJECT_DIR$/app/libraries/fmt" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/libraries/lz4" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/libraries/oboe" vcs="Git" /> <mapping directory="$PROJECT_DIR$/app/libraries/oboe" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/libraries/tinyxml2" vcs="Git" /> <mapping directory="$PROJECT_DIR$/app/libraries/tinyxml2" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/libraries/vkhpp" vcs="Git" /> <mapping directory="$PROJECT_DIR$/app/libraries/vkhpp" vcs="Git" />

View File

@ -18,6 +18,8 @@ set(CMAKE_POLICY_DEFAULT_CMP0048 OLD)
add_subdirectory("libraries/tinyxml2") add_subdirectory("libraries/tinyxml2")
add_subdirectory("libraries/fmt") add_subdirectory("libraries/fmt")
add_subdirectory("libraries/oboe") add_subdirectory("libraries/oboe")
add_subdirectory("libraries/lz4/contrib/cmake_unofficial")
include_directories("libraries/lz4/lib")
include_directories("libraries/oboe/include") include_directories("libraries/oboe/include")
include_directories("libraries/vkhpp/include") include_directories("libraries/vkhpp/include")
set(CMAKE_POLICY_DEFAULT_CMP0048 NEW) set(CMAKE_POLICY_DEFAULT_CMP0048 NEW)
@ -40,6 +42,7 @@ add_library(skyline SHARED
${source_DIR}/skyline/os.cpp ${source_DIR}/skyline/os.cpp
${source_DIR}/skyline/loader/loader.cpp ${source_DIR}/skyline/loader/loader.cpp
${source_DIR}/skyline/loader/nro.cpp ${source_DIR}/skyline/loader/nro.cpp
${source_DIR}/skyline/loader/nso.cpp
${source_DIR}/skyline/kernel/memory.cpp ${source_DIR}/skyline/kernel/memory.cpp
${source_DIR}/skyline/kernel/ipc.cpp ${source_DIR}/skyline/kernel/ipc.cpp
${source_DIR}/skyline/kernel/svc.cpp ${source_DIR}/skyline/kernel/svc.cpp
@ -101,5 +104,5 @@ add_library(skyline SHARED
${source_DIR}/skyline/vfs/nacp.cpp ${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) target_compile_options(skyline PRIVATE -Wno-c++17-extensions)

View File

@ -212,6 +212,7 @@ namespace skyline {
namespace loader { namespace loader {
class NroLoader; class NroLoader;
class NsoLoader;
} }
namespace kernel { namespace kernel {
@ -322,6 +323,7 @@ namespace skyline {
friend class type::KTransferMemory; friend class type::KTransferMemory;
friend class type::KProcess; friend class type::KProcess;
friend class loader::NroLoader; friend class loader::NroLoader;
friend class loader::NsoLoader;
friend void svc::SetMemoryAttribute(DeviceState &state); friend void svc::SetMemoryAttribute(DeviceState &state);

View File

@ -14,6 +14,7 @@ namespace skyline::loader {
*/ */
enum class RomFormat { enum class RomFormat {
NRO, //!< The NRO format: https://switchbrew.org/wiki/NRO 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 XCI, //!< The XCI format: https://switchbrew.org/wiki/XCI
NSP, //!< The NSP format from "nspwn" exploit: https://switchbrew.org/wiki/Switch_System_Flaws NSP, //!< The NSP format from "nspwn" exploit: https://switchbrew.org/wiki/Switch_System_Flaws
}; };

View File

@ -0,0 +1,65 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#include <lz4.h>
#include <nce.h>
#include <os.h>
#include <kernel/memory.h>
#include "nso.h"
namespace skyline::loader {
NsoLoader::NsoLoader(const std::shared_ptr<vfs::Backing> &backing) : Loader(backing) {
u32 magic{};
backing->Read(&magic);
if (magic != util::MakeMagic<u32>("NSO0"))
throw exception("Invalid NSO magic! 0x{0:X}", magic);
}
std::vector<u8> NsoLoader::GetSegment(const std::shared_ptr<vfs::Backing> &backing, const NsoSegmentHeader &segment, u32 compressedSize) {
std::vector<u8> outputBuffer(segment.decompressedSize);
if (compressedSize) {
std::vector<u8> compressedBuffer(compressedSize);
backing->Read(compressedBuffer.data(), segment.fileOffset, compressedSize);
LZ4_decompress_safe(reinterpret_cast<char *>(compressedBuffer.data()), reinterpret_cast<char *>(outputBuffer.data()), compressedSize, segment.decompressedSize);
} else {
backing->Read(outputBuffer.data(), segment.fileOffset, segment.decompressedSize);
}
return outputBuffer;
}
Loader::ExecutableLoadInfo NsoLoader::LoadNso(const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state, size_t offset) {
NsoHeader header{};
backing->Read(&header);
if (header.magic != util::MakeMagic<u32>("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<kernel::type::KProcess> process, const DeviceState &state) {
auto loadInfo = LoadNso(backing, process, state);
state.os->memory.InitializeRegions(loadInfo.base, loadInfo.size, memory::AddressSpaceType::AddressSpace39Bit);
}
}

View File

@ -0,0 +1,92 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#pragma once
#include <common.h>
#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<u8> GetSegment(const std::shared_ptr<vfs::Backing> &backing, const NsoSegmentHeader &segment, u32 compressedSize);
public:
NsoLoader(const std::shared_ptr<vfs::Backing> &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<vfs::Backing> &backing, const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state, size_t offset = 0);
void LoadProcessData(const std::shared_ptr<kernel::type::KProcess> process, const DeviceState &state);
};
}

View File

@ -3,6 +3,7 @@
#include "vfs/os_backing.h" #include "vfs/os_backing.h"
#include "loader/nro.h" #include "loader/nro.h"
#include "loader/nso.h"
#include "nce/guest.h" #include "nce/guest.h"
#include "os.h" #include "os.h"
@ -14,8 +15,11 @@ namespace skyline::kernel {
if (romType == loader::RomFormat::NRO) { if (romType == loader::RomFormat::NRO) {
state.loader = std::make_shared<loader::NroLoader>(romFile); state.loader = std::make_shared<loader::NroLoader>(romFile);
} else } else if (romType == loader::RomFormat::NSO) {
state.loader = std::make_shared<loader::NsoLoader>(romFile);
} else {
throw exception("Unsupported ROM extension."); throw exception("Unsupported ROM extension.");
}
auto process = CreateProcess(constant::BaseAddress, 0, constant::DefStackSize); auto process = CreateProcess(constant::BaseAddress, 0, constant::DefStackSize);
state.loader->LoadProcessData(process, state); state.loader->LoadProcessData(process, state);

View File

@ -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 { try {
runOnUiThread { adapter.clear() } 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 { runOnUiThread {
if (!foundNros) if (!foundRoms)
adapter.addHeader(getString(R.string.no_rom)) adapter.addHeader(getString(R.string.no_rom))
try { try {

View File

@ -24,8 +24,9 @@ import java.util.*
*/ */
enum class RomFormat(val format: Int){ enum class RomFormat(val format: Int){
NRO(0), NRO(0),
XCI(1), NSO(1),
NSP(2), 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) 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 { fun getAppEntry(uri : Uri) : AppEntry {
return if (hasAssets(instance)) { return if (hasAssets(instance)) {