天天看点

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

快一个多月了,一直想要在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, ( *  + ) *     /* 尝试显示一个字符 */
    mov ah, 
    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 :now    /* 这行代码极为重要,它使程序强行运行在了新定义的GDT对应的段里面 */
now:
    call kmain    /* 这时我们可以跳转到C语言环境去执行自己想做的事了 */

    mov edi, ( *  + ) *    /* 再次显示字符,看看会不会出错 */
    mov ah, 
    mov al, '!'
    mov [gs:edi], ax

    mov ax, *    /* 加载TSS,任务状态段,现在还用不到 */
    ltr ax

;   mov edi, (80 * 12 + 4) * 2

    jmp $
           

上面我觉得最重要的就是jmp 8:now那一行了,它使当前程序强制用新的GDT对应的选择子,这样在后续的程序调用,中断处理等地方就不会出现调用返回时,因为返回的段的选择子在GDT中找不到对应段而出现GP异常了。

现在说说在kstart里面干的事情

void kstart(void)
{
    int i = ;
    unsigned short *p = (unsigned short *);

    *(p +  *  + ) = () | ('A');
    *(p +  *  + ) = () | ('l');
    *(p +  *  + ) = () | ('l');
    *(p +  *  + ) = () | ('e');
    *(p +  *  + ) = () | ('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 *);
    struct Gate_Desciptor     *idt = (struct Gate_Desciptor     *);
    int i;

    for(i = ; i < ; i++)
        set_segdesc(gdt + i, , , );
    for(i = ; i < ; i++)
        set_gatedesc(idt + i, (int)(vector_others), *, );

    set_segdesc((struct Segment_Descriptor *)( + *), , , (DA_32 | DA_CR));  /* 范围是整块内存的代码段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , , (DA_32 | DA_DRW)); /* 范围是整块内存的数据段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , (int)stack_ring0, (DA_32 | DA_DRW | DA_DPL0));  /* ring0的堆栈段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , (int)stack_ring1, (DA_32 | DA_DRW | DA_DPL1));  /* ring1的堆栈段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , (int)stack_ring2, (DA_32 | DA_DRW | DA_DPL2));  /* ring2的堆栈段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , (int)stack_ring3, (DA_32 | DA_DRW | DA_DPL3));  /* ring3的堆栈段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , , (DA_DRW | DA_DPL3));  /* 显存的段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , , (DA_32 | DA_CR));    /* handler的段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , (int)LABEL_TSS, DA_386TSS);  /* 用来存放TSS */


    set_gatedesc((struct Gate_Desciptor     *)( + *), (int)(vector13_handler), *, );
    set_gatedesc((struct Gate_Desciptor     *)( + *), (int)(vector33_handler), *, );

    load_gdt(*, );
    load_idt(*, );
}
           

写GDT描述符和IDT描述符的方法,两位老师的都可以用,只是一个数据结构罢了。

其实,也没做多少事,就跳回汇编程序里面了。

在之后的汇编程序里面则写了中断handler和一些cli,sti等必须用汇编写的代码,给一个中断处理程序的例子:

vector33_handler:
pushad
    mov ax, *
    mov gs, ax
    mov edi, ( *  + ) * 
    mov al, 
    out , al    ;响应中断请求,为下一次中断做准备
    in al,     ;获取键盘读入的键盘编码(不是ASCII码)
    mov dl, al
    mov cl, 
    .disp_loop:    ;把键盘编码显示出来
        cmp cl, 
        jz .disp_loop_end
        dec ecx
        mov al, dl
        shr al, cl
        and al, 
        add al, '0'
        mov ah, 
        mov [gs:edi], ax
        add edi, 
        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)

最后的源代码

继续阅读