从0创建一个OS (十) 32-bit模式下的GDT

本文介绍了32bit模式下全局描述符表(GDT)的相关知识。先阐述了保护模式的由来及GDT寻址方法,包括如何寻找GDT、描述符描述数据存储位置的方式等。还说明了GDT的一些特性,如最多含8192个描述符等。最后给出实现GDT结构的源码示例及描述符选择子的相关内容。

本节将学习如何使用汇编完成一个32bit模式下的全局描述符表(GDT)

关键字: GDT

目标:用汇编实现GDT结构

理论基础

之前的实模式下,我们用的寻址方法就是段地址 << 4 + 偏移地址,但是随着计算机理论的发展,人们发现,如果不在各种地址段之间加上访问权限的限制,计算机数据的安全性难以保障,恶意软件可以通过访问大量地址获得存储在计算机上的,并不属于本软件的信息.

因此人们开发出一整套机制来完成保护数据的任务,这也是"保护模式"的由来.在32-bit模式下,人们使用的寻址方法为描述符寻址,即,对于任意存储在计算机中的数据/代码,需要先寻找全局描述符表(GDT),它存有各种描述符,再通过各种描述符,寻找具体的数据所在位置.

那么怎样寻找全局描述符表(GDT)呢?找东西,用描述符,因此会有一个GDT描述符,那么如何找GDT描述符呢?难道会有GDT描述符描述符吗?不,人们人为定义了GDT描述符的位置,根据系统的不同而不同.

还有一个问题,描述符如何"描述"数据的存储位置呢?
当然是数据所在段的基地址,还有数据所在段的大小.这样可以确定我们的目标数据段,在其中寻找目标数据,而不超越该段,越界访问其它数据.

当然,为了使数据访问更加安全,人们为描述符设置了除基地址和大小之外的属性,这些属性更加精细地控制该描述符可以访问的数据类型.如访问类型(读/写/执行),描述符权限等.

以下图示说明GDT寻址过程:
在这里插入图片描述

还有一些额外数据
GDT最多含有8192个描述符,每个描述符8个字节.
为什么可能会经历多级描述符呢?
多级描述符可以寻址更大的地址空间.
在经历多级描述符时,除了最底层的描述符,上层的描述符都被称为描述符选择子,其作用是定位下一级描述符表.

源码

了解完了GDT,来看一下如何实现一个GDT结构为后续的寻址提供便利,本节的源码与上一节一样,暂时无法实验,给出的源码示例如何写一个GDT,实现其物理结构.

gdt_start: ; 不要认为这些标签可有可无,它们可以帮助计算GDT的size、jump范围

    ; GDT的开头是8个字节的空值
    dd 0x0 ; 4字节
    dd 0x0 ; 4字节

; 代码段的gdt, base = 0x00000000, length = 0xffffff
; 对于gdt中的flags,参见https://wiki.osdev.org/Global_Descriptor_Table
gdt_code:
    dw 0xffff   ; 段长度(segment length), bit0 - bit15
    dw 0x0      ; 段基址(segment base), bit0 - bit15
    db 0x0      ; 段基址(segment base), bit16 - bit23
    db 10011010b ; Access Byte (flags)(8bits) 
    db 11001111b ; 段长度(段长度共20bit,该字节低4bit为段长度的高4bit)
    			 ; flags标志位,共4bit,在该字节的高4bit
    db 0x0       ; ; 段基址(segment base), bit24 - bit31

; 数据段的gdt, base和length和代码段一样
; 部分标志位(flags)有所不同
gdt_data:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10010010b
    db 11001111b
    db 0x0

gdt_end:

; GDT描述符(descriptor)
gdt_descriptor:
    dw gdt_end - gdt_start - 1 ; 描述符中指明的GDT的大小(16bit),该大小总是比实际大小小1
    dd gdt_start ; 描述符中指明的GDT的地址(32bit)

; 定义一些常数,供后续使用
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

需要说明的是,GDT中的描述符选择子的Flags和Access Byte的内容如下:
在这里插入图片描述
Pr: Present bit. 1bit 对于所有有效的描述符(描述符选择子),Pr都必须为1.
Privl: Privilege. 2bit 包含优先级,0最高,3最低.
S: Descriptor Type. 数据段和代码段应该将该bit值1, 系统段如TSS段,应该将bit置0.
Ex: Executable bit. 如果为1, 则是代码段选择子,如果为0,则为数据段选择子.
DC: Direction bit / Conforming bit.这是个多功能bit.

对于数据段,该bit给出了数据增长方向,如果为0,那么数据段向上增长,如果为1,那么数据段向下增长(之前学过的stack就是向下增长).

对于代码段,该bit给出了代码一致性,如果为0,那么该段代码可被同级权限或更低级权限执行.如果为1,该段代码只能被Privl域设置的权限执行.

RW:可读/可写bit,对于代码段,这是一个是否可读bit,代码段不可写.对于数据段,这是一个是否可写bit,数据段一直是可读的.

Ac: Accessed,默认为0,只有在CPU访问它时,才自动设置为1

Gr: 粒度bit,如果为0,那么Limit的单位是字节,如果为1,Limit的单位是页(Page),一页为4KiB.

Sz: Size bit. 如果为0, 选择子定义16bit的保护模式,如果为1,定义32bit的保护模式,可以同时存在16bit和32bit的选择子.

bootloader.asm: ; ============================== ; bootloader.asm - 完整可运行的最小引导程序 ; 编译: nasm -f bin bootloader.asm -o bootloader.bin ; ============================== section .text global _start bits 16 _start: ; 初始化寄存器 cli xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0xFFFE ; 打印 'S' 表示开始 mov ah, 0x0E mov al, 'S' int 0x10 ; 加载内核到 0x1000:0x0000 (物理地址 0x10000) mov ah, 0x02 ; BIOS 磁盘读取功能 mov al, 0x10 ; 读取1个扇区 mov ch, 0x00 ; 柱面0 mov cl, 0x02 ; 扇区2(LBA 1) mov dh, 0x00 ; 磁头0 mov dl, 0x80 ; 第一块硬盘 mov bx, 0x1000 ; ES:BX = 0x1000:0000 mov es, bx xor bx, bx int 0x13 jc .disk_error ; 显示 'O' 表示成功 mov al, 'O' mov ah, 0x0E int 0x10 jmp enter_protected_mode .disk_error: mov al, 'X' mov ah, 0x0E int 0x10 hlt .hang: hlt jmp .hang ; ======================================== ; 进入保护模式部分 ; ======================================== enter_protected_mode: cli lgdt [gdt_descriptor] ; ✅ 加载 GDT mov eax, cr0 or eax, 1 ; ✅ 设置 PE 位 mov cr0, eax mov dword [0xB8000], 0x0F52 ; 白字黑底的 'R' jmp CODE_SEG:flush ; ✅ 远跳转刷新流水线(far jump) mov dword [0xB8000], 0x0F52 ; 白字黑底的 'R' mov dword [0xB8000], 0x0F52 ; 白字黑底的 'R' mov dword [0xB8000], 0x0F52 ; 白字黑底的 'R' bits 32 flush: ; 设置段寄存器为数据段选择子(DATA_SEG) mov ax, DATA_SEG mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax ; 设置栈指针(可选) mov ebp, 0x90000 mov esp, ebp ; ✅ 写显存显示 'R' mov dword [0xB8000], 0x0F52 ; 白字黑底的 'R' ; ❌ 不要再使用 int 0x10,BIOS 中断只在实模式有效! ; ✅ 跳转到内核入口地址(物理地址 0x10000) jmp CODE_SEG:0x10000 ; ======================================== ; GDT 定义 ; ======================================== gdt_start: dq 0x0 ; Null段 gdt_code: dw 0xFFFF ; Limit (low) dw 0x0000 ; Base (low) db 0x00 ; Base (middle) db 0x9A ; Present, DPL=0, S=1, Executable, Readable db 0xCF ; Granularity, 32-bit db 0x00 ; Base (high) gdt_data: dw 0xFFFF ; Limit (low) dw 0x0000 ; Base (low) db 0x00 ; Base (middle) db 0x92 ; Present, DPL=0, S=1, Writable db 0xCF ; Granularity, 32-bit db 0x00 ; Base (high) gdt_end: gdt_descriptor: dw gdt_end - gdt_start - 1 dd gdt_start CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start ; ======================================== ; 引导扇区结束 ; ======================================== times 510 - ($ - $$) db 0 dw 0xAA55 kernel_entry.axm: ; ============================== ; kernel_entry.asm ; 功能: 32位内核入口,设置保护模式环境并调用kernel_main ; 编译: nasm -f elf32 kernel_entry.asm -o kernel_entry.o ; ============================== bits 32 section .text global start_pm extern kernel_main ; 声明外部C函数 start_pm: ; 打印 'R' 到屏幕左上角 mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' mov dword [0xB8000], 0x0F52 ; 绿色背景白字的 'R' ; 设置段寄存器 mov ax, DATA_SEG mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax ; 设置栈指针 mov ebp, 0x90000 mov esp, ebp ; 调用C内核入口 call kernel_main ; 如果返回,则挂起 hang: cli hlt jmp hang ; GDT符号(需与bootloader.asm一致) CODE_SEG equ 0x08 DATA_SEG equ 0x10 linker.ld: /* 内核链接器脚本 */ OUTPUT_FORMAT("elf32-i386") ENTRY(start_pm) SECTIONS { /* 内核加载地址为0x10000 (与bootloader中的0x1000:0x0000对应) */ . = 0x10000; .text : { *(.text) } .data : { *(.data) *(.rodata*) } .bss : { *(COMMON) *(.bss) } /* 确保内核大小不超过8KB (16个扇区) */ /DISCARD/ : { *(.note*) *(.comment*) } } Makefile: # Makefile for MyOS (兼容 Windows, WSL 和 Ubuntu 18.04.6 LTS) # 工具定义 ifeq ($(OS),Windows_NT) # Windows 设置 NASM := nasm GCC := gcc LD := ld RM := del /Q MKDIR := mkdir QEMU := qemu-system-x86_64 OBJCOPY := objcopy else # Linux/WSL 设置 NASM := nasm GCC := gcc LD := ld RM := rm -f MKDIR := mkdir -p QEMU := qemu-system-x86_64 OBJCOPY := objcopy endif # 编译选项 NASMFLAGS := -f elf32 GCCFLAGS := -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector \ -nostartfiles -nodefaultlibs -Wall -Wextra -c \ -Ikernel/include LDFLAGS := -m elf_i386 -T linker.ld # 目标文件 BOOTLOADER := boot/bootloader.bin KERNEL_ENTRY_OBJ := boot/kernel_entry.o # 新增:添加 process_switch.o KERNEL_OBJS := kernel/main.o kernel/memory_manager.o kernel/process.o kernel/vga.o kernel/process_switch.o KERNEL_BIN := kernel/kernel.bin KERNEL_ELF := kernel/kernel.elf OS_IMAGE := myos.img # 默认目标 all: $(OS_IMAGE) # 构建引导扇区 $(BOOTLOADER): boot/bootloader.asm $(NASM) -f bin $< -o $@ # 构建内核入口 $(KERNEL_ENTRY_OBJ): boot/kernel_entry.asm $(NASM) $(NASMFLAGS) $< -o $@ # 构建C内核对象文件 kernel/%.o: kernel/%.c $(GCC) $(GCCFLAGS) $< -o $@ # 特别处理 .S 汇编文件(注意使用 $(CC) 来启用预处理器) kernel/%.o: kernel/%.S $(GCC) $(GCCFLAGS) $< -o $@ # 链接内核 $(KERNEL_ELF): $(KERNEL_ENTRY_OBJ) $(KERNEL_OBJS) $(LD) $(LDFLAGS) $^ -o $@ # 生成纯二进制内核 $(KERNEL_BIN): $(KERNEL_ELF) $(OBJCOPY) -O binary $< $@ # 创建OS镜像 $(OS_IMAGE): $(BOOTLOADER) $(KERNEL_BIN) @echo "Creating OS image..." @dd if=/dev/zero of=$@ bs=512 count=2880 2>/dev/null @dd if=$(BOOTLOADER) of=$@ conv=notrunc 2>/dev/null @dd if=$(KERNEL_BIN) of=$@ seek=1 conv=notrunc 2>/dev/null # 清理 clean: $(RM) $(BOOTLOADER) $(KERNEL_ENTRY_OBJ) $(KERNEL_OBJS) $(KERNEL_ELF) $(KERNEL_BIN) $(OS_IMAGE) # 运行QEMU run: $(OS_IMAGE) $(QEMU) -drive format=raw,file=$< 这些程序在QEMU中运行时,为什么会导致不断地运行到设置PE位附近时重新开始运行
最新发布
06-15
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值