x86从开机到系统启动

x86 操作系统基础知识

1. x86操作系统初始化流程

x86操作系统是一种运行在x86架构的中央处理器(CPU)上的操作系统,例如Windows,Linux和Mac OS。x86操作系统的初始化流程是指从开机到加载操作系统内核的一系列步骤,主要包括以下几个阶段:

  • BIOS阶段:
    BIOS(Basic Input/Output System)是一种固化在主板上的软件,负责检测和初始化硬件设备,以及寻找并执行启动设备上的第一个扇区(MBR或GPT)中的引导程序。
    ##引导阶段:引导程序是一种位于启动设备上的小型程序,负责加载并跳转到操作系统的引导加载器(bootloader)。不同的操作系统有不同的引导程序,例如Windows的bootmgr,Linux的GRUB或LILO,Mac OS的boot.efi等。
  • 引导加载器阶段:
    引导加载器是一种位于启动设备上的程序,负责显示操作系统的选择菜单(如果有多个操作系统),以及加载并跳转到所选操作系统的内核。引导加载器通常需要读取配置文件和内核映像文件,因此需要能够识别文件系统的格式。
  • 内核阶段:
    内核是操作系统的核心部分,负责管理系统资源,提供硬件抽象层,以及创建和调度进程。内核在被引导加载器加载后,会进行自身的初始化,包括设置保护模式,建立中断和异常处理机制,初始化内存管理单元,检测和配置硬件设备,创建内核线程和进程等。内核初始化完成后,会启动第一个用户态进程(通常是init或systemd),从而进入用户态阶段。

2. 写一个X86操作系统

2.1 编写启动扇区

启动扇区是一个512字节的程序,它是操作系统的第一个部分,它在计算机开机时被加载到内存中,并开始执行。启动扇区的作用是初始化硬件设备,建立内存空间映射表,加载内核等。启动扇区需要用汇编语言编写,并且要符合一定的格式,否则计算机无法识别它。

为了编写启动扇区,你需要一个汇编器,如NASM或者FASM。你可以参考以下的代码示例:

masm
nasm
; boot.asm
; a simple boot sector program

org 0x7c00 ; the address where the boot sector is loaded
bits 16 ; use 16-bit mode

start:
	cli ; disable interrupts
	xor ax, ax ; set ax to 0
	mov ds, ax ; set ds to 0
	mov es, ax ; set es to 0
	mov ss, ax ; set ss to 0
	mov sp, 0x7c00 ; set sp to 0x7c00

	mov si, msg ; set si to point to the message
	call print ; call the print function

	jmp $ ; loop forever

print: ; a function to print a string
	lodsb ; load a byte from si to al and increment si
	or al, al ; check if al is zero
	jz done ; if zero, return
	mov ah, 0x0e ; set ah to 0x0e, the BIOS function to print a character
	int 0x10 ; call the BIOS interrupt
	jmp print ; loop until done

done:
	ret ; return from the function

	msg db "Hello, world!", 0 ; the message to print, terminated by zero

	times 510 - ($ - $$) db 0 ; fill the rest of the sector with zeros
	dw 0xaa55 ; the boot signature

为了编译和运行这个程序,你需要一个链接器,如LD或者GOLINK。你还需要一个模拟器,如QEMU或者BOCHS。你可以使用以下的命令:

nasm -f bin boot.asm -o boot.bin # compile the assembly code to a binary file
qemu-system-i386 -fda boot.bin # run the binary file on QEMU

如果一切顺利,你应该能看到屏幕上显示"Hello, world!"。

2.2 编写内核

内核是操作系统的核心部分,它负责管理进程,内存,文件系统,设备驱动等。内核可以用C语言或者其他高级语言编写,但是需要注意一些特殊的约定和限制。例如,内核不能使用标准库函数,因为它们依赖于操作系统提供的服务。内核也不能使用浮点数,因为它们需要初始化浮点数单元。内核还需要遵循一定的调用约定,以便和汇编代码交互。

为了编写内核,你需要一个C编译器,如GCC或者CLANG。你还需要一个汇编器,如NASM或者FASM。你可以参考以下的代码示例:

// kernel.c
// a simple kernel program

#include <stdint.h> // for uint16_t and uint8_t

#define VGA_WIDTH 80 // the width of the VGA text mode
#define VGA_HEIGHT 25 // the height of the VGA text mode

uint16_t* vga_buffer = (uint16_t*)0xB8000; // the address of the VGA buffer
uint8_t vga_index = 0; // the index of the current VGA position

// a function to set the VGA color
void set_vga_color(
	uint8_t foreground, 
	uint8_t background) 
{
	vga_index = (background << 4) | (foreground & 0x0F);
}

// a function to put a character on the VGA buffer
void put_vga_char(char c, 
	uint8_t x, 
	uint8_t y) 
{
	uint16_t index = y * VGA_WIDTH + x; // calculate the index of the buffer
	vga_buffer[index] = (uint16_t)vga_index << 8 | c; // write the character and the color to the buffer
}

// a function to print a string on the VGA buffer
void print_vga_string(
	char* str, 
	uint8_t x, 
	uint8_t y) {
	uint8_t i = 0; // initialize a counter
	while (str[i] != '\0')
	{ // loop until the end of the string
		put_vga_char(str[i], x, y); // put each character on the buffer
		i++; // increment the counter
		x++; // increment the x position
	}
}

// a function to clear the VGA buffer
void clear_vga_buffer() 
{
	for (uint8_t x = 0; x < VGA_WIDTH; x++) 
	{ // loop through each column
		for (uint8_t y = 0; y < VGA_HEIGHT; y++) 
		{ // loop through each row
			put_vga_char(' ', x, y); // put a space character on the buffer
		}
	}
}

// the entry point of the kernel
void kernel_main () 
{
	clear_vga_buffer(); // clear the buffer
	set_vga_color(15, 0); // set the color to white on black
	print_vga_string("Welcome to my x86 operating system!", 10, 12); // print a message on the buffer
}
; kernel_entry.asm
; a simple kernel entry point

global kernel_entry ; make the kernel_entry symbol global

extern kernel_main ; declare an external symbol for kernel_main

kernel_entry:
	call kernel_main ; call the kernel_main function from C code
	hlt ; halt the CPU

为了编译和运行这个程序,你需要一个链接器,如LD或者GOLINK。你还需要一个模拟器,如QEMU或者BOCHS。你可以使用以下的命令:

nasm -f elf32 kernel_entry.asm -o kernel_entry.o # compile the assembly code to an object file
gcc -m32 -c kernel.c -o kernel.o # compile the C code to an object file
ld -m elf_i386 -T link.ld -o kernel.bin kernel_entry.o kernel.o # link the object files to a binary file using a linker script
qemu-system-i386 -kernel kernel.bin # run the binary file on QEMU

如果一切顺利,你应该能看到屏幕上显示"Welcome to my x86 operating system!"。

2.3 扩展内核功能

写一个x86操作系统的基本步骤就是这样,但是这只是一个非常简单的操作系统,它还不能做很多事情。如果你想让你的操作系统更加强大和有趣,你需要扩展内核的功能,例如:

  • 实现中断和异常处理,以便响应硬件事件和错误情况。
  • 实现分页机制,以便管理虚拟内存和保护内存空间。
  • 实现多任务机制,以便运行多个进程和线程。

3. x86 操作系统实现中断和异常处理

3.1中断和异常的概念和分类

在本节中,我将介绍 x86 操作系统是如何实现中断和异常处理的。中断和异常是操作系统与硬件交互的重要机制,它们可以让操作系统响应外部设备的事件,处理程序执行过程中的错误,以及提供系统调用等功能。我将从以下几个方面来说明这个问题:

  • 中断和异常的概念和分类
  • x86 处理器的中断和异常机制
  • 操作系统的中断和异常处理流程
  • 操作系统的中断和异常服务例程

中断和异常都是指处理器在执行指令的过程中,由于某些原因而改变执行流程的事件。它们的区别在于:

  • 中断是由外部设备或软件主动发出的信号,通知处理器需要进行某些服务。中断是异步的,与当前执行的指令无关。例如,键盘按下一个键时,会产生一个键盘中断,请求处理器读取键盘输入。
  • 异常是由处理器自身检测到的错误或特殊情况,要求操作系统进行处理。异常是同步的,与当前执行的指令有关。例如,除法指令遇到除数为零时,会产生一个除法错误异常,要求操作系统进行恢复或终止。

根据不同的原因和处理方式,x86 处理器将中断和异常分为以下四类:

  • 外部中断(Interrupt):由外围设备或软件通过 int 指令发出的信号,请求处理器提供服务。外部中断可以被屏蔽或开启,通过 eflags 寄存器的 IF 位控制。外部中断总是返回到下一条指令。
  • 故障(Fault):由处理器检测到的可恢复的错误,要求操作系统进行修正或终止。故障通常由无效的指令或数据引起。故障返回到当前指令,以便重新执行或跳过。
  • 陷阱(Trap):由处理器检测到的正常或预期的情况,要求操作系统进行处理。陷阱通常用于实现调试、单步执行、系统调用等功能。陷阱返回到下一条指令。
  • 终止(Abort):由处理器检测到的不可恢复的错误,要求操作系统进行终止或重启。终止通常由严重的硬件故障或内部一致性检查失败引起。终止不会返回。

3.2 x86 处理器的中断和异常机制

x86 处理器使用一个叫做中断描述符表(Interrupt Descriptor Table, IDT)的数据结构来管理中断和异常。IDT 是一个由 256 个条目组成的数组,每个条目对应一个 8 位的中断向量号(Interrupt Vector Number)。每个条目包含一个段选择子(Segment Selector)和一个偏移量(Offset),用于指定处理该中断或异常的服务例程(Interrupt Service Routine, ISR)在内存中的位置。
中断门
在64位模式下,IDT索引是通过将中断向量缩放到16而形成的。一个64位模式的中断门的前8个字节(字节7:0)与传统的32位中断门相似,但并不完全相同。类型字段(位11:8字节)。中断堆栈表(IST)字段(字节长度为4:0)。“中断堆栈表”中描述的堆栈切换机制使用。字节11:8以规范形式保持目标RIP(中断段偏移量)的上32位。如果软件试图使用非规范形式的目标RIP引用一个中断门,则会生成一个通用保护异常(#GP)。中断门所引用的目标码段必须是64位码段(CS.L = 1、CS.D = 0)。如果目标不是一个64位的代码段,则会生成一个通用保护异常(#GP),并将IDT向量号报告为错误代码。在IA-32e模式(64位模式和兼容模式)中,只能引用64位中断和陷阱门。传统的32位中断或陷阱门类型(0EH或0FH)被重新定义为64位中断和陷阱门类型。没有32位的中断或陷阱gat
在这里插入图片描述

IDT 的基地址和限长存放在一个特殊的寄存器 IDTR 中,可以通过 lidt 和 sidt 指令来加载和保存。操作系统在启动时会初始化 IDT,并填充各个条目对应的服务例程地址。

当一个中断或异常发生时,处理器会根据以下步骤来处理:

  • 保存当前的执行状态,包括 eflags,cs,eip,error code(如果有)等,压入栈中。
    中断服务例程压栈

  • 关闭中断,将 eflags 的 IF 位清零,以防止嵌套中断的发生。

  • 根据中断或异常的类型和原因,确定一个中断向量号,并从 IDT 中读取相应的条目。

  • 检查条目的有效性和特权级别,如果无效或权限不够,则产生一个通用保护异常(General Protection Fault)。

  • 加载条目中的段选择子到 cs 寄存器,并将偏移量加载到 eip 寄存器,从而跳转到服务例程的入口处。

  • 执行服务例程,完成中断或异常的处理。

  • 执行 iret 指令,从栈中恢复原来的执行状态,并开启中断,返回到被中断或异常打断的指令处。

3.3 操作系统的中断和异常处理流程

操作系统在接管处理器后,会建立自己的 IDT,并实现各种中断和异常的服务例程。这些服务例程通常分为两部分:一部分是用汇编语言编写的,负责完成一些低层次的工作,如保存寄存器,切换堆栈,识别中断源等;另一部分是用高级语言编写的,负责完成一些高层次的工作,如调度进程,响应设备请求,处理系统调用等。

操作系统的中断和异常处理流程大致如下:

  • 当一个中断或异常发生时,处理器会根据前面介绍的机制,跳转到相应的服务例程入口处。
  • 服务例程首先会执行汇编代码部分,保存所有的通用寄存器和段寄存器,以防止服务例程破坏它们的值。然后判断当前是否在用户态运行,如果是,则切换到内核态的堆栈,并保存用户态的 ss 和 esp。接着判断是否需要识别中断源,如果是,则调用相应的函数来获取中断源,并将其压入堆栈。最后调用高级语言编写的服务例程函数,并将 error code(如果有)和中断向量号作为参数传递。
  • 服务例程函数会执行高级语言代码部分,根据不同的中断或异常类型和原因,进行相应的处理。例如,如果是时钟中断,则更新系统时间,并检查是否需要进行进程调度;如果是键盘中断,则读取键盘输入,并将其放入缓冲区;如果是除法错误异常,则终止当前进程;如果是系统调用,则根据系统调用号和参数,执行相应的内核功能。
  • 服务例程函数执行完毕后,返回到汇编代码部分。此时会恢复所有的通用寄存器和段寄存器,并判断是否需要从内核态切换回用户态。如果是,则恢复用户态的 ss 和 esp,并设置 eflags 的 IOPL 位为 3,以允许用户态访问 I/O 端口。最后执行 iret 指令,从栈中弹出 eflags, cs, eip, error code(如果有)等,并返回到被中断或异常打断的指令处。

3.4 操作系统的中断和异常服务例程

在 x86 操作系统中,中断和异常是两种不同的事件,它们都会导致处理器暂停当前的执行流程,转而执行一个特定的服务例程。中断是由外部设备或软件产生的信号,用于通知处理器发生了某些事件,例如键盘输入、时钟滴答、磁盘读写等。异常是由处理器自身检测到的错误或异常情况,例如除零、缺页、保护错误等。中断和异常的服务例程是操作系统内核的一部分,它们负责响应和处理这些事件,以保证系统的正常运行。
中断是操作系统与硬件设备之间进行通信的一种方式,它可以让操作系统在执行程序的过程中,响应硬件设备的请求,或者处理异常情况。中断例程是操作系统为处理不同类型的中断而预先定义好的一段代码,它通常存放在内存中的一个特定区域,称为中断向量表(Interrupt Vector Table,IVT)。当某个中断发生时,CPU会根据中断号,在IVT中找到对应的中断例程的入口地址,然后跳转到该地址执行中断处理。

x86操作系统实现中断例程初始化的步骤大致如下:

  1. 在操作系统启动之初,将IVT的起始地址设置为0x00000,即内存的最低端。IVT占用1KB的空间,每个表项占用4字节,共有256个表项,对应256个不同的中断号。每个表项由两部分组成:低16位是中断例程的偏移地址(Offset),高16位是中断例程的段地址(Segment)。这样,一个表项就可以表示一个32位的线性地址(Segment * 16 + Offset),也就是中断例程的入口地址。
    一般来说,操作系统会将 IDT 分为两部分:前 32 个条目用于预定义的异常,后 224 个条目用于可屏蔽的中断。预定义的异常是由处理器规范定义的,它们对应一些常见的错误或异常情况,例如除零、缺页、保护错误等。可屏蔽的中断是由外部设备或软件产生的,它们可以通过设置处理器的标志寄存器来屏蔽或开启。操作系统可以根据自己的需要,为不同的设备或软件分配不同的向量号,并编写相应的服务例程。
    在这里插入图片描述
    在这里插入图片描述

文档材料来源于Intel® 64 and IA-32 Architectures Software Developer’s Manual 3A

//
// Entry of Interrupt Descriptor Table (IDTENTRY) (32 bit mode)
//

typedef struct _KIDTENTRY {
   USHORT Offset;
   USHORT Selector;
   USHORT Access;
   USHORT ExtendedOffset;
} KIDTENTRY;
//
// Define Interrupt Descriptor Table (IDT) entry structure and constants. (64 bit mode)
//

typedef union _KIDTENTRY64 {
   struct {
       USHORT OffsetLow;
       USHORT Selector;
       USHORT IstIndex : 3;
       USHORT Reserved0 : 5;
       USHORT Type : 5;
       USHORT Dpl : 2;
       USHORT Present : 1;
       USHORT OffsetMiddle;
       ULONG OffsetHigh;
       ULONG Reserved1;
   };

   ULONG64 Alignment;
} KIDTENTRY64, *PKIDTENTRY64;
//
// Define macro to initialize an IDT entry.
//
// KiInitializeIdtEntry (
//     IN PKIDTENTRY64 Entry,
//     IN PVOID Address,
//     IN USHORT Level
//     )
//
// Arguments:
//
//     Entry - Supplies a pointer to an IDT entry.
//
//     Address - Supplies the address of the vector routine.
//
//     Dpl - Descriptor privilege level.
//
//     Ist - Interrupt stack index.
//

#define KiInitializeIdtEntry(Entry, Address, Level, Index)                  \
    (Entry)->OffsetLow = (USHORT)((ULONG64)(Address));                      \
    (Entry)->Selector = KGDT64_R0_CODE;                                     \
    (Entry)->IstIndex = Index;                                              \
    (Entry)->Type = 0xe;                                                    \
    (Entry)->Dpl = (Level);                                                 \
    (Entry)->Present = 1;                                                   \
    (Entry)->OffsetMiddle = (USHORT)((ULONG64)(Address) >> 16);             \
    (Entry)->OffsetHigh = (ULONG)((ULONG64)(Address) >> 32)   

代码来源于Windows ntoskrnl工程,可上github上获取

  1. 在操作系统加载到内存后,根据不同类型的中断,编写相应的中断例程,并将它们存放在内存中合适的位置。然后,将这些中断例程的入口地址写入到IVT中对应的表项里。这样,当某个中断发生时,CPU就可以通过IVT找到相应的中断例程,并执行它。

  2. 在操作系统初始化过程中,对可编程中断控制器(Programmable Interrupt Controller,PIC)进行编程设置。PIC是一个芯片,它负责接收外部设备产生的可屏蔽中断(Maskable Interrupt)信号,并根据优先级向CPU发出中断请求。
    PIC有两种工作模式:

  • 8259A模式和APIC模式。在8259A模式下,PIC使用两片8259A芯片进行级联,一片作为主片连接到CPU的INTR引脚上,另一片作为从片连接到主片的IR2引脚上。这样就可以支持15个外部设备的可屏蔽中断,不过在CPU不断的迭代更新过程中,8259A模式也就被废弃了。
  • 在APIC模式下,PIC使用一个集成在CPU内部的本地APIC(Local APIC)和一个连接到总线上的IOAPIC(IO APIC)来管理外部设备的可屏蔽中断。这样就可以支持更多的外部设备和更高的中断优先级。操作系统需要根据PIC的工作模式,对其进行相应的编程设置,包括指定IRQ线与IDT表项之间的映射关系、设置中断屏蔽寄存器(IMR)来控制哪些IRQ线可以被屏蔽、设置EOI(End Of Interrupt)命令来通知PIC中断处理结束等。
    在这里插入图片描述

Intel® 64 and IA-32 Architectures Software Developer’s Manual 3A @11.4 LOCAL APIC

  1. 在操作系统初始化过程中,对实时时钟(Real Time Clock,RTC)进行编程设置。RTC是一个芯片,它负责提供实时时间服务,并周期性地向CPU发送时钟中断信号。操作系统需要对RTC进行编程设置,包括指定时钟中断的频率、设置寄存器B来控制是否开启周期性中断和更新结束后中断、读取寄存器C来复位未决的中断状态等。

  2. 在操作系统初始化过程中,对CPU的控制寄存器(Control Register)进行编程设置。CPU的控制寄存器是一组特殊的寄存器,它们用来控制CPU的工作模式和特性。操作系统需要对CPU的控制寄存器进行编程设置,包括设置CR0来控制是否开启保护模式和分页机制、设置CR2来存放最近一次产生缺页异常的线性地址、设置CR3来存放页目录表的物理地址、设置CR4来控制是否开启虚拟8086模式和分页扩展等。

通过以上步骤,x86操作系统就可以实现中断例程的初始化,并准备好响应各种类型的中断了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值