SerenityOS 移植 Quake III Arena:ioquake3 八个补丁的逐项深度解析
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
导读:本文以 SerenityOS 仓库中 Ports/quake3/patches/ReadMe.md 为骨架,逐项拆解将 ioquake3(Quake III Arena 的开源引擎)移植到 SerenityOS 所需的八个补丁。你将了解到每个补丁解决了什么编译/链接/运行问题、修改了哪些文件、底层原因是什么,以及这套补丁在 SerenityOS 移植体系中的生成与使用机制。读完本文,你既能按图索骥地理解这些补丁的每一行改动,也能举一反三地把类似的大型 C 语言游戏引擎移植到 SerenityOS 上。
背景:为什么需要这八个补丁
SerenityOS 是一个从零开始、使用自定义内核与用户态库实现类 Unix 操作系统的开源项目。它的 C 库(LibC)、POSIX 语义、动态链接器(ld.so)、mmap与anon_create等内核接口都有独特的实现方式。因此,将 ioquake3 这样体量庞大、年代久远的 C 引擎直接交叉编译,必然会在平台探测、头文件、链接库、可执行文件命名、可执行内存映射等方面遇到一系列不兼容问题。
在 SerenityOS 中,外部软件通过Ports/目录下的移植脚本体系进行构建。每个 port 目录包含一个package.sh描述文件、一个可选的patches/补丁目录以及对应的ReadMe.md补丁说明。quak3 的移植位于 Ports/quake3/package.sh,它:
- 从
https://github.com/ioquake/ioq3拉取固定提交6d74896557d8c193a9f19bc6845a47e9d0f77db2,版本号标记为1.34,并对归档做 SHA-256 校验; - 依赖
SDL2(depends=("SDL2")); - 安装到
/usr/local/games/quake3/,启动器命令为/usr/local/games/quake3/ioquake3,并注册到&Games分类; - 安装完成后通过
post_install生成autoexec.cfg(强制cl_renderer "opengl1"、r_fullscreen "0"、cg_drawfps "1"),并为可执行文件在/etc/fstab.d/quake3中写入wxallowed挂载选项以允许匿名可执行内存。
这套补丁正是围绕上述目标,由移植者(Jesse Buhagiar)在 2022 年 3 月 25–26 日之间连续提交,按0001–0008编号顺序应用。下面逐项解析。
补丁应用机制:从 package.sh 到 patch 的完整链路
在深入补丁内容之前,先理解 SerenityOS 移植脚本是如何应用这些补丁的,这决定了补丁的格式与顺序要求。
Ports/.port_include.sh 中定义了补丁应用逻辑:
patch_internal()(约第 402–429 行)遍历${PORT_META_DIR}/patches/*.patch,对每个补丁先尝试git am --keep-cr --keep-non-patch(若源码仓库是 git 克隆),失败则回退到patch -p"$patchlevel"(patchlevel=1,即默认剥掉一级路径前缀);- 应用成功后打
patched标签标记; do_patch()(第 560 行)在构建流程中调用pre_patch与patch_internal,属于fetch → patch → configure → build → install链路上的一环。
而本文主角patches/ReadMe.md本身也不是手写的:do_generate_patch_readme()(第 649–722 行)会用git mailinfo从每个.patch文件中提取Subject:与提交说明正文,自动生成## 补丁文件名+ 描述 的 Markdown 结构(第 710–715 行)。也就是说,ReadMe.md 的内容严格来源于补丁自身的 commit message,这保证了文档与补丁改动的一一对应关系。Ports 构建流程还支持generate_patch_readme子命令,以及rebuild时通过git am --3way检测上游变更并自动重新生成补丁(第 781–826 行)。
0001-Meta-Refactor-Makefile-to-support-Serenity.patch
主题:重构 ioquake3 的 Makefile 以支持 Serenity 平台。
这是整个移植的奠基性补丁,改动集中在Makefile(16 处插入、28 处删除),核心内容如下。
硬编码平台与架构
原 Makefile 通过uname动态探测COMPILE_PLATFORM与COMPILE_ARCH(含 i686→x86、arm→arm 的 sed 归一化,以及 arm64/aarch64 特判)。但交叉编译到 SerenityOS 时,宿主机的uname结果是构建机自身,无法代表目标平台。补丁将这两项直接改为:
COMPILE_PLATFORM=serenity COMPILE_ARCH=${SERENITY_ARCH}其中${SERENITY_ARCH}由 SerenityOS 移植环境注入(构建时通过环境变量提供,典型取值如x86_64、i686等)。
关闭不适用于 SerenityOS 的构建选项
补丁在ifndef默认值处批量将以下选项从默认启用改为显式关闭:
| 选项 | 原默认值 | 补丁后默认值 | 含义 |
|---|---|---|---|
BUILD_BASEGAME | 空 | 1 | 构建 baseq3 游戏逻辑(保持开启) |
BUILD_MISSIONPACK | 空 | 0 | 不构建资料片 missionpack |
BUILD_RENDERER_OPENGL2 | 空 | 0 | 不构建 OpenGL 2 渲染器 |
USE_OPENAL | 1 | 0 | 关闭 OpenAL 音频 |
USE_OPENAL_DLOPEN | 1 | 0 | 关闭 OpenAL 动态加载 |
USE_CURL | 1 | 0 | 关闭 cURL 网络下载 |
USE_CURL_DLOPEN | 1 | 0 | 关闭 cURL 动态加载 |
USE_CODEC_VORBIS | 1 | 0 | 关闭 Vorbis 音频解码 |
USE_CODEC_OPUS | 1 | 0 | 关闭 Opus 音频解码 |
USE_MUMBLE | 1 | 0 | 关闭 Mumble 语音 |
USE_VOIP | 1 | 0 | 关闭 VoIP 语音 |
这样做的原因很直接:SerenityOS 的 ports 体系采用“按需依赖”策略,package.sh只声明了SDL2依赖,OpenAL、cURL、Vorbis、Opus、Mumble 等并未移植或未启用,若保持默认开启会导致链接失败或运行期缺失动态库。渲染器也只保留与 SerenityOS 软渲染/GL 环境匹配的opengl1。
新增 Serenity 平台构建分支
补丁将原本的 OpenBSD 平台分支改写为 Serenity 分支:
ifeq ($(PLATFORM),serenity) BASE_CFLAGS = -Wall -fno-strict-aliasing -Wimplicit -Wstrict-prototypes \ -pipe -DUSE_ICON -DMAP_ANONYMOUS=MAP_ANON CLIENT_CFLAGS += $(SDL_CFLAGS)注意其中的-DMAP_ANONYMOUS=MAP_ANON:ioquake3 源码中大量使用MAP_ANONYMOUS,而 SerenityOS 头文件使用MAP_ANON命名,这一宏映射避免了大量源码改动。同时补丁还删除了 darwin 分支里“若 CC 是 cc/gcc 则清空”的交叉编译逻辑,避免覆盖 SerenityOS 工具链注入的CC。
0002-Engine-Add-Serenity-so-q_platform.h.patch
主题:在q_platform.h中新增 Serenity 平台定义块。
code/qcommon/q_platform.h是 ioquake3 的平台抽象头文件,所有源码都依赖它来确定字节序、路径分隔符、架构字符串、动态库扩展名等。补丁在文件末尾(Q3VM 段之前)追加了 29 行:
#if defined(__serenity__) #include <sys/types.h> #define Q3_LITTLE_ENDIAN #define OS_STRING "serenity" #define ID_INLINE inline #define PATH_SEP '/' #ifdef __i386__ #define ARCH_STRING "x86" #elif defined __amd64__ #undef idx64 #define idx64 1 #define ARCH_STRING "x86_64" #endif #define DLL_EXT ".so" #endif要点解析:
- 判定宏
__serenity__:SerenityOS 的编译环境(Meta/CMake与工具链)会全局定义该宏,作为所有平台适配代码的开关; Q3_LITTLE_ENDIAN:SerenityOS 目前支持的目标架构(x86、x86_64 等)均为小端,显式声明避免运行时字节序探测开销与误判;ARCH_STRING与idx64:供启动横幅与版本信息使用;x86_64 下#undef idx64再定义,确保与引擎内其他判定一致;DLL_EXT ".so":SerenityOS 的动态库扩展名与 Linux 一致,渲染器插件renderer_opengl1_*.so依赖此定义被正确加载。
0003-Engine-Add-sys-select.h-include-for-Serenity.patch
主题:为 SerenityOS 补充<sys/select.h>头文件包含。
这是最能体现“同一套 POSIX 接口在不同系统上分布不同”的补丁。补丁说明指出:Quake III 的网络代码大量使用select()系统调用,而 Linux 恰好能在某些被间接包含的头文件中得到select()声明,SerenityOS 则严格要求显式#include <sys/select.h>,否则会产生隐式声明错误(implicit declaration error)。
补丁在三处文件各加了 4 行条件包含:
code/qcommon/net_ip.c:网络核心(UDP socket 轮询)——在#include <sys/filio.h>之后、typedef int SOCKET之前插入;code/sys/con_tty.c:终端控制台输入轮询(tty 模式)——在<sys/time.h>之后插入;code/sys/sys_unix.c:Unix 平台系统层——在<sys/wait.h>之后插入。
同一补丁还顺带将net_ip.c中 IPv6 组播相关的NET_JoinMulticast6/NET_LeaveMulticast6用#ifndef __serenity__整体包起来——因为 SerenityOS 网络栈(至少在该版本)不支持 IPv6 组播,且函数体内引用了 SerenityOS 头文件中不存在的结构。
0004-Meta-Add-ldl-library-for-Serenity-target.patch
主题:为 Serenity 平台目标追加-ldl链接库。
这是对 0001 新增的 Serenity Makefile 分支的一处补丁级修正,改动仅 1 行:
THREAD_LIBS=-lpthread - LIBS=-lm + LIBS=-lm -ldldl(dlopen/dlsym/dlclose)在 ioquake3 中用于运行时动态加载渲染器插件(USE_RENDERER_DLOPEN)与游戏模块(.so)。Linux 的 glibc 从 2.34 起把dl符号并入 libc,无需显式-ldl;而 SerenityOS 的 LibDL 是独立库,必须显式链接,否则渲染器插件加载相关的符号会出现“undefined reference”链接错误。补丁同时保留-lpthread(线程库)与-lm(数学库)。
0005-Engine-Move-ifdef-to-more-sensible-location.patch
主题:把#ifdef移动到更合理的位置。
这是对 0003 的代码整洁性修正(提交说明只有一句 "No linker errors in this dojo!",暗示此前的写法虽能编译通过但结构不佳)。0003 曾把整个NET_JoinMulticast6/NET_LeaveMulticast6函数体用#ifndef __serenity__包住,导致 Serenity 分支下函数体为空但函数签名仍然保留,且#endif落在函数体末尾、右花括号之后。
0005 将其重构为“函数签名与花括号始终存在、仅函数体内部条件编译”的标准写法:
void NET_JoinMulticast6(void) { #ifndef __serenity__ int err; /* ... 原实现 ... */ #endif } void NET_LeaveMulticast6() { #ifndef __serenity__ /* ... 原实现 ... */ #endif }这样在任意平台下函数原型都保持一致,#endif位置正确,避免了空函数体/花括号错位可能引发的告警与链接一致性问题。
0006-Meta-Add-ARCH-to-TOOLS_CFLAGS.patch
主题:把架构字符串注入 TOOLS_CFLAGS。
ioquake3 构建体系中有两类编译目标:主程序(client/server/game)与构建期工具(TOOLS,如q3asm、q3lcc等用于编译 QVM 字节码的交叉工具)。补丁在 Serenity 分支的BASE_CFLAGS后追加:
TOOLS_CFLAGS += -DARCH_STRING=\"$(COMPILE_ARCH)\"这样工具程序在编译期就能通过宏拿到目标架构字符串(例如-DARCH_STRING="x86_64"),用于在生成 QVM 或打印 banner 时报告正确的架构。结合 0002 中q_platform.h里ARCH_STRING的定义,可以推断:主程序运行时由q_platform.h提供ARCH_STRING,而构建期工具因为编译路径不同(可能不经过完整平台头文件),需要由 Makefile 显式注入该宏。
0007-Meta-Remove-extension-from-main-game-exe.patch
主题:去掉主游戏可执行文件的扩展名。
ioquake3 默认的CLIENTBIN会带.$(ARCH)之类后缀(FULLBINEXT),例如ioquake3.x86_64。这不符合 SerenityOS 可执行文件的习惯命名,也会让package.sh中写死的启动器路径/usr/local/games/quake3/ioquake3对不上。
补丁对Makefile做了 5 处替换($(CLIENTBIN)$(FULLBINEXT)→$(CLIENTBIN)),覆盖:
TARGETS目标列表(含USE_RENDERER_DLOPEN与否两条路径);- 主可执行文件的链接规则(
$(B)/$(CLIENTBIN): $(Q3OBJ) $(LIBSDLMAIN)); - 安装规则(
$(INSTALL) ... $(COPYBINDIR)/$(CLIENTBIN))。
同时renderer_opengl1_$(SHLIBNAME)等渲染器插件的.so命名保持不变,说明只对主二进制去扩展名,动态库插件命名不受影响。
0008-Engine-Use-Serenity-style-PROT_EXEC-mmap.patch
主题:改用 SerenityOS 风格的可执行内存映射。
这是技术含量最高的一个补丁,触及 Quake III VM(虚拟机)JIT 的核心。Quake III 的Q3VM会把字节码即时编译(JIT)为 x86 机器码,存放在一段需要PROT_EXEC权限的内存中。原实现(code/qcommon/vm_x86.c的VM_Compile)的做法是:
vm->codeBase = mmap(NULL, compiledOfs, PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); /* 写入机器码后 */ mprotect(vm->codeBase, compiledOfs, PROT_READ|PROT_EXEC);即先以可写映射分配、写入后再mprotect提升为可执行。SerenityOS 出于安全考虑,对mmap(PROT_EXEC)与mprotect到可执行的行为有更严格的管控(可执行内存需要经过显式的匿名内存对象授权,即wxallowed挂载选项,参见package.sh的post_install)。补丁为 SerenityOS 分支重写了这段逻辑(27 处插入、2 处删除):
#ifdef __serenity__ // Round up by a page for `anon_create` (so we don't blow up in the Kernel) int compiledOfsPageAligned = compiledOfs + (4096u - (compiledOfs % 4096u)); // Create the fd... int fd = anon_create(compiledOfsPageAligned, O_CLOEXEC); if (fd == -1) Com_Error(ERR_FATAL, "VM_CompileX86: anon_create failed (a very bad thing!)"); vm->codeBase = mmap_with_name(NULL, compiledOfs, PROT_WRITE|PROT_READ|PROT_EXEC, MAP_SHARED, fd, 0, "Quake3 VM Page"); if(vm->codeBase == MAP_FAILED) Com_Error(ERR_FATAL, "VM_CompileX86: can't mmap memory"); close(fd); #else /* 原 mmap + mprotect 路径保留 */ #endif关键点:
anon_create()是 SerenityOS 特有的系统调用,创建一个匿名的、可授权给mmap的内存对象(fd),并显式声明对页面的执行权限;O_CLOEXEC防止 fd 泄漏给execve的子进程;- 长度先按 4096 字节页向上取整(
compiledOfs + (4096u - (compiledOfs % 4096u))),注释直言“以免在内核里爆炸”(避免内核因非页对齐大小拒绝); mmap_with_name(...)同样是 SerenityOS 扩展接口,除了映射还附带调试友好的名字"Quake3 VM Page",方便在/proc或内核调试器中识别这块 JIT 内存;- 补丁顶部为
vm_x86.c增加了#include <fcntl.h>、<unistd.h>(注释标明是为pledge())与<serenity.h>(anon_create/mmap_with_name的声明所在); - 由于 Serenity 分支在映射时就直接授予了
PROT_EXEC,后续的mprotect步骤被#ifndef __serenity__跳过。
补丁顺序与依赖关系小结
八个补丁按编号顺序应用,逻辑上存在清晰的依赖链条:
| 编号 | 层次 | 解决问题 | 关键文件 |
|---|---|---|---|
| 0001 | 构建系统 | 平台/架构识别、裁剪特性、新增 Serenity 分支 | Makefile |
| 0002 | 平台抽象 | __serenity__平台定义块 | code/qcommon/q_platform.h |
| 0003 | 头文件 | select()隐式声明、IPv6 组播裁剪 | net_ip.c、con_tty.c、sys_unix.c |
| 0004 | 链接 | 追加-ldl | Makefile |
| 0005 | 代码质量 | 修正 0003 的#ifdef位置 | net_ip.c |
| 0006 | 构建系统 | 工具程序注入ARCH_STRING | Makefile |
| 0007 | 构建系统 | 主可执行文件去扩展名 | Makefile |
| 0008 | 运行期内存 | JIT 可执行内存改用anon_create+mmap_with_name | code/qcommon/vm_x86.c |
依赖关系:0002 提供__serenity__语义基础,0003/0008 的条件编译都依赖它;0004、0006、0007 都是对 0001 所建 Serenity 分支的增量修正;0005 修复 0003 引入的代码风格问题。应用顺序不可随意调换。
移植完成后的运行与验证
补丁全部应用并构建成功后,Quake III Arena 通过 Ports/quake3/package.sh 的启动器元数据(launcher_name="Quake III Arena"、launcher_category="&Games"、launcher_command=/usr/local/games/quake3/ioquake3)出现在 SerenityOS 的游戏菜单中。
需要注意的运行时前提:
- 游戏数据:
post_install明确提示用户需要自行从正版 Quake 3 安装中拷贝baseq3数据到/usr/local/games/quake3/,本移植只负责引擎本身; - wxallowed 挂载:
post_install在/etc/fstab.d/quake3写入bind,nodev,nosuid,wxallowed,这是 0008 中 JIT 可执行内存能够成功映射的配套前提——若缺失,anon_create/mmap_with_name申请PROT_EXEC会被内核拒绝; - 渲染与显示:生成的
autoexec.cfg强制cl_renderer "opengl1"(SerenityOS 上可用的渲染后端)、r_fullscreen "0"、cg_drawfps "1",对应 0001 中仅保留 OpenGL 1 渲染器的构建决策; - 网络:0003/0005 裁掉了 IPv6 组播路径,
select()驱动的传统 UDP 客户端/服务器网络功能保留可用。
从本移植可借鉴的 SerenityOS 移植方法论
结合Ports/.port_include.sh的机制与 quake3 补丁,可以总结出移植大型 C 项目到 SerenityOS 的通用套路:
- 平台探测要硬编码而非运行时探测(0001):交叉编译场景下
uname不可信,用__serenity__宏 + 环境变量(如SERENITY_ARCH)替代; - 头文件依赖要显式(0003):不要指望“恰好被间接包含”,
<sys/select.h>、<sys/types.h>等要按 POSIX 规范显式包含; - 按需裁剪特性(0001 的选项表):未移植的依赖库(OpenAL/cURL/Vorbis 等)一律关闭,减少链接与运行期风险;
- 独立库要显式链接(0004):SerenityOS 的 LibDL 等不并入 libc,
-ldl、-lpthread等缺一不可; - 遵守内存安全模型(0008):可执行内存必须走
anon_create+mmap_with_name(或等价授权路径),并配套wxallowed挂载,且注意页对齐; - 命名与路径符合系统习惯(0007):可执行文件不带架构后缀,安装路径与启动器元数据保持一致;
- 善用自动生成机制:补丁说明 ReadMe.md 由
generate_patch_readme子命令从 commit message 自动生成,补丁可用git am(源码仓库存在时)或patch -p1(普通源码包)两种方式应用,无需手工维护文档与补丁的对应关系。
参考文件索引
- 补丁清单与说明:Ports/quake3/patches/ReadMe.md
- 补丁源码(8 个):Ports/quake3/patches/
- Port 描述与安装逻辑:Ports/quake3/package.sh
- 补丁应用与 ReadMe 生成机制:Ports/.port_include.sh(
patch_internal、do_patch、do_generate_patch_readme) - 涉及的上游引擎文件(补丁目标):
code/qcommon/q_platform.h、code/qcommon/net_ip.c、code/qcommon/vm_x86.c、code/sys/con_tty.c、code/sys/sys_unix.c、Makefile
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考