开发单片类Unix操作系统-GDT和IDT(5)

在上一篇文章中,我们实现了动态内存管理器。
今天,我们将介绍在Intel i386处理器的保护模式下工作的基础知识。
即:全局描述符表和中断向量表。


目录


构建系统(make,gcc,gas)。 初始引导(多次引导)。 启动(qemu)。 C库(strcpy,memcpy,strext)。
C库(sprintf,strcpy,strcmp,strtok,va_list ...)。 以内核模式和用户应用程序模式构建库。
内核系统日志。 显存 输出到终端(kprintf,kpanic,kassert)。
动态内存,堆(kmalloc,kfree)。
内存和中断处理的组织(GDT,IDT,PIC,syscall)。 例外情况
虚拟内存(页面目录和页面表)。
过程。 策划人 多任务处理。 系统调用(kill,exit,ps)。
内核(initrd),elf及其内部文件系统。 系统调用(执行)。
字符设备驱动程序。 系统调用(ioctl,fopen,fread,fwrite)。 C库(fopen,fclose,fprintf,fscanf)。
Shell作为内核的完整程序。
用户保护模式(ring3)。 任务状态段(tss)。

线性寻址


英特尔处理器有2种主要操作模式:保护模式x32和IA-32e x64。
总的来说,祖布科夫对此写得很好,很容易理解,我建议您阅读它,尽管原则上也可以使用《英特尔手册》,但它并不复杂,但多余且庞大。
他们为系统编程提供了单独的卷,我建议阅读。
首先有更多的俄语信息,因此,我们将简要考虑要点。
寻址有两种类型:线性寻址和页面寻址。 线性表示整个物理空间是连续描述的,并且与物理空间一致,因为通常,段描述符的基数为零,因为它更容易。
在这种情况下,对于内核模式,您需要创建三个描述内存的描述符:代码,堆栈和数据。 它们具有一些硬件保护功能。
每个这样的段都有一个以0为底的基数,以及一个由机器字的最大大小确定的限制。 堆栈沿相反的方向增长,为此,描述符中还有一个标志。
因此,使用此格式的三条记录,我们可以满足所需的所有内容:

/* * Global descriptor table entry */ struct GDT_entry_t { u16 limit_low: 16; u16 base_low: 16; u8 base_middle: 8; u8 type: 4; /* whether code (0b1010), data (0b0010), stack (0b0110) or tss (0b1001) */ u8 s: 1; /* whether system descriptor */ u8 dpl: 2; /* privilege level */ u8 p: 1; /* whether segment prensent */ u8 limit_high: 4; u8 a: 1; /* reserved for operation system */ u8 zero: 1; /* zero */ u8 db: 1; /* whether 16 or 32 segment */ u8 g: 1; /* granularity */ u8 base_high: 8; } attribute(packed); 


每个段寄存器(cs,ds,ss)在GDT中都有其自己的描述符,因此,当我们在代码段中编写某些内容时,会出错,因为描述符中存在写保护。
为此,我们需要将以下格式的结构加载到GDTR寄存器中:

 /* * Global descriptor table pointer */ struct GDT_pointer_t { u16 limit; u32 base; } attribute(packed); 


限制是GDT表的结尾减去1,基数是它在内存中的开头。
GDT像这样被加载到寄存器中:

/*
* Load global descriptor table
* void asm_gdt_load(void *gdt_ptr)
*/
asm_gdt_load:
mov 4(%esp),%eax # eax = gdt_ptr
lgdt (%eax)
mov $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
mov %ax,%ss
jmp $0x08,$asm_gdt_load_exit
asm_gdt_load_exit:
ret


之后,我们立即将内核数据选择器加载到所有段寄存器中,从而指示数据描述符(零保护环)。
之后,所有内容都准备好包括分页,但稍后会介绍更多内容。
顺便说一句,多引导引导加载程序建议立即设置其GDT,尽管它们是自己执行的,但说起来更可靠。
在视频教程中了解如何从技术上正确地完成所有这些操作。

中断处理


与GDT类似,中断表具有自己的IDTR寄存器,您还需要在其中装载类似的指针,但该指针已经在IDT上。
中断表本身由以下条目描述:

 /* * Interrupt table entry */ struct IDT_entry_t { u16 offset_lowerbits; u16 selector; u8 zero; u8 type_attr; u16 offset_higherbits; }; 


中断网关通常是一种类型,因为我们要专门处理中断。 我们还不考虑陷阱和呼叫网关,因为它离TSS和保护环更近。
让我们创建一个与您一起使用这些表的界面。 它们只需要设置一次并被忘记。

 /* * Api */ extern void gdt_init(); extern void idt_init(); 


现在,我们将声明IDT记录本身中列出的中断处理程序。
首先,编写硬件错误处理程序:

 /* * Api - IDT */ extern void ih_double_fault(); extern void ih_general_protect(); extern void ih_page_fault(); extern void ih_alignment_check(); extern void asm_ih_double_fault(); extern void asm_ih_general_protect(); extern void asm_ih_page_fault(); extern void asm_ih_alignment_check(); 


然后是键盘中断处理程序:

 /* * Api - IRQ */ extern void ih_keyboard(); extern void asm_ih_keyboard(); 


现在该初始化IDT表了。
看起来像这样:

 extern void idt_init() { size_t idt_address; size_t idt_ptr[2]; pic_init(); /* fill idt */ idt_fill_entry(INT_DOUBLE_FAULT, (size_t)asm_ih_double_fault); idt_fill_entry(INT_GENERAL_PROTECT, (size_t)asm_ih_general_protect); idt_fill_entry(INT_ALIGNMENT_CHECK, (size_t)asm_ih_alignment_check); idt_fill_entry(INT_KEYBOARD, (size_t)asm_ih_keyboard); /* load idt */ idt_address = (size_t)IDT; idt_ptr[0] = (LOW_WORD(idt_address) << 16) + (sizeof(struct IDT_entry_t) * IDT_SIZE); idt_ptr[1] = idt_address >> 16; asm_idt_load(idt_ptr); } 


在这里,我们注册了三个硬件错误处理程序和一个中断。
为此,我们需要使用基数和限制将特殊指针加载到IDTR寄存器中:

/*
* Load interrupt table
* void asm_idt_load(unsigned long *addr)
*/
asm_idt_load:
push %edx
mov 8(%esp), %edx
lidt (%edx)
pop %edx
ret


需要限制才能了解表中有多少条记录。
是时候编写键盘中断处理程序了:

/*
* Handle IRQ1
* void asm_ih_keyboard(unsigned int)
*/
asm_ih_keyboard:
pushal
call ih_keyboard
popal
iretl


注意:在下文以及代码的各处,“下半部分”等同于Linux中的“上半部分”。 与“上”分别相反。 我很抱歉,相反的话出现在我的脑海:D

实际上,它将把代码传递给高级处理程序。
这将依次调用注册了处理此中断请求的相应驱动程序下半部分的处理程序。
在我们的例子中,它将是字符设备驱动程序。
需要下半部分来快速处理中断而不降低其他中断的速度,然后,如果有时间,上半部分的处理器将逐渐执行其他工作,因为这样的处理器已经很拥挤(被中断)。

 /* * Api - Keyboard interrupt handler */ extern void ih_keyboard() { printf("[IH]: irq %u\n", 1); u_char status = asm_read_port(KEYBOARD_STATUS_PORT); if (status & 0x01) { char keycode = asm_read_port(KEYBOARD_DATA_PORT); if (keycode < 1) { goto end; } /* call low half (bottom) interrupt handler */ } end: asm_write_port(PIC1_CMD_PORT, 0x20); /* end of interrupt */ } 


现在,当我们按下键盘键时,每次我们都会在内核系统日志中看到相应的条目。

参考文献


现在, 打开本文的视频教程。
并并行观看git存储库(您需要一个lesson5分支)

参考文献


1.詹姆斯·莫洛伊(James Molloy)。 滚动自己的玩具UNIX克隆操作系统。
2.牙齿。 DOS,Windows,Unix的汇编器
3.卡拉什尼科夫。 汇编程序很简单!
4. Tanenbaum。 操作系统。 实施与开发。
5.罗伯特·洛夫(Robert Love)。 Linux内核 开发过程的描述。

Source: https://habr.com/ru/post/zh-CN467289/


All Articles