☰
Eglantine 中的 HOS 内核:SVC 分发、地址空间与进程内核对象实现剖析
2026/11/2 11:08:15 网站建设 项目流程

【免费下载链接】Eglantine

VERY experimental Nintendo Switch 2 emulator

项目地址:https://gitcode.com/gh_mirrors/eg/Eglantine
点击查看免费下载

本文以仓库内的 HOS Kernel 子系统文档 为主线,讲清 Nintendo Switch(HOS)内核在 Eglantine 模拟器中是如何被"以 HLE 方式"重建的:svc #imm指令如何进入 C++ 处理函数、32/36/39 位地址空间如何区分、每个进程持有哪九类内核结构,以及内存红黑树、句柄表、线程本地页与调度器在多核模拟中的实际取舍。读完之后,你应当能够在源码中定位任意一条 SVC 的实现入口,并理解新增或修补一条 SVC 所需的完整链路。

一、文档给出的事实基线:HOS 是一个微内核

原文档开篇给出了几条后续所有实现都依赖的前提:

  • HOS 内核是微内核,所有服务与程序都运行在用户态;
  • 用户态之间的主要通信方式是HIPC(文档中明确说明不展开);
  • 其余内核原语大多以svc #imm系统调用的形式存在;
  • 内核同时支持32 位与 64 位程序,并可为被创建的进程使用32、36、39 位地址空间;
  • 网络栈大部分派生自 FreeBSD 的网络栈。

接着文档划定了模拟器的能力边界:模拟器实现了"与 HOS 内核本身相关的大部分 SVC",而这里所说的"HOS 内核"严格指系统调用的处理机制及其副作用(例如页表记账),Eglantine并不做 supervisor-level(EL1 及以上)级别的仿真,也无法加载一个自定义的底层内核。这一点与仓库的整体设计取向一致:设计概览 说明项目采用 HLE(High-Level Emulation),即模拟"可见的 I/O 接口"而非逐个子系统精确仿真。

由此得到一个重要的阅读视角:阅读src/core/hle/kernel/时不应该把它当成"一颗真实的内核在启动",而应理解成一组被系统调用触发、并维护内核对象状态的回调集合。

二、svc #imm如何被分发到处理函数

文档中"大多数 NX 程序最终都会执行一条svc,并根据其立即数被分发到某个具体的系统调用处理器"这句话,对应代码里的三个层次。

2.1 统一入口Kernel::Svc::Call

入口位于 src/core/hle/kernel/svc.cpp。它做四件事:

  1. 取当前进程与当前物理核,把 8 个通用参数保存到std::array<uint64_t, 8> args;
  2. 打印一条LOG_TRACE,随后按立即数分发;
  3. 通过SvcId(imm)的switch转到具体的SvcWrap_*包装函数;
  4. 处理完成后把结果写回寄存器上下文。

参数搬运依赖架构差异处理:src/core/hle/kernel/svc.cpp 中提供了GetArg32/SetArg32/GetArg64/SetArg64与一个能安全处理"源与目标宽度不同"的Convert<T>模板,并用大量static_assert锁死各枚举与结构的尺寸(例如Handle、Result、MemoryState均为 4 字节,lp64::LastThreadContext为 32 字节、ilp32::LastThreadContext为 16 字节)。这些断言是 ABI 正确性的第一道防线。

2.2 32 位与 64 位程序的两套派发表

Call根据process.Is64Bit()决定进入Call64还是Call32,两个函数各自维护一份switch:

void Call(Core::System& system, u32 imm) { auto& kernel = system.Kernel(); auto& process = GetCurrentProcess(kernel); std::array<uint64_t, 8> args; kernel.CurrentPhysicalCore().SaveSvcArguments(process, args); // ... if (imm == 0x80) { // Switch 2 专用:svcSetGpuMemoryAttribute } else if (process.Is64Bit()) { Call64(system, imm, args); } else { Call32(system, imm, args); } kernel.CurrentPhysicalCore().LoadSvcArguments(process, args); }

32 位路径使用SvcWrap_*64From32形式的包装器,这类包装器负责把 32 位程序的参数/返回值"投影"到统一的 64 位槽位上,再调用共享实现。这让大部分 SVC 的实现只要写一份,而由包装层承担位宽适配,正是svc_generator.py生成代码的核心思路。

Is64Bit()的实现很轻量,读取创建进程时记录的标志位(见 src/core/hle/kernel/k_process.h):

bool Is64Bit() const { return True(m_flags & Svc::CreateProcessFlag::Is64Bit); }

需要注意,32 位/64 位不等于地址空间宽度:Is64Bit只是CreateProcessFlag的最低比特,地址空间宽度由另一个字段决定(见第四节)。

2.3 未实现的 SVC 会直接触发断言

两个派发表的default分支都是:

default: UNREACHABLE_MSG("Unhandled SVC {:#x}!", imm);

也就是说,Eglantine 对"内核接口覆盖不全"的态度是显式失败,而不是静默返回成功。这一行为对排查"游戏在某条指令处崩溃"很有价值:日志里出现Unhandled SVC 0x..就意味着对应系统调用尚未实现。原文档也提醒过,usb:u、pcie这类功能位于专门服务中,目前尚未被模拟——这类缺失通常不会表现为 SVC 未实现,而是表现为服务端返回错误或桩实现。

2.4 代码是自动生成的:tools/svc_generator.py

svc.cpp与svc.h顶部都有明确注释:"This file is automatically generated using svc_generator.py. DO NOT MODIFY IT MANUALLY"。生成器 tools/svc_generator.py 内维护了一张 SVC 表,例如:

SVCS = [ [0x01, "Result SetHeapSize(Address* out_address, Size size);"], [0x02, "Result SetMemoryPermission(Address address, Size size, MemoryPermission perm);"], [0x03, "Result SetMemoryAttribute(Address address, Size size, uint32_t mask, uint32_t attr);"], [0x06, "Result QueryMemory(Address out_memory_info, PageInfo* out_page_info, Address address);"], [0x08, "Result CreateThread(Handle* out_handle, ThreadFunc func, Address arg, Address stack_bottom, int32_t priority, int32_t core_id);"], ... [0x45, "Result CreateEvent(Handle* out_write_handle, Handle* out_read_handle);"], ]

生成器内部实现了 ARM 过程调用标准意义上的寄存器分配器(reg_alloc、add_single、add),再由emit_gather/emit_scatter生成"从寄存器数组取值、把结果写回寄存器数组"的代码。这解释了为什么svc.cpp里会出现大量逐参数搬运的样板代码:它是被机器生成的 ABI 胶水层,而不是手写的业务逻辑。

实用结论:要新增或修改一条 SVC,应该改生成器的 SVC 表和对应的手写实现(src/core/hle/kernel/svc/下),再重新生成,而不是直接编辑svc.cpp。

2.5 用环境变量跟踪 SVC 时序

Call中还有一段调试设施,通过环境变量EDEN_NX2_SVC_TRACE控制:

  • EDEN_NX2_SVC_TRACE=1:记录每一条 SVC 及其入参与返回值;
  • EDEN_NX2_SVC_TRACE=<first>:<last>:只在第first到last个 present(帧)之间记录,用于帧级性能剖析。

日志会带上线程 ID,例如svc {:#04x} in t{} ...与svc {:#04x} out t{} ...。对于"某帧里发生了什么"这类问题,这比全量LOG_TRACE更可用,因为后者噪声极大。

三、SVC 版本号与"哪些 SVC 允许被调用"

HOS 对可调用 SVC 集合做了版本化与权限化的双重约束,仓库在 src/core/hle/kernel/svc_version.h 与 src/core/hle/kernel/svc_types.h 中如实建模:

概念代码位置取值/说明
SVC 主版本 ↔ SDK 主版本ConvertToSvcMajorVersionsvc = sdk + 4
最低支持版本RequiredKernelMajorVersion/MinorVersion3.0(Nintendo 不再支持低于 3.0 的程序)
当前支持的最高版本SupportedKernelMajorVersion/MinorVersionConvertToSvcMajorVersion(22)、ConvertToSvcMinorVersion(2)
版本编码EncodeKernelVersionminor占 bit 0..3,major占 bit 4..16
SVC 总数上限NumSupervisorCalls0xC0(即 192)
允许访问集合SvcAccessFlagSetstd::bitset<NumSupervisorCalls>

SvcAccessFlagSet是关键:SVC 的可调用性不是"实现了就能调",而是由进程的 capabilities 位图决定,这与"微内核 + 最小权限"的模型一致。调试时若出现权限类错误,应优先检查进程 capability 而不是 SVC 实现本身。

四、地址空间:32 / 36 / 39 位如何落地

文档提到"可为被创建的进程使用 32、36 和 39 位地址空间"。选择方式在CreateProcessFlag中(src/core/hle/kernel/svc_types.h):

enum class CreateProcessFlag : u32 { Is64Bit = (1 << 0), AddressSpaceShift = 1, AddressSpaceMask = (7 << AddressSpaceShift), AddressSpace32Bit = (0 << AddressSpaceShift), AddressSpace64BitDeprecated = (1 << AddressSpaceShift), AddressSpace32BitWithoutAlias = (2 << AddressSpaceShift), AddressSpace64Bit = (3 << AddressSpaceShift), EnableDebug = (1 << 4), EnableAslr = (1 << 5), IsApplication = (1 << 6), DeprecatedUseSecureMemory = (1 << 7), PoolPartitionShift = 7, // ... };

位宽的映射在 src/core/hle/kernel/k_page_table_base.h:

static constexpr size_t GetAddressSpaceWidth(Svc::CreateProcessFlag as_type) { switch (static_cast<Svc::CreateProcessFlag>(as_type & Svc::CreateProcessFlag::AddressSpaceMask)) { case Svc::CreateProcessFlag::AddressSpace64Bit: return 39; case Svc::CreateProcessFlag::AddressSpace64BitDeprecated: return 36; case Svc::CreateProcessFlag::AddressSpace32Bit: case Svc::CreateProcessFlag::AddressSpace32BitWithoutAlias: return 32; default: UNREACHABLE(); } }

各宽度的区域布局是一张静态表(src/core/hle/kernel/k_address_space_info.cpp),区域类型为MapSmall / MapLarge / Map39Bit / Heap / Stack / Alias:

位宽区域起始地址大小
32MapSmall2 MiB1 GiB − 2 MiB
32MapLarge1 GiB4 GiB − 1 GiB
32Alias无效(动态)1 GiB
32Heap无效(动态)1 GiB
36MapSmall128 MiB2 GiB − 128 MiB
36MapLarge2 GiB64 GiB − 2 GiB
36Heap无效8 GiB
36Alias无效6 GiB
39Map39Bit128 MiB512 GiB − 128 MiB(启用HAS_NCE时为 256 GiB − 128 MiB)
39MapSmall无效64 GiB
39Heap无效8 GiB
39Alias无效64 GiB
39Stack无效2 GiB

两个值得注意的实现细节:

  1. NCE 下地址空间被压缩。源码注释说明,启用 NCE(原生代码执行)时因为内存限制使用 38 位地址空间,"safely truncate ASLR region",表中Map39Bit大小相应从 512 GiB 降为 256 GiB。
  2. AddressSpace32BitWithoutAlias会把 Alias 区并入 Heap 区。InitializeForProcess中有对应逻辑:若该标志被设置,则heap_region_size += alias_region_size; alias_region_size = 0;,并且进程的最大可用内存随之从GetHeapRegionSize()变为GetHeapRegionSize() + GetAliasRegionSize()(见 src/core/hle/kernel/k_process.cpp 中按AddressSpaceMask分支的m_max_process_memory计算)。

五、进程持有的九类结构:文档清单与源码对照

原文档列出了每个进程都要维护的结构:

  • Name(13 字符)
  • 64-bit ID
  • 一个句柄表
  • Exclusive monitor
  • Threads
  • Held locks
  • Thread local pages
  • 每个进程一张页表

对到 src/core/hle/kernel/k_process.h 的成员上,可以逐条验证:

文档条目源码成员说明
Name(13 字符)std::array<char, 13> m_name初始化时memcpy进程名并补\0,见KProcess::Initialize
64-bit IDu64 m_process_id初始进程 ID 区间1..0x50,用户进程从0x51起(InitialProcessIdMin/Max、ProcessIdMin/Max)
句柄表KHandleTable m_handle_tableMaxTableSize = 1024,线性 ID + 空闲链表分配
Exclusive monitorstd::unique_ptr<Core::ExclusiveMonitor> m_exclusive_monitor按 CPU 后端(Dynarmic/NCE)分别绑定到各核
ThreadsThreadList m_thread_listIntrusiveListMemberTraits<&KThread::m_process_list_node>
Held locksKLightLock m_state_lock、KLightLock m_list_lock、KAddressArbiter m_address_arbiter另有KConditionVariable m_cond_var用于等待/同步
Thread local pagesTLPTree m_fully_used_tlp_tree、TLPTree m_partially_used_tlp_tree两棵红黑树分别管理"已用满"和"部分使用"的 TLP
Page tableKProcessPageTable m_page_table内部组合KPageTable,继承自KPageTableBase

补充几点源码层面的细节,这些是原文档没有展开、但对理解行为很关键的:

  • 进程名在结构中是 13 字节数组,而Svc::CreateProcessParameter::name是 12 字节,初始化时会把第 13 字节写成0(m_name[sizeof(params.name)] = 0;)。
  • 句柄表上限 1024,并且伪句柄不占表项:CurrentThread = 0xFFFF8000、CurrentProcess = 0xFFFF8001,InvalidHandle = 0(src/core/hle/kernel/svc_common.h)。判断函数IsPseudoHandle是查表前必须经过的分支。
  • 线程本地页的粒度:Svc::ThreadLocalRegionSize = 0x200(512 字节),而页面PageSize = 1 << 12(4 KiB),因此每页可容纳RegionsPerPage = PageSize / ThreadLocalRegionSize个区域(src/core/hle/kernel/k_thread_local_page.h)。IsAllUsed()/IsAllFree()决定该页挂在"已满"还是"部分使用"的红黑树上——这正是文档中"用树结构跟踪进程空间"的具体出处之一。
  • 进程还额外持有KResourceLimit* m_resource_limit、KSystemResource* m_system_resource、m_exception_thread,以及一组统计计数器(m_num_supervisor_calls、m_num_ipc_messages等),它们支撑svcGetProcessInfo一类查询。

六、内存记账:FreeBSD 风格侵入式红黑树

文档的关键论断是:"很多系统调用都与内存交互;被模拟的内核有一套内部树状结构,借自 FreeBSD 的 intrusive red-black tree;用于跟踪和查找新增或移除的映射。"

这条描述在代码中完全可验证:

  • 通用实现位于 src/common/intrusive_red_black_tree.h,其节点内部使用freebsd::RBEntry<IntrusiveRedBlackTreeNode>,根为freebsd::RBHead<IntrusiveRedBlackTreeNode>,并有Enum:节点尺寸的static_assert保证布局稳定。
  • 进程地址空间用KMemoryBlockManager维护(src/core/hle/kernel/k_memory_block_manager.h):
class KMemoryBlockManager final { public: using MemoryBlockTree = Common::IntrusiveRedBlackTreeBaseTraits<KMemoryBlock>::TreeType<KMemoryBlock>; using MemoryBlockLockFunction = void (KMemoryBlock::*)(KMemoryPermission new_perm, bool left, bool right); // ... };

每个KMemoryBlock描述一段连续且属性一致的地址区间。修改映射时,内核会切断受影响的块、修改属性、再尝试与左右相邻块合并——这是KMemoryBlockDisableMergeAttribute(DeviceLeft、IpcLeft、Locked、DeviceRight等)存在的目的:某些状态下一旦切开就不允许再合并,否则会丢失 IPC 锁定等语义。KMemoryBlockManagerUpdateAllocator固定MaxBlocks = 2,意味着单次操作最多新增两个块,超出则返回OutOfResource。

七、内存状态、属性与权限:三位一体的描述模型

svcQueryMemory返回给用户态的信息,本质上是这些字段的"用户可见投影"。源码把它们拆成三层(src/core/hle/kernel/k_memory_block.h):

  1. 状态:Svc::MemoryState(Free=0x00、Io=0x01、Static=0x02、Code=0x03、Normal=0x05、Shared=0x06、Alias=0x07、Stack=0x0B、ThreadLocal=0x0C、Transferred=0x0D、Insecure=0x17等 24 种),内核侧用KMemoryState在同一低 8 位之外叠加 23 个能力标志,例如FlagCanUseIpc、FlagCanAlias、FlagCanTransfer、FlagCanChangeAttribute、FlagLinearMapped。源码用一长串static_assert固定这些组合值(如KMemoryState::Code == 0x04DC7E03)。
  2. 属性:Svc::MemoryAttribute包含Locked(1<<0)、IpcLocked(1<<1)、DeviceShared(1<<2)、Uncached(1<<3)、PermissionLocked(1<<4),以及 Switch 2 新增的GpuAccessible(1<<5)。
  3. 权限:Svc::MemoryPermission为Read=1、Write=2、Execute=4,另有DontCare(1<<28);内核侧KMemoryPermission把用户权限左移KernelShift=3得到内核权限位,并用NotMapped表示"未映射"。

svcQueryMemory填充的Svc::MemoryInfo结构(src/core/hle/kernel/svc_types.h)为:

struct MemoryInfo { u64 base_address; u64 size; MemoryState state; MemoryAttribute attribute; MemoryPermission permission; u32 ipc_count; u32 device_count; u32 padding; };

即 8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 = 40 字节。转换由KMemoryInfo::GetSvcMemoryInfo()完成,它会用KMemoryState::Mask、KMemoryAttribute::UserMask、KMemoryPermission::UserMask把内部标志位裁剪成用户态可见的值——这也是排查"为什么 QueryMemory 看不到某个属性"时的第一处检查点。

八、以SetHeapSize与MapMemory为例看 SVC 的副作用

文档强调"系统调用会带来页表记账等副作用",具体形态可以看两条典型实现。

8.1svcSetHeapSize(0x01)

实现位于 src/core/hle/kernel/svc/svc_physical_memory.cpp:

Result SetHeapSize(Core::System& system, u64* out_address, u64 size) { LOG_TRACE(Kernel_SVC, "called, heap_size={:#x}", size); R_UNLESS(Common::IsAligned(size, HeapSizeAlignment), ResultInvalidSize); R_UNLESS(size < MainMemorySizeMax, ResultInvalidSize); KProcessAddress address{}; R_TRY(GetCurrentProcess(system.Kernel()) .GetPageTable() .SetHeapSize(std::addressof(address), size")); *out_address = GetInteger(address); R_SUCCEED(); }

关键约束有两条:2 MiB 对齐(HeapSizeAlignment = 2_MiB,见svc_common.h),以及不得超过主机内存上限MainMemorySizeMax。进入KPageTableBase::SetHeapSize后还会依次检查size <= heap_region_end - heap_region_start、size <= m_max_heap_size;缩小堆时要求尾部区间的状态为KMemoryState::Normal且权限为UserReadWrite、属性为None,否则拒绝收缩。也就是说,堆既能扩也能缩,但收缩受"尾部必须干净"的约束,这与游戏动态释放堆内存的行为直接相关。

8.2svcMapMemory/svcUnmapMemory的共用校验

两者共用MapUnmapMemorySanityChecks(src/core/hle/kernel/svc/svc_memory.cpp),错误码与检查顺序如下:

检查失败返回
目标地址 4 KiB 对齐ResultInvalidAddress
源地址 4 KiB 对齐ResultInvalidSize
size != 0ResultInvalidSize
size4 KiB 对齐ResultInvalidSize
dst + size不溢出ResultInvalidCurrentMemory
src + size不溢出ResultInvalidCurrentMemory
src落在本进程地址空间内ResultInvalidCurrentMemory

注意第 2 项:源地址未对齐返回的是ResultInvalidSize而非InvalidAddress——这类"看起来不一致"的错误码在 HOS 上是真实存在的,代码保持了原样。写兼容性测试或复现问题时不要"顺手修正",否则会与真实固件行为分叉。

九、线程、IPC 与"内核对用户态服务只是通道"

9.1CreateThread与核心亲和性折叠

src/core/hle/kernel/svc/svc_thread.cpp 顶部定义了三个小工具:

constexpr bool IsValidVirtualCoreId(int32_t core_id) { return (0 <= core_id && core_id < static_cast<int32_t>(Core::Hardware::NUM_CPU_CORES)); } constexpr int32_t FoldExtraCoreId(int32_t core_id) { constexpr auto num_cores = static_cast<int32_t>(Core::Hardware::NUM_CPU_CORES); return core_id >= num_cores && core_id < 2 * num_cores ? core_id - num_cores : core_id; } constexpr u64 FoldExtraCoreMask(u64 mask) { constexpr u64 low = (1ULL << Core::Hardware::NUM_CPU_CORES) - 1; return (mask & low) | ((mask >> Core::Hardware::NUM_CPU_CORES) & low); }

注释说明:Switch 2 硬件有八个核,而模拟器只模拟"应用最多能拿到的六个",因此超出范围的 core id 与亲和掩码会被折回低编号核,而不是直接报错。CreateThread的调用顺序是先用IdealCoreUseProcessValue替换默认 magic,再FoldExtraCoreId,然后才校验IsValidVirtualCoreId、(1 << core_id) & process.GetCoreMask()、优先级范围与process.CheckThreadPriority(priority)。

svcSetThreadCoreMask(0x0F)复用同一套折叠逻辑:先按真实进程掩码做合法性校验(ResultInvalidCoreId/ResultInvalidCombination),再折叠到被模拟的核上,最后由thread->SetCoreMask(...)落地。这个"先按真实语义校验、再按模拟规模折叠"的顺序很重要,否则会掩盖本应报错的调用。

9.2SendSyncRequest只是把请求转交给会话对象

src/core/hle/kernel/svc/svc_ipc.cpp 中:

Result SendSyncRequestImpl(KernelCore& kernel, uintptr_t message, size_t buffer_size, Handle session_handle) { KScopedAutoObject session = GetCurrentProcess(kernel).GetHandleTable().GetObject<KClientSession>(kernel, session_handle); R_UNLESS(session.IsNotNull(), ResultInvalidHandle); KScopedAutoObject parent = {kernel, session->GetParent()}; ASSERT(parent.IsNotNull()); R_RETURN(session->SendSyncRequest(kernel, message, buffer_size)); }

这与文档的判断一致:内核只负责句柄解析与生命周期保活(KScopedAutoObject就是引用计数的 RAII 包装),实际的 HIPC 报文格式与语义由会话对象及上层服务实现。SendSyncRequestWithUserBuffer额外要求缓冲区 4 KiB 对齐、长度非零且对齐、message + size不溢出,并通过page_table.LockForIpcUserBuffer(...)给用户缓冲区打上 IPC 锁定;失败路径上用ON_RESULT_FAILURE保证解锁,避免地址空间出现"永久 IPC 锁定"的泄漏。

ReplyAndReceive则是反向路径:如果指定了reply_target,先KServerSession::SendReply(...),然后进入接收等待;失败时会把*out_index置为-1。超时以HardwareTimer().GetTick()换算,这是"内核时间"而非主机时间。

十、调度:文档中的 4 核与当前源码的 6 核

原文档这一段是全文最容易与当前源码产生偏差的地方,需要分开陈述。

10.1 文档陈述

模拟器主动把自身限制为只使用 4 个线程(以模拟 4 个核),因为大多数现有应用从更多核数中获益不大,反而可能因额外竞争而受损。这等价于每个进程约 4 个ArmInterface槽位;这些槽位会被重定向到该核上最后运行的线程的最后一个pc,随后运行它,并在返回(halt 或中断)时重新调度。

文档同时坦承调度器并非 100% 忠实(例如原机是协作式而非抢占式),且有较大的时序方差,因为宿主系统的时序差异极大。

10.2 当前源码

src/core/hardware_properties.h 现在是:

// Switch 1 applications get cores 0..3. Switch 2 applications ask for 0..5 (title 4's npdm), and // its job system starts a worker per core, so emulate six: with four, cores 0 and 1 each ran two // of its workers. Switch 1 titles leave cores 4 and 5 idle. constexpr u32 NUM_CPU_CORES = 6; // Number of CPU Cores - sync with dynarmic exclusive_monitor.h

也就是说:"4 核"是文档写作时的状态;为支持按核起 worker 的 Switch 2 标题,核数已提升到 6,而 Switch 1 标题会让核 4、5 空闲。VirtualToPhysicalCoreMap与ConvertVirtualCoreMaskToPhysical()负责虚拟核到物理核的映射,KProcess中的m_arm_interfaces、m_running_threads、m_pinned_threads都是按NUM_CPU_CORES开的数组,CpuManager::Initialize()也按num_cores(多核时为NUM_CPU_CORES,单核时退化为 1)启动宿主线程。

10.3 调度器的真实结构

  • 每个物理核一个KScheduler,初始化时绑定一个main thread与一个idle thread(KernelCore内的InitializePhysicalCores(),见 src/core/hle/kernel/kernel.cpp);
  • 全局调度上下文GlobalSchedulerContext持有MAX_THREADS = 256的线程列表、一个KSchedulerPriorityQueue(优先级范围HighestThreadPriority = 0到LowestThreadPriority = 63)以及调度器锁;
  • KScheduler提供Schedule / RescheduleCurrentCore / PreemptSingleCore / RequestScheduleOnInterrupt等入口,IsIdle()通过比较m_current_thread与m_idle_thread判断;
  • KThread::IdleThreadPriority = Svc::LowestThreadPriority + 1,即空闲线程优先级比任何用户线程都低。

文档描述的"重定向到该核最后运行线程的pc、运行、返回后重新调度",在源码中对应KScheduler::Unload / Reload与被注释掉部分功能的ScheduleImpl路径——KScheduler::Initialize里还能看到被注释的SetSchedulerUpdateNeeded与中断处理注册代码(src/core/hle/kernel/k_scheduler.cpp),这也从侧面印证了文档"调度器尚不完整/不完全忠实"的说法。

十一、Switch 2 扩展:svc 0x80与GpuAccessible

原文档没有涉及 Switch 2 的新增系统调用,但当前代码里有一个直接在Call内联处理的特例,值得作为"如何扩展 SVC"的示范:

if (imm == 0x80) { // Switch 2 only: svcSetGpuMemoryAttribute(address, size, mask, attr). const u64 address = GetArg64(args, 0); const u64 size = GetArg64(args, 1); const u32 mask = static_cast<u32>(GetArg64(args, 2)); const u32 attr = static_cast<u32>(GetArg64(args, 3)); constexpr u32 Supported = static_cast<u32>(Svc::MemoryAttribute::GpuAccessible); // 对齐、长度、mask/attr 组合、区间归属检查,然后: result = page_table.SetMemoryAttribute(address, size, static_cast<KMemoryAttribute>(mask), static_cast<KMemoryAttribute>(attr)); }

注释说明了动机:nn::gpu::sdk与nn::gs在把内存注册给 GPU 之前会置上"GPU 可访问"属性(0x20),销毁时清除;NVN 可以通过QueryMemory看到这一位。实现上复用了svcSetMemoryAttribute的检查顺序,但把Supported收窄到只剩GpuAccessible:地址未对齐返回ResultInvalidAddress,size为 0 或未对齐返回ResultInvalidSize,(attr & ~mask) != 0或包含不支持的位返回ResultInvalidCombination,越界或溢出返回ResultInvalidCurrentMemory。

在KMemoryAttribute中对应GpuAccessible = static_cast<u8>(Svc::MemoryAttribute::GpuAccessible),并且它被放进了SetMask(与Uncached、PermissionLocked并列),意味着这一位是可被SetMemoryAttribute路径修改的;而Locked、IpcLocked、DeviceShared只能由内核在特定操作中隐式设置。

十二、调试与排查路线

结合文档与源码,可以把"内核相关问题"的定位压缩成一张检查清单:

  1. 先看 SVC 是否实现:Unhandled SVC 0x..来自两个Call32/Call64的default分支;确认后到 tools/svc_generator.py 的 SVC 表中查是否登记,再看src/core/hle/kernel/svc/是否有对应.cpp。
  2. 怀疑权限而非实现:核对SvcAccessFlagSet(NumSupervisorCalls = 0xC0)与进程 capabilities;ResultInvalidCoreId、ResultInvalidCombination这类错误多半来自 capability/掩码校验而非算法错误。
  3. 内存类错误看Result语义:InvalidAddress通常是地址未 4 KiB 对齐,InvalidSize可能是长度问题也可能是源地址未对齐(MapUnmapMemorySanityChecks的历史行为),InvalidCurrentMemory多为区间越界或溢出,OutOfMemory常见于堆上限与m_max_heap_size。
  4. 时序问题用EDEN_NX2_SVC_TRACE:需要唯一帧时用<first>:<last>形式,避免全量日志淹没现场。
  5. 跨进程/跨服务问题落到会话对象:SVC 层只看得到KClientSession/KServerSession,报文内容需要往 HIPC 与服务实现方向追。
  6. 多核相关行为注意核数变化:核对NUM_CPU_CORES与VirtualToPhysicalCoreMap;文档所述的 4 核模型与当前 6 核模型会影响"线程被放在哪个核上"的直觉判断。

十三、能力边界再确认

回到文档最后几句,这里的三条限制至今仍然成立,且能直接在代码中找到依据:

  • 没有 supervisor-level 仿真:KernelCore只维护内核对象(进程、线程、句柄表、页表、资源限制、中断/定时器),不存在完整的 EL1 异常向量与 MMU 硬件模型;地址空间是"被记账的软件结构",物理内存以MainMemoryAddress = 0x80000000的固定映射为基准(src/core/hle/kernel/board/nintendo/nx/k_memory_layout.h)。
  • 专有设备不在内核中模拟:usb:u、pcie等位于服务层,文档明确指出"尚未被模拟";内核侧只提供了CreateDeviceAddressSpace、MapDeviceAddressSpaceByForce/Aligned、CreateIoPool、CreateIoRegion这类抽象原语(见 src/core/hle/kernel/svc/svc_device_address_space.cpp 与 src/core/hle/kernel/svc/svc_io_pool.cpp),真实设备语义由上层决定。
  • 调度不忠实且有时序方差:KScheduler的注释化代码、PreemptSingleCore与中断调度入口表明其仍在演进;文档所说"原机是协作式而非抢占式",与 HOS 实际的协作调度模型一致,而模拟器在宿主抢占式线程之上运行,抖动无法消除。

十四、继续深入的文件索引

目标建议入口
SVC 分发与 ABI 胶水src/core/hle/kernel/svc.cpp、src/core/hle/kernel/svc.h
各 SVC 的具体实现src/core/hle/kernel/svc/(按功能拆成svc_memory.cpp、svc_thread.cpp、svc_ipc.cpp、svc_process.cpp等 38 个文件)
SVC 生成器tools/svc_generator.py
SVC 类型与常量src/core/hle/kernel/svc_types.h、src/core/hle/kernel/svc_common.h、src/core/hle/kernel/svc_version.h
进程与线程对象src/core/hle/kernel/k_process.h、src/core/hle/kernel/k_thread.h
地址空间布局src/core/hle/kernel/k_address_space_info.cpp、src/core/hle/kernel/k_page_table_base.h
内存块与红黑树src/core/hle/kernel/k_memory_block.h、src/core/hle/kernel/k_memory_block_manager.h、src/common/intrusive_red_black_tree.h
调度器src/core/hle/kernel/k_scheduler.cpp、src/core/hle/kernel/global_scheduler_context.h
核数与核映射src/core/hardware_properties.h、src/core/cpu_manager.cpp
内核初始化src/core/hle/kernel/kernel.cpp、src/core/hle/kernel/kernel.h
HLE 总体设计取向docs/DesignOverview.md

阅读时建议始终带着本文反复出现的那条主线:Eglantine 并不"运行一个内核",而是在svc #imm到达时,用一套忠实度逐步逼近的内核对象模型去回应它;文档描述的是这套模型的意图,源码记录的是它当前的真实形态,两者之间的差异(最典型的是 4 核与 6 核)正是排查行为偏差时最有价值的线索。

【免费下载链接】Eglantine

VERY experimental Nintendo Switch 2 emulator

项目地址:https://gitcode.com/gh_mirrors/eg/Eglantine
点击查看免费下载

相关推荐

上一篇:Comark流式渲染原理:auto-close 如何治愈 LLM 半截 Markdown,告别闪烁与跳变
下一篇:fullstack-agent社区与贡献指南:Discord交流、issue署名机制与AGPL-3.0许可免费商用规则

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询