文章目录
- 1. 类型统一 types
- 2. IO读写之port
- 2.1 通过C语言利用汇编指令对IO的读写控制
- 2.2 port8bit的定义和实现
- 3. 全局描述之GDT
- 3.1 CPU的工作模式(寻址方式)和GDT的恩怨纠葛
- 3.2 相关知识点
- 3.3 GDT的定义和实现
- 4. 中断之IDT
- 4.1 中断是什么?
- a. 一个完整的中断过程包括
- b. 中断的作用
- c. 中断的产生和相应的概念
- d. 中断源:
- 4.2 中断描述符
- 4.3 门描述符
- 4.4 分段机制与GDT|LDT
- 4.5 实际代码
- 还有很多细节没有解释清楚, 等回头再继续整理添加, 未完待续...
myos1
中完成了引导和基础框架, 但是一个完整的OS还有很多的基础工具需要完成, 比如GDT
和IDT
, 本文主要的工作就是完成这俩核心技术, 最后能够实现通过键盘实现中断处理
; 本文的所有代码都上传到了myos2里面了, 可以点击下载通过make iso进行生成测试
整个工程文档如下
myos2_common$ tree
.
├── Makefile # 统一编译文件
├── gdt.cpp
├── gdt.h
├── interrupts.cpp
├── interrupts.h
├── interruptstubs.s
├── kernel.cpp
├── linker.ld # 链接工具
├── loader.s # 引导文件
├── mykernel.iso # 生成的镜像, 虚拟机启动
├── port.cpp
├── port.h
├── readme.md
└── types.h
1. 类型统一 types
类型统一的意义是:
- 为了保证可移植性; 很多产品用的芯片是不断升级的。从8位到16位,再到32位甚至更高。为了代码最大限度的可重用,要自己定义不同位数的类型。比如16位机定义一个16位整数类型Int16。可以typedef int int16;
如果要移植到32位机,只需改一行代码。typedef short int int16;
// 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
2. IO读写之port
操作系统之IO Linux有一个思想是 一切皆文件; 也就是说, 不管是外设也好, 硬盘数据的读写也好, 从代码层面都是把这些看做是一个文件, 对外设等进行操作, 就是对他们里面的寄存器进行读写操作;
什么是IO?
对于电子工程师而言,I/O 硬件就是芯片、导线、电源和其他组成硬件的物理设备。而我们程序员眼中的 I/O 其实就是硬件提供给软件的接口,比如硬件接受到的命令、执行的操作以及反馈的错误。
2.1 通过C语言利用汇编指令对IO的读写控制
// outb 写给端口一个字节 data → portnumber
__asm__ volatile("outb %0, %1" : "=a" (data) : "Nd" (portnumber));
// inb从IO端口读取一个字节, portnumber → result
__asm__ volatile("inb %1, %0" : "=a" (result) : "Nd" (portnumber));
注意: 这里其实涉及到一个知识点(C语言调用汇编指令)
2.2 port8bit的定义和实现
// port.h
#ifndef __PORT_H
#define __PORT_H
#include "types.h"
class Port {
protected:
uint16_t portnumber;
Port(uint16_t portnumber);
~Port();
};
class Port8Bit : public Port {
public:
Port8Bit(uint16_t portnumber);
~Port8Bit();
virtual void Write(uint8_t data);
virtual uint8_t Read();
};
class Port8BitSlow : public Port8Bit {
public:
Port8BitSlow(uint16_t portnumber);
~Port8BitSlow();
virtual void Write(uint8_t data);
};
#endif
// port.cpp
#include "port.h"
Port::Port(uint16_t portnumber)
: portnumber(portnumber) {}
Port::~Port() {}
Port8Bit::Port8Bit(uint16_t portnumber)
: Port(portnumber) {}
Port8Bit::~Port8Bit() {}
void Port8Bit::Write(uint8_t data) {
// outb 写给端口一个字节 data → portnumber
__asm__ volatile("outb %0, %1" : "=a" (data) : "Nd" (portnumber));
}
uint8_t Port8Bit::Read() {
uint8_t result;
// inb从IO端口读取一个字节, portnumber → result
__asm__ volatile("inb %1, %0" : "=a" (result) : "Nd" (portnumber));
return result;
}
Port8BitSlow::Port8BitSlow(uint16_t portnumber)
: Port8Bit(portnumber) {}
Port8BitSlow::~Port8BitSlow() {}
void Port8BitSlow::Write(uint8_t data) {
// 其实就是多跳了一句没用的语句
__asm__ volatile("outb %0, %1\njmp 1f\n1: jmp 1f\n1:" : "=a" (data) : "Nd" (portnumber));
}
其实很简单, 就是利用汇编对端口号portnumber
的读写
而这个portnumber
我们把它当成一个文件的fd
就清楚了
3. 全局描述之GDT
摘自
linux分段内存管理操作系统之GDT和IDT(三)
3.1 CPU的工作模式(寻址方式)和GDT的恩怨纠葛
在i386
, X86
等架构下, CPU
有两种工作模式
- 实模式: CPU复位或加电时候, 以实模式启动, 处理器工作在实模式下; 此时CPU寻址方式与8086寻址相同
乘16相当于右移4位
形成20位物理地址
这就是所谓的[段寄存器基址:段内偏移量]方式的寻址
所以此时最大的寻址空间是1M()
低16位地址可以是有效范围内的任意值(64K空间), 所以最大分段能力64K()
这样,就意味着你可以随意(读和写)访问物理内存中的任何位置的数据,这样就非常的不安全。到了80286,考虑到实地址模式的潜在危险,intel将寻址方式改成了保护模式,但改的不够彻底,能从实地址模式转到保护模式,但不能反向转换。
从80386开始才算是彻底转换成功,从此开始了32位cpu的时代, 因为这个时代, 真正的32位寄存器, 32位的地址总线, 一个寄存器就具有了4G的寻址能力。考虑到向前兼容性,80386还得支持实地址模式,所以只能基于原来的4个段寄存器(每个8位)进行修补。其设计思想是:在保护模式下,改变段寄存器的功能,不再表示单一的一个基地址,而是变成指向一个数据结构的指针。这个数据机构就是段描述符Segment Descripter(长度为64bit,8个字节,每一描述符描述一个段的信息),而段描述符是聚集在一起存储的,那个结构叫段描述符表,Segment Descripter Table。又根据这个段描述符表里存的是全局共同的,还是进程私有的,分成了全局段描述符表(GDT)和局部段描述符表(LDT)。 - 保护模式: 寻址采用32位段和偏移量,最大寻址空间4GB,最大分段4GB 。在保护模式下CPU可以进入虚拟8086方式,这是在保护模式下的实模式程序运行环境。
- 二者最大的区别在于: 保护模式一是提供了段间的保护机制,防止程序间胡乱访问地址带来的问题,二是访问的内存空间变大,80386具有32位寄存器,寻址可达到4GB
也就是实地址模式下, Base Address+Offset就是一个内存绝对地址; 一个段具备两个因素:Base Address和Limit(段的最大长度),而对一个内存地址的访问,则是需要指出:使用哪个段?以及相对于这个段Base Address的Offset,这个Offset应该小于此段的Limit。当然对于16-bit系统,Limit不要指定,默认为最大长度64KB,而 16-bit的Offset也永远不可能大于此Limit。我们在实际编程的时候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment,CPU将段寄存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的Base Address。
到了保护模式,内存的管理模式分为两种,段模式和页模式,其中页模式也是基于段模式的。也就是说,保护模式的内存管理模式事实上是:纯段模式和段页式。进一步说,段模式是必不可少的,而页模式则是可选的——如果使用页模式,则是段页式;否则这是纯段模式。
既然是这样,我们就先不去考虑页模式。对于段模式来讲,访问一个内存地址仍然使用Segment:Offset的方式,这是很自然的。由于保护模式运行在32位系统上,那么Segment的两个因素:Base Address和Limit也都是32位的。IA-32允许将一个段的Base Address设为32-bit所能表示的任何值(Limit则可以被设为32-bit所能表示的,以2^12为倍数的任何值),而不象实时模式下,一个段的Base Address只能是16的倍数(因为其低4-bit是通过左移运算得来的,只能为0,从而达到使用16-bit段寄存器表示20-bit Base Address的目的),而一个段的Limit只能为固定值64 KB。另外,保护模式,顾名思义,又为段模式提供了保护机制,也就说一个段的描述符需要规定对自身的访问权限(Access)。所以,在保护模式下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。这种情况下,如果我们直接通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段寄存器装入这个段描述符。但Intel为了保持向后兼容,将段寄存器仍然规定为16-bit(尽管每个段寄存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段寄存器就是16-bit的),那么很明显,我们无法通过16-bit长度的段寄存器来直接引用64-bit的段描述符。怎么办?
解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13-bit的内容作为索引)。这个全局的数组就是GDT。事实上,在GDT中存放的不仅仅是段描述符,还有其它描述符,它们都是64-bit长,我们随后再讨论。
GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。
GDT是保护模式所必须的数据结构,也是唯一的——不应该,也不可能有多个。另外,正象它的名字(Global Descriptor Table)所揭示的,它是全局可见的,对任何一个任务而言都是这样。
总结一下:
- 在保护模式下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符:
- 段描述符使用数组存储,使用LGDT指令将GDT的入口地址装入GDTR寄存器。
- 段选择子,一个16位的数据结构; 索引号即作为GDT数组的下标,索引号只有13位,所以,GDT数组最多有8192个元素。
3.2 相关知识点
C语言 struct结构体的变量声明加冒号
// 位域列表的形式:类型说明符位域名:位域长度
struct bs {
int a:8;
int b:2;
int c:6;
int :2; // 位域可以无位域名
} data;
// data为bs变量,其中位域a占8位,位域b占2位,位域c占6位
// 指针类型变量不能指定所占的位数, 指针类型统一占4字节, 不能更改内存对齐
// gcc中对齐准则, 4的倍数,
struct T{
char ch ;
double d ;
} ;
// sizeof(T) = 12字节
typedef struct{
char c : 2 ;
double i ;
int c2 : 4 ;
} N3 ;
// sizeof(T) = 16字节__attribute__
struct S {
short b[3];
} __attribute__ ((aligned (8))); // 指定struct S变量分配空间时采用8字节对齐方式
struct S {
uint8_t a;
} __attribute__((packed)); // 不设置对齐
volatile
的意义asm volatile("lgdt (%0)": :"p" (((uint8_t *)i) + 2)); // 表示这句话不能被编译器优化, 必须原模原样执行
3.3 GDT的定义和实现
// gdt.h
#ifndef __GDT_H
#define __GDT_H
#include "types.h"
// 段描述符的类, 根据上面的图来进行定义
class GlobalDescriptorTable {
public:
// 段描述符寄存器 GDTR
class SegmentDescriptor {
public:
SegmentDescriptor( uint32_t base, // 段基址
uint32_t limit, // 段界限
uint8_t type); // 属性
uint32_t Base();
uint32_t Limit();
private:
// 根据 3.1 图 从地址0→63
uint16_t limit_lo;
uint16_t base_lo;
uint8_t base_hi;
uint8_t type;
uint8_t flags_limit_hi;
uint8_t base_vhi;
} __attribute__((packed)); // 不要存内存对齐, 见3.3解释
// 声明四个段, 16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment
SegmentDescriptor nullSegmentDescriptor;
SegmentDescriptor unusedSegmentDescriptor;
SegmentDescriptor codeSegmentDescriptor;
SegmentDescriptor dataSegmentDescriptor;
public:
GlobalDescriptorTable();
~GlobalDescriptorTable();
uint16_t CodeSegmentSelector();
uint16_t DataSegmentSelector();
};
#endif
// gdt.cpp4. 中断之IDT
#include "gdt.h"
GlobalDescriptorTable::GlobalDescriptorTable()
: nullSegmentDescriptor(0, 0, 0),
unusedSegmentDescriptor(0, 0, 0),
codeSegmentDescriptor(0, 64 * 1024 * 1024, 0x9a), // 代码段: 寻址64M大小, 0x9a的含义是跟图的位含义相关
dataSegmentDescriptor(0, 64 * 1024 * 1024, 0x92) {
uint32_t i[2]; // 存放 gdt Descripter
i[1] = (uint32_t)this;
i[0] = sizeof(GlobalDescriptorTable) << 16;
asm volatile("lgdt (%0)": :"p" (((uint8_t *)i) + 2));
}
GlobalDescriptorTable::~GlobalDescriptorTable() {}
// 数据段
uint16_t GlobalDescriptorTable::DataSegmentSelector() {
return ((uint8_t*)&dataSegmentDescriptor - (uint8_t*)this) << 3;
}
// 代码段 得到偏移的字节数
uint16_t GlobalDescriptorTable::CodeSegmentSelector() {
return ((uint8_t*)&codeSegmentDescriptor - (uint8_t*)this) << 3;
}
GlobalDescriptorTable::SegmentDescriptor::SegmentDescriptor(uint32_t base,
uint32_t limit,
uint8_t type) {
uint8_t* target = (uint8_t*)this;
if (limit < 1048576) { // 寻址能力限制到2^16次方, 不需要用保护模式
target[6] = 0x40;
} else { // 否则的话就需要使用保护模式进行2^20次方安排
if ((limit & 0xfff) != 0xfff) {
limit = (limit >> 12) - 1;
} else {
limit = limit >> 12;
}
target[6] = 0xC0;
}
target[0] = limit & 0xff;
target[1] = (limit >> 8) & 0xff;
target[6] |= (limit >> 16) & 0xf;
target[2] = base & 0xff;
target[3] = (base >> 8) & 0xff;
target[4] = (base >> 16) & 0xff;
target[7] = (base >> 24) & 0xff;
target[5] = type;
}
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;
}
摘自
可管理中断并处理中断方式I/O计算机组成原理——程序中断方式
4.1 中断是什么?
程序中断是指在计算机执行实现程序的过程中,出现某些急需处理的异常情况或特殊请求,CPU暂时中止现行程序,而转去这些异常情况或特殊请求进行处理,在处理完毕后CPU又自动返回到现行程序的断点处,继续执行原程序。 中断系统是计算机实现中断功能的软、硬件总称。在CPU一侧配置了中断机构,在设备一侧配置了中断控制接口,在软件上设计了相应的中断服务程序。
a. 一个完整的中断过程包括
**中断请求:**是指中断源(引起中断的事件或设备)向 CPU
发出的请求中断的要求。
**中断判优:**当有多个中断源发出请求时,需要通过适当的办法决定先处理哪个中断请求;
**中断响应:**指CPU中止现行程序转至中断服务程序的过程;
**中断处理:**就是指CPU执行中断服务程序;
中断返回: 执行完中断服务程序后,返回到被中断的程序
b. 中断的作用
- CPU与I/O设备并行工作
- 硬件故障处理
- 实现人机联系:在计算机工作过程中,如果用户要干预机器,如查看计算的中间结果,了解机器的工作状态,给机器下达临时性的命令等。在没有中断系统的计算机里这些功能几乎是无法实现的。
- 实现多道程序和分时操作
- 实现实时处理
- 实现应用程序和操作系统的联系
- 多处理机系统各处理机间的联系
c. 中断的产生和相应的概念
d. 中断源:
引起中断的事件,即发出中断请求的来源。
- 外中断:I/O设备等来自主机外部设备的中断。(通常所说的中断就是外中断)
- 内中断:处理器硬件故障或程序出错引起的中断。(也叫异常)
- 软中断:由“Trap”指令产生的软中断,这是在程序中预先安排好的
- 中断触发器:每个中断源的接口电路中都有一个“中断触发器”,用于保存中断源向CPU的中断请求信号。多个中断触发器构成中断寄存器。
主要上图所示的中断分类将CPU内部的异常、例外、陷入都有归为了内中断的行列中,不去区分是硬件引起还是软件引起的了
4.2 中断描述符
IDT,Interrupt Descriptor Table,即中断描述符表,和GDT类似,他记录了0~255的中断号和调用函数之间的关系。
- 段描述符使用数组存储,使用LIDT指令将IDT的入口地址装入IDTR寄存器。
中断描述符表IDT`将每个异常或中断向量分别与它们的处理过程联系起来。与`GDT`和`LDT`表类似,`IDT`也是由`8字节
长描述符组成的一个数组。与GDT不同的是,表中第一项可以包含描述符。为了构成``IDT表中的一个索引值,处理器把异常或中断的向量号*8。因为最多只有
256个中断或异常向量,所以IDT无需包含多于256个描述符。IDT中可以含有少于256个描述符,因为只有可能发生的异常或中断才需要描述符。不过
IDT`中所有空描述符项应该设置其存在位标志为0。
IDT表
可以驻留在线性地址空间
的任何地方,处理器使用IDTR寄存器
来定位IDT表
的位置。这个寄存器中含有IDT表
32位的基地址和16位的长度(限长)值。IDT表基地址应该对其在8字节边界上以提高处理器的访问效率。限长值是以字节为单位的IDT表的长度。
IDTR寄存器是6个字节, 即6*8 48个位
4.3 门描述符
IDT 表中可以存放三种类型的门描述符:
- 中断门描述符
- 陷阱门描述符
- 任务门描述符
中断门和陷阱门含有一个长指针(即段选择符和偏移值),处理器使用这个长指针把程序执行权转移到代码段中的异常或中断的处理程序中。这两个段的主要区别在于处理器操作EFLAGS寄存器IF标志上。IDT中任务门描述符的格式与GDT和LDT中任务门的格式相同。
任务门描述符中含有一个任务TSS段的选择符,该任务用于处理异常和/或中断。
中断门、陷阱门和任务门描述符格式如下图所示
4.4 分段机制与GDT|LDT
摘自
其实LDT与我们在《 操作系统篇-浅谈实模式与保护模式》中提到过的GDT是差不多的,区别在于:
- 全局(Global)和局部(local)
- LDT表存放在LDT类型的段之中,此时GDT必须含有LDT的段描述符
- LDT本身是一个段,而GDT不是。
查找GDT
在线性地址中的基地址,需要借助GDTR
;而查找LDT
相应基地址,需要的是GDT
中的段描述符。访问LDT
需要使用段选择符,为了减少访问LDT
时候的段转换次数,LDT
的段选择符,段基址,段限长都要放在LDTR
寄存器之中。
对于操作系统来说,每个系统必须定义一个GDT
,用于系统中的所有任务和程序。可选择性定义若干个LDT
。GDT
本身不是一个段,而是线性地址空间的一个数据结构;GDT
的线性基地址和长度必须加载进GDTR
之中。因为每个描述符长度是8,所以GDT
的基地址最好进行8字节对齐。
4.5 实际代码
// 操作的是中断请求寄存器还有很多细节没有解释清楚, 等回头再继续整理添加, 未完待续…
class InterruptManager {
public:
InterruptManager( uint16_t hardwareInterruptOffset, //
GlobalDescriptorTable* dgt);
~InterruptManager();
// CPU开启中断和关闭中断
void Activate();
void Deactivate();
private:
struct GateDescriptor { // 门描述符, 中断门, 陷阱门, 任务门
uint16_t handlerAddressLowBits;
uint16_t handlerAddressHighBits;
uint16_t gdt_codeSegmentSelector;
uint16_t reserved;
uint8_t access;
} __attribute__((packed)); // 不进行内存对齐
static GateDescriptor interruptDescriptorTable[256]; // 总共256个中断, IDT是中断描述符
// 定义IDT
struct InterruptDescriptorTablePointer{
uint16_t size;
uint32_t base;
} __attribute__((packed));
static void SetInterruptDescriptorTableEntry (
uint8_t InterruptNumber,
uint16_t codeSegmentSelectorOffset,
void (*handler)(),
uint8_t DescriptorPrivilegeLevel, // 优先级
uint8_t GateDescriptorType
); // 中断入口地址
uint16_t hardwareInterruptOffset;
static void InterruptIgnore(); // 中断初始化占位
// 20个异常中断和17个外部中断 的 请求处理函数, 这个esp就是栈底, 最后在回到esp的位置, 主函数继续向下执行
static uint32_t handleInterrupt(uint8_t InterruptNumber, int32_t esp);
// 这里在汇编中调用方式如下
// .extern __ZN16InterruptManager15handleInterruptEhi
// /* 压栈函数 */
// int_bottom:
// pusha
// /* 重要的寄存器压栈 */
// pushl %ds
// pushl %es
// pushl %fs
// pushl %gs
// /* 调用interruptManager::handleInterrupt函数之前, 需要把两个参数进行压栈操作 */
// pushl %esp /*压入栈顶*/
// push (interruptnumber) /* 数据压栈 */
// call __ZN16InterruptManager15handleInterruptEhi
// /* 出栈操作 */
// movl %eax, %esp /* 返回值 esp */
// popl %gs
// popl %fs
// popl %es
// popl %ds
// popa
// 异常中断20个 处理函数
static void HandleException_0x00();
static void HandleException_0x01();
static void HandleException_0x02();
static void HandleException_0x03();
static void HandleException_0x04();
static void HandleException_0x05();
static void HandleException_0x06();
static void HandleException_0x07();
static void HandleException_0x08();
static void HandleException_0x09();
static void HandleException_0x0A();
static void HandleException_0x0B();
static void HandleException_0x0C();
static void HandleException_0x0D();
static void HandleException_0x0E();
static void HandleException_0x0F();
static void HandleException_0x10();
static void HandleException_0x11();
static void HandleException_0x12();
static void HandleException_0x13();
// 外部中断17个 处理函数
static void HandleInterruptRequest_0x00();
static void HandleInterruptRequest_0x01();
static void HandleInterruptRequest_0x02();
static void HandleInterruptRequest_0x03();
static void HandleInterruptRequest_0x04();
static void HandleInterruptRequest_0x05();
static void HandleInterruptRequest_0x06();
static void HandleInterruptRequest_0x07();
static void HandleInterruptRequest_0x08();
static void HandleInterruptRequest_0x09();
static void HandleInterruptRequest_0x0a();
static void HandleInterruptRequest_0x0b();
static void HandleInterruptRequest_0x0c();
static void HandleInterruptRequest_0x0d();
static void HandleInterruptRequest_0x0e();
static void HandleInterruptRequest_0x0f();
static void HandleInterruptRequest_0x31();
// 定义中断占用的端口
// https://zhuanlan.zhihu.com/p/149117980
// 主中断控制器
Port8BitSlow picMasterCommand;
Port8BitSlow picMasterData;
// 副中断控制器
Port8BitSlow picSlaveCommand;
Port8BitSlow picSlaveData;
};
更多参考:
从头开始写一个操作系统