目录
14天的时候我们已经完成了启动代码的编写并且从16位实模式转换到了32位保护模式,如果不清楚可以看看《2021-05-11【30天制作操作系统系列】14天从汇编到C语言》这篇,同时也从汇编过渡到了C语言,后面的处理几乎都是用C语言,5~8天作者讲的是键盘与鼠标信号的处理
简而言之,要实现的功能就是按下键盘,屏幕上要显示相应的字符,移动鼠标,鼠标指针要进行移动
C语言基础
结构体
在OS启动的汇编里面定义了屏幕分辨率,在地址0x0ff4处写入一个十进制数320(2字节),在内存0x0ff6写入了一个十进制数200(2字节)
SCRNX EQU 0x0ff4 ; 分辨率X
SCRNY EQU 0x0ff6 ; 分辨率Y
...
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
在C语言里面要取这两个值怎么取呢,地址是确定的,当然是用指针,如下
short sizex,sizey;//定义值
short *scrnx,*scrny;//定义指针
//指针赋值,赋的是内存地址的值
scrnx = (short *) 0x0ff4;
scrny = (short *) 0x0ff6;
//内存地址所代表的值取出来
sizex = *scrnx;
sizey = *scrny;
// 还可以合到一起写
sizex = *( (short *) 0x0ff4 );
scrnx是地址,*scrnx就是该地址的值320,当然此处只有两个变量,那如果变量很多并且能够归纳为同一类,那么就可以用结构体,用struct定义
struct SOMEINFO {
short scrnx, scrny;
}
short是2个字节,那么SOMEINFO一共就是四个字节,赋值的时候如下赋值
//定义值
short sizex,sizey;
//定义指针
struct SOMEINFO *info;
//指针赋值,赋变量开始位置的内存地址
info = (struct SOMEINFO *)0x0ff4;
//取出变量值
sizex = (*info).scrnx;
sizey = (*info).scrny;
还可以用箭头符号直接表示
sizex = info->scrnx;
sizey = info->scrny
作者是将启动信息封装到了结构体中,下面是汇编中定义的启动信息内存地址
; BOOT_INFO相关
CYLS EQU 0x0ff0 ; 引导扇区设置
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 关于颜色的信息
SCRNX EQU 0x0ff4 ; 分辨率X
SCRNY EQU 0x0ff6 ; 分辨率Y
VRAM EQU 0x0ff8 ; 图像缓冲区的起始地址
;.... 一系列赋值操作
MOV BYTE [VMODE],8 ; 屏幕的模式(参考C语言的引用)
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
MOV DWORD [VRAM],0x 000a0000
C中结构体定义
//结构体
struct BOOTINFO {
char cyls, leds, vmode, reserve;
short scrnx, scrny;
char *vram;
};
//赋值
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
//取值
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
指针
这里再讲一下指针:特别要注意VRAM,它有点像一个嵌套指针,和SCRNX对比一下就知道差别了,SCRNX地址对应的内存里面装的是一个真正的值,而VRAM地址对应的内存里面装的是000a0000,它不是一个真正的值,它也是一个地址值,它对应的内存里面装的才是真正的值
指针的赋值
以下指针的赋值表示指针vram往后第i个地址的值为c
vram[i] = c;
完全等价于
*(vram+i) = c;
或者
*(i+vram) = c;
再或者
i[vram] = c
本质上讲,以上几种学法没有区别,因为编译后的汇编都是一样的!一定要注意指针和数组的区别,千万不要混淆,指针和数组没有半毛钱关系!
还需要注意一点的是指针的加法中蕴含着乘法运算,比如*(vram+1) = 100;与*(vram+2) = 200;并不表示每次内存地址只是移动了一个字节,因为vram指针是四个字节的,所以每次都会移动四个字节,如下图所示
模块化
所谓模块化就是对源文件进行逻辑归类而已,把含有相同功能的一些功能放到一个单独的.c文件里面,这样方便管理,在06_day里面,作者将原来冗长的bootpack.c分成了以下三大块
- 处理图像处理的模块:graphic.c
- 初始化GDT/IDT的模块:dsctbl.c
- 主函数模块:bootpack.c
头文件
当然分了模块以后,各个模块定义的函数如果要相互引用,就要将他们放到.h头文件里面,所以有个bootpack.h,里面除了定义啥也没有,每个模块都会导入这个头文件,这样就可以引用别的模块甚至是汇编里面定义的函数了,关系如下所示
编译规则
但是分了模块以后,.c源文件就会增加,每增加一个都要新增一个编译规则吗?当然不是,编译规则也有通配符规则,如下,作者在Makefile里面年增加了通配符编译规则
%.gas : %.c Makefile # 将所有.c源代码文件编译成gas汇编
$(CC1) -o $*.gas $*.c
%.nas : %.gas Makefile # 将所有gas汇编转换成nas汇编
$(GAS2NASK) $*.gas $*.nas
%.obj : %.nas Makefile # 将所有nas汇编编译成了obj二进制
$(NASK) $*.nas $*.obj $*.lst
GDT/IDT初始化
GDT(Global Descriptor Table)和IDT(Interrupt Descriptor Table)其实在《1~4天从汇编到C语言》已经提到过了,在16位实模式转换成32位保护模式的时候就已经初始化过GDT了,为啥此处还需要初始化一次&