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

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

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

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

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

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

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

  4. 可选定时器:可用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:编写任务切换函数(核心)

任务切换的核心是:

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

  2. 将当前SP保存到当前TCB_SP

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

  4. 从新任务的栈恢复寄存器

  5. 执行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中断处理:

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

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

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

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


六、注意事项

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

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

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

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

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

相关推荐
AI科技星1 天前
第三卷:质数王朝志(全卷定稿)
c语言·开发语言·汇编·electron·概率论
H Journey1 天前
汇编基础知识:CPU的寻址逻辑
汇编·cpu寻址
AI科技星2 天前
《全域数学/数术工坊》体系总览
c语言·开发语言·汇编·electron·概率论
H Journey2 天前
用汇编语言写一个hello world,并进行汇编和编译
汇编·assembly·寄存器
逆境不可逃2 天前
一篇速成 汇编程序语言设计之 8086 汇编核心指令
汇编
疯狂打码的少年3 天前
【程序语言与编译】程序设计语言分类(机器/汇编/高级)
汇编·笔记
iCxhust3 天前
qeum能否制定真实u盘启动
虚拟机·微机原理·qeum
JAMSAN09303 天前
16.0% 高增长!全球异构计算架构服务市场扩容态势
汇编·人工智能·架构
iCxhust5 天前
8086汇编 word ptr
汇编·单片机·嵌入式硬件·微机原理·8088单板机