欢迎光临
我们一直在努力

用汇编在8088单板机上创建一个进程

在8088单板机上用汇编语言创建进程,本质上是实现一个极简的多任务切换机制。由于8088工作在实模式、无MMU,所有进程共享同一物理地址空间,进程切换的核心是保护/恢复CPU寄存器现场(即"上下文切换")。

一、整体思路:协作式多任务模型

在资源有限的8088单板机上(典型配置:RAM约32KB、ROM 64KB、无MMU),常见的实现方式是协作式、单地址空间、中断驱动的微型OS。其核心特点:

  • 单地址空间:所有任务共享同一内存地址空间,任务间独立依赖编程约定,没有硬件保护

  • 协作式调度:任务必须主动调用调度函数(yield)让出CPU,非抢占式

  • 切换核心:保存当前任务的寄存器到其私有栈,恢复下一个任务保存的寄存器,通过IRET返回执行

  • 可选定时器:可用8253定时器产生周期中断(如每20ms),在中断中调用调度器

  • 本文采用协作式实现(无定时器抢占),这样代码最简洁,适合教学理解。你也可以在此基础上加入中断调用。

    二、核心数据结构:任务控制块(TCB)

    每个任务需要一个任务控制块(TCB)来记录其运行状态。在8088实模式下,一个极简TCB只需保存:

    • 任务的下一个TCB指针(用于循环调度)

    • 任务的栈指针备份(SS:SP)

    • 任务的程序入口(CS:IP)

    为实现简单,我们直接让每个任务拥有独立栈区域,TCB只保存栈指针备份。

    text

    ; TCB结构定义(4字节)
    TCB_Next dw 0 ; 下一任务TCB指针
    TCB_SP dw 0 ; 任务栈指针备份(SS:SP)

    实际实现中,还可以添加一个字节作为任务状态标志(就绪/运行/等待),但协作式调度下最简单的是循环就绪队列。

    三、实现步骤详解

    步骤1:定义TCB数组和任务栈

    ; 定义最大任务数和TCB区域
    MAX_TASKS EQU 3
    TCB_TABLE DW MAX_TASKS DUP(0, 0) ; 每任务占2个字:Next指针 + SP备份
    CurrentTask DW 0 ; 当前运行的任务索引 (0~MAX_TASKS-1)

    ; 定义各任务的私有栈区域(每任务256字节)
    Task0Stack DB 256 DUP(0)
    Task1Stack DB 256 DUP(0)
    Task2Stack DB 256 DUP(0)

    步骤2:初始化TCB与任务栈

    在系统启动时,为每个任务设置栈指针和任务入口。

    InitTasks:
    mov cx, MAX_TASKS ; 任务个数
    mov si, offset Task0Stack ; 指向任务0的栈底部
    mov di, offset TCB_TABLE ; 指向TCB表
    xor bx, bx ; 任务索引,从0开始

    InitTask:
    ; 计算任务栈顶(栈底+256-2,因为栈向下增长)
    mov ax, ss ; 内核栈段
    add si, 256 – 2
    mov [di+2], si ; 保存栈指针备份到TCB_SP
    ; 链接TCB: 下一任务指针
    mov ax, bx
    inc ax
    cmp ax, MAX_TASKS
    jb SetNext
    xor ax, ax ; 最后一个任务指向任务0
    SetNext:
    shl ax, 1 ; 每个TCB占4字节(Next + SP),索引*2得偏移
    add ax, offset TCB_TABLE
    mov [di], ax ; 保存Next指针
    ; 更新到下一个任务
    add di, 4
    inc bx
    add si, 256 – 2 – 256 ; si恢复到下一个任务的栈底
    loop InitTask

    ret

    更常用的做法:在每个任务的私有栈顶预先压入任务入口CS:IP和FLAGS,这样第一次切换时通过IRET即可直接进入任务执行。下面展示如何在任务栈中构造初始上下文:

    ; 初始化任务0栈(构造返回上下文)
    mov ax, SEG Task0_Entry
    mov bx, OFFSET Task0_Entry
    push ax ; 压入CS
    push bx ; 压入IP
    pushf ; 压入FLAGS(模拟IRET弹出的状态)

    将Task0Stack作为栈底,栈指针指向上述压栈后的位置,存到TCB_SP。切换时用IRET直接弹出FLAGS、IP、CS进入任务。

    步骤3:编写任务切换函数(核心)

    任务切换的核心是:

  • 保存当前任务的寄存器(AX,BX,CX,DX,SI,DI,BP,DS,ES等)到其栈

  • 将当前SP保存到当前TCB_SP

  • 从当前TCB.Next获取下一个TCB的SP并加载到SS:SP

  • 从新任务的栈恢复寄存器

  • 执行IRET返回新任务继续执行

  • 实际代码实现如下:

    TaskSwitch:
    ; 保存当前任务现场到栈中
    push ax
    push bx
    push cx
    push dx
    push si
    push di
    push bp
    push ds
    push es

    ; 保存当前任务的栈指针到TCB_SP
    mov bx, [CurrentTask] ; 当前任务索引
    shl bx, 1 ; 索引*4得TCB偏移(每个TCB4字节)
    shl bx, 1
    add bx, offset TCB_TABLE + 2 ; TCB_SP的偏移
    mov [bx], sp ; 保存当前SP到TCB_SP

    ; 切换到下一个任务
    sub bx, 2 ; bx指向TCB.Next
    mov bx, [bx] ; 取下一任务的TCB指针
    mov ax, [bx] ; 取下一任务的TCB_SP
    mov sp, ax ; 切换栈

    ; 恢复新任务现场
    pop es
    pop ds
    pop bp
    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax

    iret

    步骤4:创建新任务的入口

    每个任务是一个独立的程序段,任务结束后必须调用调度器或无限循环,避免CPU失控:

    Task0_Entry:
    ; 任务0代码
    call Task1_Start
    jmp $ ; 或调用调度器循环

    为方便演示,可让每个任务在控制台输出字符串后主动让出CPU:

    Task0_Entry:
    mov dx, offset Msg0
    call PrintString
    call Yield ; 主动让出CPU
    jmp Task0_Entry ; 无限循环

    Task1_Entry:
    mov dx, offset Msg1
    call PrintString
    call Yield
    jmp Task1_Entry

    步骤5:让出CPU函数(Yield)

    Yield函数调用TaskSwitch实现切换:

    Yield:
    call TaskSwitch
    ret

    或使用软件中断实现系统调用:

    ; 设置INT 21h系统调用
    SysCall:
    cmp ah, 1
    je Yield_Service
    iret

    Yield_Service:
    call TaskSwitch
    iret

    四、完整代码示例

    下面给出一个完整的协作式多任务切换汇编程序,在8088/DOS下可直接汇编运行。

    ; **************************************************
    ; 8088 单板机多任务切换演示(协作式调度)
    ; 编译: MASM mytask.asm
    ; 链接: LINK mytask.obj
    ; 运行: mytask.exe
    ; **************************************************

    CODE SEGMENT
    ASSUME CS:CODE, DS:DATA, SS:STACK

    START:
    ; 初始化DS
    mov ax, DATA
    mov ds, ax

    ; 初始化TCB表
    call InitTasks

    ; 设置当前任务为任务0
    mov [CurrentTask], 0

    ; 设置初始栈(从任务0开始)
    mov bx, offset TCB_TABLE + 2 ; TCB_SP of task0
    mov sp, [bx]

    ; 模拟IRET 进入任务0
    pop ax
    pop bx
    pop cx
    pop dx
    pop si
    pop di
    pop bp
    pop ds
    pop es
    iret

    ; 初始化任务栈和TCB
    InitTasks:
    mov cx, MAX_TASKS
    mov si, offset Task0Stack
    mov di, offset TCB_TABLE
    xor bx, bx

    InitLoop:
    ; 为任务栈顶构造初始返回上下文
    mov ax, SEG Task0_Entry
    push ax
    mov ax, OFFSET Task0_Entry
    push ax
    pushf

    ; 保存栈指针到TCB
    mov sp, si
    add sp, 254
    mov [di+2], sp

    ; 创建TCB链表
    mov ax, bx
    inc ax
    cmp ax, MAX_TASKS
    jb SetNext
    xor ax, ax
    SetNext:
    shl ax, 1
    add ax, offset TCB_TABLE
    mov [di], ax

    add di, 4
    inc bx
    add si, 256
    loop InitLoop
    ret

    ; 任务切换函数
    TaskSwitch:
    push ax
    push bx
    push cx
    push dx
    push si
    push di
    push bp
    push ds
    push es

    mov bx, [CurrentTask]
    shl bx, 1
    shl bx, 1
    add bx, offset TCB_TABLE + 2
    mov [bx], sp

    sub bx, 2
    mov bx, [bx]
    mov sp, [bx + 2]

    pop es
    pop ds
    pop bp
    pop di
    pop si
    pop dx
    pop cx
    pop bx
    pop ax

    iret

    ; Yield系统调用
    Yield:
    call TaskSwitch
    ret

    ; 任务0入口
    Task0_Entry:
    mov dx, offset Msg0
    call PrintString
    call Yield
    jmp Task0_Entry

    ; 任务1入口
    Task1_Entry:
    mov dx, offset Msg1
    call PrintString
    call Yield
    jmp Task1_Entry

    ; 简单字符打印(调用DOS中断)
    PrintString:
    mov ah, 9
    int 21h
    ret

    CODE ENDS

    DATA SEGMENT
    Msg0 db 'Task 0 running…', 0Dh, 0Ah, '$'
    Msg1 db 'Task 1 running…', 0Dh, 0Ah, '$'
    MAX_TASKS equ 2
    TCB_TABLE dw MAX_TASKS DUP(0, 0)
    CurrentTask dw 0
    DATA ENDS

    STACK SEGMENT
    Task0Stack db 256 DUP(?)
    Task1Stack db 256 DUP(?)
    STACK ENDS

    END START

    五、在此基础上扩展

    如果你想在此基础上加入定时器抢占(时间片轮转) ,可以在代码中加入8253/8259中断处理:

  • 初始化8253定时器:设置每20ms产生一次中断

  • 编写中断服务例程:在中断中保存当前寄存器、调用TaskSwitch切换任务、恢复新任务

  • 在中断向量表中注册:将中断向量指向定时器ISR,清除中断屏蔽寄存器允许中断

  • 这样就能实现简单的基于时间片的抢占式多进程了。


    六、注意事项

  • 堆栈溢出风险:每个任务栈只分配了256字节,实际使用要注意避免递归或大局部变量导致栈溢出

  • 共享资源冲突:多任务共享打印等资源时,需要自己加锁或确保互斥(协作式下可依靠主动让出规避)

  • 无内存保护:一个任务的错误可能破坏其他任务数据,需要开发者严格遵守编程约定

  • 单板机运行:如果是真正的8088单板机(无DOS环境),需要自己实现字符输出的硬件级代码,通过8255并口或串口驱动显示

  • 希望这份代码能帮助你在8088上跑起自己的微型"多进程"系统!

    赞(0)
    未经允许不得转载:171主机测评 » 用汇编在8088单板机上创建一个进程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址