
第四章:模块链接器
欢迎回到shadPS4!
在前一章 第3章:文件系统中,我们学习了模拟器如何将PS4游戏对文件的请求转换为PC文件系统的操作。
-
访问文件固然重要,但游戏的核心是其
可执行代码和依赖的库文件。 -
这些内容需要从
文件加载到内存中才能被CPU执行。
模块链接器在此处扮演关键角色。我们可以将其想象为游戏表演的舞台监督。
游戏并非单一巨型代码块,而是由多个组件构成——包括主程序(可执行文件)和各种系统库(PS4的.sprx文件,类似Windows的.dll或Linux的.so文件),这些库提供核心功能(如第1章:内核/系统服务(HLE)所述的核服务、图形功能或输入处理)。
Linux的.so文件
.so文件是Linux中的动态链接库(类似Windows的.dll),包含可被多个程序共享的代码和资源,运行时才加载,节省内存并方便更新。
(静态的话,就存在拷贝多份,占用内存)
当游戏启动时,模块链接器负责以下任务:
- 加载:从可执行文件和库中读取代码与数据
- 放置:将这些组件(称为模块或库)安排到模拟器的
PS4内存空间中,通常使用内存管理器 - 连接(链接):确保所有组件能相互定位与通信,例如当游戏代码需要调用系统库函数时,链接器会找到该库函数的正确内存地址
- 配置:执行必要的重定位调整,因为代码和数据加载到内存后的
最终地址可能与编译时的预设不同 - 启动:准备主程序代码执行环境
若没有模块链接器,游戏的不同部分将无法在内存中找到彼此,游戏将无法运行。
应用场景:游戏启动加载Eboot.bin
链接器最基础的应用场景是启动主程序。
该程序通常存储在eboot.bin文件中。
以下是shadPS4启动游戏时的核心加载流程(聚焦于Linker组件处理的加载与链接过程)

流程图体现了链接器如何与文件系统交互获取数据,通过内存管理器分配内存空间,最终完成内部链接与重定位的过程。
⭕模块文件内部结构(ELF格式)
前文:[OS] vDSO + vvar(频繁调用的处理) | 存储:寄存器(高效)和栈(空间大)| ELF标准包装规范(加速程序加载)
模块链接器深度依赖ELF(可执行与可链接格式)文件。
ELF文件包含多个段,其中**程序头(Program Headers)和动态段(Dynamic Section)**对加载至关重要:

- 程序头(PT_LOAD):描述需要加载到内存的段,包含文件偏移、建议内存地址、空间大小和权限标志。链接器据此向内存管理器申请内存
- 动态段(PT_DYNAMIC):包含动态链接信息:
DT_SCE_NEEDED_MODULE:依赖的模块列表DT_SCE_EXPORT_LIB/DT_SCE_IMPORT_LIB:导出/导入的库信息DT_SCE_SYMTAB:符号表(函数/变量定义)DT_SCE_STRTAB:字符串表(符号名称存储)DT_SCE_RELA:重定位表(需要修正的地址位置)
Core::Loader::Elf类(src/core/loader/elf.h/cpp)负责解析这些结构:
class Elf
{
public:
void Open(const std::filesystem::path& file_name); // 打开ELF/SELF文件
[[nodiscard]] elf_header GetElfHeader() const; // 获取ELF头信息
void LoadSegment(u64 virtual_addr, u64 file_offset, u64 size); // 加载段到内存
};
符号解析与重定位
链接器通过符号表和重定位表完成地址修正:
- 符号解析:在HLE符号表(模拟器实现的系统函数)和已加载模块的导出符号中查找目标地址
- 重定位计算:根据重定位类型(如
R_X86_64_JUMP_SLOT)计算修正值 - 内存修补:将修正后的地址写入代码/数据段
bool Linker::Resolve(const std::string& name, /*...*/)
{
// 首先检查HLE符号表
if (m_hle_symbols.FindSymbol(sr)) return true;
// 其次查找其他模块的导出符号
if (FindExportedModule(/*...*/)) return true;
// 最后使用桩函数占位
return AeroLib::GetStub(/*...*/);
}
void Linker::Relocate(Module* module)
{
module->ForEachRelocation([&](elf_relocation* rel)
{
// 计算重定位地址并写入内存
std::memcpy(rel_virtual_addr, &rel_value, sizeof(rel_value));
});
}
线程本地存储(TLS)
链接器管理线程本地存储的初始化和访问:
void* Linker::AllocateTlsForThread(bool is_primary)
{
// 计算TLS总大小并分配内存
memory->MapMemory(/*...*/);
// 初始化TCB和DTV表
tcb->tcb_dtv = new DtvEntry[num_dtvs + 2];
// 复制模块的初始TLS数据
std::memcpy(dest, src, m->tls.init_image_size);
}
void* Linker::TlsGetAddr(u64 module_index, u64 offset)
{
// 通过DTV表定位模块的TLS基地址
return dtv_table[module_index + 1].pointer + offset;
}
执行流程总控
Linker::Execute函数协调整个启动过程:
void Linker::Execute(const std::vector<std::string> args)
{
// 重定位所有静态模块
for (const auto& m : m_modules) Relocate(m.get());
// 启动主线程
main_thread.Run([this]
{
// 加载依赖的共享库
LoadSharedLibraries();
// 准备入口参数
EntryParams params{main_module->GetEntryAddress()};
// 跳转到游戏入口点
ExecuteGuest(RunMainEntry, ¶ms);
});
}
总结
模块链接器是模拟器的核心组件,负责:
- 将
ELF模块加载到模拟内存 解析动态链接信息- 通过符号
解析构建模块间调用关系 - 执行
重定位修正内存地址 管理线程本地存储- 协调主程序与依赖库的
启动顺序
正是这些精密配合的机制,使得分散的代码模块能够在模拟环境中形成完整的可执行体系。
第五章:输入处理器
在第四章:模块链接器中,我们探讨了模拟器如何
- 将游戏的可执行代码和库加载到内存并为运行做好准备
- 解析依赖关系以使游戏的各个部分能够相互调用。
既然游戏代码已载入内存且模拟的 CPU 可以开始执行,那么我们作为玩家如何实际与之互动?
这正是输入处理器的职责。输入处理器充当物理控制设备(接入电脑的键盘、鼠标、游戏手柄)与 PS4 游戏期望接收输入的虚拟控制器之间的解释器和翻译官。
PS4 游戏默认连接 DualShock 4 控制器进行开发,它期待特定的按钮按压信号(如 Cross、Circle、L1)和模拟摇杆位置。当我们在 PC 上运行时,各种硬件设备会发送不同类型的信号(如 ‘W’ 或 ‘Space’ 的键码、鼠标移动,或是通过 SDL 等库识别的游戏手柄按钮/轴标识符)。
输入处理器捕捉来自 PC 的这些多样化输入,并将其转换为模拟的 PS4 游戏所能理解的标准数字按钮状态和模拟量值。这就像使用万能适配器,允许任何设备接入任意接口。
应用实例:按压 PC 控制器按钮
假设我们按下键盘的 ‘X’ 键或 PC 手柄的 ‘South’ 按钮(通常是 ‘A’ 或 ‘Cross’)。
游戏预期看到 PS4 的 “Cross” 按钮被按下。这个信号如何传递?
以下是信号旅程的简化视图:
- 按压按钮(如键盘的 ‘X’,手柄的 ‘South’)
- PC 操作系统检测到这个物理输入事件
- shadPS4 用于与硬件交互的库(如 SDL)从操作系统捕获此事件
- shadPS4 的输入处理器组件接收此 SDL 事件(例如 ‘X’ 的
SDL_EVENT_KEY_DOWN,或 ‘South’ 的SDL_EVENT_GAMEPAD_BUTTON_DOWN) - 输入处理器使用其内部映射(通过设置配置)将此特定的 PC 输入事件转换为对应的 PS4 按钮(例如将 ‘X’ 或 ‘South’ 转换为 “Cross” 按钮状态)
- 更新其维护的虚拟 PS4 控制器的状态
- 当游戏通过内核/系统服务 (HLE) 进行系统调用检查控制器状态时(例如"Cross 按钮是否被按下?")
- 游戏接收预期信息(如"是,Cross 被按下")并作出相应反应(如角色跳跃)
通过序列图可视化该流程:

上图就是物理操作到系统响应的完整传递过程,经由输入处理器转换后更新模拟控制器状态。
输入处理器的核心概念
输入处理器通过以下核心机制实现其魔法:
- 输入捕获:从 PC 接收原始输入事件。使用 SDL(
sdl_window.cpp)等库直接从宿主操作系统捕获键盘、鼠标和手柄事件 - 输入映射/配置:定义哪些 PC 输入(如特定按键、鼠标按钮或手柄轴)对应哪些 PS4 控制器按钮或轴。通常通过配置文件加载,并通过用户界面管理(QT GUI 中的
ControlSettings和KBMSettings) - 输入处理:根据映射关系处理原始输入事件,确定模拟的 PS4 控制器状态。包括跟踪当前按下的按钮和模拟轴的精确数值
- 模拟控制器状态:维护表示 PS4 控制器当前状态的数据结构(
Input::State),包括按钮按压、摇杆位置、扳机值、触摸板状态甚至运动传感器数据(陀螺仪/加速度计) - 状态供给:当游戏通过系统调用请求时提供模拟控制器状态(
Input::GameController处理此过程)
输入捕获与初始处理(通过 SDL)
主窗口处理代码(sdl_window.cpp)是处理 SDL 事件的第一接触点:
— File: src/sdl_window.cpp(简化版) —
// ... SDL 相关头文件...
#include "input/input_handler.h" // 包含 Input::InputEvent, Input::InputBinding 等
namespace Frontend {
// ... WindowSDL 类...
void WindowSDL::WaitEvent() {
SDL_Event event;
if (!SDL_WaitEvent(&event)) {
return;
}
// ... 检查 ImGui 事件、GUI 事件 ...
switch (event.type) {
// ... 处理窗口调整大小、焦点事件 ...
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL:
case SDL_EVENT_MOUSE_WHEEL_OFF:
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
OnKeyboardMouseInput(&event); // 处理键盘/鼠标输入
break;
// ... 处理手柄插拔事件 ...
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
OnGamepadEvent(&event); // 处理手柄事件
break;
// ... 处理传感器事件、退出事件 ...
default:
break;
}
}
该事件循环持续等待 SDL 事件,通过 OnKeyboardMouseInput 和 OnGamepadEvent 方法将事件转换为通用输入事件格式,并更新输入状态。
模拟控制器状态
Input::State 结构体保存 PS4 控制器状态的即时快照:
— File: src/input/controller.h(简化版) —
// ... 包含头文件...
#include "core/libraries/pad/pad.h" // OrbisPadButtonDataOffset 等定义
namespace Input {
struct State {
u64 time; // 状态记录时间戳
Libraries::Pad::OrbisPadButtonDataOffset buttonsState = Libraries::Pad::OrbisPadButtonDataOffset::None; // 按钮状态位图
int axes[static_cast<int>(Axis::AxisMax)]; // 模拟摇杆和扳机的轴值
// 触摸板双指状态
struct TouchState
{
bool state; // 是否接触
u16 x, y; // 触摸坐标
} touchpad[2];
Libraries::Pad::OrbisFVector3 angularVelocity{}; // 陀螺仪数据
Libraries::Pad::OrbisFVector3 acceleration{}; // 加速度计数据
// 根据输入事件更新状态的方法
void OnButton(Libraries::Pad::OrbisPadButtonDataOffset button, bool isPressed);
void OnAxis(Axis axis, int value);
};
该结构体通过位操作和值存储精确反映控制器状态,支持按钮、轴、触摸板和传感器数据的更新。
游戏控制器管理
Input::GameController 类管理控制器状态历史记录,并通过系统调用向游戏提供状态:
— File: src/input/controller.h(简化版) —
class GameController
{
static constexpr u32 MAX_STATES = 64; // 状态缓冲区大小
public:
void ReadState(State* state, bool* isConnected, int* connectedCount);
int ReadStates(State* states, int states_num, bool* isConnected, int* connectedCount);
void CheckButton(int id, Libraries::Pad::OrbisPadButtonDataOffset button, bool is_pressed);
void Axis(int id, Input::Axis axis, int value);
private:
std::unique_ptr<Engine> m_engine; // 实际输入捕获引擎
State m_states[MAX_STATES]; // 循环状态缓冲区
// ... 同步锁和其他内部状态 ...
};
该类维护状态缓冲区,通过 AddState 方法添加新状态快照,并通过 ReadState 向游戏提供历史状态数据。
配置与映射
输入映射配置通过 QT GUI 界面管理:
— File: src/qt_gui/kbm_gui.cpp(简化版) —
void KBMSettings::SaveKBMConfig(bool close_on_save) {
// ... 从 UI 元素构建配置行 ...
std::string output_string = "cross";
std::string input_string = ui->CrossButton->text().toStdString();
lines.push_back(output_string + " = " + input_string);
// ... 写入配置文件 ...
// 重新加载配置
Input::ParseInputConfig(RunningGameSerial);
}
配置保存后通过 Input::ParseInputConfig 重新加载映射规则,确保输入处理器使用最新配置。
总结
输入处理器是我们与游戏世界的连接桥梁。
它通过 SDL 等库捕获 PC 外设输入,根据可配置的映射规则将这些输入转换为模拟的 PS4 控制器状态,维护虚拟控制器的当前状态,并在游戏通过系统调用请求时提供该状态。
这个关键组件确保我们的按键操作和摇杆移动能被模拟的 PS4 游戏正确解析,实现无缝游戏体验。
下一章我们将从处理输入转向图形输出,探索Vulkan 渲染器。





