至此(day1-day4)代码详解
ipl10.nas
nasm
; ==================== 第一阶段引导程序 ====================
; 功能:读取磁盘数据并跳转到第二阶段加载程序
; 编译参数:nask -o ipl10.bin ipl10.nas
CYLS EQU 10 ; 预设读取柱面数(实际值由BIOS决定)
ORG 0x7c00 ; BIOS加载引导程序的标准内存地址
; ---------- FAT12文件系统头部信息(必须严格符合规范)----------
JMP entry
DB 0x90 ; 跳转指令的填充字节
DB "HARIBOTE" ; 引导扇区名称(8字节)
DW 512 ; 每扇区字节数(必须512)
DB 1 ; 每簇扇区数(必须1)
DW 1 ; FAT表起始扇区(从第1扇区开始)
DB 2 ; FAT表数量(必须2)
DW 224 ; 根目录最大文件数(标准FAT12值)
DW 2880 ; 磁盘总扇区数(1.44MB软盘标准值)
DB 0xf0 ; 磁盘类型标识(0xf0表示3.5寸软盘)
DW 9 ; 每个FAT表长度(9扇区)
DW 18 ; 每磁道扇区数(标准1.44MB软盘)
DW 2 ; 磁头数量(双面)
DD 0 ; 隐藏扇区数(无分区设为0)
DD 2880 ; 磁盘总扇区数(与前面相同)
DB 0,0,0x29 ; 扩展引导标记(固定值)
DD 0xffffffff ; 卷序列号(随机生成)
DB "HARIBOTEOS "; 卷标(11字节)
DB "FAT12 " ; 文件系统类型(8字节)
RESB 18 ; 保留空间(填充18字节)
; ---------- 程序主体 ----------
entry:
; 初始化段寄存器
MOV AX,0 ; 将AX清零
MOV SS,AX ; 堆栈段寄存器
MOV SP,0x7c00 ; 堆栈指针指向引导程序起始地址
MOV DS,AX ; 数据段寄存器
; 设置磁盘读取参数
MOV AX,0x0820 ; 数据加载到内存0x8200处
; ES 是段寄存器(Extra Segment Register),在实模式下,x86 处理器使用段寄存器和偏移寄存器组合来形成内存地址。
; ES 通常用于附加数据段,与偏移寄存器 BX 配合使用,即 ES:BX 可以构成一个 20 位的内存地址。
; 在这段代码里,ES 用于指定磁盘读取数据要加载到的内存段地址,后续通过 ES:BX 确定数据加载的具体目标地址。
; 在后续的代码中,`MOV BX,0` 结合当前的 ES 寄存器值,确定了磁盘读取数据的起始目标地址;
; 并且在 `next` 标签处,通过修改 ES 的值来更新目标地址,以实现连续读取磁盘数据到不同内存位置的功能。
MOV ES,AX ; ES:BX构成目标地址
MOV CH,0 ; 起始柱面号0
MOV DH,0 ; 磁头号0
MOV CL,2 ; 起始扇区号2(跳过引导扇区)
readloop:
MOV SI,0 ; 错误计数器清零
retry:
; 调用BIOS磁盘读取中断(INT 13h AH=02h)
MOV AH,0x02 ; 功能号:读取扇区
MOV AL,1 ; 读取1个扇区
MOV BX,0 ; ES:BX=目标地址
MOV DL,0x00 ; 驱动器号0(A盘)
; 调用BIOS磁盘读取中断(INT 13h AH=02h),以下是该中断使用的寄存器及返回值说明:
; 输入寄存器:
; AH = 0x02 表示读取扇区功能
; AL = 要读取的扇区数量,此处为1
; CH = 起始柱面号,此处为0
; CL = 起始扇区号,此处为2(跳过引导扇区)
; DH = 磁头号,此处为0
; DL = 驱动器号,此处为0x00(A盘)
; ES:BX = 数据加载的目标内存地址,此处ES = 0x0820,BX = 0,即目标地址为0x8200
;
; 返回寄存器:
; CF(进位标志):如果CF = 0,表示操作成功;如果CF = 1,表示操作失败
; AH = 返回状态码,0 表示成功,其他值表示错误
; AL = 实际读取的扇区数量
INT 0x13
JNC next ; 成功则继续,JNC 英文全称为 Jump if Not Carry
; 错误处理(最多重试5次)
ADD SI,1 ; 错误计数加1
CMP SI,5 ; 超过5次则报错
; JAE 是 "Jump if Above or Equal" 的缩写,意思是如果高于或等于则跳转
JAE error
MOV AH,0x00 ; 驱动器复位功能
MOV DL,0x00 ; 驱动器号0
; 调用BIOS磁盘中断服务,此处AH = 0x00 表示驱动器复位功能,DL = 0x00 表示驱动器号为A盘,
; 该中断用于在磁盘读取失败时尝试复位驱动器,以便重新进行读取操作。
INT 0x13
JMP retry ; 重试读取
next:
; 准备读取下一个扇区
MOV AX,ES ; 当前内存地址
ADD AX,0x0020 ; 每次增加512字节(0x200)
MOV ES,AX ; 更新目标地址
ADD CL,1 ; 扇区号加1
CMP CL,18 ; 是否读完1磁道(18扇区)
JBE readloop ; 未读完继续
; 切换磁头/柱面
MOV CL,1 ; 扇区号重置为1
ADD DH,1 ; 切换磁头(0->1)
CMP DH,2
JB readloop ; 磁头0/1切换
MOV DH,0 ; 磁头重置为0
ADD CH,1 ; 柱面号加1
CMP CH,CYLS ; 是否读完预设柱面
JB readloop ; 继续读取
; ---------- 加载完成 ----------
MOV [0x0ff0],CH ; 保存实际读取的柱面数到0x0ff0
JMP 0xc200 ; 跳转到第二阶段加载程序(asmhead)
; ---------- 错误处理 ----------
error:
MOV SI,msg ; 加载错误信息地址
putloop:
; LODSB 指令用于从 DS:SI 所指向的内存地址加载一个字节到 AL 寄存器中。
; 在此处,SI 寄存器已经在前面的代码中被赋值为 msg 标签的地址,即错误信息字符串的起始地址。
; 因此,当前加载的地址就是错误信息字符串在内存中的地址,由 DS 段寄存器和 SI 偏移地址共同确定。
; 随着每次执行 LODSB 指令,SI 寄存器的值会自动加 1,指向下一个字符的地址。
LODSB ; 加载字符到AL
CMP AL,0 ; 是否字符串结尾
; JE 是 "Jump if Equal" 的缩写,意思是如果相等则跳转。
; 此处将 AL 寄存器的值与 0 比较后,若相等(即字符串结束),则跳转到 fin 标签处,结束错误信息的显示。
JE fin
MOV AH,0x0e ; BIOS显示字符功能
MOV BX,15 ; 显示颜色(白色)
INT 0x10 ; 调用BIOS视频中断服务,功能为显示字符。
;在前面已经设置了AH = 0x0e(BIOS显示字符功能)和BX = 15(显示颜色为白色),通过该中断将AL寄存器中的字符显示在屏幕上。
JMP putloop
fin:
HLT ; 停止CPU
JMP fin ; 进入死循环
msg:
DB 0x0a, 0x0a ; 两个换行符
DB "load error"; 错误信息
DB 0x0a ; 换行符
DB 0 ; 字符串结束符
RESB 0x7dfe-$ ; 填充至510字节
DB 0x55, 0xaa ; 引导扇区结束标志(0x55AA)
;; 常用段寄存器使用场景:
; --------------------------------------------------
; 1. ES(附加段寄存器)使用场景:
;MOV AX,0x0820
;MOV ES,AX ; ◀ 与BX配合指定磁盘数据加载地址0x8200
; BIOS中断INT13h要求目标地址必须通过ES:BX指定
; 2. DS(数据段寄存器)使用场景:
; MOV DS,AX ; ◀ 用于访问程序数据段(如msg字符串)
;MOV AL,[SI] ; ◀ 默认使用DS段访问内存
; 3. SS(堆栈段寄存器)使用场景:
;MOV SS,AX ; ◀ 设置堆栈段地址
;MOV SP,0x7c00 ; ◀ SP与SS配合构成堆栈地址
; 4. SI(源索引寄存器)典型用法:
;MOV SI,msg ; ◀ 作为字符串/数组的偏移地址
;LODSB ; ◀ 从DS:SI地址读取字节到AL
; 5. DI(目标索引寄存器)典型用法:
;MOV DI,0x8200 ; ◀ 作为磁盘数据加载地址
;MOV BX,DI ; ◀ 与ES配合指定磁盘数据加载地址0x8200
;MOV [DI],AL ; ◀ 默认使用DS段访问内存
;各段寄存器使用规则:
;1. ES :用于需要额外数据段的场景(磁盘I/O、字符串操作)
;2. DS :默认数据访问段(变量/数组访问)
;3. SS :堆栈操作自动使用(PUSH/POP/CALL/RET指令)
;4. SI/DI :
;- SI通常与DS配合(DS:SI)作为源地址
;- DI通常与ES配合(ES:DI)作为目标地址
;- 在ipl10.nas中SI被用作错误计数器是特殊用法
asmhead.nas
nasm
; ==================== 第二阶段加载程序 ====================
; 功能:设置保护模式、初始化硬件、加载操作系统核心
; 编译参数:nask -o asmhead.bin asmhead.nas
; ---------- 内存地址常量 ----------
BOTPAK EQU 0x00280000 ; 操作系统核心加载地址
DSKCAC EQU 0x00100000 ; 磁盘缓存区(主)
DSKCAC0 EQU 0x00008000 ; 磁盘缓存区(临时)
; ---------- 引导信息结构体地址 ----------
CYLS EQU 0x0ff0 ; 柱面数存储地址(来自ipl)
LEDS EQU 0x0ff1 ; 键盘指示灯状态
VMODE EQU 0x0ff2 ; 视频模式(8位=256色)
SCRNX EQU 0x0ff4 ; 水平分辨率(X轴)
SCRNY EQU 0x0ff6 ; 垂直分辨率(Y轴)
VRAM EQU 0x0ff8 ; 显存起始地址
ORG 0xc200 ; 程序加载地址(ipl读取的数据)
; ---------- 视频模式初始化 ----------
MOV AL,0x13 ; 设置VGA模式13h(320x200x8位)
MOV AH,0x00 ; 功能号:设置视频模式
; INT 0x10 是BIOS提供的视频服务中断。在前面的代码中,已经将AL寄存器设置为0x13,AH寄存器设置为0x00。
; AL = 0x13 表示要设置的VGA模式为13h,即320x200像素的8位彩色模式。
; AH = 0x00 是BIOS视频服务的功能号,代表设置视频模式。
; 当执行 INT 0x10 指令时,BIOS会根据AH和AL寄存器的值来设置相应的视频模式。
INT 0x10 ; 调用BIOS视频中断
; 保存视频参数到引导信息结构体
; BYTE、WORD、DWORD 用于指定操作数的大小,具体根据存储数据的实际需求和对应内存地址所代表的数据类型来确认。
; VMODE 存储颜色位数,8 位即可表示,所以用 BYTE(1 字节)。
MOV BYTE [VMODE],8 ; 记录颜色位数
; SCRNX 和 SCRNY 分别存储水平和垂直分辨率,通常用 16 位整数表示,所以用 WORD(2 字节)。
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
; VRAM 存储显存起始地址,32 位系统中地址用 32 位表示,所以用 DWORD(4 字节)。
MOV DWORD [VRAM],0x000a0000 ; 标准VGA显存地址
; ---------- 获取键盘状态 ----------
MOV AH,0x02 ; 功能号:获取键盘指示灯状态
; INT 0x16 是BIOS提供的键盘服务中断。在前面的代码中,已经将AH寄存器设置为0x02,代表功能号为获取键盘指示灯状态。
; 当执行 INT 0x16 指令时,BIOS会根据AH寄存器的值执行相应的操作,并将键盘指示灯的状态存储在AL寄存器中。
INT 0x16 ; 调用键盘BIOS中断
MOV [LEDS],AL ; 保存状态到引导信息
; ---------- 禁用中断控制器 ----------
; IRQ(Interrupt Request)即中断请求,是外部设备向CPU发出的请求信号,用于告知CPU有紧急事件需要处理。
; PIC(Programmable Interrupt Controller)即可编程中断控制器,负责管理和分配来自外部设备的中断请求。
;主PIC的端口号是0x20 - 0x21,从PIC的端口号是0xA0 - 0xA1。
; NOP(No Operation)是一条空操作指令,执行该指令时CPU不进行任何实际操作,仅消耗一个时钟周期,常用于硬件延时等待。
; OUT指令用于将数据从CPU寄存器输出到指定的I/O端口。
; CLI(Clear Interrupt Flag)是一条汇编指令,用于清除CPU的中断标志位,禁止CPU响应外部中断请求。
MOV AL,0xff ; 屏蔽所有IRQ,将AL寄存器设置为0xff,0xff的二进制表示为全1,意味着屏蔽所有中断请求
OUT 0x21,AL ; 主PIC,将AL的值输出到主PIC的中断屏蔽寄存器(端口号0x21),从而屏蔽主PIC管理的所有中断
NOP ; 延时等待(硬件要求),执行空操作以满足硬件的时序要求
OUT 0xa1,AL ; 从PIC,将AL的值输出到从PIC的中断屏蔽寄存器(端口号0xa1),从而屏蔽从PIC管理的所有中断
CLI ; 禁止CPU响应中断,清除CPU的中断标志位,使CPU不再响应外部中断请求
; ---------- 启用A20地址线 ----------
; 在实模式下,CPU只能访问1MB的内存空间,这是因为地址线只有20根(A0 - A19)。
; 而A20地址线的作用是允许CPU访问超过1MB的内存空间,进入保护模式后通常需要启用它。
; 键盘控制器可以用来控制A20地址线的状态,下面的代码就是通过键盘控制器来启用A20地址线。
; 调用waitkbdout函数,等待键盘控制器的输入缓冲区为空,即键盘控制器空闲。
; 因为在向键盘控制器发送新命令之前,需要确保它已经处理完之前的请求。
CALL waitkbdout ; 等待键盘控制器空闲
; 将命令码0xd1移动到AL寄存器中。0xd1这个命令的作用是告诉键盘控制器,接下来要向其输出端口写入数据。
MOV AL,0xd1 ; 命令:写入输出端口
; 通过OUT指令将AL寄存器中的命令码0xd1发送到键盘控制器的命令端口0x64。
; 0x64是键盘控制器的命令端口,用于接收各种控制命令。
OUT 0x64,AL ; 发送到键盘控制器
; 再次调用waitkbdout函数,等待键盘控制器处理完刚才发送的命令,确保其准备好接收下一个数据。
CALL waitkbdout
; 将值0xdf移动到AL寄存器中。0xdf这个值用于启用A20门信号,从而允许访问超过1MB的内存空间。
MOV AL,0xdf ; 启用A20门信号
; 通过OUT指令将AL寄存器中的值0xdf发送到键盘控制器的数据端口0x60。
; 0x60是键盘控制器的数据端口,用于发送和接收数据。
OUT 0x60,AL
; 最后一次调用waitkbdout函数,等待键盘控制器处理完启用A20门信号的操作。
CALL waitkbdout
; ---------- 切换到保护模式 ----------
[INSTRSET "i486p"] ; 启用486指令集
; LGDT 是一条汇编指令,用于加载全局描述符表寄存器(GDTR)。
; [GDTR0] 表示从内存地址 GDTR0 处读取数据,该数据包含全局描述符表(GDT)的界限和基地址。
; 全局描述符表是保护模式下用于管理内存段的重要数据结构,通过加载 GDTR,CPU 可以知道 GDT 的位置和大小。
LGDT [GDTR0] ; 加载全局描述符表
; CR0 是控制寄存器 0,它包含了多个控制位,用于控制 CPU 的工作模式和状态。
; 这里将 CR0 的值移动到 EAX 寄存器中,目的是为了后续对其进行修改。
MOV EAX,CR0 ; 获取控制寄存器
; 0x7fffffff 的二进制表示为除了最高位(第 31 位)为 0 外,其余位全为 1。
; 通过 AND 操作,将 EAX 的第 31 位清零,即关闭分页功能。在切换到保护模式初期,通常先关闭分页。
AND EAX,0x7fffffff ; 关闭分页(bit31=0)
; 0x00000001 的二进制表示为最低位(第 0 位)为 1,其余位为 0。
; 通过 OR 操作,将 EAX 的第 0 位置为 1,这一位是保护模式标志位,置 1 表示开启保护模式。
OR EAX,0x00000001 ; 开启保护模式(bit0=1)
; 将修改后的 EAX 的值写回到 CR0 寄存器中,从而更新 CPU 的控制状态,正式进入保护模式。
MOV CR0,EAX ; 更新控制寄存器
; 当切换到保护模式后,CPU 的指令流水线中可能还存在实模式下的指令。
; 执行 JMP 指令跳转到 pipelineflush 标签处,目的是清空指令流水线,确保后续执行的是保护模式下的指令。
JMP pipelineflush ; 清空指令流水线
pipelineflush:
; 设置段寄存器为数据段选择子(索引1)
; 在保护模式下,段寄存器存储的是段选择子,而不是段基址。
; 段选择子是一个16位的值,用于在全局描述符表(GDT)中索引对应的描述符。
; 每个描述符的大小为8字节,因此索引乘以8得到段选择子的值。
; 这里选择索引为1的描述符,即GDT中的第1项(索引从0开始)。
MOV AX,1*8 ; 选择子=1(GDT第1项)
; DS(Data Segment)是数据段寄存器,用于指向数据段。
; 将段选择子加载到DS寄存器,这样后续的数据访问操作将基于该数据段。
MOV DS,AX ; 数据段
; ES(Extra Segment)是附加段寄存器,通常用于辅助数据操作。
; 同样将段选择子加载到ES寄存器,使其与DS指向相同的数据段。
MOV ES,AX ; 附加段
; FS(Far Segment)和GS(General Segment)是额外的段寄存器,可用于各种特殊用途。
; 这里也将它们设置为相同的段选择子,以确保数据访问的一致性。
MOV FS,AX ; 额外段...
MOV GS,AX
; SS(Stack Segment)是堆栈段寄存器,用于指向堆栈段。
; 将段选择子加载到SS寄存器,确定堆栈段的位置。
MOV SS,AX
; ---------- 复制操作系统核心 ----------
; 复制bootpack到高端内存
MOV ESI,bootpack ; 源地址(编译时确定)
MOV EDI,BOTPAK ; 目标地址0x280000
MOV ECX,512*1024/4 ; 复制512KB(每次4字节)
CALL memcpy
; 复制启动扇区到缓存区
MOV ESI,0x7c00 ; 源地址(ipl自身)
MOV EDI,DSKCAC ; 目标地址0x100000
MOV ECX,512/4 ; 512字节
CALL memcpy
; 复制剩余磁盘数据
MOV ESI,DSKCAC0+512 ; 源地址(临时缓存+512)
MOV EDI,DSKCAC+512 ; 目标地址(主缓存+512)
MOV ECX,0
MOV CL,BYTE [CYLS] ; 获取实际柱面数
IMUL ECX,512*18*2/4 ; 计算总扇区数(柱面*18扇区*2磁头)
SUB ECX,512/4 ; 减去已复制的启动扇区
CALL memcpy
; ---------- 启动操作系统核心 ----------
MOV EBX,BOTPAK ; 核心程序入口地址
MOV ECX,[EBX+16] ; 获取代码段长度
ADD ECX,3 ; 对齐到4字节边界
SHR ECX,2 ; 转换为双字数
JZ skip ; 无需复制则跳过
MOV ESI,[EBX+20] ; 源地址(ELF结构中的偏移)
ADD ESI,EBX ; 转换为绝对地址
MOV EDI,[EBX+12] ; 目标地址(程序加载地址)
CALL memcpy ; 执行复制
skip:
MOV ESP,[EBX+12] ; 设置堆栈指针
JMP DWORD 2*8:0x0000001b ; 跳转到核心入口
; ---------- 辅助函数 ----------
waitkbdout: ; 等待键盘控制器就绪
IN AL,0x64 ; 读取状态寄存器
AND AL,0x02 ; 检查输入缓冲区状态
JNZ waitkbdout ; 忙则继续等待
RET
memcpy: ; 内存复制函数(4字节对齐)
MOV EAX,[ESI] ; 读取4字节
ADD ESI,4 ; 源地址+4
MOV [EDI],EAX ; 写入4字节
ADD EDI,4 ; 目标地址+4
SUB ECX,1 ; 计数器减1
JNZ memcpy ; 循环直到ECX=0
RET
; ---------- GDT定义 ----------
ALIGNB 16 ; 16字节对齐
GDT0:
RESB 8 ; 空描述符(必须存在)
; 数据段描述符(可读写,32位)
DW 0xffff,0x0000,0x9200,0x00cf
; 代码段描述符(可执行,32位)
DW 0xffff,0x0000,0x9a28,0x0047
DW 0 ; 填充字节
GDTR0: ; GDT寄存器值
DW 8*3-1 ; GDT界限(3个描述符*8字节-1)
DD GDT0 ; GDT基地址
ALIGNB 16 ; 对齐到16字节
; 目前这里为空,可能是因为操作系统核心程序的具体内容还未编写完成,
; 或者是在开发过程中暂时预留该入口,后续再填充具体的代码逻辑。
; 下面可以添加一些示例代码作为占位,实际开发时需要替换为真实的核心程序代码。
bootpack:
; 示例:打印一条简单的消息到屏幕(伪代码,实际需要具体实现)
; MOV AL, 'H'
; MOV AH, 0x0E ; BIOS 显示字符功能号
; INT 0x10
; 可以继续添加更多初始化或启动相关的代码
RET ; 返回调用处,实际核心程序可能不会返回
bootpack.c
c
/* 硬件操作接口声明 */
void io_hlt(void); // 暂停CPU执行
void io_cli(void); // 清除中断标志
void io_out8(int port, int data); // 向指定端口输出8位数据
int io_load_eflags(void); // 读取EFLAGS寄存器
void io_store_eflags(int eflags); // 写入EFLAGS寄存器
/* 图形相关函数声明 */
void init_palette(void); // 初始化调色板
void set_palette(int start, int end, unsigned char *rgb); // 设置调色板
void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1); // 绘制矩形
void init_screen(char *vram, int x, int y); // 初始化屏幕布局
void putfont8(char *vram, int xsize, int x, int y, char c, char *font); // 显示8x16字体
/* 颜色定义 */
#define COL8_000000 0 // 黑色
#define COL8_FF0000 1 // 亮红色
#define COL8_00FF00 2 // 亮绿色
#define COL8_FFFF00 3 // 亮黄色
#define COL8_0000FF 4 // 亮蓝色
#define COL8_FF00FF 5 // 亮紫色
#define COL8_00FFFF 6 // 亮青色
#define COL8_FFFFFF 7 // 白色
#define COL8_C6C6C6 8 // 亮灰色
#define COL8_840000 9 // 暗红色
#define COL8_008400 10 // 暗绿色
#define COL8_848400 11 // 暗黄色
#define COL8_000084 12 // 暗蓝色
#define COL8_840084 13 // 暗紫色
#define COL8_008484 14 // 暗青色
#define COL8_848484 15 // 暗灰色
/* 引导信息结构体(由asmhead.nas传递)*/
struct BOOTINFO {
char cyls; // 读取的柱面数
char leds; // 键盘LED状态
char vmode; // 视频模式
char reserve; // 保留字段
short scrnx; // 屏幕X分辨率
short scrny; // 屏幕Y分辨率
char *vram; // 显存起始地址
};
/* 主函数 */
void HariMain(void)
{
// 从内存0x0ff0获取引导信息
// 就是那个asmhead.nas文件给引导结构体分配的地址空间
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
// 字母'A'的点阵数据(8x16像素)
static char font_A[16] = {
0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24,
0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00
};
init_palette(); // 初始化调色板
init_screen(binfo->vram, binfo->scrnx, binfo->scrny); // 绘制屏幕
// 在(10,10)位置显示白色字母A
putfont8(binfo->vram, binfo->scrnx, 10, 10, COL8_FFFFFF, font_A);
// 无限循环保持系统运行
for (;;) {
io_hlt();
}
}
/* 初始化调色板 */
void init_palette(void)
{
// 16色RGB配置表(R, G, B 各占1字节)
static unsigned char table_rgb[16 * 3] = {
0x00, 0x00, 0x00, // 0:黑色
0xff, 0x00, 0x00, // 1:亮红色
0x00, 0xff, 0x00, // 2:亮绿色
0xff, 0xff, 0x00, // 3:亮黄色
0x00, 0x00, 0xff, // 4:亮蓝色
0xff, 0x00, 0xff, // 5:亮紫色
0x00, 0xff, 0xff, // 6:亮青色
0xff, 0xff, 0xff, // 7:白色
0xc6, 0xc6, 0xc6, // 8:亮灰色
0x84, 0x00, 0x00, // 9:暗红色
0x00, 0x84, 0x00, // 10:暗绿色
0x84, 0x84, 0x00, // 11:暗黄色
0x00, 0x00, 0x84, // 12:暗蓝色
0x84, 0x00, 0x84, // 13:暗紫色
0x00, 0x84, 0x84, // 14:暗青色
0x84, 0x84, 0x84 // 15:暗灰色
};
set_palette(0, 15, table_rgb); // 设置0-15号颜色
}
/* 设置VGA调色板 */
void set_palette(int start, int end, unsigned char *rgb)
{
int i, eflags;
eflags = io_load_eflags(); // 保存中断标志
io_cli(); // 禁用中断
// 设置调色板寄存器
io_out8(0x03c8, start); // 起始颜色编号
for (i = start; i <= end; i++) {
// 写入RGB值(VGA寄存器需要0-63范围的值)
io_out8(0x03c9, rgb[0] / 4); // 红色分量
io_out8(0x03c9, rgb[1] / 4); // 绿色分量
io_out8(0x03c9, rgb[2] / 4); // 蓝色分量
rgb += 3; // 移动到下一个颜色
}
io_store_eflags(eflags); // 恢复中断标志
}
/* 矩形填充函数 */
void boxfill8(unsigned char *vram, int xsize, unsigned char c,
int x0, int y0, int x1, int y1)
{
int x, y;
// 遍历矩形区域
for (y = y0; y <= y1; y++) {
for (x = x0; x <= x1; x++) {
vram[y * xsize + x] = c; // 计算显存地址并写入颜色
}
}
}
/* 初始化屏幕布局 */
void init_screen(char *vram, int x, int y)
{
// 绘制背景
boxfill8(vram, x, COL8_008484, 0, 0, x-1, y-29);
// 绘制状态栏边框
boxfill8(vram, x, COL8_C6C6C6, 0, y-28, x-1, y-28);
boxfill8(vram, x, COL8_FFFFFF, 0, y - 27, x - 1, y - 27);
boxfill8(vram, x, COL8_C6C6C6, 0, y - 26, x - 1, y - 1);
boxfill8(vram, x, COL8_FFFFFF, 3, y - 24, 59, y - 24);
boxfill8(vram, x, COL8_FFFFFF, 2, y - 24, 2, y - 4);
boxfill8(vram, x, COL8_848484, 3, y - 4, 59, y - 4);
boxfill8(vram, x, COL8_848484, 59, y - 23, 59, y - 5);
boxfill8(vram, x, COL8_000000, 2, y - 3, 59, y - 3);
boxfill8(vram, x, COL8_000000, 60, y - 24, 60, y - 3);
boxfill8(vram, x, COL8_848484, x - 47, y - 24, x - 4, y - 24);
boxfill8(vram, x, COL8_848484, x - 47, y - 23, x - 47, y - 4);
boxfill8(vram, x, COL8_FFFFFF, x - 47, y - 3, x - 4, y - 3);
boxfill8(vram, x, COL8_FFFFFF, x - 3, y - 24, x - 3, y - 3);
return;
}
/* 显示8x16点阵字体 */
// c 填的是颜色
void putfont8(char *vram, int xsize, int x, int y, char c, char *font)
{
int i;
char *p, d;
for (i = 0; i < 16; i++) { // 遍历16行
p = vram + (y + i) * xsize + x; // 计算当前行起始地址
d = font[i]; // 获取点阵数据
// 检查每个bit位是否置1
if ((d & 0x80) != 0) p[0] = c; // 第1像素
if ((d & 0x40) != 0) p[1] = c; // 第2像素
if ((d & 0x20) != 0) { p[2] = c; }
if ((d & 0x10) != 0) { p[3] = c; }
if ((d & 0x08) != 0) { p[4] = c; }
if ((d & 0x04) != 0) { p[5] = c; }
if ((d & 0x02) != 0) { p[6] = c; }
if ((d & 0x01) != 0) { p[7] = c; }
}
return;
}
主要功能模块说明:
- 硬件交互 :通过 io_ 系列函数实现端口操作和中断控制
- 颜色管理 :16色调色板配置,包含亮色/暗色系
- 显存操作 :
- boxfill8 实现矩形填充
- putfont8 实现字符显示(8像素宽 x 16像素高)
- 屏幕布局 : init_screen 创建窗口化界面效果
- 引导信息 :通过 BOOTINFO 结构体获取屏幕参数
该代码实现了操作系统的图形界面基础功能,包括:
- 调色板初始化
- 屏幕绘制
- 字符显示
- 硬件中断管理
- 显存直接操作
naskfunc.nas
nasm
; 操作系统底层硬件操作接口
; 编译参数:nask -o naskfunc.obj naskfunc.nas
[FORMAT "WCOFF"] ; 生成Windows COFF格式目标文件
[INSTRSET "i486p"] ; 支持486保护模式指令集
[BITS 32] ; 生成32位代码
[FILE "naskfunc.nas"] ; 源文件名(调试信息用)
; 全局函数声明(供C语言调用)
GLOBAL _io_hlt, _io_cli, _io_sti, _io_stihlt
GLOBAL _io_in8, _io_in16, _io_in32
GLOBAL _io_out8, _io_out16, _io_out32
GLOBAL _io_load_eflags, _io_store_eflags
[SECTION .text] ; 代码段开始
; --------------------------------------------------
; void io_hlt(void);
; 功能:暂停CPU直到中断发生
_io_hlt:
HLT ; 执行HLT指令
RET
; --------------------------------------------------
; void io_cli(void);
; 功能:禁用中断(Clear Interrupt)
_io_cli:
CLI ; 清除中断标志
RET
; --------------------------------------------------
; void io_sti(void);
; 功能:启用中断(Set Interrupt)
_io_sti:
STI ; 设置中断标志
RET
; --------------------------------------------------
; void io_stihlt(void);
; 功能:启用中断并暂停CPU
_io_stihlt:
STI ; 先启用中断
HLT ; 然后暂停CPU
RET
; --------------------------------------------------
; int io_in8(int port);
; 功能:从指定端口读取8位数据
_io_in8:
MOV EDX,[ESP+4] ; 从栈中获取端口号(参数1)
MOV EAX,0 ; 清空EAX高位
IN AL,DX ; 从端口DX读取8位到AL
RET ; 返回值通过EAX传递
; --------------------------------------------------
; int io_in16(int port);
; 功能:从指定端口读取16位数据
_io_in16:
MOV EDX,[ESP+4] ; 获取端口号
MOV EAX,0 ; 清空EAX高位
IN AX,DX ; 读取16位到AX
RET
; --------------------------------------------------
; int io_in32(int port);
; 功能:从指定端口读取32位数据
_io_in32:
MOV EDX,[ESP+4] ; 获取端口号
IN EAX,DX ; 读取32位到EAX
RET
; --------------------------------------------------
; void io_out8(int port, int data);
; 功能:向指定端口写入8位数据
_io_out8:
MOV EDX,[ESP+4] ; 获取端口号(参数1)
MOV AL,[ESP+8] ; 获取数据(参数2,8位)
OUT DX,AL ; 向端口DX写入AL
RET
; --------------------------------------------------
; void io_out16(int port, int data);
; 功能:向指定端口写入16位数据
_io_out16:
MOV EDX,[ESP+4] ; 获取端口号
MOV EAX,[ESP+8] ; 获取数据(16位)
OUT DX,AX ; 写入AX到端口
RET
; --------------------------------------------------
; void io_out32(int port, int data);
; 功能:向指定端口写入32位数据
_io_out32:
MOV EDX,[ESP+4] ; 获取端口号
MOV EAX,[ESP+8] ; 获取数据(32位)
OUT DX,EAX ; 写入EAX到端口
RET
; --------------------------------------------------
; int io_load_eflags(void);
; 功能:读取EFLAGS寄存器值
_io_load_eflags:
PUSHFD ; 将EFLAGS压入栈
POP EAX ; 弹出到EAX作为返回值
RET
; --------------------------------------------------
; void io_store_eflags(int eflags);
; 功能:设置EFLAGS寄存器值
_io_store_eflags:
MOV EAX,[ESP+4] ; 获取参数值
PUSH EAX ; 压入栈
POPFD ; 弹出到EFLAGS
RET