前言
本系列是实现一个简单的操作系统
中的第 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