🌐 English version: README.en.md
在《反恐精英 2》(Counter-Strike 2) 的控制台里,用一堆 .cfg 配置文件实现的一颗 RISC-V 处理器。
它不依赖任何模组、插件或外部程序,完全由起源 2 引擎自带的 alias、exec、echo 等控制台指令“拼”出来,支持 RV32I 基础指令集 + Zmmul 乘法扩展,能真正地取指、译码、执行,并跑起 helloworld、fibonacci 之类的程序。其理论上可在竞技模式等 VAC 保护服务器上使用,无需 sv_cheats 1 或外部辅助,使用 say_team 输出。
严格来说它不是一颗“芯片”,而是一台用控制台 alias 系统写成的、以十六进制位宽运算的 CPU 模拟器。至于它能跑起来这件事,已经是奇迹了。
- 纯
.cfg实现:不用 C++、不用插件,所有逻辑都是 Source 2 控制台命令。 - RV32I 完整基础指令集:R / I / S / B / U / J 六种指令格式全部覆盖。
- Zmmul 乘法扩展:支持
mul、mulh、mulhsu、mulhu(不含除法)。 - 32 个通用寄存器 x0–x31:其中 x0 硬连线为 0,符合 RISC-V 规范。
- 64 KB 可寻址内存:字节粒度读写,支持符号扩展的
lb/lh/lbu/lhu/lw与sb/sh/sw。 - 系统调用:通过
ecall实现字符输出、字符输入与停机。 - 自带的 ELF→存储 转换工具链:能把交叉编译出的 RISC-V ELF 直接灌进 cfg 内存。
- 三种运行模式:手动单步、连续运行、异步后台运行。
把 ./Main 目录里所有文件(注意不是把 Main 文件夹本身)复制到 CS2 的 cfg 文件夹内,例如:
D:\SteamLibrary\steamapps\common\Counter-Strike Global Offensive\game\csgo\cfg
从 ./Storages 里挑一个预设好的 storage(例如 helloworld),把整个文件夹(这次要带上文件夹)放进上面同一个 CS2 的 cfg 文件夹内。
预设镜像:
| 文件夹 | 内容 |
|---|---|
Storages/helloworld |
打印 Hello world! 的 C 程序 |
Storages/fibonacci |
斐波那契数列 |
Storages/speedtest |
性能测试用镜像 |
由于输出用的是 say_team,请先进入一张练习模式地图,再打开控制台执行:
exec cpu_run_manual
loop
之后每输出一个字符,都需要再手动输入一遍 loop 让它继续(这是为了防止控制台一次性刷屏卡死)。
Main 里提供了三种入口:
| 入口 | 命令 | 说明 |
|---|---|---|
| 手动 | exec cpu_run_manual |
每输出一个字符需手动再敲一次 loop,最稳 |
| 连续 | exec cpu_run |
直接 loop 跑到底 |
| 异步 | exec cpu_run_async |
用 exec_async 在后台循环,通过 break 让出帧 |
相关控制指令(见 Main/cpu_predefine.cfg):
cpu_pause:暂停执行,之后loop不再继续。cpu_resume:恢复执行。exit:程序停机,输出!!!程序运行完毕!!!。
输出前缀统一为
[[CPU]],配合say_team显示在队伍聊天里。控制字符(换行、回车等)会以[LF]、[CR]这类可读形式显示。
把 ELF / 二进制灌进内存的脚本都在 ./Compiling 下,最终生成一个 storage 文件夹(即 RAM 镜像)。
- 在
./Compiling/C里写好自己的 C 代码(可参考helloworld.c和syscall.h)。 - 运行
Compile.py,它调用 make 交叉编译,产出.elf到./Compiling/Mem。 - 运行
Store.py,选择 elf 文件,输入区块数与栈区块数,生成的存储放进./Compiling/storage。
也可以直接运行 编译+储存.py:这是一个粗糙的编译脚本,它会自动编译 ./Compiling/C 里的代码、生成 storage,并复制到 ./Main/storage 覆盖掉当前内存镜像。
Assembly with riscv-none-elf-as.py:使用 GNU 工具链(riscv-none-elf-as/ld)把./Compiling/Assembly里的.asm汇编成.elf。Assembly with rvasm.py:使用pip install rvasm的轻量汇编器,产出.mem(十六进制文本)。
编译完成后同样运行 Store.py 将其写入 cfg 存储。
指令译码在 Main/inst/cpu_inst_predefine.cfg 中完成,按 opcode 前缀分发到各指令类型:
| 类型 | 指令 |
|---|---|
| R | add sub and or xor sll srl sra slt sltu |
| I | addi andi ori xori slti sltiu slli srli srai |
| L | lb lh lw lbu lhu |
| S | sb sh sw |
| B | beq bne blt bge bltu bgeu |
| J | jal jalr |
| U | lui auipc |
| SYSTEM | ecall ebreak |
| Zmmul | mul mulh mulhsu mulhu |
fence / fence.i(FENCE 类)被识别但忽略——单线程 CPU 用不上内存屏障。
ecall 根据 a7(x17)的值分发(见 Main/inst/cpu_inst_ecall.cfg):
| 调用号 | 名称 | 行为 |
|---|---|---|
10 |
exit |
停机 |
11 |
print_char |
输出 a0(x10)的低 8 位字符 |
12 |
read_char |
暂停并从控制台读入一个字符到 a0 |
对应的 C 内联封装在 Compiling/C/syscall.h:sys_exit()、sys_print_char()、sys_read_char()、sys_print()。
Source 引擎的 alias 本质上是一个文本替换宏:执行一行时,控制台会把别名展开成它保存的字符串再逐条执行。因此:
- 变量:
alias rX0 0—— 把rX0定义成一个“值”,读取它就是在执行替换。 - 赋值:
alias rX0 5—— 重新定义rX0。 - 函数调用:
alias foo "echo hi",执行foo即调用。 - 跳转 / 条件:利用
alias 0 .../alias 1 ...把单个十六进制位或二进制位映射成不同的后续动作。
整颗 CPU 就是由几千行这样的 alias 编织出来的:取指 → 译码 → 读寄存器 → 运算 → 写回 → PC 自增,一步步在控制台里展开执行。
本项目选择以十六进制作为基本位宽。一个十六进制位(0–f)代表 4 bit,通过 alias 0 ... 到 alias f ... 的查表就能一次处理半字节;如果换成纯二进制,指令和数据的展开层数会多出数倍,速度会慢到更没法看。
CPU 内部用若干“内置寄存器”承担数据搬运(定义在 Main/cpu_inner_registers.cfg):
rL:8 bit(2 个十六进制位),内存初步读写。rM:32 bit(8 个十六进制位),通用寄存器读写中转。rX/rY:32 bit,运算操作数。rT:32 bit,临时缓存。rI:当前指令,bI:指令的二进制表示。rP:程序计数器(PC,4 个十六进制位,覆盖 64 KB 地址空间)。rR:内存读取指针(独立于 rP 的内存读取指针,4 个十六进制位,覆盖 64 KB 地址空间)。
每个“寄存器”其实是一串 rXn(n=0–7)的 alias 位,配合 rXncl 这类“加载器”实现按位读写的查表。
以一条 addi 为例,大致流程:
rP指向当前指令地址,通过内存寻址把 32 bit 指令读进rM/rI/bI。cpu_inst_step依次读取 opcode 低 7 位的二进制位,经op_load查表命中opcode_I。cpu_inst_type_i拆出立即数、rs1、funct3、rd,取出rs1进rX,立即数进rY。- 执行
cpu_logic_add_y_to_x(加法由Scripts/generators/AddGenerate.py生成的大查表完成)。 - 结果写回
rd,rP加 4,进入下一条。
逻辑运算(加减、与或异或、移位、比较、乘法)都在 Main/logic/ 里,大部分是预生成的大查表——把 16×16 的结果直接展开成 alias,换取“一步到位”的执行。
- 理论总空间 64 KB,地址从
0x0000开始,用 4 个十六进制位(16 bit)寻址。 - 内存被切成 256 字节一块的“区块”(chunk),每块对应一个
cpu_storage_chunkXX.cfg。由于完整的 64 KB 内存所需文件与别名数过多,本项目只会生成程序所用内存与栈内存所在区块。 - 每个字节存储为两个十六进制字符;寻址器
cpu_adr_chunkXX.cfg负责把 4 位地址路由到具体区块与字节。 - 高地址预留若干区块作为栈使用(
Store.py生成时指定栈区块数)。 - 访问越界时命中
out_load,把读写操作替换为空,不会崩控制台。
生成的每个 chunk 会再拆成若干子文件(__c_0、__c1_0、__lp 等),原因是单个 cfg 里的 alias 展开量过大,需要分片 exec 加载。
RiscV/
├── Main/ # CPU 主体(所有 cfg 逻辑)
│ ├── cpu_main.cfg # 入口,按顺序加载各部分
│ ├── cpu_predefine*.cfg # 预定义“常量/工具/别名”
│ ├── cpu_registers*.cfg # x0–x31 通用寄存器
│ ├── cpu_inner_registers*.cfg # 内部寄存器 rL/rM/rX/rY/rT/rI/rP
│ ├── cpu_run*.cfg # 三种运行入口
│ ├── cpu_system_*.cfg # 系统调用(putchar / readchar)
│ ├── inst/ # 指令译码与各指令类型实现
│ ├── logic/ # 算术/逻辑/移位/比较/乘法 单元
│ ├── storage/ # 当前载入的内存镜像(可被编译脚本覆盖)
│ ├── _cpu_testcases.cfg # 测试用例
│ └── _debug/ # 调试用内存镜像
├── Compiling/ # 程序 → storage 的编译/灌入工具
│ ├── Compile.py # 交叉编译 C 代码
│ ├── Store.py # 解析 ELF/.mem 并写入 cfg 存储
│ ├── 编译+储存.py # 一键编译+灌入+复制到 Main
│ ├── makefile # 交叉编译规则
│ ├── compact.ld # 链接脚本(64KB、代码从 0 开始)
│ ├── C/ # C 源码(helloworld.c、syscall.h)
│ ├── Assembly/ # 汇编测试(speedtest.asm、bugtest.asm)
│ ├── CLI/ # cli_beautify 命令行美化小工具
│ └── Mem/ # 编译产物(.elf/.o/.mem)
├── Scripts/ # 开发辅助脚本
│ ├── 0ExpansionToolkits.py # 交互式 alias 循环展开工具
│ └── generators/ # 生成查表逻辑(加法、移位、与或等)
├── Storages/ # 预设内存镜像(helloworld / fibonacci / speedtest)
├── testcase制造方法.md # 测试用例的生成方法
└── README.md
运行 CPU 【只需要 CS2 本体】,无需任何额外软件。下面这些只在**【自己编译程序】或【编写cfg代码】**时才需要:
| 依赖 | 用途 | 安装 |
|---|---|---|
riscv-none-elf- GNU 工具链 |
交叉编译 C / 汇编 | 需自行安装(前缀可在脚本顶部修改) |
mingw32-make |
Windows 下执行 makefile | 随 MinGW 安装 |
| Python 3 | 运行编译/灌入脚本 | python.org |
pyperclip |
0ExpansionToolkits.py 读写剪贴板 |
pip install pyperclip |
rvasm |
轻量汇编(可选路径) | pip install rvasm |
Compiling/CLI/cli_beautify.py在 Windows 上依赖ctypes/msvcrt,Linux/macOS 走termios,跨平台可用。
通过 Compiling/Assembly/speedtest.asm(300 条 addi + 停机)测得,执行效率达到惊人的:
9.1 Hz
对,不是 MHz,不是 KHz,是 Hz。
由于语言本身的限制(alias 是文本替换而非原生指令),整颗 CPU 没有任何“并行执行”可言,每一条指令都要经历无数次文本展开与查表。说实话,这玩意儿能跑起来,本身就已经是奇迹了。
主要 CPU 模拟器代码全部由人工“古法非遗”编写。AI 方面,主要使用 Deepseek-V4-flash,以下为使用 AI 生成 / 辅助的部分:
Compiling/Store.py中.elf文件的读取(parse_elf)Compiling/Assembly/下的汇编测试(测试速度与 bug,完全由 AI 生成)- C 语言相关(用于生成 RAM,与主要 CPU 代码关系不大)
Compiling/C/syscall.hCompiling/C/helloworld.cCompiling/CLI/*命令行美化小工具Compiling/compact.ld链接脚本Compiling/makefile
Scripts/0ExpansionToolkits.py展开工具,基础框架与十六进制展开由 AI 完成- 开发过程中询问了 AI 以了解 rv32i_zmmul 指令集
Main/_cpu_testcases.cfg中部分测试用例由 AI 生成- 本
README.md由 AI 在readme_待润色.md基础上润色补充
Q:为什么要进练习模式地图?
A:输出用的是 say_team,只有在服务器/地图里(练习模式即可)队伍聊天才会正常显示。
Q:为什么每输出一个字符就要再敲一次 loop?
A:CS2引擎不允许执行 say_team 过快,使用 echo 的话,输出又容易被 exec 产生的大量信息淹没。换用 cpu_run(连续)或 cpu_run_async(异步,需要 sv_cheats 1 )可以避免手动执行 loop ,但是需要先将输出 cfg 代码中的 say_team 替换为 echo。
Q:pause 为什么不生效?
A:pause 是 CS2 的保留指令,项目里改用 cpu_pause / cpu_resume。
Q:能跑多复杂的程序? A:内存只有 64 KB,且速度极慢,适合小型演示程序(helloworld、fibonacci 等)与教学/整活用途。
Q:我可以自己改指令集吗?
A:可以。译码在 Main/inst/cpu_inst_predefine.cfg,各指令在 Main/inst/,运算单元在 Main/logic/,生成器在 Scripts/generators/。