操作系统真象还原<第五部分>

(。・∀・)ノ゙嗨起来!!!

中断

中断概念

中断处理,就是处理器接受到中断信号后,暂停当前执行的任务,转而去查找中断向量表,去执行中断服务程序,执行完后,恢复到中断前的状态,继续执行刚才的程序。

形象一点就是有个人或有个急事打断你现在做的事情,让你不得不处理这件紧急的事情,当你处理完这件事情后(当然也有可能继续被另一件事情打断),再做回你刚才没做完的事情。

并发和并行的区别:

  1. 单核cpu-并发:单位时间内的积累工作量,如在一秒内cpu处理了100个请求量
  2. 多核cpu-并行:真正同时进行的工作量,如在任意瞬间cpu正在同时处理100个请求量

系统有了中断,才能够并发运行,能够不断的切换进程。

操作系统是中断驱动的,本身是一个死循环while(1){ 操作系统代码 },所以系统是在循环中等待事件的发生,被动的调用资源去处理,而事件是由中断去通知系统的的。

中断类型

外部中断

外部中断又可以分为可屏蔽中断和不可屏蔽中断。

可屏蔽中断:可以屏蔽的中断,像时钟、键盘、硬盘、网卡等发出的中断都是可以屏蔽的。

不可屏蔽中断:既有不能屏蔽的意思,更准确地来说,一旦出现这种中断,操作系统将面临奔溃,无论你是否屏蔽似乎都没什么意义了,出现这种中断大多数是硬件问题,需要专业的硬件工程师做修理,像电源掉电、内存读写错误、奇偶校验错误都是不可屏蔽中断。

可屏蔽中断处理分为两部分,上半部和下半部。上半部是需要处理器紧急处理的,下半部是可以推迟一点处理的。例如,网卡接受到的网络数据将要装满缓冲区,它就会向处理器发出可屏蔽中断,通知处理器做上半部的处理,因为再不搬,数据就要丢失了,之后处理器就会将网卡里的网络数据搬到内存里,完成中断的上半部。下半部的话,就是对网络数据做一定的处理,这部分倒不是很紧急,放在下半部挺合适,处理器找个时间处理一下就好。

外部中断是通过两根信号线通知处理器的,一条是INTR(INTeRrupt),另一条是NMI(Non Maskable,Interrupt),分别用于传输可屏蔽中断信号和不可屏蔽中断信号。

image-20210218143934728

不可屏蔽中断的中断向量号为 2。

内部中断

内部中断分为软中断和异常。

软中断

软中断是用户主动发起的中断。

来自书本原话

• “ int 8位立即数”。这是我们以后常用的指令,我们要通过它进行系统调用,8位立即数可表示256种中断,这与处理器所支持的中断数是相吻合的。
• “ int 3”。这可不是 int 空格3,它们之间无间隙。 int 3是调试断点指令,其所触发的中断向量号是3,以后在中断和异常表中大家会看到。我们用 gdb 或 bochs 调试程序时,实际上就是调试器 fork 了一个子进程,子进程用于运行被调试的程序。调试器中经常要设置断点,其原理就是父进程修改了子进程的指令,将其用 int 3指令替换,从而子进程调用了 int 3指令触发中断。用此指令实现调试的原理是 int 3指令的机器码是 Oxcc ,断点本质上是指令的地址,调试器(父进程)将被调试进程(子进程)断点起始地址的第1个字节备份好之后在原地将该指令的第1字节修改为 Oxcc 。这样指令执行到断点处时,会去执行机器码为 Oxcc 的 int 3指令,该指令会触发3号中断,从而会去执行3号中断对应的中断处理程序,由于中断处理程序在运行时也要用到寄存器,为了保存所调试进程的现场,该中断处理程序必须先将当前的寄存器和相关内存单元压栈保存(提醒,当前寄存器和相关内存都属于那个被调试的进程),用户在査看寄存器和变量时就是从栈中获取的。当恢复执行所调试的进程时,中断处理程序需要将之前备份的1字节还原至断点处,然后恢复各寄存器和内存单元的值,修改返回地址为断点地址,用 iret 指令退出中断,返回到用户进程继续执行。
• into 。这是中断溢出指令,它所触发的中断向量号是4。不过,能否引发4号中断是要看 eflags 标志寄存器中的 OF 位是否为1,如果是1才会引发中断,否则该指令悄悄地什么都不做,低调得很。
• bound 。这是检査数组索引越界指令,它可以触发5号中断,用于检査数组的索引下标是否在上下边界之内。该指令格式是 “bound 16/32位寄存器,16/32位内存”。目的操作数是用寄存器来存储的,其内容是待检测的数组下标值。源操作数是内存,其内容是数组下标的下边界和上边界。当执行 bound 指令时,若下标处于数组索引的范围之外,则会触发5号中断。
• ud 2。未定义指令,这会触发第6号中断。该指令表示指令无效, CPU 无法识别。主动使用它发起中断,常用于软件测试中,无实际用途。

总结一下如下

  • “int 8位立即数”,通过它进行系统调用
  • int3,int和3之间无空格,用于调试
  • into,中断溢出指令,当OF位也为1时,触发4号中断
  • bound,检查数组索引越界指令,越界时触发5号中断
  • ud2,未定义指令,触发6号中断

补充:软中断int3断点调试指令,常用在GDB和Bochs调试器,具体的原理是

  1. 父进程(调试器)fork了一个子进程,来运行被调试的程序,当用户在调试器(Bochs举例)上输入b 0xXXXXXXXX指令时,父进程会将该地址的指令的第一个字节备份,再用0xcc(int3的机器指令)替换
  2. 这样再输入c运行,子进程运行到断点处就会触发3号中断,进行中断处理程序,但在此之前,还需保存当前的寄存器和栈的状态,所以用户查看寄存器和栈都是在栈中的值。
  3. 当继续运行调试程序即输入n,父进程又会把原先备份替换回去,并把寄存器和栈恢复,修改返回地址为断点地址,用iret退出中断。

除第一种的“int 8 位立即数”之外,其他的几种又可以称为异常。

因为它们既具备软中断的“主动”行为,又具备异常的“错误”结果。

int指令是比较常用的软中断,像Linux系统int 80就是用来实现系统调用的,所以调用的很频繁。

异常

异常是执行指令时CPU内部遇到错误。

只要中断关系到“正常”运行,就不受 IF 位影响。

运行错误的意思是 例如 执行div命令 , 处理器发现分母为0。将会引发 0 号异常 ( 中断

异常根据严重分为三种:

第一种是最轻的异常称为故障(Fault),这种异常是可以修复的,像缺页异常(Page fault)就是这种,处理器给软件以此改过自新的机会。LINUX虚拟内存就是基于page fault。

第二种是陷阱(Trap),这种异常一般是用户用于调试设下的。

第三种是最严重的,终止(Abort)。一旦无法修复,程序就没办法再运行下去,操作系统为了自保,只能将这个进程杀死。

异常和中断的表格

image-20210218145952797

中断描述符

中断描述符表(Interrupt Descriptor Table,IDT)是保护模式下用于存储中断处理程序入口的表,当CPU 接收一个中断时,需要用中断向量在此表中检索对应的描述符,在该描述符中找到中断处理程序的起始地址,然后执行中断处理程序。

实模式下用于中断处理程序入口的表叫做中断向量表(IVT),保护模式下则是中断描述符表(IDT)。

IVT在实模式下位于0~0x3ff共1024个字节,又知IVT可容纳256个中断向量,故每个中断向量用4字节描述;对比IVT,IDT表地址不受限制,在哪里都可以,每个描述符用8字节描述。这里主要讨论IDT,在IDT中描述符称之为门,也就是之前介绍过的门,这里再区别一下门和段描述符

  • 段描述符中描述的是一片内存区域
  • 门描述符描述的是一段代码,除调用门外,任务门、中断门、陷阱门都可以存在于中断描述符中

对比中断向量表,中断描述符表有两个区别。

(1)中断描述符表地址不限制,在哪里都可以。

(2)中断描述符表中的每个描述符用 8 字节描述。

IDT位置不固定,故CPU找到它需要通过一个寄存器IDTR,如下图,其中015位是表界限,也就是IDT大小减一,第1647位是IDT的基地址,和之前的GDTR是一个原理

image-20210219123836841

6位的表界限范围是00xffff,即64KB,可容纳的描述符个数是64KB/8=8K=8192个。特别注意的是GDT中的第0个段描述符是不可用的,但IDT却无此限制,第0个门描述符也是可用的,处理器只支持256个中断,即0254,中断描述符中其他的描述符不可用,还需要注意的是门描述符中的P位,构建IDT时需要将其置为0,表示门描述符的中断处理程序不在内存中。

加载IDTR需要用到lidt指令,用法是lidt 48位内存数据

在这 48 位内存数据中,前 16 位是 IDT 表界限,后 32 位是 IDT 线性基地址。

中断的处理过程总结如下

  1. 处理器根据中断向量号定位中断门描述符
  2. 处理器进行特权级检查
  3. 执行中断处理程序

image-20210219123939837

中断发生之后需要执行中断处理程序,该中断处理程序是通过中断门描述符中保存的代码段选择子和段内偏移找到的,这个时候就需要重新加载段寄存器,也就是说需要在栈中保存一些寄存器信息(CS:EIP、eflags等),保证中断之后执行的流程正确,当特权级变化的时候,压栈如下图所示

image-20210219124014697

图A、B:在发生中断是通过特权级的检测,发现需要向高特权级转移,所以要保存当前程序栈的SS和ESP的值,在这里记为ss_old, esp_old,然后在新栈中压入当前程序的eflags寄存器。

图C、D:由于要切换目标代码段,这种段间转移,要对CS和EIP进行备份,同样将其存入新栈中。某些异常会有错误码,用来标识异常发生在哪个段上,对于有错误码的情况,要将错误码也压入栈中。

当特权级没有变化的时候,就不需要压入旧栈的SS和EIP

image-20210219124744872

返回的时候通过指令 iret 完成,iret 指令会从栈顶依次弹出EIP、CS、EFLAGS,根据特权级的变化还有ESP、SS。但是该指令并不验证数据的正确性,而且他从栈中弹出数据的顺序是不变的,也就是说,在有error_code的情况下,iret返回时并不会主动跳过这个数据,需要我们手动进行处理。、

特权级检查

特权级别检查也和调用门的类似。

  由于中断触发时,不用指定选择子,只有中断向量号,因此也不需要检查RPL。所以只需要用CPL与中断描述符的DPL和目标代码段的DPL相比较。

中断是通过中断向量号通知处理器的,所以不涉及RPL(如代码段描述符需要用段选择子来通知)。

  1. 如果是软中断int n,int3,into等,这些是用户进程主动引起的,数值上要求目标代码段DPL <= CPL <= 门描述符DPL,为了避免特权级为3的用户进程主动调用某些只用于内核的例程(设置了门槛下限

  2. 如果是外部中断、异常,这些是进程被动引发的,所以不需要考虑上面的情况,数值上要求目标代码段DPL <= CPL

if位

eflags寄存器中开关中断的标志位,仅限于限制外部设备的中断。

调用中断门需要关闭,陷阱门和任务门不用

  1. 由于中断门要避免中断嵌套,即处理过程中又调用中断,会触发一般保护性异常;
  2. 处理器允许陷阱门嵌套优先级更高的中断,任务门必须要嵌套才能实现多任务并发

改变IF位的方式采用cli和sti专门的指令:由于通过pushf压栈的方式涉及到内存的访问,可以拆分成多步骤,不满足原子性要求

中断错误码

错误码最主要的部分就是选择子,只不过此选择子可以在多种表中检索描述符。

image-20210219130241525

EXT 表示 EXTernal event,即外部事件,用来指明中断源是否来自处理器外部,如果中断源是不可屏蔽中断 NMI 或外部设备,EXT 为 1,否则为 0。

IDT 表示选择子是否指向中断描述符表 IDT,IDT 位为 1,则表示此选择子指向中断描述符表,否则指向全局描述符表 GDT 或局部描述符表 LDT。

TI 和选择子中 TI 是一个意思,为 0 时用来指明选择子是从 GDT 中检索描述符,为 1 时是从 LDT 中检索描述符。当然,只有在 IDT 位为 0 时 TI 位才有意义。

选择子高 13 位索引就是选择子中用来在表中索引描述符用的下标。高 16 位是保留位,全 0。

有时候不仅错误码的高 16 位全为 0,低 16 位也全为 0,那一个全 0 的错误码能指明什么信息?当全 0 的错误码出现时,表示中断的发生与特定的段无关,或者引用了一个空描述符,引用描述符就是往段寄存器中加载选择子的时候,处理器发现选择子指向的描述符是空的。

中断返回时,iret 指令并不会把错误码从栈中弹出,所以在中断处理程序中需要手动用栈指针跨过错误码或将其弹出。否则栈顶处若不是 EIP(EIP_old)的话,iret 返回时将会载入错误的值到后续寄存器。

通常能够压入错误码的中断属于中断向量号在 0~32 之内的异常,而外部中断(中断向量号在 32~255 之间)和 int 软中断并不会产生错误码。通常我们并不用处理错误码。

8259A芯片

可屏蔽中断的代理8259A芯片,当多个中断到来时,代理可以用队列(IRR)存储中断,判别哪个中断会被处理器处理,并与处理器通信。

中断设备会连接到代理相应的IRQ接口,多个8259A芯片级联在一起可以支持更多的中断信号,一个8259芯片支持8种中断,两个芯片支持15种中断(其中1个IRQ用来连接从片)。

image-20210219142913136

芯片内部结构如下图:

image-20210219142942847

各个模块的作用:

• INT : 8259 A 选出优先级最髙的中断请求后,发信号通知 CPU 。

• INTA : INT Acknowledge ,中断响应信号。位于8259 A 中的 INTA 接收来自 CPU 的 INTA 接口的中断响应信号。

• IMR: InterruptMaskRegister, 中断屏蔽寄存器,宽度是 8 位,用来屏蔽某个外设的中断。

• IRR : Interrupt Request Register ,中断请求寄存器,宽度是8位。它的作用是接受经过 IMR 寄存器过滤后的中断信号并锁存,此寄存器中全是等待处理的中断,“相当于“5259 A 维护的未处理中断信号队列。

• PR : Priority Resolver ,优先级仲裁器。当有多个中断同时发生,或当有新的中断请求进来时,将它与当前正在处理的中断进行比较,找出优先级更高的中断。

• ISR: In-Service Register, 中断服务寄存器,宽度是 8 位。当某个中断正在被处理时,保存在此寄存器中。

工作原理

  当中断到来时,首先会经过中断屏蔽寄存器(IMR),查看该中断是否被屏蔽了,如果屏蔽的话IMR对应的位是为1的(比如主片IMR最低位为1的话,时钟中断就被屏蔽了,看上面那个级联的图)。如果没有被屏蔽,进入到IRR中断请求寄存器里,将该中断对应的位置为1,这个操作相当于将中断放进了队列里,因为有可能会有多个中断等待处理,所以需要一个队列来做缓存。

  再讲讲处理中断的流程:

​ ①PR优先权判别器会从IRR里挑选出优先级最高的中断(IRQx,x越低优先级最高,所以时钟优先级最高),准备让处理器处理,代理通过INTR向处理器发送INT信号,告诉处理器有中断要处理。

  ②处理器处理好现在执行的指令后,通过INTA发送信号告诉代理,我已经准备好处理中断信号了。

  ③代理接受该信号后,将刚才选出来最高优先级的中断在ISR对应位置为1,IRR对应位置为0。

  ④处理器再次发送INTR信号给代理,告诉代理我需要这个中断的中断向量号。

  ⑤代理通过数据总线向处理器发送PR判别的中断向量号。

  ⑥如果8259A芯片的EOI(End Of Interrupt信号)被置为非自动模式(手工模式), 处理器处理中断后,向代理发送一个EOI(End Of Interrupt信号),代理接受后知道中断已经处理完了,将ISR对应位置为0;

  ⑦如果8259A芯片的EOI(End Of Interrupt信号)被置为自动模式,在接受到第二个INTR信号就会将ISR对应位置为0。

  以上就是接收中断和处理中断的流程。

  还有一种情况是,如果在代理发送中断向量号之前,来了一个优先级别更高的中断,就会将ISR之前准备处理中断对应位置为0,IRR对应位置为1,将新来的中断的向量号发给处理器。

中断处理框架:

(1) 构造好IDT

(2) 提供中断向量号

8259A内的两组寄存器: ICW (初始化命令字)+ OCW (操作命令字)

ICW1 : 0 0 0 1 LTIM(中断检测方式) ADI(8085的时间间隔) SNGL(单片或级联) IC4(是否需要ICW4)

ICW2: T7 T6 T5 T4 T3 ID2 ID1 ID0 (只需要填写T3~T7)

ICW3: (仅在ICW的SNGL = 1 的情况下)

    主片: S7 S6 S5 S4 S3 S2 S1 S0

    从片: 0 0 0 0 0 ID2 ID1 ID0 (第三位来指定和主片相连的接口)

ICW4: 0 0 0 SFNM BUF M/S AEOI μPM

OCW1:M7~M0 (对应 IRQ0 ~ IRQ7) IMR屏蔽中断

OCW2:写入到主片的0x20和从片的)0xA0端口

编写中断处理程序

通过操作8259A芯片实现第一个中断处理程序,本质上是一个可编程中断控制器,处理流程如下,init_all负责初始化所有设备及结构体,然后调用idt_init初始化中断相关内容,内部分别调用了pic_initidt_desc_init实现,其中pic_init初始化8259A,idt_desc_init负责对中断描述符IDT表进行初始化,最后再对IDT表进行加载

我们需要进行以下几个步骤

  1. 用汇编语言实现中断处理程序
  2. 创建中断描述符表IDT,安装中断处理程序
  3. 用内联汇编实现端口I/O函数(对端口的读写操作)
  4. 设置8259A

with_asm

kernel.S 中断处理程序的编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
[bits 32]

; 对于CPU会自动压入错误码的中断类型,无需额外的操作
%define ERROR_CODE nop
; 如果CPU没有压入错误码,为了保持处理逻辑的一致性,我们需要手动压入一个0
%define ZERO push 0

extern put_str

section .data
intr_str db "interrupt occur!", 0xa, 0
global intr_entry_table
intr_entry_table:

; 中断处理程序宏定义
%macro VECTOR 2
section .text
intr%1entry: ;每个中断处理程序都要压入中断向量号
;所以一个中断类型一个中断处理程序
;自己知道自己的中断向量号是多少
%2
push intr_str
call put_str
add esp, 4 ;跳过参数
;如果是从片上进入的中断,除了往从片上发送 EOI 外,还要往主片上发送 EOI

mov al, 0x20 ;中断结束命令 EOI
out 0xa0, al ;向从片发送
out 0x20, al ;向主片发送

add esp, 4 ;跨过 error_code
iret ;从中断返回,32 位下等同指令 iretd

section .data
dd intr%1entry ;存储各个中断入口程序的地址
;形成 intr_entry_table 数组

%endmacro

VECTOR 0x00, ZERO
VECTOR 0x01, ZERO
VECTOR 0x02, ZERO
VECTOR 0x03, ZERO
VECTOR 0x04, ZERO
VECTOR 0x05, ZERO
VECTOR 0x06, ZERO
VECTOR 0x07, ZERO
VECTOR 0x08, ZERO
VECTOR 0x09, ZERO
VECTOR 0x0a, ZERO
VECTOR 0x0b, ZERO
VECTOR 0x0c, ZERO
VECTOR 0x0d, ZERO
VECTOR 0x0e, ZERO
VECTOR 0x0f, ZERO
VECTOR 0x10, ZERO
VECTOR 0x11, ZERO
VECTOR 0x12, ZERO
VECTOR 0x13, ZERO
VECTOR 0x14, ZERO
VECTOR 0x15, ZERO
VECTOR 0x16, ZERO
VECTOR 0x17, ZERO
VECTOR 0x18, ZERO
VECTOR 0x19, ZERO
VECTOR 0x1a, ZERO
VECTOR 0x1b, ZERO
VECTOR 0x1c, ZERO
VECTOR 0x1d, ZERO
VECTOR 0x1e, ERROR_CODE
VECTOR 0x1f, ZERO
VECTOR 0x20, ZERO

interrupt.c

将中断处理程序地址装载到中断描述符中。

加载 IDT,开启中断,把中断描述符表 IDT 的信息加载到 IDTR 寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# include "stdint.h"
# include "global.h"
# include "io.h"
# include "interrupt.h"

# define IDT_DESC_CNT 0x21 //支持的中断数
# define PIC_M_CTRL 0x20
# define PIC_M_DATA 0x21
# define PIC_S_CTRL 0xa0
# define PIC_S_DATA 0xa1

/*中断门描述符结构体*/
struct gate_desc {
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount; //此项为双字计数字段,是门描述符中的第 4 字节
//此项固定值,不用考虑
uint8_t attribute;
uint16_t func_offset_high_word;
};

static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; // idt 是中断描述符表
//本质上就是个中断门描述符数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在 kernel.S
// 中的中断处理函数入口数组

/**
* 创建中断门描述符.
*/
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (uint32_t) function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t) function & 0xFFFF0000) >> 16;
}

/**
* 初始化中断描述符表.
*/
static void idt_desc_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
put_str("idt_desc_init done.\n");
}

static void pic_init(void) {
// 初始化主片
outb(PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联 8259, 需要 ICW4
outb(PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为 0x20
// 也就是 IR[0-7] 为 0x20 ~ 0x27
outb(PIC_M_DATA, 0x04); // ICW3: IR2 接从片
outb(PIC_M_DATA, 0x01); // ICW4: 8086 模式, 正常 EOI

outb(PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联 8259, 需要 ICW4
outb(PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为 0x28
// 也就是 IR[8-15]为 0x28 ~ 0x2F
outb(PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的 IR2 引脚
outb(PIC_S_DATA, 0x01); // ICW4: 8086 模式, 正常 EOI
/*打开主片上 IR0,也就是目前只接受时钟产生的中断 */
outb(PIC_M_DATA, 0xfe);
outb(PIC_S_DATA, 0xff);

put_str("pic_init done.\n");
}

/*完成有关中断的所有初始化工作*/
void idt_init() {
put_str("idt_init start.\n");
idt_desc_init(); // 初始化中断描述符表
pic_init(); // 初始化 8259A

// 加载idt
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t) ((uint32_t) idt << 16)));
asm volatile ("lidt %0" : : "m" (idt_operand));
put_str("idt_init done.\n");
}

interrupt.h

1
typedef void* intr_handler;

global.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#ifndef _KERNEL_GLOBAL_H
#define _KERNEL_GLOBAL_H

# include "stdint.h"

# define RPL0 0
# define RPL1 1
# define RPL2 2
# define RPL3 3

# define TI_GDT 0
# define TI_LDT 1

# define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0)
# define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0)
# define SELECTOR_K_STACK SELECTOR_K_DATA
# define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0)

/* IDT描述符属性 */
# define IDT_DESC_P 1
# define IDT_DESC_DPL0 0
# define IDT_DESC_DPL3 3
# define IDT_DESC_32_TYPE 0xE // 32 位的门
# define IDT_DESC_16_TYPE 0x6 // 16 位的门

# define IDT_DESC_ATTR_DPL0 \
((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE)

# define IDT_DESC_ATTR_DPL3 \
((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE)

#endif

内联汇编实现端口 I/O 函数

io.h

io.h 中就定义了 4 个函数,分别是。

(1)一次写入 1 个字节的 outb 函数。

(2)一次写入多个字的 outsw 函数,注意,是以 2 字节为单位的。

(3)一次读入 1 个字节的 inb 函数。

(4)一次读入多个字的 insw 函数,同样以 2 字节为单位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/******************机器模式 ******************* 
b -- 输出寄存器 QImode 名称,即寄存器中的最低 8 位:[a-d]l
w -- 输出寄存器 HImode 名称,即寄存器中 2 个字节的部分,如[a-d]x

HImode
"Half-Integer"模式,表示一个两字节的整数
QImode
"Quarter-Integer"模式,表示一个一字节的整数
******************************************************/

#ifndef _LIB_IO_H
#define _LIB_IO_H

#include "stdint.h"

/**
* 向指定的端口写入一个字节的数据.
对端口指定 N 表示 0~255, d 表示用 dx 存储端口号,
%b0 表示对应 al , %w1 表示对应 dx */
*/
static inline void outb(uint16_t port, uint8_t data) {
asm volatile ("outb %b0, %w1" : : "a" (data), "Nd" (port));
}

/**
* 将addr起始处的word_cnt个字节写入端口port.
'+' 表示此限制即做输入,又做输出.
outsw 是把 ds:esi 处的 16 位的内容写入 port 端口,我们在设置段描述符时,
已经将 ds,es,ss 段的选择子都设置为相同的值了,此时不用担心数据错乱。
*/
static inline void outsw(uint16_t port, const void* addr, uint32_t word_cnt) {
asm volatile ("cld; rep outsw" : "+S" (addr), "+c" (word_cnt) : "d" (port));
}

/**
* 将从端口port读入的一个字节返回.
*/
static inline uint8_t inb(uint16_t port) {
uint8_t data;
asm volatile ("inb %w1, %b0" : "=a" (data) : "Nd" (port));
return data;
}

/**
* 将从port读取的word_cnt字节写入addr.
insw 是将从端口 port 处读入的 16 位内容写入 es:edi 指向的内存,
我们在设置段描述符时,已经将 ds,es,ss 段的选择子都设置为相同的值了,
此时不用担心数据错乱。
*/
static inline void insw(uint16_t port, void* addr, uint32_t word_cnt) {
asm volatile ("cld; rep insw" : "+D" (addr), "+c" (word_cnt) : "d" (port) : "memory");
}

#endif

init.c 触发的

1
2
3
4
5
6
7
8
9
#include "init.h" 
#include "print.h"
#include "interrupt.h"

/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); //初始化中断
}

init.h

1
void init_all(void);

main.c 测试代码

1
2
3
4
5
6
7
8
9
10
11
# include "kernel/print.h"
# include "init.h"

void main(void) {
put_str("I am kernel.\n");
init_all();
put_str("Init finished.\n");
asm volatile ("sti");
put_str("Turn on the interrupt.\n");
while (1);
}

新添加中断后的文件树如下所示,build中是生成后的文件,device中存放的是为了提高中断频率对8253计数器的操作,kernel中新加的interrupt是对中断初始化的主要文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.
├── boot
│ ├── include
│ │ └── boot.inc
│ ├── loader.bin
│ ├── loader.S
│ ├── mbr.bin
│ └── mbr.S
├── build
│ ├── init.o
│ ├── interrupt.o
│ ├── kernel.bin
│ ├── kernel.o
│ ├── main.o
│ ├── print.o
├── kernel
│ ├── global.h
│ ├── init.c
│ ├── init.h
│ ├── interrupt.c
│ ├── interrupt.h
│ ├── kernel.S
│ └── main.c
└── lib
├── kernel
│ ├── io.h
│ ├── print.h
│ ├── print.o
│ └── print.S
├── stdint.h
└── user

编译如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//编译c程序,生成目标文件,这里需要关闭栈保护并指定32位程序
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/init.o kernel/init.c
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/main.o kernel/main.c
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/interrupt.o kernel/interrupt.c

//编译汇编
sudo nasm -f elf -o build/print.o lib/kernel/print.S
sudo nasm -f elf -o build/kernel.o kernel/kernel.S

//链接
sudo ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o


//写入img
//sudo dd if=build/kernel.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=200 seek=9 conv=notrunc
sudo dd if=./kernel.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=200 seek=9 conv=notrunc

sudo bin/bochs -f bochsrc.disk

运行结果 ( 凭单身手速截图 不容易

image-20210221023901056

improve

除了外部声明了一个中断处理程序数组外,修改部分都在宏定义内,过程主要是

  1. 压入错误码(具体看注释)
  2. 调用c程前将段寄存器和8个通用寄存器压栈
  3. 设置结束方式和中断优先级
  4. 压入中断号
  5. 调用c程:表中每个地址都为32位(4字节),所以[idt_table+中断号*4]即可访问
  6. 跳过错误码,弹栈,跳过中断号
  7. 中断返回

image-20210221025911419

image-20210221030030159

interrupt.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# include "stdint.h"
# include "global.h"
# include "io.h"
# include "interrupt.h"

# define IDT_DESC_CNT 0x21 //支持的中断数
# define PIC_M_CTRL 0x20
# define PIC_M_DATA 0x21
# define PIC_S_CTRL 0xa0
# define PIC_S_DATA 0xa1

/*中断门描述符结构体*/
struct gate_desc {
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount; //此项为双字计数字段,是门描述符中的第 4 字节
//此项固定值,不用考虑
uint8_t attribute;
uint16_t func_offset_high_word;
};

/**
* 中断的名称.
*/
char* intr_name[IDT_DESC_CNT]; //用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT];
//定义中断处理程序数组,在 kernel.S 中定义的 intrXXentry
//只是中断处理程序的入口,最终调用的是 ide_table 中的处理程序
extern intr_handler intr_entry_table[IDT_DESC_CNT];
//声明引用定义在 kernel.S 中的中断处理函数入口数组

static void init_custom_handler_name();
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; // idt 是中断描述符表
//本质上就是个中断门描述符数组


/**
* 通用的中断处理函数.
*/
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) {
// 伪中断,无需处理
return;
}
put_str("int vector: 0x");
put_int(vec_nr);
put_char('\n');
}

/**
* 通用(默认)的异常/中断处理器注册.
*/
static void exception_handler_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
idt_table[i] = general_intr_handler;
//idt_table 数组中的函数是在进入中断后根据中断向量号调用的
intr_name[i] = "unknown";
}

init_custom_handler_name();
}

/**
* 设置需要自定义的中断名称.
*/
static void init_custom_handler_name() {
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "#BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
intr_name[16] = "#MF 0x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}

/**
* 创建中断门描述符.
*/
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (uint32_t) function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t) function & 0xFFFF0000) >> 16;
}

/**
* 初始化中断描述符表.
*/
static void idt_desc_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
put_str("idt_desc_init done.\n");
}

static void pic_init(void) {
// 初始化主片
outb(PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联 8259, 需要 ICW4
outb(PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为 0x20
// 也就是 IR[0-7] 为 0x20 ~ 0x27
outb(PIC_M_DATA, 0x04); // ICW3: IR2 接从片
outb(PIC_M_DATA, 0x01); // ICW4: 8086 模式, 正常 EOI

outb(PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联 8259, 需要 ICW4
outb(PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为 0x28
// 也就是 IR[8-15]为 0x28 ~ 0x2F
outb(PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的 IR2 引脚
outb(PIC_S_DATA, 0x01); // ICW4: 8086 模式, 正常 EOI
/*打开主片上 IR0,也就是目前只接受时钟产生的中断 */
outb(PIC_M_DATA, 0xfe);
outb(PIC_S_DATA, 0xff);

put_str("pic_init done.\n");
}

/*完成有关中断的所有初始化工作*/
void idt_init() {
put_str("idt_init start.\n");
idt_desc_init(); // 初始化中断描述符表
exception_handler_init();
pic_init(); // 初始化 8259A

// 加载idt
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t) ((uint32_t) idt << 16)));
asm volatile ("lidt %0" : : "m" (idt_operand));
put_str("idt_init done.\n");
}

kernel.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
[bits 32]

; 对于CPU会自动压入错误码的中断类型,无需额外的操作
%define ERROR_CODE nop
; 如果CPU没有压入错误码,为了保持处理逻辑的一致性,我们需要手动压入一个0
%define ZERO push 0

extern put_str
; 中断处理函数数组
extern idt_table

section .data
intr_str db "interrupt occur!", 0xa, 0
global intr_entry_table
intr_entry_table:


; 中断处理程序宏定义
%macro VECTOR 2
section .text
intr%1entry: ;每个中断处理程序都要压入中断向量号
;所以一个中断类型一个中断处理程序
;自己知道自己的中断向量号是多少

%2
; 保存上下文
push ds
push es
push fs
push gs
pushad

mov al, 0x20 ;中断结束命令 EOI
out 0xa0, al ;向从片发送
out 0x20, al ;向主片发送

push %1

; 调用C的中断处理函数
call [idt_table + 4 * %1]
jmp intr_exit

section .data
dd intr%1entry

%endmacro

section .text
global intr_exit
intr_exit:
add esp, 4
popad
pop gs
pop fs
pop es
pop ds
add esp, 4
iretd

VECTOR 0x00, ZERO
VECTOR 0x01, ZERO
VECTOR 0x02, ZERO
VECTOR 0x03, ZERO
VECTOR 0x04, ZERO
VECTOR 0x05, ZERO
VECTOR 0x06, ZERO
VECTOR 0x07, ZERO
VECTOR 0x08, ZERO
VECTOR 0x09, ZERO
VECTOR 0x0a, ZERO
VECTOR 0x0b, ZERO
VECTOR 0x0c, ZERO
VECTOR 0x0d, ZERO
VECTOR 0x0e, ZERO
VECTOR 0x0f, ZERO
VECTOR 0x10, ZERO
VECTOR 0x11, ZERO
VECTOR 0x12, ZERO
VECTOR 0x13, ZERO
VECTOR 0x14, ZERO
VECTOR 0x15, ZERO
VECTOR 0x16, ZERO
VECTOR 0x17, ZERO
VECTOR 0x18, ZERO
VECTOR 0x19, ZERO
VECTOR 0x1a, ZERO
VECTOR 0x1b, ZERO
VECTOR 0x1c, ZERO
VECTOR 0x1d, ZERO
VECTOR 0x1e, ERROR_CODE
VECTOR 0x1f, ZERO
VECTOR 0x20, ZERO

编译如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//编译c程序,生成目标文件,这里需要关闭栈保护并指定32位程序
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/init.o kernel/init.c
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/main.o kernel/main.c
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/interrupt.o kernel/interrupt.c

//编译汇编
sudo nasm -f elf -o build/print.o lib/kernel/print.S
sudo nasm -f elf -o build/kernel.o kernel/kernel.S

//链接
sudo ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o


//写入img
//sudo dd if=build/kernel.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=200 seek=9 conv=notrunc
sudo dd if=./kernel.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=200 seek=9 conv=notrunc

sudo bin/bochs -f bochsrc.disk

运行结果

image-20210221033310276

定时器 8253

时钟表示的是设备运行的频率,工作节拍

内部时钟:由晶体振荡器产生,内部硬件固定设置好的,无法改变(ns级别)

外频:由内部时钟频率经过分频就是主板的外频,用于CPU和南北桥的通信

主频:外频×倍频,处理器取指令、执行指令的频率

外部时钟:CPU与外设(接在南桥上)之间通信的时钟(ms或s级别)

要处理外部和内部两个不同时钟的设备能够同步通信,有两个方法:

  1. 在软件上设定循环计时器,但白白占用CPU运算资源,不采用
  2. 在硬件上可以用到定时器来给内部时钟分频产生外部时钟,独立于CPU运行,提升cpu资源利用率,采用

main.c

1
2
int cycle_cnt = 90000; 
while(cycle_cnt-- > 0);

这个例子是让处理器执行 9 万次空循环,通过这种延迟方式达到一定的定时作用,但这种空兜处理器的代价是白白消耗时钟周期,处理器的资源是很宝贵的,可不能随意浪费。

8253 内部有 3 个独立的计数器 它们的端口分别是 0x40~0x42

image-20210221045652176

寄存器资源包括一个 16 位的计数初值寄存器、一个计数器执行部件和一个输出锁存器。

image-20210221045808130

每个计数器都有三个引脚:CLK,GATE,OUT。

  1. CLK 表示时钟输入信号,即计数器自己工作的节拍,也就是计数器自己的时钟频率。每当此引脚收到一个时钟信号,减法计数器就将计数值减1。连接到此引脚的脉冲频率最高为10MHz,8253为2MHz。

  2. GATE 表示门控输入信号,在某些工作方式下用于控制计数器是否可以开始计数,在不同工作方式下 GATE 的作用不同,到时候同工作方式一同介绍。

  3. OUT 表示计数器输出信号。当定时工作结束,也就是计数值为 0 时,根据计数器的工作方式,会在 OUT 引脚上输出相应的信号。此信号用来通知处理器或某个设备:定时完成。这样处理器或外部设备便可以执行相应的行为动作。

image-20210221045932530

8253控制寄存器的端口为0x43,是一个8位寄存器

image-20210221050042830

RW1 和 RW0 位是读/写/锁存操作位,即Read/Write/Latch,用来设置待操作计数器(通道)的读写及锁存方式。

M2~M0 这三位是工作方式(模式)选择位,即 Method 或 Mode。

工作方式:

image-20210221050246361

工作模式:

image-20210221050908349

初始化 8253 的步骤:

1.往控制字寄存器端口 0x43 中写入控制字

2.在所指定使用的计数器端口中写入计数初值

timer

timer.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# include "io.h"
# include print.h"

# define IRQ0_FREQUENCY 1000
# define INPUT_FREQUENCY 1193180
# define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
# define COUNTER0_PORT 0x40
# define COUNTER_MODE 2
# define COUNTER0_NO 0
# define READ_WRITE_LATCH 3
# define PIT_CONTROL_PORT 0x43

/* 把操作的计数器 counter_no、 读写锁属性 rwl、 计数器模式
counter_mode 写入模式控制寄存器并赋予初始值 counter_value */
static void frequency_set(uint8_t counter_port,
uint8_t counter_no,
uint8_t rwl,
uint8_t counter_mode,
uint16_t counter_value) {
/* 往控制字寄存器端口 0x43 中写入控制字 */
outb(PIT_CONTROL_PORT, (uint8_t) (counter_no << 6 | rwl << 4 | counter_mode << 1));
/* 先写入 counter_value 的低 8 位 */
outb(counter_port, (uint8_t) counter_value);
/* 先写入 counter_value 的高 8 位 */
outb(counter_port, (uint8_t) counter_value >> 8);
}

/**
* 初始化PIT 8253.
*/
void timer_init() {
put_str("timer_init start.\n");
/* 设置 8253 的定时周期,也就是发中断的周期 */
frequency_set(COUNTER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
put_str("timer_init done.\n");
}

timer.h

1
void timer_init();

timer_init 函数加在文件 init.c 。

init.c

1
2
3
4
5
6
7
8
9
# include "init.h"
# include "print.h"
# include "timer.h"

void init_all() {
put_str("init_all.\n");
idt_init();
timer_init();
}

当前目录如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.
├── boot
│ ├── include
│ │ └── boot.inc
│ ├── loader.bin
│ ├── loader.S
│ ├── mbr.bin
│ └── mbr.S
├── build
│ ├── init.o
│ ├── interrupt.o
│ ├── kernel.bin
│ ├── kernel.o
│ ├── main.o
│ ├── print.o
│ └── timer.o
├── device
│ ├── timer.c
│ └── timer.h
├── kernel
│ ├── global.h
│ ├── init.c
│ ├── init.h
│ ├── interrupt.c
│ ├── interrupt.h
│ ├── kernel.S
│ └── main.c
└── lib
├── kernel
│ ├── io.h
│ ├── print.h
│ ├── print.o
│ └── print.S
├── stdint.h
└── user

编译如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//编译c程序,生成目标文件,这里需要关闭栈保护并指定32位程序
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/init.o kernel/init.c
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/main.o kernel/main.c
sudo gcc -m32 -fno-stack-protector -I lib/kernel -I lib/ -I kernel -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
sudo gcc -m32 -fno-stack-protector -I lib/kernel -c -o build/timer.o device/timer.c

//编译汇编
sudo nasm -f elf -o build/print.o lib/kernel/print.S
sudo nasm -f elf -o build/kernel.o kernel/kernel.S
sudo nasm -f elf -o build/kernel.o kernel/kernel.S

//链接
sudo ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o build/timer.o

//写入img
//sudo dd if=build/kernel.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=200 seek=9 conv=notrunc
sudo dd if=./kernel.bin of=/home/fyz/sc/bochs-2.6.2/hd60M.img bs=512 count=200 seek=9 conv=notrunc

sudo bin/bochs -f bochsrc.disk

运行结果

image-20210221054859824

Reward
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • © 2015-2021 John Doe
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信