快一个多月了,一直想要在ELF格式内核中实现中断,参考的是两本书,一本是于渊的orange’s,另一本是川合秀实的30天自制。。。前期,使用的是于渊的方法进入保护模式,加载并运行ELF内核;进入ELF内核后,变使用川合秀实的方式实现了图形界面(仅仅只是显示图形功能),发现各种错误(由其是中断向量号为13的#GP错误,常规保护异常)
因为于渊的方法是,在loader里面加载ELF,然后跳转到ELF执行,跳转过后,在我们编辑代码的时候已经用C语言了,没有办法使用loader汇编文件里面定义的那个GDT,所以于老师为了方便管理,就把原来的GDT复制到内核空间,那样就还可以用。
川合秀实则不一样,前期他用他自己做的工具,跳过了ELF格式,直接可以用C语言编译,链接,所以他第一次建立GDT就是在C语言环境下。这样就不会出现跳转到ELF后再重新建立GDT就不知道当前运行的代码所在的段了。
所以我的做法是,先加载ELF,跳转到ELF内核运行,然后用C语言重新写一个新的GDT和IDT,然后强行跳转到新的GDT里面的某一个代码段运行(这个代码段就当做是内核里面最起始的代码段了),这样,我们运行的程序就在新的GDT里面有对应的描述符可以找到了。
这是刚跳转如ELF内核的汇编代码
kernel_start:
mov edi, (80 * 11 + 1) * 2 /* 尝试显示一个字符 */
mov ah, 0x0F
mov al, 'L'
mov [gs:edi], ax
; mov esi, interrupt_handler /* 这一段代码可以先不管 */
; mov edi, 0x28000 /* 我是想把中断处理函数复制到内存指定位置 */
; mov ecx, handler_length
; handler_cp:
; cmp ecx, 0
; jz handler_cp_end
; dec ecx
; mov al, byte [cs:esi]
; mov byte [es:edi], al
; inc esi
; inc edi
; jmp handler_cp
; handler_cp_end:
mov esp, Stack0Top /* 使用新的堆栈 */
call kstart /* 跳转到C语言去,准备用C语言建立新的GDT,和一些其它准备工作 */
;------------------------------------------------------------------------------
/* 在C语言函数kstart里面建立了GDT后,选择子为1*8(对应第一个段描述符)对应的段为
一个基址为0,范围是整个内存空间的可读可执行的代码段,特权级为0
*/
jmp 8:now /* 这行代码极为重要,它使程序强行运行在了新定义的GDT对应的段里面 */
now:
call kmain /* 这时我们可以跳转到C语言环境去执行自己想做的事了 */
mov edi, (80 * 21 + 12) * 2 /* 再次显示字符,看看会不会出错 */
mov ah, 0x0F
mov al, '!'
mov [gs:edi], ax
mov ax, 9*8 /* 加载TSS,任务状态段,现在还用不到 */
ltr ax
; mov edi, (80 * 12 + 4) * 2
jmp $
上面我觉得最重要的就是jmp 8:now那一行了,它使当前程序强制用新的GDT对应的选择子,这样在后续的程序调用,中断处理等地方就不会出现调用返回时,因为返回的段的选择子在GDT中找不到对应段而出现GP异常了。
现在说说在kstart里面干的事情
void kstart(void)
{
int i = 1;
unsigned short *p = (unsigned short *)0xB8000;
*(p + 80 * 11 + 4) = (0x0A00) | ('A');
*(p + 80 * 11 + 5) = (0x0B00) | ('l');
*(p + 80 * 11 + 6) = (0x0C00) | ('l');
*(p + 80 * 11 + 7) = (0x0D00) | ('e');
*(p + 80 * 11 + 8) = (0x0E00) | ('n');
io_cli();
init_gdt_idt();
init_pic();
io_sti();
}
在kstart里面,首先尝试输出几个字符,然后我把中断关闭了,设置了新的GDT和IDT,还有初始化了8259A中断控制器,然后又打开了中断,准备接受中断请求了。
其中的init_gdt_idt是比较重要的,里面定义了几个段和中断门,然后重新加载GDT和IDT
void init_gdt_idt(void)
{
struct Segment_Descriptor *gdt = (struct Segment_Descriptor *)0x00000800;
struct Gate_Desciptor *idt = (struct Gate_Desciptor *)0x00000000;
int i;
for(i = 0; i < 8192; i++)
set_segdesc(gdt + i, 0, 0, 0);
for(i = 0; i < 256; i++)
set_gatedesc(idt + i, (int)(vector_others), 3*8, 0x8E);
set_segdesc((struct Segment_Descriptor *)(0x00000800 + 1*8), 0xFFFFFFFF, 0x00000000, (DA_32 | DA_CR)); /* 范围是整块内存的代码段 */
set_segdesc((struct Segment_Descriptor *)(0x00000800 + 2*8), 0xFFFFFFFF, 0x00000000, (DA_32 | DA_DRW)); /* 范围是整块内存的数据段 */
set_segdesc((struct Segment_Descriptor *)(0x00000800 + 3*8), 0x7FF, (int)stack_ring0, (DA_32 | DA_DRW | DA_DPL0)); /* ring0的堆栈段 */
set_segdesc((struct Segment_Descriptor *)(0x00000800 + 4*8), 0x7FF, (int)stack_ring1, (DA_32 | DA_DRW | DA_DPL1)); /* ring1的堆栈段 */
set_segdesc((struct Segment_Descriptor *)(0x00000800 + 5*8), 0x7FF, (int)stack_ring2, (DA_32 | DA_DRW | DA_DPL2)); /* ring2的堆栈段 */
set_segdesc((struct Segment_Descriptor *)(0x00000800 + 6*8), 0x7FF, (int)stack_ring3, (DA_32 | DA_DRW | DA_DPL3)); /* ring3的堆栈段 */
set_segdesc((struct Segment_Descriptor *)(0x00000800 + 7*8), 0xFFFF, 0xB8000, (DA_DRW | DA_DPL3)); /* 显存的段 */
set_segdesc((struct Segment_Descriptor *)(0x00000800 + 8*8), 0xFFFFF, 0x28000, (DA_32 | DA_CR)); /* handler的段 */
set_segdesc((struct Segment_Descriptor *)(0x00000800 + 9*8), 0x68, (int)LABEL_TSS, DA_386TSS); /* 用来存放TSS */
set_gatedesc((struct Gate_Desciptor *)(0x00000000 + 13*8), (int)(vector13_handler), 1*8, 0x8E);
set_gatedesc((struct Gate_Desciptor *)(0x00000000 + 33*8), (int)(vector33_handler), 1*8, 0x8E);
load_gdt(8191*8, 0x00000800);
load_idt(255*8, 0x00000000);
}
写GDT描述符和IDT描述符的方法,两位老师的都可以用,只是一个数据结构罢了。
其实,也没做多少事,就跳回汇编程序里面了。
在之后的汇编程序里面则写了中断handler和一些cli,sti等必须用汇编写的代码,给一个中断处理程序的例子:
vector33_handler:
pushad
mov ax, 7*8
mov gs, ax
mov edi, (80 * 12 + 4) * 2
mov al, 0x61
out 0x0020, al ;响应中断请求,为下一次中断做准备
in al, 0x0060 ;获取键盘读入的键盘编码(不是ASCII码)
mov dl, al
mov cl, 8
.disp_loop: ;把键盘编码显示出来
cmp cl, 0
jz .disp_loop_end
dec ecx
mov al, dl
shr al, cl
and al, 0x01
add al, '0'
mov ah, 0x0B
mov [gs:edi], ax
add edi, 2
jmp .disp_loop
.disp_loop_end:
call write_vedio ;尝试调用函数,看看会不会出错
call kmain
popad
iretd ;中断处理函数的返回
nop
在这里,注意处理函数所在的段,我这里把它暂时放到了最开始的段,因为这样就不用在内存中复制代码了,如果想要单独建立一个段来存中断处理函数(一般是要这么做的),就需要把我们的代码复制到那个段所在的位置(包括汇编代码和C语言代码)。
还有一点就是键盘编码,我们通过0x60端口读到的按键数据,不是ASCII码(一开始我以为是ASCII,结果这么也对不上),它是IBM PC键盘扫描码,它是在键盘上每一个键都对应一个8位二进制数(包括小键盘和Fn等,凡是你在键盘上能按到的,都有一个编码,而且按下,按住和弹起对应最高位不同)
以前我在看其它一些工程代码的时候,在最开始的地方总是会有一个start.c和main.c,当时以为两个作用相同,都只是程序刚开始的地方,和成一个写也没关系。但就最近的学习来看,不是的,而是必须写成两个.c文件,一开始的start.c使用C语言编写了一写后面操作要用到的东西,比如GDT,IDT。然后,在start.c的内容执行完后,我们必须要执行一段汇编代码,来初始化一些东西,如把某一个段的内容写进新的GDT对应的段,然后才可以正常跳转到C语言环境继续执行(虽然有时候,我们跳转到start.c后就加一个while1死循环,不会出错,但涉及到段间跳转就可能会出错,这时候就需要那个jmp 8:now)
最后的源代码