Day1
第四节我贴出部分源代码:
; hello-os
; TAB=4
ORG 0x7c00 ; 指明装载的地址
; 以下的记述用于标准FAT12格式的软盘
JMP entry
DB 0x90
DB "HELLOIPL" ; 启动区扇的名称可以是任意字符串(8字节)
DW 512 ; 每个扇区sector的大小(必为512字节)
DB 1 ; 簇cluster的大小
DW 1 ; FAT的起始位置(一般从第一个扇区开始)
DB 2 ; FAT的个数(必须为2)
DW 224 ;根目录的大小(一般为224项)
DW 2880 ; 该磁盘的大小(必须为2880扇区)
DB 0xf0 ; 磁盘的种类(必须为0xf0)
DW 9 ; FAT的长度(必须是9扇区)
DW 18 ; 1个磁道track有几个扇区(必须是18)
DW 2 ; 磁头数(必须是2)
DD 0 ;不使用分区(必须是0)
DD 2880 ; 重写一次磁盘的大小
DB 0,0,0x29 ;
DD 0xffffffff ;
DB "HELLO-OS " ; 磁盘的名称(11字节)
DB "FAT12 " ; 磁盘格式名称(8字节)
RESB 18 ;
后面就不贴了,有一些之前没遇见的,mark一下:
RESB 0x1fe-$
RESB reserver byte 预约字节 比如RESB 10,对比nask ,不仅是把指定的地址空出来,还会自动填充0x00
美元符号可以告诉我们这一行现在的字节数,以便汇编语言自动计算出需要输出多少个00
day2
第一节主要讲了一些汇编语言的知识,之前上课都有学过所以还是比较轻松的,主要解释了一下; hello-os
; TAB=4
ORG0x7c00; 指明装载的地址
; 以下的记述用于标准FAT12格式的软盘
JMPentry
DB0x90
;省略
; 程序主体
entry:
MOVAX,0; 初始化寄存器
MOVSS,AX
MOVSP,0x7c00
MOVDS,AX
MOVES,AX
MOVSI,msg
putloop:
MOVAL,[SI]
ADDSI,1; 给SI加1
CMPAL,0
JEfin
MOVAH,0x0e; 显示一个文字
MOVBX,15; 指定字符颜色
INT0x10; 调用显卡BIOS
JMPputloop
fin:
HLT; 让CPU停止,等待指令
JMPfin; 无限循环
msg:
DB0x0a, 0x0a; 换行两次
DB”hello, world”
DB0x0a; 换行
DB0
第二节:BIOS是basic input output systemPU等待的指令,可以让CPU进入待机状态,省电
还有显示文字的一些操作
第三节讲了Makefile文件的入门
day 3
IPL initial program loader 启动程序装载器
这次新添加的代码
MOV AX,0x0820
MOV ES,AX
MOV CH,0 ; 柱面0
MOV DH,0 ; 磁头0
MOV CL,2 ; 扇区2
MOV AH,0x02 ; AH=0x02 : 读盘
MOV AL,1 ; 1个扇区
MOV BX,0
MOV DL,0x00 ; A驱动器
INT 0x13 ; 调用磁盘BIOS
JC error ;jump if carry 如果标志位是1的话跳转
es=0x0820,bx=0,所以软盘的数据将被装载到内存中0x8200~0x83ff,0x8000~0x81ff的512字节是留给启动区的。
这里还考虑了出错太多次,所以加了代码,设置出错五次后就真正放弃。
补充一下一些软盘的知识:
每个扇区可存储128×2的N次方(N=0.1.2.3)字节的数据(一般为512B),扇区为数据存储的最小单元
一般向一个空软盘保存文件时,
(1)文件名会卸载0x002600以后的地方
(2)文件的内容会写在0x004200以后的地方
在后面几节引入了C语言,顿时多了很多文件,也不知道啥意思
Day 4
C语言与画面显示的练习
- 与C语言能联合使用的只有EAX,ECX,EDX这三个,至于其他寄存器,只能使用值,不能改变值。
- CLI 将中断标志置0的指令
- STI 将中断标志置1的指令
- 像素坐标(x,y)对应的VRAM地址计算公式:
- 0xa0000+x+y*320
- boxfill8用一个双层循环,将像素点填充
Day 5
结构体、文字显示与GDT/IDT初始化
在第四天的bootpack.c中绘制界面部分,都是将一些数字直接写入程序,这样的程序是没法维护的。所以第五天首先把这些数字用变量代替,后期更改需要更改的时候只需更改一处即可。
源程序意外准备的数据,都需要加上extern属性:
extern char hankaku[4096]
sprintf函数
GDT global segment descriptor table 全局段号记录表
IDT interrupt descriptor table 中断记录表
中断机制
- 这里采用分段内存管理, 段寄存器表示 段的起始地址
段需要有以下信息
段的大小
段的起始地址
段的管理属性
但是, cpu的段寄存器只有16bit, 那么可以模仿调色板的做法, 建立段寄存器与段号之间的一种映射关系, 由于段寄存器德尔低三位表示权限, 可以使用的只有13bit, 因而, 可以处理 0 ~ 8191 的区域 (64KB)这个就是我们的 GDT(全局段号记录表)
IDT 是中断记录表
为了处理外设的各种事件, 一般可以采用轮询方式, 不过轮询的频率不好控制, 太慢, 外设得不到响应, 太快, cpu做无意义处理, 因而, 一般采用 中断方式
本质上相当于, 将监控外设的任务委托给了第三方对象, 让第三方对象负责处理监控事件, 然后cpu专心处理手头的任务, 也可以理解成 cpu 请了一个小蜜
Day6
主要讲了一些程序简化的问题,首先是源程序分割:
源程序分割:
graphic.c 关于描画的处理
dsctbl.c 关于GDT,IDT等descriptor table 的处理
bootpack.c 其他处理
接下来是整理makefile文件,对于相同的可以归纳为一般规则,“%”
然后是整理头文件,将各个源程序的函数定义等都放到头文件里
下面讲这个函数:
_load_gdtr: ; void load_gdtr(int limit, int addr);
MOV AX,[ESP+4] ; limit
MOV [ESP+6],AX ;为了能读到FF FF 00 27 00 00
LGDT [ESP+6]
RET
DWORD[ESP+4]里存放段上限,DWORD[ESP+8]里存放的是地址(0x0000ffff和0x00270000)。按字节写:FF FF 00 00 00 27 00
struct SEGMENT_DESCRIPTOR {
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;
};
段基址:base 32位 分别为low(2字节),mid(1字节),high(1字节)
上限: limit 20位(1M) 标志位Gbit,标志位是1的时候,limit的单位解释为page,1页4k,4k×1M=4G
段属性:acess_right 12位 其中高4位在limit_high的高4位里
PIC programable interrupt controller 可编程中断控制 是将8个中断信号集合成一个中断信号的装置
主PIC 从PIC 通过2号IRQ相连
PIC的寄存器:都是8位
IMR interrupt mask register 中断屏蔽寄存器
ICW initial control word 初始化控制数据
ICW2 决定了IR 以哪一号中断通知CPU
鼠标是IRQ12 键盘是IRQ1, INT 0x2c INT 0x21
CPU如何查找IDT?
与GDT的原理一样,利用了一个叫IDTR的寄存器(注意此处是寄存器,不是内存了,需要和IDT区别对待)。GDTR中高32位表示GDT在内存中的基地址,低16位表示表界限,所以就明确的给出了GDT在内存中的存储信息。
Day7
1.获取按键编码
io_out8(PIC0_OCW2,0X61)
2.加快中断处理
将在中断处理程序inthandler21()中打印按键码的功能分离开来,使的“打印字符”这一耗时的功能由主函数来负责,从而降低中断服务程序的执行时间,从而更早地解放PIC,使之来得及响应其他中断。
先将按键的编码接收下来,保存到变量里,然后由hariMain偶尔查看这个变量。如果发现有了数据,就把它显示出来。
结构体:keybuf
3.io_stihlt()
4.FIFO缓冲区
5.FIFO缓冲区改进:在同一个数组中维护同写入和读入数据,当到达尾部时重新回到开头
6.
struct FIFO8 {
unsigned char *buf;
int p, q, size, free, flags;
};
size:缓冲区的总字节数
free:缓冲区里没有数据的字节数
buf:缓冲区的地址
p:next_w
q:next_r
7.要让鼠标控制电路和鼠标本身都有效
鼠标控制电路包含在键盘控制电路里
8.主机和鼠标之间的通信命令有很多。主机向鼠标发出的每一个字节和命令鼠标都必须采用0xFA应答,但是重传命令0xFE除外。
0xF4表示设置鼠标使能。
9.鼠标电路的初始化
鼠标电路对应的一个端口是 0x64, 通过读取这个端口的数据来检测鼠标电路的状态,内核会从这个端口读入一个字节的数据,如果该字节的第二个比特位为0,那表明鼠标电路可以接受来自内核的命令,因此,在给鼠标电路发送数据前,内核需要反复从0x64端口读取数据,并检测读到数据的第二个比特位,知道该比特位为0时,才着手发送控制信息,代码如下:
Day 8
1.变量mouse_phase用来记住接收鼠标数据的工作进展到了什么阶段。接收到的数据就放在mouse_dbuf[0~2]中。
等待鼠标的0xfa的状态
2.创建了一个结构体mouse_dec,把解读鼠标所需要的变量都归总到一起,还将MariMain()函数中,对从鼠标队列中取数据的操作独立出来,制成mouse_decode()函数,简化了MariMain()函数。在enable_mouse()函数中将phase置0,初始化。
3.增加结构体struct MOUSE_DEC的成员,x、y、btn,x和y好像分别记录的是鼠标的左、右移动的信息,btn记录的是鼠标按键的信息:无按键时btn为0;左按键按下时为1,右按键时为2,中间按键时为4。
4.鼠标移动的显示
5.32位:
代码部分:
首先设置PIC,把中断全部屏蔽掉
置为A20位,使得cpu可以访问1mb以上的地址空。向
控制电路的附属端口输出0xdf,位了让A20GATE信号线编程ON状态。
使用486指令,设置临时GDT和CRO寄存器,进行不用颁保护模式,然后设置段寄存器,建立段寄存器与GDT的关系。
重新划分内存空间,即将1MB以内的程序,复制到1MB以外的内存空间
配置bootpack.Hrb,然后跳转到bootpack.Hrb,执行其中第一条指令,
6。保护模式:在这种模式下,应用程序既不能随便改变段的设定,又不能使用操作系统专用的段。操作系统受到CPU的保护
CR0寄存器:
启用保护模式PE(Protected Enable)位(位0)和开启分页PG(Paging)位(位31)分别用于控制分段和分页机制
PE用于控制分段机制。如果PE=1,处理器就工作在开启分段机制环境下,即运行在保护模式下。如果PE=0,则处理器关闭了分段机制,并如同8086工作于实地址模式下。PG用于控制分页机制。如果PG=1,则开启了分页机制。如果PG=0,分页机制被禁止,此时线性地址被直接作为物理地址使用。
memecpy(转送源地址,转送目的地址,转送数据的大小)
转送数据大小是以双字节为单位的,所以数据大小用字节数除以4来指定。
Day 9
1.分割源程序
2.先检查是386还是 486,再进行内存检查。
cache memory 高速缓冲存储器
eflag寄存器的第18位应该是AC标志位
内存检查:内存检查即是通过对内存的所有存储单元先写入数据,再读出数据,检查所读出的数据与原来写入的数据是否相等,若内存无错(包括有内存),则应该相等,否则不等。在我们的程序中,检查内容通过反转进行两步检查,使结果可靠性更高。这里由于编译器会优化程序,所以我们用汇编语言来实现。
*p^=0xffffffff *p=*p^0xffffffff
3.内存管理:内存分配,内存释放
用一个结构体表示内存总体使用情况,结构体主要有两个成员,一个整形变量记录空闲内存块数,一个结构体数组记录每个内存块的地址和大小(内存块大小不是固定的),这相对于上面的方法更具灵活性。而且占用内存少,分配和释放操作快,但是缺点当内存空间过于零乱时,结构体数组要非常大,否则容易丢失内存块。
struct FREEINFO { /* 可用状况 */
unsigned int addr, size;
};
struct MEMMAN { /* 内存管理 */
int frees, maxfrees, lostsize, losts;
struct FREEINFO free[MEMMAN_FREES];
};
Day 10
1.内存管理
将内存块的申请和释放以4KB为单位,以此节省man->free[i]的使用量。
向下舍入:把最几位数字强制变成0:与运算
向上舍入:先判断最后几位,如果本来是0则什么也不做,不是零就
if((i&0xfff)!=0)
{i=(i&0xfffff000)+0x1000;}
程序如下:
unsigned int memman_alloc_4k(struct MEMMAN *man, unsigned int size)
{
unsigned int a;
size = (size + 0xfff) & 0xfffff000;//向上舍入:先+0xfff再向下舍入,0xfff相当于判断进位
a = memman_alloc(man, size);
return a;
}
int memman_free_4k(struct MEMMAN *man, unsigned int addr, unsigned int size)
{
int i;
size = (size + 0xfff) & 0xfffff000;
i = memman_free(man, addr, size);
return i;
}
2.在画面上进行叠加显示,类似于将绘制了图案的透明图层叠加在一起
结构体说明:
struct SHEET { //透明图层
unsigned char *buf;
int bxsize, bysize, vx0, vy0, col_inv, height, flags;
};
struct SHTCTL { //图层管理
unsigned char *vram;
int xsize, ysize, top;//top代表最上面图层的高度
struct SHEET *sheets[MAX_SHEETS];
struct SHEET sheets0[MAX_SHEETS];
};
首先使用memman_alloc_4k来分配用于记忆图层控制变量的内存空间。
sheet_refresh 从下到上描绘所有的图层
关键在于bx在for语句中并不是在0到bxsize之间循环,而是在bx0到bx1之间循环
Day 11
1.让鼠标可以超出桌面范围以外
2.实现画面外的支持:
2.shtctl的指定省略:在struct sheet中加入struct shectl *ctl
在init中增加一句,给每个图层的ctl赋值。还行需要修改各个函数调用的参数,以及头文件。
3.显示窗口:先准备一张图层,然后在图层缓冲区内描绘一个窗口
make_window8(),窗口的图层是设置为1
4.高速计数器:不做HLT,全力计数
5.取消闪烁:
仅对refresh对象及其以上的图层进行刷新就可以了。refreshsub加一个参数:h0 for循环从h=h0开始,修改相应参数
开辟一块内存,大小和VRAM一样,称为map,这块内存用来表示画面是的点都是那个图层的像素
day12
PIT 可编程的间隔型定时器 连接着IQR0
我们可以通过设定PIT让定时器每隔多少秒就产生一次中断。io_out8(PIT_CNT0, 0x9c); //中断周期的低八位
io_out8(PIT_CNT0, 0x2e); //中断周期的高八位 2e9c 11932 100HZ 1秒中断100次设置多个定时器:
优化:将timeout的含义从”所剩时间”改变为”予定时间”,这样就可以去掉inthandler20(int*)函数里的”timerctl.timer[i].timeout–”。 现在的定时器,每隔42949673秒(497天)后count就是0xFFFFFFFF了,在这之前必须重启计算机,否则程序就会出错。因此让OS每隔一年自动调整一次。
追加变量timectl.next,记住下一个时刻
timer数组按timeout升序排序,在inthandler20(int*)中每次只检查第一个timer元素即可。
上一步中,发现超时时,inthandler20(int*)会准备下一个要检查的timer,这延长了处理时间。为解决这个问题,增加变量using,用于记录有几个定时器处于活动中(需要检查)。
day13
- 用一个函数将原来的涂背景,写字符,刷新进行了简化。
将缓冲区集中成一个,通过向缓冲区内写入的定时器数据的不同来进行定时器的判断。
性能测试:
0043422332
0044179237
0043324329
需要注意的是要启动3秒后,将count置为0.将定时器、键盘输入、鼠标输入都放在同一个FIFO缓冲区里。
结构体 fifo23 全局变量-
之前提到的到移位,对于定时器少的情况,移位对性能的影响不大, 但对于多任务,很多应用程序同时运行,每个程序都使用定时器,如果还使用移位的话,就有点浪费时间了。尤其在中断处理程序中进行大量的移位,更是不优雅。
我们的具体做法是,当一个定时器超时时,通过next将下一个要超时的定时器的地址赋给存放所有定时器地址的数组的第一位,那么下次超时出现时,数组的第一位存的就是当前发生超时的定时器的地址。也就是我们不必再维护一个存有正在使用定时器地址的数组了,只需设一个变量t0用来存储下一个要超时的定时器的地址,所有这些简化都要归功于一开始创建各个定时器时,根据超时时间,用各自的属性next,以链表的方式从早到晚链接起来,设想一下,第一个(也就是最早的)定时器超时了,将它的next赋给t0,那么t0存的就是下一个超时的定时器的地址,依次进行下去。这样就简化了不少,而且逻辑清晰。
使用数据结构中的”哨兵”概念简化上一步的链表处理函数。”哨兵”是为了简化循环的边界条件而引入的。在timers链表最后加上一个timeout为0xFFFFFFFF的定时器(作为哨兵)。由于OS会在1年后将定时器count重置,所以这个哨兵定时器永远不会到达触发的时候
day 14
- 继续的性能测试
-
VBE:显卡公司虽然无法合并为一,但市场不接受各自为政的混乱标准,因此多家显卡公司协商成立了VBE,即视频电子标准协会。VBE制作了专用的BIOS,基本上可以兼容所有的显卡分辨率设置。这个BIOS就称为VBE。可以说VBE就是显卡公司之间统一的度量衡。
设置320*200这样的低分辨率时,使用”AH=0; AL=画面模式号码;INT 0x10;”
设置640*480等高分辨率时,要使用”AX=0x4f02;BX=画面模式号码;INT 0x10;”VBE的画面模式号如下:
0x101……640*480*8bit彩色
0x103……800*600*8bit彩色
0x105……1024*768*8bit彩色
0x107……1280*1024*8bit彩色实际使用的时候,要把画面模式号加上0x4000,再赋值到BX中。
-
要进行三个判断:
VBE是否存在
版本够不够
模式是否有问题:颜色数是否为8;是否为调色板模式 ;画面模式号码可否加上0x4000再进行指定首先要判断是否能使用VBE:给ES赋值为0x9000,DI赋值为0,AX赋值为0x4f00,再执行INT 0x10。如果VBE存在,AX就会变成0x004f,否则就只能使用320*200的分辨率了。
day15
TSS:任务状态段 其中EIP是CPU用来记录下一条需要执行的指令位于内存中的地址的寄存器,因此被称为”指令指针”。实际上JMP指令就是修改了EIP的值。
TR寄存器:作用是让CPU记住当前正在运行哪一个任务。其存储的值是”当前任务所在的段号*8”。只需在操作系统启动时对其赋值一次,以后进行任务切换时,CPU会自动调整TR的值。给TR赋值只能用汇编实现。
DAY16
-
任务管理自动化
sel 用来存放GDT的编号 1正在使用的标志/不工作的状态 2活动中标志
runing 正在运行的任务数量
now 正在运行的是哪个任务“休眠”就是从tasks链表中去掉一个任务A,”唤醒”就是把这个任务A重新加入tasks链表。
休眠的时机:任务A的消息队列为空(没有待处理的消息)时。
唤醒的时机:任务A的消息队列获得新的消息时。
DAY17
- 创建了一个闲置任务,里面是io_hlt()函数,放在最下层level,当闲置时就执行该任务执行hlt指令。当fifo中有数据写入时任务A就会被唤醒。
- 创建命令行窗口:就是在屏幕上画一个窗口出来,
- 实现用tab键对输入窗口的切换,先将窗口标题栏的颜色改一下。
新增了个key_to变量 - 实现向CMD窗口中输入字符串。只要在键盘被按下的时候,往CMD任务的FIFO中发送数据即可。将FIFO加到任务结构体TASK中。
DAY18
- 只接受键盘输入的窗口有光标闪烁,而其他窗口是不接受光标的
curcos_c为负数 - 实现CMD窗口的光标的控制,将光标开始闪烁定义为2,停止闪烁定义为3.
- 回车符:接下来我们要修改CMD任务函数console-task我们再其中创建一个cursor_y变量,当按下回车键的时候,cursor_y增加16个像素(一个字符宽8高16像素)
- 窗口滚动
优化思路:
屏锁 屏锁密码可开 复合颜色 蜂鸣器
VK_LWIN 5B Left Windows 键 (Microsoft自然键盘)
VK_RWIN 5C Right Windows 键 (Msti
icrosoft自然键盘)