mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-06-15 07:29:27 -04:00
[hle] support passing arguments to homebrew applications (#4013)
Homebrew (.NRO) program args support Adds a 'program_args' setting and delivers it to NRO homebrews via libnx's homebrew ABI. NROs previously had no way to receive CLI flags (e.g. NZ:P's -noglsl). Setting: program_args, Category::System, startup-only. Surfaced in Qt + Android Debug sections. NRO loader: builds a 4-entry 'ConfigEntry' table + argv at the data segment tail; prepends 'homebrew ' so user args land at argv[1]; scans for 'svc #7' to use as LR. Drops the stale NSO-style argdata append. KProcess: stores loader-provided guest addresses; 'Run' switches to the homebrew entry (x0=config_ptr, x1=-1, lr=svc7) and patches the real MainThreadHandle into the ConfigEntry slot. Legacy NSO path unchanged. Tested on NZP — args reach 'argv' correctly. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4013 Reviewed-by: Lizzie <lizzie@eden-emu.dev> Reviewed-by: MaranBr <maranbr@eden-emu.dev>
This commit is contained in:
parent
026974211e
commit
78a1cd0533
9 changed files with 137 additions and 19 deletions
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
|
|
@ -11,6 +11,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
|
|||
enum class StringSetting(override val key: String) : AbstractStringSetting {
|
||||
DRIVER_PATH("driver_path"),
|
||||
DEVICE_NAME("device_name"),
|
||||
PROGRAM_ARGS("program_args"),
|
||||
|
||||
WEB_TOKEN("eden_token"),
|
||||
WEB_USERNAME("eden_username")
|
||||
|
|
|
|||
|
|
@ -125,6 +125,13 @@ abstract class SettingsItem(
|
|||
// List of all general
|
||||
val settingsItems = HashMap<String, SettingsItem>().apply {
|
||||
put(StringInputSetting(StringSetting.DEVICE_NAME, titleId = R.string.device_name))
|
||||
put(
|
||||
StringInputSetting(
|
||||
StringSetting.PROGRAM_ARGS,
|
||||
titleId = R.string.program_args,
|
||||
descriptionId = R.string.program_args_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
|
||||
|
|
|
|||
|
|
@ -1286,6 +1286,7 @@ class SettingsFragmentPresenter(
|
|||
add(HeaderSetting(R.string.general))
|
||||
|
||||
add(ShortSetting.DEBUG_KNOBS.key)
|
||||
add(StringSetting.PROGRAM_ARGS.key)
|
||||
|
||||
add(HeaderSetting(R.string.gpu_logging_header))
|
||||
add(BooleanSetting.GPU_LOGGING_ENABLED.key)
|
||||
|
|
|
|||
|
|
@ -434,6 +434,9 @@
|
|||
<string name="cpu_accuracy">CPU accuracy</string>
|
||||
<string name="value_with_units">%1$s%2$s</string>
|
||||
|
||||
<string name="program_args">Homebrew Args</string>
|
||||
<string name="program_args_description">Command-line arguments passed to homebrew at launch (e.g. -noglsl).</string>
|
||||
|
||||
<!-- System settings strings -->
|
||||
<string name="device_name">Device name</string>
|
||||
<string name="use_docked_mode">Docked Mode</string>
|
||||
|
|
|
|||
|
|
@ -779,7 +779,13 @@ struct Values {
|
|||
bool record_frame_times;
|
||||
Setting<bool> use_gdbstub{linkage, false, "use_gdbstub", Category::Debugging};
|
||||
Setting<u16> gdbstub_port{linkage, 6543, "gdbstub_port", Category::Debugging};
|
||||
Setting<std::string> program_args{linkage, std::string(), "program_args", Category::Debugging};
|
||||
SwitchableSetting<std::string> program_args{linkage,
|
||||
std::string(),
|
||||
"program_args",
|
||||
Category::System,
|
||||
Specialization::Default,
|
||||
true, // save_ — persist in config file
|
||||
false}; // runtime_modifiable_ — startup-only
|
||||
Setting<bool> dump_exefs{linkage, false, "dump_exefs", Category::Debugging};
|
||||
Setting<bool> dump_nso{linkage, false, "dump_nso", Category::Debugging};
|
||||
Setting<bool> dump_shaders{
|
||||
|
|
|
|||
|
|
@ -211,6 +211,9 @@ Result KProcess::Initialize(const Svc::CreateProcessParameter& params, KResource
|
|||
m_version = params.version;
|
||||
m_program_id = params.program_id;
|
||||
m_code_address = params.code_address;
|
||||
m_arg_pointer = 0;
|
||||
m_arg_return_address = 0;
|
||||
m_main_thread_handle_addr = 0;
|
||||
m_code_size = params.code_num_pages * PageSize;
|
||||
m_is_application = True(params.flags & Svc::CreateProcessFlag::IsApplication);
|
||||
|
||||
|
|
@ -995,9 +998,27 @@ Result KProcess::Run(s32 priority, size_t stack_size) {
|
|||
Handle thread_handle;
|
||||
R_TRY(m_handle_table.Add(std::addressof(thread_handle), main_thread));
|
||||
|
||||
// Set the thread arguments.
|
||||
main_thread->GetContext().r[0] = 0;
|
||||
main_thread->GetContext().r[1] = thread_handle;
|
||||
// Set the thread arguments. Two distinct entry conventions:
|
||||
// * Kernel/NSO entry (no homebrew ABI): x0 = 0, x1 = thread_handle
|
||||
// * Homebrew/NRO ABI (loader set arg ptr): x0 = ConfigEntry ptr, x1 = -1ULL
|
||||
// libnx's switch_crt0.s tests `x0==0 || x1==0xFFFFFFFFFFFFFFFF` to take
|
||||
// its normal init path; any other combination is interpreted as a user
|
||||
// exception handler entry.
|
||||
if (GetInteger(m_arg_pointer) != 0) {
|
||||
main_thread->GetContext().r[0] = GetInteger(m_arg_pointer);
|
||||
main_thread->GetContext().r[1] = UINT64_MAX;
|
||||
main_thread->GetContext().lr = GetInteger(m_arg_return_address);
|
||||
// Patch the MainThreadHandle entry in the ConfigEntry table now that
|
||||
// the actual handle exists. libnx stores this verbatim and uses it
|
||||
// for thread-control SVCs later; a pseudo-handle wouldn't survive
|
||||
// svcCloseHandle on exit.
|
||||
if (GetInteger(m_main_thread_handle_addr) != 0) {
|
||||
this->GetMemory().Write32(m_main_thread_handle_addr, thread_handle);
|
||||
}
|
||||
} else {
|
||||
main_thread->GetContext().r[0] = 0;
|
||||
main_thread->GetContext().r[1] = thread_handle;
|
||||
}
|
||||
|
||||
// Pass the thread handle to the thread local region.
|
||||
this->GetMemory().Write32(GetInteger(main_thread->GetTlsAddress()) + 0x110, thread_handle);
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ private:
|
|||
Core::Memory::Memory m_memory;
|
||||
KCapabilities m_capabilities{};
|
||||
KProcessAddress m_code_address{};
|
||||
KProcessAddress m_arg_pointer{};
|
||||
KProcessAddress m_arg_return_address{};
|
||||
KProcessAddress m_main_thread_handle_addr{};
|
||||
KHandleTable m_handle_table;
|
||||
KProcessAddress m_plr_address{};
|
||||
ThreadList m_thread_list{};
|
||||
|
|
@ -219,6 +222,16 @@ public:
|
|||
return m_code_address;
|
||||
}
|
||||
|
||||
void SetArgPointer(KProcessAddress addr) {
|
||||
m_arg_pointer = addr;
|
||||
}
|
||||
void SetArgReturnAddress(KProcessAddress addr) {
|
||||
m_arg_return_address = addr;
|
||||
}
|
||||
void SetMainThreadHandleAddr(KProcessAddress addr) {
|
||||
m_main_thread_handle_addr = addr;
|
||||
}
|
||||
|
||||
size_t GetMainStackSize() const {
|
||||
return m_main_thread_stack_size;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,15 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "common/alignment.h"
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging.h"
|
||||
|
|
@ -23,7 +29,6 @@
|
|||
#include "core/hle/kernel/k_thread.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
#include "core/loader/nro.h"
|
||||
#include "core/loader/nso.h"
|
||||
#include "core/memory.h"
|
||||
|
||||
#ifdef HAS_NCE
|
||||
|
|
@ -174,19 +179,6 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
|
|||
codeset.segments[i].size = PageAlignSize(nro_header.segments[i].size);
|
||||
}
|
||||
|
||||
if (!Settings::values.program_args.GetValue().empty()) {
|
||||
const auto arg_data = Settings::values.program_args.GetValue();
|
||||
codeset.DataSegment().size += NSO_ARGUMENT_DATA_ALLOCATION_SIZE;
|
||||
NSOArgumentHeader args_header{
|
||||
NSO_ARGUMENT_DATA_ALLOCATION_SIZE, static_cast<u32_le>(arg_data.size()), {}};
|
||||
const auto end_offset = program_image.size();
|
||||
program_image.resize(static_cast<u32>(program_image.size()) +
|
||||
NSO_ARGUMENT_DATA_ALLOCATION_SIZE);
|
||||
std::memcpy(program_image.data() + end_offset, &args_header, sizeof(NSOArgumentHeader));
|
||||
std::memcpy(program_image.data() + end_offset + sizeof(NSOArgumentHeader), arg_data.data(),
|
||||
arg_data.size());
|
||||
}
|
||||
|
||||
// Default .bss to NRO header bss size if MOD0 section doesn't exist
|
||||
u32 bss_size{PageAlignSize(nro_header.bss_size)};
|
||||
|
||||
|
|
@ -203,6 +195,47 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
|
|||
|
||||
codeset.DataSegment().size += bss_size;
|
||||
program_image.resize(static_cast<u32>(program_image.size()) + bss_size);
|
||||
struct ConfigEntry {
|
||||
u32_le key;
|
||||
u32_le flags;
|
||||
u64_le value[2];
|
||||
};
|
||||
static_assert(sizeof(ConfigEntry) == 0x18);
|
||||
// AArch64 encoding for svc #0x7 (ExitProcess).
|
||||
constexpr u32 kSvcExitProcessInstruction = 0xD40000E1;
|
||||
constexpr size_t kNumEntries = 4; // MainThreadHandle, AppletType, Argv, EndOfList
|
||||
constexpr size_t kConfigTableSize = kNumEntries * sizeof(ConfigEntry);
|
||||
std::string argv_string;
|
||||
size_t args_offset_in_image = 0;
|
||||
std::optional<size_t> exit_process_offset_in_image;
|
||||
const auto& program_args = Settings::values.program_args.GetValue();
|
||||
if (!program_args.empty()) {
|
||||
argv_string = "homebrew ";
|
||||
argv_string += program_args;
|
||||
argv_string.push_back('\0');
|
||||
|
||||
const auto& code = codeset.CodeSegment();
|
||||
const size_t code_end = (std::min)(program_image.size(), code.offset + code.size);
|
||||
for (size_t offset = code.offset; offset + sizeof(u32) <= code_end; offset += sizeof(u32)) {
|
||||
u32 instruction{};
|
||||
std::memcpy(&instruction, program_image.data() + offset, sizeof(instruction));
|
||||
if (instruction == kSvcExitProcessInstruction) {
|
||||
exit_process_offset_in_image = offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exit_process_offset_in_image) {
|
||||
LOG_WARNING(Loader,
|
||||
"Unable to find svcExitProcess in NRO; returning from main may fault");
|
||||
}
|
||||
|
||||
const size_t entries_and_argv =
|
||||
Common::AlignUp(kConfigTableSize + argv_string.size(), Core::Memory::YUZU_PAGESIZE);
|
||||
|
||||
args_offset_in_image = program_image.size();
|
||||
codeset.DataSegment().size += static_cast<u32>(entries_and_argv);
|
||||
program_image.resize(args_offset_in_image + entries_and_argv);
|
||||
}
|
||||
size_t image_size = program_image.size();
|
||||
|
||||
#ifdef HAS_NCE
|
||||
|
|
@ -264,6 +297,37 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process,
|
|||
// Load codeset for current process
|
||||
codeset.memory = std::move(program_image);
|
||||
process.LoadModule(std::move(codeset), process.GetEntryPoint());
|
||||
if (!argv_string.empty()) {
|
||||
constexpr u32 kEntryEndOfList = 0;
|
||||
constexpr u32 kEntryMainThreadHandle = 1;
|
||||
constexpr u32 kEntryArgv = 5;
|
||||
constexpr u32 kEntryAppletType = 7;
|
||||
constexpr u32 kAppletTypeApplication = 0;
|
||||
|
||||
const u64 base = GetInteger(process.GetEntryPoint());
|
||||
const u64 config_addr = base + args_offset_in_image;
|
||||
const u64 argv_addr = config_addr + kConfigTableSize;
|
||||
|
||||
const ConfigEntry entries[kNumEntries] = {
|
||||
{kEntryMainThreadHandle, 0, {0, 0}}, // Value[0] patched in Run()
|
||||
{kEntryAppletType, 0, {kAppletTypeApplication, 0}},
|
||||
{kEntryArgv, 0, {0, argv_addr}},
|
||||
{kEntryEndOfList, 0, {0, 0}},
|
||||
};
|
||||
process.GetMemory().WriteBlock(Common::ProcessAddress{config_addr}, entries,
|
||||
sizeof(entries));
|
||||
process.GetMemory().WriteBlock(Common::ProcessAddress{argv_addr},
|
||||
argv_string.data(), argv_string.size());
|
||||
|
||||
constexpr size_t kMainThreadHandleValueOffset = offsetof(ConfigEntry, value);
|
||||
process.SetArgPointer(Kernel::KProcessAddress{config_addr});
|
||||
if (exit_process_offset_in_image) {
|
||||
process.SetArgReturnAddress(
|
||||
Kernel::KProcessAddress{base + *exit_process_offset_in_image});
|
||||
}
|
||||
process.SetMainThreadHandleAddr(
|
||||
Kernel::KProcessAddress{config_addr + kMainThreadHandleValueOffset});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,6 +301,8 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
|
|||
tr("Controls the seed of the random number generator.\nMainly used for speedrunning."));
|
||||
INSERT(Settings, rng_seed_enabled, QString(), QString());
|
||||
INSERT(Settings, device_name, tr("Device Name"), tr("The name of the console."));
|
||||
INSERT(Settings, program_args, tr("Homebrew Args"),
|
||||
tr("Command-line arguments passed to homebrew at launch (e.g. -noglsl)."));
|
||||
INSERT(Settings, custom_rtc, tr("Custom RTC Date:"),
|
||||
tr("This option allows to change the clock of the console.\n"
|
||||
"Can be used to manipulate time in games."));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue