引言:在狂暴的指令洪流来临前,筑起我们的合金大坝

现代程序员在编写模拟器时,喜欢用昂贵的运行时异常捕捉(如 try-catch)或者依赖操作系统的虚拟内存保护。但在 DOS 6.22 冰冷的 640KB 常规内存荒原上,我们没有任何退路:一个越界的 far 指针踩踏、一次无意的除以零内耗、或者一次显存跨度的计算错位,都会在万分之一微秒内直接将你的 DOSBox-X 烧成一具死锁的僵尸。

我们的模拟器核心目前就像一台裸露着气缸的 V8 发动机。接下来,M68K 的变长指令解码器一旦挂上高档,每秒将触发数十万次的寻址和像素对调。

今天,我们要暂时合上主循环的逻辑,退回工兵阵地。 我们要用纯粹的位运算、行指针自增算法和严密的断言机制,手搓出一套完全属于 16 位实模式的特种工具库。这是我们决战 M68K 指令集心脏前,必须完成的最后一次战术筑基!

一、 军工级内存边界探测库:守护 64KB 主 RAM 净土 (MEM_CHK.C)

世嘉 MD 的主内存(68K RAM)只有区区 64KB,物理地址死死卡在 $FF0000 - $FFFFFF 之间。在 TC3 中,我们用一个标准的远数组来映射它:uint8 far main_ram[65536];

但是在复杂的 CISC 指令运行中,游戏代码经常会用错误的变长寻址去踩踏边界。我们必须手搓一组高效的拦截断言,在常规内存的边缘拉起高压电网:

#include <stdio.h>
#include <stdlib.h>
#include "base_lib.h"

/* 声明我们在大模式远堆区分配的 64KB 主 RAM 物理载体 */
extern uint8 far *main_ram;

/* =====================================================================
   1. 主内存安全边界审查:assert_ram_bounds()
   每当 M68K 指令试图向主 RAM 写入数据时,必须强制通过此关卡拦截。
   利用位运算极速剥离地址,确保每一次吞吐都死死锁在 64KB 净土内!
   ===================================================================== */
void assert_ram_bounds(uint32 addr, uint16 size) {
    /* 屏蔽高位,并剪出主内存的 16位 实际段内偏移量 */
    uint32 real_offset = addr & 0x0000FFFFL;
    
    /* 物理死线检查:一旦发现写入跨度加上偏移量顶爆了 64KB 的物理天花板 */
    if (real_offset + size > 65536UL) {
        printf("\n[FATAL] RAM ACCESS VIOLATION at 0x%08LX (Size: %u)\n", addr, size);
        printf("M68K Core dumped. Protection fault triggered in Real Mode!\n");
        
        /* 强制抛出异常并关闭系统,防止踩踏 DOS 自身的系统引导区 */
        exit(4);
    }
}

/* =====================================================================
   2. 异常中断向量仿真器:trigger_hardware_exception()
   当 M68K 核心在后续解码中撞上非法的未定义操作码,或者除法指令中
   除数为 0 时,此函数将仿真真实的 M68K 硬件异常挂起逻辑。
   ===================================================================== */
void trigger_hardware_exception(uint8 vector_id) {
    printf("\n[CPU EXCEPTION] M68K Vector Interrupt Triggered: #%u\n", vector_id);
    switch(vector_id) {
        case 4:  /* Illegal Instruction */
            printf("  Error: Executed unknown or obfuscated Opcode!\n"); break;
        case 5:  /* Zero Divide */
            printf("  Error: Division by zero inside ALU hardware!\n"); break;
        default:
            printf("  Error: Unhandled core register fault.\n"); break;
    }
    exit(vector_id);
}

二、 卡带总线完整拼图:单字节 UDS/LDS 分流提取器 (ROM_LOAD.C)

在第三篇中,我们手搓了高精度的 get_rom_word(),完成了 16 位宽的总线对账。但游戏在运行中,除了读取 16 位的字,还会频繁使用 MOVE.B 来提取单字节(8位)的数据或字符。

对应我们第二篇起底的 “UDS/LDS 奇偶寻址物理选通阀” 原理,单字节读取天然允许在奇数地址发生!我们必须用纯软件逻辑无缝拼上这块总线拼图:

#include <stdio.h>
#include "base_lib.h"

/* 声明第三篇实现的软 MMU 全局结构体与视口刷新函数 */
extern struct Virtual_MMU mmu;
void far swap_rom_window(uint32 target_addr);

/* =====================================================================
   3. 总线单字节选通提取器局:get_rom_byte()
   完美还原 M68K 芯片上 UDS/LDS 选通电路的分流艺术。
   允许在任何奇偶地址上安全吞吐 8 位数据,绝不触发 Address Error!
   ===================================================================== */
uint8 get_rom_byte(uint32 addr) {
    uint32 real_addr = M68K_ADDR_MASK(addr);
    uint16 offset;
    uint8 far *ptr;
    
    /* 提取当前 window_buffer 的物理段基址与偏移量天线 */
    uint16 w_seg = FP_SEG(mmu.window_buffer);
    uint16 w_off = FP_OFF(mmu.window_buffer);

    /* 边界雷达:若目标字节越过当前 64KB 视口,立刻拉动滑窗页面置换 */
    if (real_addr < mmu.win_base || real_addr >= mmu.win_base + 65536UL) {
        swap_rom_window(real_addr);
    }

    /* 降维计算:算出单字节在 64KB 舞台内的绝对字节偏移量 */
    offset = (uint16)(real_addr - mmu.win_base);

    /* =====================================================================
       【硬核对账】:由于我们在滑窗调入时,已经用内联汇编 XCHG 暴风洗脑了 64KB 区域。
       此时内存里的字节序已经和小端序的 PC 完全对齐!
       我们直接用 PC_MAKE_FAR_PTR 物理对齐单字节远指针,一发入魂抠出数据!
       ===================================================================== */
    ptr = (uint8 far *)PC_MAKE_FAR_PTR(w_seg, w_off + offset);
    return *ptr;
}

三、 降维打击视频流:行指针消隐计算器 (VDP_CALC.C)

现在,让我们把目光投向模拟器最恐怖的算力吞噬者 —— VDP 图形渲染与 VGA 映射。

世嘉 MD 的画面分辨率是 320x224,它是按 8x8 像素的 Tile(图块) 乱序锁在 64KB VRAM 显存里的;而我们的 PC VGA Mode 13h 是平坦的 320x200 连续线性字节阵列。
如果我们在后续每秒 60 帧、262 条扫描线的大循环内,每一次画点都用最平庸的乘法公式去算地址:vga_mem[y * 320L + x] = color; ── Pentium 芯片内部那本就不富裕的乘法器管线会被彻底揉碎卡死!

我们要搞,就搞重工业级的“行指针高速路(Row-Pointer Shortcut)”:

#include <dos.h>
#include "base_lib.h"

/* =====================================================================
   4. 显存行指针自增超频器:get_vga_line_ptr()
   传入当前的扫描线行数(0 - 199 行有效显示区)。
   拒绝主循环内所有的 32 位乘法!利用 TC3 的 MK_FP 宏,在行循环开头
   一瞬间直接算出这一行平坦 VGA 显存的物理远指针。
   ===================================================================== */
unsigned char far *get_vga_line_ptr(uint16 scanline) {
    /* 极其精妙的算力对账:由于 VGA Mode 13h 一行死死固定为 320 字节 */
    /* 我们用一行简单的长整型位移与加法,在微观硬件层面直接代替乘法:(scanline * 256) + (scanline * 64) */
    uint32 row_offset = ((uint32)scanline << 8) + ((uint32)scanline << 6);
    
    /* 直插 PC 显存生死线 0xA000:0000,焊死这一行像素的高速入口指针 */
    return (unsigned char far *)MK_FP(0xA000, (uint16)row_offset);
}

/* =====================================================================
   5. VDP 瓦片像素微观剥离器:get_vram_tile_pixel_index()
   传入正在渲染的 Tile 编号(tile_id),以及当前扫描线落在该方块内部的
   微观横纵坐标(x:0-7, y:0-7)。
   极速从虚拟 VRAM 显存中剥离出该点对应的 4位 调色板颜色索引。
   ===================================================================== */
uint8 get_vram_tile_pixel_index(uint16 far *vram_array, uint16 tile_id, uint8 x, uint8 y) {
    /* MD 的一个 8x8 Tile 占用 32 字节。每 1 字节并排存放 2 个像素的 4位颜色索引 */
    /* 1. 算出该 Tile 在显存数组中的起始字节偏移量 */
    uint32 tile_offset = (uint32)tile_id << 5; /* tile_id * 32 */
    
    /* 2. 定位到当前像素所在的具体行字节(每一行占 4 字节) */
    uint16 byte_index = (uint16)(tile_offset + (y << 2) + (x >> 1));
    
    /* 3. 从显存数组中把这个包含双像素的混合字节揪出来 */
    uint8 raw_byte = *(vram_array + byte_index);
    
    /* =====================================================================
       【分流对账】:根据 X 坐标的奇偶性,解析当前是在读高 4 位还是低 4 位
       ===================================================================== */
    if (x & 1) {
        return raw_byte & 0x0F;        /* 奇数点,卡出低 4 位颜色索引 */
    } else {
        return (raw_byte >> 4) & 0x0F; /* 偶数点,切出高 4 位颜色索引 */
    }
}

四、 工兵连合体演练:特种后勤底盘的疯狂测绘

现在,让我们把今天手搓出来的主 RAM 边界保护网、UDS/LDS 单字节总线选通阀、以及无乘法行指针高速路全部并网运行!

我们将写一个集结主函数,让这套工兵工具箱在 DOS 屏幕上疯狂测绘,为下一章决战 M68K 指令集心脏打下最坚固的后勤地基:

#include <stdio.h>
#include <conio.h>
#include <alloc.h>
#include "base_lib.h"

/* 挂载我们今晚刚刚合龙的三个工兵工具库 */
void assert_ram_bounds(uint32 addr, uint16 size);
uint8 get_rom_byte(uint32 addr);
unsigned char far *get_vga_line_ptr(uint16 scanline);

/* 全局物理硬件镜像在常规内存里的虚空现身 */
uint8 far *main_ram = NULL;
struct Virtual_MMU mmu; /* 确保上一篇的分页结构体可见 */

void main(void) {
    uint16 line;
    unsigned char far *row_scout;
    uint8 test_char_high, test_char_low;
    
    clrscr();
    printf("====================================================\n");
    printf("     MILITARY TOOLKIT ENGAGED: LOGISTICS OK         \n");
    printf("====================================================\n\n");

    /* 1. 激活 64KB 主 RAM 物理载体 */
    main_ram = (uint8 far *)farmalloc(65536UL);
    if (main_ram == NULL) {
        printf("ERROR: Main RAM allocation collapsed!\n");
        exit(1);
    }
    printf("-> Step 1: 64KB Main RAM shield initialized.\n");

    /* 2. 重新强吻第三篇的 1MB 卡带总线 */
    init_software_mmu("D:\\SONIC.BIN", 1048576UL);
    printf("-> Step 2: High-speed ROM single-byte bus armed.\n");

    /* =====================================================================
       【实战测绘 A:单字节总线分流测试】
       读取卡带 $000100 处的系统标识。我们将奇数地址和偶数地址的数据分别提取,
       看它们能否完美避开 Address Error,在屏幕上拼出 SEGA 的钢铁烙印!
       ===================================================================== */
    printf("\n>>> [TEST A] Total Bus Byte-Fetch Validation:\n");
    test_char_high = get_rom_byte(0x000100UL); /* 偶数地址 */
    test_char_low  = get_rom_byte(0x000101UL); /* 奇数地址 */
    printf("    Cartridge total bus handshake byte: '%c%c'\n", test_char_high, test_char_low);

    /* =====================================================================
       【实战测绘 B:安全边界电网测试】
       模拟游戏代码试图向物理地址 $FFFF8000 写入 2 字节。
       我们的边界探测器将对其进行极速安检。
       ===================================================================== */
    printf("\n>>> [TEST B] Shield Assertion Grid Validation:\n");
    assert_ram_bounds(0xFFFF8000UL, 2U);
    printf("    Success: Memory access safe. Bounds assertion passed!\n");

    /* =====================================================================
       【实战测绘 C:行指针无乘法超频测绘】
       模拟 VDP 图形引擎在一瞬间拉通平坦 VGA 显存的前 5 行。
       看我们的行指针高速路能否零内耗地直接吐出物理基址。
       ===================================================================== */
    printf("\n>>> [TEST C] Row-Pointer Shortcut Matrix:\n");
    for (line = 0; line < 5; line++) {
        row_scout = get_vga_line_ptr(line);
        /* 打印出大模式下的【段地址 : 偏移量】物理对账单 */
        printf("    VGA Scanline #%03u Target Shortcut PTR ──> 0x%04X:0x%04X\n", 
               line, FP_SEG(row_scout), FP_OFF(row_scout));
    }

    /* 功成身退,解绑战车 */
    fclose(mmu.rom_fp);
    farfree(mmu.window_buffer);
    farfree(main_ram);
    
    printf("\n====================================================\n");
    printf("  ALL LOGISTICS SECURED! WE ARE OFFICIALLY READY!   \n");
    printf("====================================================\n");
    getch();
}

当你按下 Ctrl+F9,看着屏幕上没有发生任何地址越界崩溃,而是精准无比地吐出了卡带握手字符 'SE',并且在底部瞬间拉出了整整齐齐的 0xA000:0x0000 到 0xA000:0x0500 零乘法开销的物理行指针对账单时……

我们的特种工兵后勤防线,宣布全盘大获全胜! 战车的全套装甲和防护网,已经被我们用这套特种工具库彻底焊死!

五、终极伏笔:微内核远程调试器 Stub 种子 (DEBUG_STB.C)

编写过大型底层系统的极客都明白一个铁律:一个没有调试器的模拟器,就像一艘在黑夜里没有雷达的潜艇。 随着后续 M68K 几百条变长指令全部并网跑起来,一旦游戏在某个未知的操作码上跑飞,光靠一两行 printf 是根本不可能查出到底是哪个虚拟寄存器发生踩踏的。

我们要做就做最绝的。我们要在今晚的工具箱里,秘密埋下一颗类似于 Windows 内核调试(KD)机制的“微内核调试器 Stub 种子”。

  • 跨时空大战略:
    我们不打算在可怜的 640KB 常规内存里去写一个臃肿的图形界面调试器。我们的创意是 ── 利用 PC 的 COM1(串口中断),通过 DOSBox-X 虚拟出的命名管道(Named Pipe),直接把模拟器内部的 CPU 状态,实时吐给现代 Windows 宿主机端的类似 WinDbg 调试前端!
  • 它的战术职责:
    一旦未来模拟器触发了【1号武器】的内存越界或【2号异常】的非法指令,程序不会死锁,而是立刻激活这个 Stub(桩程序),挂起 M68K 的主时钟,通过串口向宿主机发送“救灾警报(Break-in)”,允许我们在现代 Windows 电脑上单步查看虚拟寄存器、甚至实时逆向卡带内存!

虽然今天这个特种雷达还无法全面运转,但今天,我们要把它的中断处理与串口分流管道率先焊死在工具箱里:

#include <dos.h>
#include <stdio.h>
#include "base_lib.h"

/* 模拟 Windows 内核调试器的物理 COM1 串口基地址 (I/O Port) */
#define COM1_BASE_PORT 0x03F8

/* =====================================================================
   6. 串口物理级字符输出:kdb_put_char()
   绕过 DOS 的 Int 21h 慢速文件重定向,直接通过 CPU 汇编级端口操作,
   把调试字符一瞬间通过虚拟串口线,射向宿主机 WinDbg 端!
   ===================================================================== */
void kdb_put_char(uint8 ch) {
    /* 检查发送保持寄存器 (Line Status Register bit 5) 是否为空 */
    /* 如果串口忙,则利用 16位实模式的硬件级空转,卡住节拍等待 */
    while ((inportb(COM1_BASE_PORT + 5) & 0x20) == 0);
    
    /* 物理级一发入魂:数据直接吐进 TX 保持寄存器,打穿次元壁 */
    outportb(COM1_BASE_PORT, ch);
}

/* =====================================================================
   7. 跨平台内核调试断点状态机:kdb_kernel_stub_break()
   当内核发生致命惨剧(如 Address Error 异常),或未来的调试汇编指令
   触发陷阱(TRAP)时,此桩函数将瞬间冻结 M68K 的时间流。
   ===================================================================== */
void kdb_kernel_stub_break(uint8 exception_code, uint32 target_pc) {
    char panic_msg[64];
    char *p = panic_msg;
    int i;

    /* 将突发灾难格式化为一行纯正的内核日志 */
    sprintf(panic_msg, "\n[KDB] CORE BREAK-IN: Code #%u at PC=0x%08LX\r\n", 
            exception_code, target_pc);

    /* =====================================================================
       【跨次元白嫖工具链】:利用我们手搓的串口分流阀,
       将这行灾难快照直接通过物理铜线(或命名管道)强行泵向 Windows 端!
       为将来我们用类似 Windbg 的精美 Win32 界面远程调教模拟器买下完美的引信。
       ===================================================================== */
    while (*p) {
        kdb_put_char((uint8)*p);
        p++;
    }
    
    /* 种子虽然种下,但今天我们先在本地打印一行日志,向全网读者展示我们的野心 */
    printf("  KDB Stub Seed Engaged: Redirected Register dump to COM1 Port (0x03F8).\n");
}

🔮 下期预告:肢解变长机器码的十六路绞肉机与调试雷达

下一期,专栏将正式发表:《第五篇:深入魔窟 ── 手搓 M68K 十六路顶级跳转表与变长操作码肢解机器》。

我们将迎来本专栏开赛以来最巍峨的巅峰决战:

  • 硬核看点一:复刻 M68K 芯片硬件解码器硅片逻辑,亮出由 16 个函数指针组成的顶级跳转表(The Top-Level Jump Table) 源码,用 jmp [table + bx] 的闪电速度,把变长指令集肢解成 16 条清晰的支流!
  • 硬核看点二:死磕 M68K 核心寻址模式 ── 深度手搓 Effective Address (EA,有效地址计算器) 函数,复刻通用寄存器变长寻址。
  • 硬核看点三(与本篇梦幻联动!):远程调试器种子的初次啼鸣! 随着《索尼克 1》的第一条 MOVE 指令在我们的内核里跑起来,一旦程序算错一个指针,我们在今晚埋下的 COM1 串口微内核 Stub 将会瞬间睁开双眼!它会一把锁死 CPU 的咽喉,通过虚拟命名管道,在 Windows 端的接收日志里疯狂刷屏,向我们展示 real-mode 降维重构硬件的无上荣耀!

想要第一手拿到这套 16 路顶级跳转解码器、以及跨平台远程内核调试 Stub 的全套 TC3 架构源码吗?点个订阅,我们下期指令集心脏之巅,看我们如何用串口调教 640KB 常规内存!

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐