【实现一个简单的操作系统】02- 实现保护模式内存寻址GDT

【实现一个简单的操作系统】01- 在虚拟机中运行简单内核

前言

本系列是实现一个简单的操作系统中的第 2 篇博客,如果您感兴趣,建议您阅读前面的博客。本篇博客将介绍保护模式内存寻址 GDT 如何在一个简单的操作系统中实现。

1 GDT

GDT全称 Global Descriptor Table,是x86保护模式下的一个重要数据结构,在保护模式下,GDT在内存中有且只有一个。GDT的数据结构是一个描述符数组,每个描述符8个字节,可以存放在内存当中任意位置,GDT 的地址由 GDTR 保存。

在这里插入图片描述

  • 实模式下的寻址方式
    CPU通过段地址段偏移量寻址。其中段地址保存到段寄存器,包含:CS、SS、DS、ES、FS、GS。段偏移量可以保存到IP、BX、SI、DI寄存器。
    mov ds:[si], ax 为例, 对应的物理地址为 ds * 16 + si(段地址左移 4 位加上偏移地址)
  • 保护模式下的寻址方式
    在保护模式下,也是通过段寄存器段偏移量寻址,但是此时段寄存器保存的数据意义不同了。此时的段寄存器需要使用16位来定位段地址,第0、1位存储了当前的特权级(CPL),第2位存储了TI值(0代表GDT,1代表LDT),后13位相当于GDT表中某个描述符的索引(最多可以索引 8K 个段描述符),即段选择子。
    在这里插入图片描述
    mov ds:[si], ax 为例,寻址过程如下:
    ① 从GDTR寄存器中直接获取GDT的位置
    ② 根据段寄存器的后 13 位定位目标段的段描述符在 GDT 中的位置
    ③ 计算出描述符中的段基址的值加上段偏移量的结果,该结果为目标物理地址

2 段描述符

明白了 GDT 的寻址功能,还需要知道段描述符的具体细节。

一个 GDT 段描述符占用8个字节,包含三个部分:

  • 段基址:规定线性地址空间中段的开始地址(32位),占据描述符的第16~39位和第55位~63位,前者存储低16位,后者存储高16位
  • 段界限:规定段的大小(20位),占据描述符的第0~15位和第48~51位,前者存储低16位,后者存储高4位。
  • 段属性:通过一些位规定段描述符的不同功能(12位),占据描述符的第39~47位和第49~55位,段属性可以细分为8种:TYPE属性、S属性、DPL属性、P属性、AVL属性、L属性、D/B属性和G属性。

在这里插入图片描述

具体属性值的作用见段属性详解

3 GDT 的实现

所有代码均放在文末的附录上,见附录

3.1 定义数据类型

由于段描述符需要处理多个不同长度的数据,可以定义一个 types.h 文件,重新定义数据类型。
types.h

#ifndef __TYPES_H__
#define __TYPES_H__

typedef char                    int8_t;
typedef unsigned char           uint8_t;

typedef short                   int16_t;
typedef unsigned short          uint16_t;

typedef int                     int32_t;
typedef unsigned int            uint32_t;

typedef long long int           int64_t;
typedef unsigned long long int  uint64_t;

#endif

定义完数据类型后,kernel.cpp 中的代码有些数据类型可以修改成新数据类型,具体见文末附录 kernel.cpp

3.2 GDT 类定义

由之前关于 GDT 的介绍可知,GDT 由多个段描述符组成,段描述符由段基址,段界限及段属性组成。因此,在定义 GDT 类时,需要考虑以上数据结构的定义。本实现先定义GlobalDescriptorTable类,在该类内部定义SegmentDescriptor类,具体实现如下。

gdt.h

#ifndef __GDT_H__
#define __GDT_H__

#include "types.h"

class GlobalDescriptorTable{
    public:
        class SegmentDescriptor{
            private:
                uint16_t limit_lo;          // 段界限低16位
                uint16_t base_lo;           // 基地址低16位
                uint8_t base_hi;            // 基地址高16位中的低8位
                uint8_t type;               // 段属性的低8位
                uint8_t flags_limit_hi;     // 段界限,段属性的高4位
                uint8_t base_vhi;           // 段基址的高8位
            public:
                SegmentDescriptor(uint32_t base, uint32_t limit, uint8_t type);
                uint32_t Base();            // 获取段基址
                uint32_t Limit();           // 获取段界限
        } __attribute__ ((packed));         // 由于定义了不同长度的数据,禁止编译器做字节对齐

        // GDT表中可有很多个描述符,目前只定义了四个段描述符
        SegmentDescriptor nullSegmentSelector;
        SegmentDescriptor unusedSegmentSelector;
        SegmentDescriptor codeSegmentSelector;
        SegmentDescriptor dataSegmentSelector;
    
    public:
        GlobalDescriptorTable();
        ~GlobalDescriptorTable();

        uint16_t CodeSegmentSelector();
        uint16_t DataSegmentSelector();
};

#endif
3.3 GDT 类实现

根据以上类定义,我们需要实现SegmentDescriptor(), Base(), Limit(), GlobalDescriptorTable(), ~GlobalDescriptorTable(), CodeSegmentSelector(), DataSegmentSelector()这些函数。

  • SegmentDescriptor()的实现
    SegmentDescriptor() 是段描述符的构造函数,需要初始化段描述符的 8 个字节。接收三个参数,分别是 base(段基址),limit(段界限),type(段属性)。段描述符的初始化需要详细参考段属性的作用(见全局描述符表GDT详解)以及段描述符中字节的排布方式。

    SegmentDescriptor()

    GlobalDescriptorTable::SegmentDescriptor::SegmentDescriptor(uint32_t base, uint32_t limit, uint8_t type){
        uint8_t *target = (uint8_t*)this;       // this指针指向的是成员变量
        
    
        // 设置段属性
        if(limit <= 65536){     // 如果段界限小于等于64K,使用段界限粒度为 B
            target[6] = 0x40;   // 01000000
        }else{                  // 如果段界限大于64K,使用段界限粒度为 4KB
            if((limit & 0xFFF) != 0xFFF){
                limit = (limit >> 12) - 1;
            }else{
                limit = limit >> 12;
            }
    
            target[6] = 0xC0;   // 11000000
        }
        target[5] = type;
    
        // 设置段界限
        target[0] = limit & 0xF;
        target[1] = (limit >> 8) & 0xFF;
        target[6] = target[6] | ((limit >> 16) & 0xF);
    
        // 设置段基址
        target[2] = base & 0xFF;
        target[3] = (base >> 8) & 0xFF;
        target[4] = (base >> 16) & 0xFF;
        target[7] = (base >> 24) & 0xFF;
    }
    
  • Base()的实现
    Base() 函数的功能是获得段描述符的段基址,由于段描述符中的段基址不是连续存在的,需要通过计算得到。
    Base()

    uint32_t GlobalDescriptorTable::SegmentDescriptor::Base(){
        uint8_t *target = (uint8_t*)this;
        uint32_t result = target[7];
        result = (result << 8) + target[4];
        result = (result << 8) + target[3];
        result = (result << 8) + target[2];
    
        return result;
    }
    
  • Limit()的实现
    Limit() 函数的功能是获得段描述符的段界限,由于段描述符中的段界限不是连续存在的,需要通过计算得到。

    uint32_t GlobalDescriptorTable::SegmentDescriptor::Limit(){
    	uint8_t *target = (uint8_t*)this;
    	uint32_t result = target[6] & 0xF;
    	result = (result << 8) + target[1];
    	result = (result << 8) + target[0];
    
    	if((target[6] & 0xC0) == 0xC0){
        	result = (result << 12) | 0xFFF;
    	}
    
    	return result;
    }
    
  • GlobalDescriptorTable()的实现
    GlobalDescriptorTable() 是 GDT 的构造函数, GDT 初始化时需要给 GDTR 置数,即把 GDT 对象的地址给 GDTR。
    GlobalDescriptorTable()

    GlobalDescriptorTable::GlobalDescriptorTable() : 
                        nullSegmentSelector(0, 0, 0),
                        unusedSegmentSelector(0, 0, 0),
                        codeSegmentSelector(0, 64 * 1024, 0x9A),    // 10011010
                        dataSegmentSelector(0, 64 * 1024, 0x92){    // 10010010
        uint32_t i[2];
        i[1] = (uint32_t)this;
        i[0] = sizeof(GlobalDescriptorTable) << 16;
        __asm__ volatile("lgdt (%0)" : : "p" ((uint8_t*)i + 2));
    }
    
  • CodeSegmentSelector()的实现
    CodeSegmentSelector() 的作用是得到代码段描述符在 GDT 的偏移量。
    CodeSegmentSelector()

    uint16_t GlobalDescriptorTable::CodeSegmentSelector(){
        return (uint8_t*)&codeSegmentSelector - (uint8_t*)this;
    }
    
  • DataSegmentSelector()的实现
    DataSegmentSelector() 的作用是得到数据段描述符在 GDT 的偏移量。

    uint16_t GlobalDescriptorTable::DataSegmentSelector(){
        return (uint8_t*)&dataSegmentSelector - (uint8_t*)this;
    }
    
  • 在keinelMain 中创建 GDT

    extern "C" void kernelMain(void *multiboot_structure, int32_t magic_number){
        printf("Hello OS!");
        GlobalDescriptorTable gdt;
    
        while(1); // 内核程序需要保证持续运行,死循环保证持内核持续运行
    }
    

4 printf 的改进

前面实现的 printf 函数没有考虑到显示内存的限制及其换行问题,可以做一些改进避免上述问题,定义 x, y 分别表示光标所在的列和行。一行最多显示80个字符,最多显示25行。

printf()

void printf(const int8_t *str){
    static int16_t *VideoMemory = (short*)0xb8000;
    static int8_t x = 0, y = 0;
    for(int32_t i = 0; str[i] != '\0'; ++i){
        switch(str[i]){
            case '\n':  // 换行
                ++y;
                x = 0;
                break;
            default:
                VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00 | str[i]);
                ++x;
                break;
        }

        if(x >= 80){
            ++y;
            x = 0;
        }

        if(y >= 25){
            for(y = 0; y < 25; ++y){
                for(x = 0; x < 80; ++x){
                    VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00 | ' ');
                }
            }
            x = 0;
            y = 0;
        }
    }
}

5 编译运行内核

make mykernel.iso

在虚拟机中运行内核请参照 在虚拟机中运行简单内核

参考资料

[1] x86保护模式——全局描述符表GDT详解
[2] 操作系统篇-分段机制与GDT|LDT

附录

gdt.h

#ifndef __GDT_H__
#define __GDT_H__

#include "types.h"

class GlobalDescriptorTable{
    public:
        class SegmentDescriptor{
            private:
                uint16_t limit_lo;          // 段界限低16位
                uint16_t base_lo;           // 基地址低16位
                uint8_t base_hi;            // 基地址高16位中的低8位
                uint8_t type;               // 段属性的低8位
                uint8_t flags_limit_hi;     // 段界限,段属性的高4位
                uint8_t base_vhi;           // 段基址的高8位
            public:
                SegmentDescriptor(uint32_t base, uint32_t limit, uint8_t type);
                uint32_t Base();            // 获取段基址
                uint32_t Limit();           // 获取段界限
        } __attribute__ ((packed));         // 由于定义了不同长度的数据,禁止编译器做字节对齐

        // GDT表中可有很多个描述符,目前只定义了四个段描述符
        SegmentDescriptor nullSegmentSelector;
        SegmentDescriptor unusedSegmentSelector;
        SegmentDescriptor codeSegmentSelector;
        SegmentDescriptor dataSegmentSelector;
    
    public:
        GlobalDescriptorTable();
        ~GlobalDescriptorTable();

        uint16_t CodeSegmentSelector();
        uint16_t DataSegmentSelector();
};

#endif

gdt.cpp

#include "gdt.h"

GlobalDescriptorTable::SegmentDescriptor::SegmentDescriptor(uint32_t base, uint32_t limit, uint8_t type){
    uint8_t *target = (uint8_t*)this;       // this指针指向的是成员变量
    

    // 设置段属性
    if(limit <= 65536){     // 如果段界限小于等于64K,使用段界限粒度为 B
        target[6] = 0x40;   // 01000000
    }else{                  // 如果段界限大于64K,使用段界限粒度为 4KB
        if((limit & 0xFFF) != 0xFFF){
            limit = (limit >> 12) - 1;
        }else{
            limit = limit >> 12;
        }

        target[6] = 0xC0;   // 11000000
    }
    target[5] = type;

    // 设置段界限
    target[0] = limit & 0xFF;
    target[1] = (limit >> 8) & 0xFF;
    target[6] = target[6] | ((limit >> 16) & 0xF);

    // 设置段基址
    target[2] = base & 0xFF;
    target[3] = (base >> 8) & 0xFF;
    target[4] = (base >> 16) & 0xFF;
    target[7] = (base >> 24) & 0xFF;
}

uint32_t GlobalDescriptorTable::SegmentDescriptor::Base(){
    uint8_t *target = (uint8_t*)this;
    uint32_t result = target[7];
    result = (result << 8) + target[4];
    result = (result << 8) + target[3];
    result = (result << 8) + target[2];

    return result;
}

uint32_t GlobalDescriptorTable::SegmentDescriptor::Limit(){
    uint8_t *target = (uint8_t*)this;
    uint32_t result = target[6] & 0xF;
    result = (result << 8) + target[1];
    result = (result << 8) + target[0];

    if((target[6] & 0xC0) == 0xC0){
        result = (result << 12) | 0xFFF;
    }

    return result;
}

GlobalDescriptorTable::GlobalDescriptorTable() : 
                    nullSegmentSelector(0, 0, 0),
                    unusedSegmentSelector(0, 0, 0),
                    codeSegmentSelector(0, 64 * 1024 * 1024, 0x9A),    // 10011010
                    dataSegmentSelector(0, 64 * 1024 * 1024, 0x92){    // 10010010
    uint32_t i[2];
    i[1] = (uint32_t)this;
    i[0] = sizeof(GlobalDescriptorTable) << 16;
    __asm__ volatile("lgdt (%0)" : : "p" ((uint8_t*)i + 2));
}

GlobalDescriptorTable::~GlobalDescriptorTable(){

}

uint16_t GlobalDescriptorTable::CodeSegmentSelector(){
    return (uint8_t*)&codeSegmentSelector - (uint8_t*)this;
}

uint16_t GlobalDescriptorTable::DataSegmentSelector(){
    return (uint8_t*)&dataSegmentSelector - (uint8_t*)this;
}

kernel.cpp

#include "types.h"
#include "gdt.h"

void printf(const int8_t *str){
    static int16_t *VideoMemory = (short*)0xb8000;
    static int8_t x = 0, y = 0;
    for(int32_t i = 0; str[i] != '\0'; ++i){
        switch(str[i]){
            case '\n':  // 换行
                ++y;
                x = 0;
                break;
            default:
                VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00 | str[i]);
                ++x;
                break;
        }

        if(x >= 80){
            ++y;
            x = 0;
        }

        if(y >= 25){
            for(y = 0; y < 25; ++y){
                for(x = 0; x < 80; ++x){
                    VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00 | ' ');
                }
            }
            x = 0;
            y = 0;
        }
    }
}

typedef void (*constructor)();

// 在链接文件中,start_ctors 和 end_ctors实际上时两个地址
extern constructor start_ctors;
extern constructor end_ctors;

// 进行初始化操作
extern "C" void callConstructors(){
    for(constructor *i = &start_ctors; i != &end_ctors; ++i){
        (*i)();
    }
}

extern "C" void kernelMain(void *multiboot_structure, int32_t magic_number){
    printf("Hello OS!");
    GlobalDescriptorTable gdt;

    while(1); // 内核程序需要保证持续运行,死循环保证持内核持续运行
}

linker.ld

ENTRY(loader)

OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386:i386)

SECTIONS {
    . = 0x100000;

    .text : {
        *(.multiboot)
        *(.text*)
        *(.rodata)
    }

    .data : {
        start_ctors = .;
        KEEP(*(.init_array));
        KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*)));
        end_ctors = .;

        *(.data)
    }

    .bss : {
        *(.bss)
    }

    /DISCARD/ : {
        *(fini_array*)
        *(.comment)
    }
}

loader.s

.set MAGIC, 0x1badb002
.set FLAGS, (1 << 0 | 1 << 1)
.set CHECKSUM, -(MAGIC + FLAGS)

.section .multiboot
    .long MAGIC
    .long FLAGS
    .long CHECKSUM

.section .text
.extern kernelMain
.extern callConstructors
.global loader

loader:
    mov $kernel_stack, %esp
    call callConstructors
    push %eax
    push %ebx
    call kernelMain

_stop:
    cli
    hlt
    jmp _stop

.section .bss
.space 2*1024*1024
kernel_stack:

Makefile

GPPPARAMS = -m32 -fno-use-cxa-atexit -nostdlib -fno-builtin -fno-rtti -fno-exceptions -fno-leading-underscore
ASMPARAMS = --32
LDPARAMS = -melf_i386

objects = kernel.o loader.o gdt.o

%.o : %.cpp
	g++ $(GPPPARAMS) -o $@ -c $<

%.o : %.s
	as $(ASMPARAMS) -o $@ $<

mykernel.bin : linker.ld $(objects)
	ld $(LDPARAMS) -T $< -o $@ $(objects)

mykernel.iso : mykernel.bin
	mkdir iso
	mkdir iso/boot
	mkdir iso/boot/grub
	cp $< iso/boot/
	echo 'set timeout=2' >> iso/boot/grub/grub.cfg
	echo 'set default=0' >> iso/boot/grub/grub.cfg
	echo '' >> iso/boot/grub/grub.cfg
	echo 'menuentry "Mini OS" {' >> iso/boot/grub/grub.cfg
	echo '	multiboot /boot/mykernel.bin' >> iso/boot/grub/grub.cfg
	echo '	boot' >> iso/boot/grub/grub.cfg
	echo '}' >> iso/boot/grub/grub.cfg
	grub-mkrescue --output=$@ iso
	rm -rf iso

run : mykernel.iso
	virtualboxvm --startvm "Tuitorial" &

.phony : clean
clean:
	rm -rf $(objects) mykernel.bin mykernel.iso
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值