Skip to content

Repository files navigation

CS2//CPU

🌐 English version: README.en.md

在《反恐精英 2》(Counter-Strike 2) 的控制台里,用一堆 .cfg 配置文件实现的一颗 RISC-V 处理器

它不依赖任何模组、插件或外部程序,完全由起源 2 引擎自带的 aliasexececho 等控制台指令“拼”出来,支持 RV32I 基础指令集 + Zmmul 乘法扩展,能真正地取指、译码、执行,并跑起 helloworldfibonacci 之类的程序。其理论上可在竞技模式等 VAC 保护服务器上使用,无需 sv_cheats 1 或外部辅助,使用 say_team 输出。

严格来说它不是一颗“芯片”,而是一台用控制台 alias 系统写成的、以十六进制位宽运算的 CPU 模拟器。至于它能跑起来这件事,已经是奇迹了。


目录


特性

  • .cfg 实现:不用 C++、不用插件,所有逻辑都是 Source 2 控制台命令。
  • RV32I 完整基础指令集:R / I / S / B / U / J 六种指令格式全部覆盖。
  • Zmmul 乘法扩展:支持 mulmulhmulhsumulhu(不含除法)。
  • 32 个通用寄存器 x0–x31:其中 x0 硬连线为 0,符合 RISC-V 规范。
  • 64 KB 可寻址内存:字节粒度读写,支持符号扩展的 lb/lh/lbu/lhu/lwsb/sh/sw
  • 系统调用:通过 ecall 实现字符输出、字符输入与停机。
  • 自带的 ELF→存储 转换工具链:能把交叉编译出的 RISC-V ELF 直接灌进 cfg 内存。
  • 三种运行模式:手动单步、连续运行、异步后台运行。

快速开始

1. 安装 CPU 本体

./Main 目录里所有文件(注意不是把 Main 文件夹本身)复制到 CS2 的 cfg 文件夹内,例如:

D:\SteamLibrary\steamapps\common\Counter-Strike Global Offensive\game\csgo\cfg

2. 载入一份程序(内存镜像)

./Storages 里挑一个预设好的 storage(例如 helloworld),把整个文件夹(这次要带上文件夹)放进上面同一个 CS2 的 cfg 文件夹内。

预设镜像:

文件夹 内容
Storages/helloworld 打印 Hello world! 的 C 程序
Storages/fibonacci 斐波那契数列
Storages/speedtest 性能测试用镜像

3. 运行

由于输出用的是 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 镜像)。

方式一:C 语言

  1. ./Compiling/C 里写好自己的 C 代码(可参考 helloworld.csyscall.h)。
  2. 运行 Compile.py,它调用 make 交叉编译,产出 .elf./Compiling/Mem
  3. 运行 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.hsys_exit()sys_print_char()sys_read_char()sys_print()


工作原理

起源引擎的 alias 就是“变量 + 函数”

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 为例,大致流程:

  1. rP 指向当前指令地址,通过内存寻址把 32 bit 指令读进 rM / rI / bI
  2. cpu_inst_step 依次读取 opcode 低 7 位的二进制位,经 op_load 查表命中 opcode_I
  3. cpu_inst_type_i 拆出立即数、rs1funct3rd,取出 rs1rX,立即数进 rY
  4. 执行 cpu_logic_add_y_to_x(加法由 Scripts/generators/AddGenerate.py 生成的大查表完成)。
  5. 结果写回 rdrP 加 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 没有任何“并行执行”可言,每一条指令都要经历无数次文本展开与查表。说实话,这玩意儿能跑起来,本身就已经是奇迹了。


AI 使用

主要 CPU 模拟器代码全部由人工“古法非遗”编写。AI 方面,主要使用 Deepseek-V4-flash,以下为使用 AI 生成 / 辅助的部分:

  • Compiling/Store.py.elf 文件的读取(parse_elf
  • Compiling/Assembly/ 下的汇编测试(测试速度与 bug,完全由 AI 生成)
  • C 语言相关(用于生成 RAM,与主要 CPU 代码关系不大)
    • Compiling/C/syscall.h
    • Compiling/C/helloworld.c
    • Compiling/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/

About

A RISC-V processor implemented in the console of Counter-Strike 2 using a pile of .cfg configuration files.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages