[shad-PS4] 模块链接器 | ELF文件 | 输入处理器 | SDL捕获PC输入

在这里插入图片描述

第四章:模块链接器

欢迎回到shadPS4!

前一章 第3章:文件系统中,我们学习了模拟器如何将PS4游戏对文件的请求转换为PC文件系统的操作。

  • 访问文件固然重要,但游戏的核心是其可执行代码依赖的库文件

  • 这些内容需要从文件加载到内存中才能被CPU执行。

模块链接器在此处扮演关键角色。我们可以将其想象为游戏表演的舞台监督。

游戏并非单一巨型代码块,而是由多个组件构成——包括主程序(可执行文件)和各种系统库(PS4的.sprx文件,类似Windows的.dll或Linux的.so文件),这些库提供核心功能(如第1章:内核/系统服务(HLE)所述的核服务、图形功能或输入处理)。

Linux的.so文件

.so文件是Linux中的动态链接库(类似Windows的.dll),包含可被多个程序共享的代码和资源,运行时才加载,节省内存并方便更新。

(静态的话,就存在拷贝多份,占用内存)


当游戏启动时,模块链接器负责以下任务:

  1. 加载:从可执行文件和库中读取代码与数据
  2. 放置:将这些组件(称为模块)安排到模拟器的PS4内存空间中,通常使用内存管理器
  3. 连接(链接):确保所有组件能相互定位与通信,例如当游戏代码需要调用系统库函数时,链接器会找到该库函数的正确内存地址
  4. 配置:执行必要的重定位调整,因为代码和数据加载到内存后的最终地址可能与编译时的预设不同
  5. 启动:准备主程序代码执行环境

若没有模块链接器,游戏的不同部分将无法在内存中找到彼此,游戏将无法运行。

应用场景:游戏启动加载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); // 加载段到内存
};

符号解析与重定位

链接器通过符号表重定位表完成地址修正:

  1. 符号解析:在HLE符号表(模拟器实现的系统函数)和已加载模块的导出符号中查找目标地址
  2. 重定位计算:根据重定位类型(如R_X86_64_JUMP_SLOT)计算修正值
  3. 内存修补:将修正后的地址写入代码/数据段
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, &params);
    });
}

总结

模块链接器是模拟器的核心组件,负责:

  • ELF模块加载到模拟内存
  • 解析动态链接信息
  • 通过符号解析构建模块间调用关系
  • 执行重定位修正内存地址
  • 管理线程本地存储
  • 协调主程序与依赖库的启动顺序

正是这些精密配合的机制,使得分散的代码模块能够在模拟环境中形成完整的可执行体系。

下一章:输入处理


第五章:输入处理器

第四章:模块链接器中,我们探讨了模拟器如何

  • 将游戏的可执行代码和库加载到内存并为运行做好准备
  • 解析依赖关系以使游戏的各个部分能够相互调用。

既然游戏代码已载入内存且模拟的 CPU 可以开始执行,那么我们作为玩家如何实际与之互动?

这正是输入处理器的职责。输入处理器充当物理控制设备(接入电脑的键盘、鼠标、游戏手柄)与 PS4 游戏期望接收输入的虚拟控制器之间的解释器和翻译官。

PS4 游戏默认连接 DualShock 4 控制器进行开发,它期待特定的按钮按压信号(如 Cross、Circle、L1)和模拟摇杆位置。当我们在 PC 上运行时,各种硬件设备会发送不同类型的信号(如 ‘W’ 或 ‘Space’ 的键码、鼠标移动,或是通过 SDL 等库识别的游戏手柄按钮/轴标识符)。

输入处理器捕捉来自 PC 的这些多样化输入,并将其转换为模拟的 PS4 游戏所能理解的标准数字按钮状态和模拟量值。这就像使用万能适配器,允许任何设备接入任意接口。

应用实例:按压 PC 控制器按钮

假设我们按下键盘的 ‘X’ 键或 PC 手柄的 ‘South’ 按钮(通常是 ‘A’ 或 ‘Cross’)。

游戏预期看到 PS4 的 “Cross” 按钮被按下。这个信号如何传递?

以下是信号旅程的简化视图:

  1. 按压按钮(如键盘的 ‘X’,手柄的 ‘South’)
  2. PC 操作系统检测到这个物理输入事件
  3. shadPS4 用于与硬件交互的库(如 SDL)从操作系统捕获此事件
  4. shadPS4 的输入处理器组件接收此 SDL 事件(例如 ‘X’ 的 SDL_EVENT_KEY_DOWN,或 ‘South’ 的 SDL_EVENT_GAMEPAD_BUTTON_DOWN
  5. 输入处理器使用其内部映射(通过设置配置)将此特定的 PC 输入事件转换为对应的 PS4 按钮(例如将 ‘X’ 或 ‘South’ 转换为 “Cross” 按钮状态)
  6. 更新其维护的虚拟 PS4 控制器的状态
  7. 当游戏通过内核/系统服务 (HLE) 进行系统调用检查控制器状态时(例如"Cross 按钮是否被按下?")
  8. 游戏接收预期信息(如"是,Cross 被按下")并作出相应反应(如角色跳跃)

通过序列图可视化该流程:

在这里插入图片描述

上图就是物理操作系统响应的完整传递过程,经由输入处理器转换后更新模拟控制器状态。

输入处理器的核心概念

输入处理器通过以下核心机制实现其魔法:

  1. 输入捕获:从 PC 接收原始输入事件。使用 SDL(sdl_window.cpp)等库直接从宿主操作系统捕获键盘、鼠标和手柄事件
  2. 输入映射/配置定义哪些 PC 输入(如特定按键、鼠标按钮或手柄轴)对应哪些 PS4 控制器按钮或轴。通常通过配置文件加载,并通过用户界面管理(QT GUI 中的 ControlSettingsKBMSettings
  3. 输入处理:根据映射关系处理原始输入事件,确定模拟的 PS4 控制器状态。包括跟踪当前按下的按钮和模拟轴的精确数值
  4. 模拟控制器状态:维护表示 PS4 控制器当前状态的数据结构(Input::State),包括按钮按压、摇杆位置、扳机值、触摸板状态甚至运动传感器数据(陀螺仪/加速度计)
  5. 状态供给:当游戏通过系统调用请求时提供模拟控制器状态(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 事件,通过 OnKeyboardMouseInputOnGamepadEvent 方法将事件转换为通用输入事件格式,并更新输入状态。

模拟控制器状态

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 渲染器

下一章:Vulkan 渲染器

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值