书上第五六天所涉及的内容主要是GDT、IDT以及PIC。这两部分我就合在一起写好了。(主要的原因是做完第五天的以后看到代码乱得惨不忍睹,就自己开始整理了一下,随后又发现第六天就是讲分割源文件的,所以就继续看了下去。)
第五天一上来,作者就介绍了结构体。于是,出现了struct boot_info,struct seg_desc,以及struct gate_desc三者。(命名我是按照自己的习惯来改动的。)
C语言语法上的东西就不提了。
有点意思的是,继之前恶心简洁的图形化界面之后,我们这次来做文字显示。
字体怎么来呢?最简单的一种方法就是按照之前做图像的时候的方法来做,也就是用boxfill8(),但是这样也太不专业了吧= =
我们有一种好一点的方法,就是重写一个putfont8()函数,用来做字符显示。(ps:后来我改名为print_char()了)直接用作者给的hankaku.txt来导入字体。(其实这个字体包也没有多高端嘛……要用到新的工具makefont.exe。可以从hankaku.txt得到hankaku.bin。之后我们再用bin2obj.exe来将其转化为hankaku.obj文件。同时,对应的修改Makefile。
值得注意的是:一定要加上"$BIN2OBJ ......"的说明!当时就是因为抄漏而导致了大量的失败。
而显示变量名按照上面的来做基本也没问题。不过不知道为什么,到后面我多加"#include "bootpack.h""语句之后,直接就只声明“char s[40]”是不够的,要初始化!可以是"char s[40]={'0'}"。如果没有初始化,就会发现那个字符就是显示不出来,然后你还以为是print_str或者print_char的问题,对照源代码N次!!!(TAT)
再来是鼠标的指针,这个其实还是蛮无聊有趣的。为了凸显我跟作者的不一样,我把指针弄成了8x8大小的,结果……其实也还是能用的,而且大小我(我不是处女座……)也刚好合适。
文件分割不难,略过。也就是多出了graphic.c,dsctbl.c,以及后面的int.c。
有一点是新学习到的,那就是Makefile的一般规则:
# general rules for *.gas, *.nas and *.obj
%.gas : %.c Makefile
$(CC1) -o $*.gas $*.c
%.nas : %.gas Makefile
$(GAS2NASK) $*.gas $*.nas
%.obj : %.nas Makefile
$(NASK) $*.nas $*.obj $*.lst
关于Makefile,有个很详细的教程:
http://bbs.chinaunix.net/thread-408225-1-1.html
之后得找时间认真学习一下才行。
本次学习遇到的第一个重难点在于GDT以及IDT。
其实作者的文笔还是挺好的,写得很顺畅。(虽然到后来发现他有些地方机智的绕掉了……)
该记住的有:
GDT大小为2^13*(8B) = 64KB
/* segment descriptor, 8B */
/* base : base_low(2B), base_mid(1B) and base_high(1B)
* limit : limit_low(1B) and limit_high(1B)
* access_right: the highest bit is Gbit, when Gbit==1,
the unit of limit is PAGE; the highest 4bits are put into the
highest 4bits of limit_high. thus, to program, access_right
is xxxx0000xxxxxxxx; the highest 4bits are "GD00"(used after
386), G is Gbit, D means 32-mode or 16-mode.
lowest 8bits:
0x00: unused descriptor table
0x92: for system. RW-.
0x9a: for system. R-X.
0xf2: for applications. RW-.
0xfa: for applications. R-X.
*/
struct seg_desc {
short limit_low;
short base_low;
char base_mid;
char access_right;
char limit_high;
char base_high;
};
而后面的gate_desc,类比一下就好。
set_seg_desc,set_gate_desc,init_gdt_idt就慢慢看代码吧。
C不能直接给GDTR赋值,所以要用load_gdtr,在naskfunc.nas中:
_load_gdtr: ; void load_gdtr(int limit, int addr);
; "MOV [ESP+6] [ESP+4]"
MOV AX, [ESP+4] ; limit
MOV [ESP+6], AX
LGDT [ESP+6]
RET
看起来,这段函数似乎做的就是"MOV [ESP+6] [ESP+4]"但是为什么要这么做呢?为什么不直接"LGDT [ESP+4]"呢?
这其中可是大有奥秘的。
首先,GDTR的低16位是段上限,高32位是地址。我们不能直接用MOV来赋值,而只能直接指定一个内存地址,让它去读这48位。想想看,假设我们传的段上限是0x0000ffff,而地址是00270000(事实上我们用的也就是这个)。这时候,地址从ESP+4往高处走是:【FF FF 00 00 00 00 27 00】。但我们希望给GDTR的是这样一部分:【FF FF 00 00 27 00】那可以看到,作者在这里把ESP+4赋值到ESP+6,然后就变成了【FF FF FF FF 00 00 27 00】,只要直接从ESP+6开始读就可以了。
哎,在这里实在不得不感叹一声作者太神了!事实上如果你传参数的时候,两个参数的位置如果换一下的话,你会发现很不好处理!(我一开始是想这么干来着。)
set_seg_desc太高深,跳过。(作者也没讲多少)
然后就到PIC。一到硬件就各种蛋疼哎!
PIC指的是Programmable interrupt controller。
然后看图:
用图比较容易理解。
简而言之,PIC监视着输入管脚的8个中断信号,只要有一个中断信号进来,就将唯一的输出管脚信号变成ON,并通知CPU。
然后下面是一段咋看之下很不明觉厉的代码:
void init_pic(void);
#define PIC0_ICW1 0x0020
#define PIC0_OCW2 0x0020
#define PIC0_IMR 0x0021
#define PIC0_ICW2 0x0021
#define PIC0_ICW3 0x0021
#define PIC0_ICW4 0x0021
#define PIC1_ICW1 0x00a0
#define PIC1_OCW2 0x00a0
#define PIC1_IMR 0x00a1
#define PIC1_ICW2 0x00a1
#define PIC1_ICW3 0x00a1
#define PIC1_ICW4 0x00a1
/* Initialization of pic */
void init_pic (void) {
io_out8(PIC0_IMR, 0xff ); /* disable all interrupts */
io_out8(PIC1_IMR, 0xff ); /* disable all interrupts */
io_out8(PIC0_ICW1, 0x11 ); /* edge trigger mode*/
io_out8(PIC0_ICW2, 0x20 ); /* IRQ-7 is received by INT20-27 */
io_out8(PIC0_ICW3, 1 << 2); /* PIC1 is connected by IRQ2*/
io_out8(PIC0_ICW4, 0x01 ); /* no-buffer mode */
io_out8(PIC1_ICW1, 0x11 ); /* edge trigger mode */
io_out8(PIC1_ICW2, 0x28 ); /* IRQ-15 is received by INT28-2f */
io_out8(PIC1_ICW3, 2 ); /* PIC1 is connected by IRQ2 */
io_out8(PIC1_ICW4, 0x01 ); /* no-buffer mode */
io_out8(PIC0_IMR, 0xfb ); /* 11111011 disable all except PIC1 */
io_out8(PIC1_IMR, 0xff ); /* 11111111 disable all interrupt */
}
简单的翻译一下,IMR指的是interrupt mask register,ICW指的是"initial control word",都是8位寄存器。
IMR中8位分别对应8个IRQ信号,如果某一位为1,则该为对应的信号被屏蔽,PIC就忽略之。
ICW不一定是16位,因硬件而不同。有4个,分别编号1~4,共有4个字节的数据。
ICW1和4与硬件有关,忽略之。
ICW3是有关主从连接的设定,对主PIC而言,第几号IRQ与从PIC相连,是用8位来决定的。如果把这些为全部设为1,则主PIC能驱动8个从PIC。
不过呢……这个是硬件决定的,我们也无力。
所以只能改ICW2咯。
ICW2决定了IRQ以哪一号中断通知CPU。通过PIC用数据信号线传送给CPU“0xcd 0x??”来实现的。这里的0xcd实际上就是调用BIOS时用的INT指令。
这次以INT0x20~0x2f接收中断信号IRQ0~15而设定的。
开始看程序,注意鼠标时IRQ12, 键盘是IRQ1。
void inthandler21(int *esp)
/* interrupt from PS/2 keyboard */
{
struct boot_info *binfo = (struct boot_info *) BOOT_ADDR;
boxfill8(binfo->vram, binfo->scrnx, BLACK, 0, 0, 32 * 8 - 1, 15);
print_str(binfo->vram, binfo->scrnx, 0, 0, WHITE, "INT 21 (IRQ-1) : PS/2 keyboard");
for (;;) {
io_hlt();
}
}
完了之后还得让它执行IRETD:
_asm_inthandler21:
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler21
POP EAX
POPAD
POP DS
POP ES
IRETD
关于栈,不解释。
这些差不多就是今天全部的内容了。(唉,累……)