在ELF格式内核中设置GDT、IDT等相关

来源:互联网 发布:nginx部署多个网站 编辑:程序博客网 时间:2024/06/10 15:27

快一个多月了,一直想要在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 kmainpopadiretd    ;中断处理函数的返回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)

最后的源代码

原创粉丝点击