Xenia 内核文档深度解析:Kernel Shims 与 Xbox 360 内核模块实现
2026/9/24 22:55:10 网站建设 项目流程
  • 游戏开发
  • 图形学

【免费下载链接】xenia

Xbox 360 Emulator Research Project

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

本篇技术指南围绕 docs/kernel.md 展开,系统讲解 Xenia(Xbox 360 模拟器研究项目)如何在宿主进程中实现客体内核 API:从模块加载时的导入替换、syscall 桥接,到两种内核导出编写约定(已废弃的SHIM_CALL与基于模板的新式约定),再到 xboxkrnl.exe 与 xam.xex 两大内核模块的组织结构。读完本文,你将掌握 Xenia 内核层的调用链原理、参数传递 ABI,以及如何阅读和编写内核导出实现。

背景:guest 与 host 之间的内核边界

Xbox 360 上的游戏是面向 PowerPC(Xenon CPU)编译的二进制程序,运行时会调用由系统内核提供的系统服务(如音频、输入、内存、线程、文件系统等)。Xenia 并不在模拟器中运行真实的 xboxkrnl.exe,而是采用"宿主原生函数"方案:Xenia 将所有内核 API 以宿主(host)平台上的原生 C++ 函数实现(见 docs/kernel.md 的 "Kernel shims" 一节)。

这意味着每次游戏发起内核调用,都必须跨越 guest(PowerPC 虚拟地址空间)与 host(x86-64 原生代码)之间的边界。这一边界正是"kernel shims"要解决的问题,完整的 guest→host 调用转换细节在 cpu 文档 的 ABI 一节有进一步说明(x64 后端通过 thunk 过渡宿主上下文,寄存器布局见 x64_stack_layout.h)。

Kernel Shims:syscall 替换与导出链接

模块加载时的三步处理

当游戏模块(xex/exe)被加载时,loader 会对导入表执行如下处理(docs/kernel.md):

  1. 找出所有内核导入:扫描模块的导入描述,定位所有指向内核模块(如 xboxkrnl.exe、xam.xex)的函数引用;
  2. 以 syscall 指令替换:在每个内核导入的位置放入一条 PowerPCsc(system call)指令;
  3. 查找内核导出并链接:根据导入的模块名与序号(ordinal),在 Xenia 已注册的导出表中找到对应的导出实现,并将该导出"链接"到每个导入点上。

JIT 对 syscall 的处理

当 JIT(即时编译器)翻译 guest 代码时,一旦遇到sc指令,就会生成一段调用宿主函数的指令序列。源码层面可以看到这条路径:

  • 在 ppc_emit_control.cc 中,InstrEmit_scSC.LEV == 0的系统调用调用f.builtins()->syscall_handler
  • 该内置函数在 ppc_frontend.cc 中通过processor_->DefineBuiltin("SyscallHandler", SyscallHandler, ...)注册;
  • 真正的SyscallHandler读取ppc_context->r[0]作为系统调用号(见 ppc_frontend.cc),再分发到对应的导出实现。

由此形成完整的调用链:guest 代码执行sc→ JIT 生成调用 SyscallHandler 的序列 → SyscallHandler 按系统调用号分发 → 命中 Xenia 的原生导出实现

两种导出定义方式

目前定义内核导出有两种方式(原文以 xboxkrnl_audio.cc 为例):

// 方式一:SHIM_CALL 约定(已废弃) SHIM_CALL XAudioGetSpeakerConfig_shim(PPCContext* ppc_context, KernelState* kernel_state) // 方式二:模板化约定(现行) dword_result_t XAudioGetSpeakerConfig(lpdword_t config_ptr)
SHIM_CALL 约定(Deprecated)

SHIM_CALL约定已废弃,但它能更直白地展示调用机制内部:

  • ppc_context是 guest 的 PowerPC 上下文(xe::cpu::ppc::PPCContext),保存 guest 的全部寄存器;
  • 函数参数从 r3...r10 寄存器获取(对应宏SHIM_GET_ARG_32等,见 shim_utils.h),超过 8 个的附加参数从栈上加载;
  • 返回值(如有)存入 r3(宏SHIM_SET_RETURN_32ppc_context->r[3] = (uint64_t)((int32_t)v))。

参数的实际读取逻辑在 shim_utils.h:前 8 个参数(index ≤ 7)直接取ppc_context->r[3 + index],其余参数通过栈指针r[1] + 0x54 + (index - 8) * 8结合内存翻译(SHIM_MEM_32等宏完成 guest 地址到 host 地址的转换与字节序交换)来读取。

模板化约定(现行)

新式约定在语义上等价,但用 C++ 模板自动化了"加载参数、设置返回值"的过程(见 shim_utils.h 的RegisterExport)。其核心机制:

  • 每个参数类型都派生自shim::Param,在构造时通过Param::Initppc_context中完成取值:整数/指针参数从r[3 + ordinal]取,浮点参数从f[1 + float_ordinal]取(shim_utils.h);
  • 返回值类型派生自shim::Result,在调用完成后调用result.Store(ppc_context)将结果写回r[3](shim_utils.h);
  • RegisterExport生成一个Trampoline,负责计数、按标签决定是否打印调用日志(PrintKernelCall),再转发到用户实现的*_entry函数。

配套的宏DECLARE_XBOXKRNL_EXPORT1/2/3/4DECLARE_XAM_EXPORT1/2等(shim_utils.h)把"导出序号 + 分类 + 标签"打包进注册调用,声明了模块名、分类(如kAudio)以及行为标签(如kImplementedkStubkHighFrequency)。

一个完整的内核导出示例

以音频模块为例(xboxkrnl_audio.cc):

dword_result_t XAudioGetSpeakerConfig_entry(lpdword_t config_ptr) { *config_ptr = 0x00010001; return X_ERROR_SUCCESS; } DECLARE_XBOXKRNL_EXPORT1(XAudioGetSpeakerConfig, kAudio, kImplemented);

其中lpdword_t是指向 guest 内存中 32 位值(大端序xe::be<uint32_t>)的指针参数类型(shim_utils.h 中定义了一整套类型别名:dword_tqword_tfloat_tlpvoid_tlpdword_tlpstring_t等)。写入*config_ptr时,PrimitivePointerParam已经通过TranslateVirtual完成了 guest 地址到 host 地址的转换。

一个更复杂的例子XAudioRegisterRenderDriverClient_entry(xboxkrnl_audio.cc)展示了如何与模拟器的子系统交互:它读取回调指针与参数,调用audio_system->RegisterClient(...)注册客户端,并把返回的索引拼装成 guest 侧的驱动句柄0x41550000 | index——这个0x4155魔数是 guest 与 host 约定的"音频驱动"标识,后续XAudioSubmitRenderDriverFrame_entry等接口会通过driver_ptr.guest_address() & 0xFFFF0000 == 0x41550000断言校验后取回索引(xboxkrnl_audio.cc),最终把采样帧交给audio_system->SubmitFrame(...)

导出标签(ExportTag)

从源码结构看,每个导出都携带一组标签位,用于描述其实现状态与调用特性(定义在 export_resolver.h):

标签含义
kImplemented导出已有某种形式实现,可被使用
kStub导出是桩实现,可能不正确
kSketchy已知可能引发问题或实现不完整
kHighFrequency被极其频繁地调用
kImportant重要导出,应始终记录日志
kBlocking导出会阻塞调用线程
kLog/kLogResult每次调用 / 每次结果都记录日志

这些标签在运行时驱动日志与性能行为:例如 shim_utils.h 中的 Trampoline 会判断kLog标签,且高频调用(kHighFrequency)只有在 cvarlog_high_frequency_kernel_calls开启时才记录,避免日志洪泛。导出还带有分类(ExportCategory,如kAudiokInputkThreadingkVideokFileSystemkNetworking等,见 export_resolver.h)。

Kernel Modules:两大内核模块的实现

Xenia 实现了两个 Xbox 内核模块:xboxkrnl.exexam.xex(见 docs/kernel.md 的 "Kernel Modules" 一节)。

xboxkrnl.exe —— Xbox 内核

定义于 src/xenia/kernel/xboxkrnl 目录。

这是 NT 内核的一个"微调版本":大部分 API 与 MSDN 或其他公开资料上能查到的 NT 内核 API 等价,因此可以参考 NT 内核文档理解其语义。源文件按 API 功能分组组织,从目录结构可以清晰看到分组方式:

  • 音频:xboxkrnl_audio.cc、xboxkrnl_audio_xma.cc
  • 加密:xboxkrnl_crypt.cc调试:xboxkrnl_debug.cc错误处理:xboxkrnl_error.cc
  • 硬件抽象层:xboxkrnl_hal.cc输入:xboxkrnl_hid.ccIO:xboxkrnl_io.cc、xboxkrnl_io_info.cc
  • 内存:xboxkrnl_memory.cc杂项:xboxkrnl_misc.cc对象管理器:xboxkrnl_ob.cc
  • 运行时库:xboxkrnl_rtl.cc、xboxkrnl_rtl.h字符串:xboxkrnl_strings.cc
  • 线程:xboxkrnl_threading.cc、xboxkrnl_threading.h摄像头:xboxkrnl_usbcam.cc
  • 视频:xboxkrnl_video.cc、xboxkrnl_video.h系统配置:xboxkrnl_xconfig.cc

模块的注册逻辑在 xboxkrnl_module.cc:

  • 构造函数调用RegisterExportTable(export_resolver_),并通过xboxkrnl_module_export_groups.inc批量调用各分组的Register##n##Exports完成所有导出注册(xboxkrnl_module.cc);
  • 导出表是一个 4096 槽位的数组,按 ordinal 索引(xboxkrnl_module.cc),静态表(xboxkrnl_table.inc)与动态注册的导出合并后整体注册进ExportResolver(xboxkrnl_module.cc)。

除了函数导出,该模块还通过SetVariableMapping向 guest 暴露若干"内核变量"(xboxkrnl_module.cc),例如:

  • XboxHardwareInfo:16 字节硬件信息结构,flags 置为0x20、CPU 数量置为60x06),游戏中常检查 bit 26 判断是否带硬盘;
  • KeTimeStampBundle:24 字节时间戳包,由 1ms 周期的HighResolutionTimer持续刷新 guest 运行毫秒数;
  • XboxKrnlVersion:伪造的2.FFFF.FFFF.80.00版本号;
  • ExLoadedCommandLine:通过 cvarcl附加的 guest 命令行参数。

模块还定义了若干命令行开关(xboxkrnl_module.cc):--cl(附加 guest 命令行)、--kernel_debug_monitor(启用调试监视器)、--kernel_cert_monitor(启用证书监视器)、--kernel_pix(启用 PIX 支持)。

xam.xex —— Xbox 辅助方法

定义于 src/xenia/kernel/xam 目录。

该模块实现 Xbox 特有的功能,即那些不属于 NT 内核语义、而属于 Xbox 平台层的能力。从目录与源文件可以归纳出它的功能覆盖面:

  • 应用与应用管理:app_manager.cc、apps/、xam_msg.cc、xam_task.cc、xam_ui.cc
  • 内容与存储:content_manager.cc、xam_content.cc、xam_content_aggregate.cc、xam_content_device.cc
  • 用户与个人资料:user_profile.cc、xam_user.cc、xam_avatar.cc
  • 网络与语音:xam_net.cc、xam_voice.cc、xam_party.cc
  • 系统信息与本地化:xam_info.cc、xam_locale.cc、xam_enum.cc
  • 输入与通知:xam_input.cc、xam_notify.cc、xam_nui.cc
  • 视频:xam_video.cc

注册流程与 xboxkrnl 一致:xam_module.cc 在构造时调用RegisterExportTable,并通过xam_module_export_groups.inc批量注册各分组导出,最终以"xam.xex"为模块名注册进ExportResolver(xam_module.cc)。xam 中还维护了一个全局原子计数器xam_dialogs_shown_xeXamIsUIActive()据此判断系统 UI 对话框是否处于活动状态(xam_module.cc)。

导出解析机制(ExportResolver)

所有内核导出的注册与查询都经由xe::cpu::ExportResolver(export_resolver.h):

  • RegisterTable(module_name, exports):注册某个模块(如"xboxkrnl.exe""xam.xex")的导出表,表内同时维护"按 ordinal"与"按名称"两种索引,供加载器与调试器查询;
  • GetExportByOrdinal(module_name, ordinal):按模块名 + 序号解析导出,这是 loader 把 syscall 链接到宿主实现时的核心查询路径;
  • SetFunctionMapping/SetVariableMapping:前者为函数导出设置 trampoline(JIT 生成的 guest→host 桥接入口),后者为变量导出设置 guest 内存中的地址。

每个Export条目(export_resolver.h)包含 ordinal、类型(函数/变量)、96 字节名称、标签位,以及函数数据(trampoline 指针与调用计数call_count)。从ExportTag的位布局注释(ll...... cccccccc ........ ..bihssi)可以看出,标签高 16 位承载分类(CategoryShift = 16),低 6 位承载行为标签,这正是宏DECLARE_XBOXKRNL_EXPORT1kAudio等分类与行为标签打包组合的实现基础。

小结

Xenia 的内核层设计可以概括为一条清晰的边界策略:

  1. 全部内核 API 宿主化:不仿真真实内核二进制,而是用宿主原生 C++ 函数实现(docs/kernel.md);
  2. syscall 桥接:模块加载时以sc指令替换内核导入并链接到已注册导出,JIT 翻译sc时调用SyscallHandler分发(ppc_frontend.cc、ppc_emit_control.cc);
  3. 两套导出编写约定SHIM_CALL(已废弃,直观展示 r3~r10 参数传递与栈参数)与模板化约定(自动参数加载/返回值写回,见 shim_utils.h);
  4. 两大内核模块:xboxkrnl.exe 覆盖与 NT 内核等价的系统服务,xam.xex 覆盖 Xbox 平台特有功能,均按功能分组组织在 src/xenia/kernel/xboxkrnl 与 src/xenia/kernel/xam 下,由ExportResolver统一管理导出表与变量映射。

对想要深入 Xenia 内核层或为其增加内核 API 支持的开发者而言,阅读 shim_utils.h 理解参数/返回值机制、参考 xboxkrnl_audio.cc 学习导出编写范式、再对照 xboxkrnl_module.cc 与 xam_module.cc 了解模块注册流程,是一条完整的学习路径。

  • 游戏开发
  • 图形学

【免费下载链接】xenia

Xbox 360 Emulator Research Project

项目地址:https://gitcode.com/gh_mirrors/xe/xenia
点击查看免费下载
上一篇:如何快速提升Mac工作效率:3个窗口管理技巧
下一篇:如何用WPS-Zotero插件在5分钟内实现跨平台文献引用自动化

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

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

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

立即咨询