[自制操作系统] 第18回 实现用户进程(上)

目录
一、前景回顾
二、任务切换相关
三、实现TSS
四、运行测试

 

一、前景回顾

  在上一回我们已经实现了键盘的驱动编写和环形缓冲区的实现,现在让我们来想这么一个问题:

  一直以来我们的程序都在最高特权级0下工作,这意味着任何程序都和操作系统平起平坐,可以改动任何资源。如果不改变这种现状的话,某个不听话的程序甚至可以给操作系统致命一击,取而代之,那么后果将不堪设想。所以从本回开始,我们便要开始着手实现用户进程,让我们的操作系统看起来更安全一点。

二、任务切换相关

  下面的是我自己的一些见解。

  如果让我来设计任务切换,比较简单的一种思路便是:

  首先我们常说的任务,就是一个程序而已,程序在内存中被分为代码段和数据段。所以我们表征多个任务,那么便是多个代码段和数据段而已。至于任务的切换,可能需要费点心思在软件层面上实现多任务调度机制。

  然后现在问题出现了:

  我们知道代码段和数据段需要在全局描述符表GDT中存储,一个任务需要两个描述符来存储,而我们知道全局描述符表GDT最多也就只有2^13=8192个段描述符,那么理论上也就只能容纳4096个任务,除此之外在软件层面上实现的多任务调度机制有点类似今天的用户态多线程,效率不高且安全性有诸多问题。

  所以我们来看看硬件厂商和CPU厂商是如何解决任务切换的问题的,其中最主要的就是LDTTSS

  首先是LDT。

  LDT是局部描述符表,用来存储每个任务自己的私有实体资源,也就是代码和数据。LDT的地址被保存在一个段描述符中,那么理论上我们现在可以支持8192个任务了。对于当前运行的任务,其LDT的地址被存储在LDTR寄存器中,这样CPU就能根据这个地址从中拿到任务所需要的资源。每切换一个任务时,需要用lldt指令重新加载新任务的LDT地址到LDTR寄存器中。
        
  随后便是TSS。

  单核CPU要想实现多任务,唯一的方法便是多任务共享一个CPU,也就是让多个任务轮流使用CPU。前面说道,LDT是每个任务的私有资源,所以不用担心多任务时,程序的运行资源会混乱。但这还不够。

  CPU执行任务时,需要把任务所需要的数据加载到寄存器、栈和内存中,因为CPU只能直接处理这些资源中的数据,这是CPU在设计之初时工程师们决定的。于是,问题来了,任务的数据和指令是CPU的处理对象,他们被存放在内存这个低速的容器中,对于CPU来讲,内存的速度太慢了,它最喜欢寄存器。因此内存中的数据往往被加载到高速的寄存器中后再处理,等处理完毕后再将结果写入到内存中,所以,任何时候,寄存器中的内容才是任务的最新状态。当任务被换下CPU后,任务的最新状态应该被保存在某个地方,以便下次重新将此任务调度到CPU时可以恢复此任务的最新状态,这样任务才能继续执行。

  于是TSS就出现了,TSS是程序员为任务单独定义的一个结构体变量,当加载新任务时,CPU自动把当前任务(旧任务)的状态存入当前任务的TSS,然后将新任务TSS中的数据载入到对应的寄存器中。  
                               
  TSS和其他段也是一样的,本质上是一片存储数据的内存区域,也需要某个描述符结构来描述它,这就是TSS描述符。
        
  和LDT一样,CPU对TSS的处理也采用了类似的方法,提供一个名为TR的寄存器来存放当前任务的TSS位置。

  总结一下,如图所示:
                   
  CPU原生支持的任务切换方式是针对每一个任务都有一个LDT和一个TSS结构,这种任务切换方式,在任务切换时效率比较低,所以现代操作系统并未采纳。现代操作系统放弃了LDT,只采用了TSS,但是也没有完全采纳。我们是效仿Linux的任务切换方式的,所以拿Linux为例。

  Linux为每一个CPU创建一个TSS,在各个CPU上的所有任务共享一个TSS,各CPU的TR寄存器保存各CPU上的TSS,也就是说在用ltr指令加载TSS后,该TR寄存器永远指向同一个TSS,之后再也不会切换了。在进程切换时只需要把TSS中的SS0和esp0更新为新任务的内核栈的段地址和栈指针。

  那么任务的状态信息保存在哪里呢?

  对于Linux来讲,Linux只在TSS中初始化esp0和SS0以及IO位图。当CPU从低特权级进入高特权级时,也就是3特权级的用户态到0特权级的内核态时(Linux只有两个特权级)CPU会自动从TSS中获取到0特权级的栈指针,然后Linux手动执行一系列的push指令将任务的状态保存在0特权级的栈中。这个地方先留一下悬念,等后面实现的时候会再次提到。

三、实现TSS

  虽然我们不完全采纳TSS,但是因为TSS是硬件所要求的,所以我们必须构造一个TSS来应付硬件。在project/userprog目录下新建tss.c和tss.h文件,除此之外还需要在global.h文件中新加部分代码。

 1 #include "global.h"
 2 #include "thread.h"
 3 #include "print.h"
 4 #include "string.h"
 5 #include "tss.h"
 6 
 7 struct tss {
 8     uint32_t backlink;
 9     uint32_t *esp0;
10     uint32_t ss0;
11     uint32_t *esp1;
12     uint32_t ss1;
13     uint32_t *esp2;
14     uint32_t ss2;
15     uint32_t cr3;
16     uint32_t (*eip) (void);
17     uint32_t eflags;
18     uint32_t eax;
19     uint32_t ecx;
20     uint32_t edx;
21     uint32_t ebx;
22     uint32_t esp;
23     uint32_t ebp;
24     uint32_t esi;
25     uint32_t edi;
26     uint32_t es;
27     uint32_t cs;
28     uint32_t ss;
29     uint32_t ds;
30     uint32_t fs;
31     uint32_t gs;
32     uint32_t ldt;
33     uint32_t trace;
34     uint32_t io_base;
35 };
36 
37 static struct tss tss;
38 
39 /*更新tss中的esp0字段的值为pthread的0级栈*/
40 void update_tss_esp(struct task_struct *pthread)
41 {
42     tss.esp0 = (uint32_t *)((uint32_t)pthread + PG_SIZE);
43 }
44 
45 /*创建gdt描述符*/
46 static struct gdt_desc make_gdt_desc(uint32_t *desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high)
47 {
48     uint32_t desc_base = (uint32_t)desc_addr;
49     struct gdt_desc desc;
50     desc.limit_low_word = limit & 0x0000ffff;
51     desc.base_low_word = desc_base & 0x0000ffff;
52     desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16);
53     desc.base_high_byte = (desc_base >> 24);
54     desc.attr_low_byte = (uint8_t)attr_low;
55     desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)attr_high);
56     return desc;
57 }
58 
59 /*在gdt中创建tss并重新加载gdt*/
60 void tss_init(void)
61 {
62     put_str("tss_init start \n");
63     uint32_t tss_size = sizeof(tss);
64     memset(&tss, 0, tss_size);
65     tss.ss0 = SELECTOR_K_STACK;
66     tss.io_base = tss_size;
67     /*gdt的基地址为0x900,把tss放到第4个地址,也就是0x900+0x20的位置*/
68     *((struct gdt_desc *)0xc0000920) = make_gdt_desc((uint32_t *)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);
69     /*为用户进程提前作准备*/
70     /*在gdt中添加dpl为3的数据段和代码段描述符*/
71     *((struct gdt_desc *)0xc0000928) = make_gdt_desc((uint32_t *)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
72     *((struct gdt_desc *)0xc0000930) = make_gdt_desc((uint32_t *)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
73     //while(1);
74     /*gdt 16位的limit 32位的段基址*/
75     uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000900 << 16));
76     asm volatile ("lgdt %0" : : "m" (gdt_operand));
77     asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS));
78 
79     put_str("tss_init and ltr done\n");
80 }
tss.c
1 #ifndef  __USERPROG_TSS_H
2 #define  __USERPROG_TSS_H
3 #include "stdint.h"
4 
5 void tss_init(void);
6 static struct gdt_desc make_gdt_desc(uint32_t *desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high);
7 void update_tss_esp(struct task_struct *pthread);
8 #endif
tss.h
 1 ...
 2 
 3 /******************** TSS描述符属性**********************/
 4 #define     TSS_DESC_D 0
 5 #define  TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0)
 6 #define  TSS_ATTR_LOW  ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS)
 7 
 8 #define  SELECTOR_TSS  ((4 << 3) + (TI_GDT << 2) + RPL0)
 9 
10 ...
global.h

  注释写的比较清楚,我们挑重点来讲。注意这个函数:

1 /*更新tss中的esp0字段的值为pthread的0级栈*/
2 void update_tss_esp(struct task_struct *pthread)
3 {
4     tss.esp0 = (uint32_t *)((uint32_t)pthread + PG_SIZE);
5 }

  这个函数的作用就是用来更新TSS中的esp0。我们前面在实现线程的时候,线程的PCB上有一块名为中断栈的区域一直没有被使用,现在就被用上了。忘记的话点这里其实它就是这里所说的0级栈,用户进程从3特权级进入0特权级时,CPU会自动从TSS中获取到0特权级的栈指针,也就是0级栈。

  最后还需要修改一下mbr.S和loader.S文件,为什么呢?原来在loader.S文件中,我们在开头通过jmp loader_start跳转到后面执行loader部分, 在这行代码后面实现了GDT表的建立,而jmp loader_start这行代码是需要占据3个字节的内容,这样就导致GDT表位于内存0x903地址处,不利于后面的对齐,所以我们为了让GDT表位于0x900处,需要移除jmp loader_start这行代码,但是我们知道这行代码是mbr跳转执行到的,为了让mbr直接跳转到loader部分,我们需要修改mbr.S中的最后跳转语句,修改为jmp LOADER_BASE_ADDR + 0x206 这个0x206怎么来的呢,GDT表总共有64个描述符,再加上gdt指针占用6个字节,总共便是64*8+6=518个字节,也就是0x206。这里就不多啰嗦了,直接将修改好的mbr.S和loader.S附上。

  1 %include "boot.inc"
  2 section MBR vstart=0x7c00
  3     mov ax, cs
  4     mov ds, ax
  5     mov es, ax
  6     mov ss, ax
  7     mov fs, ax
  8     mov sp, 0x7c00
  9     mov ax, 0xb800
 10     mov gs, ax
 11     
 12 ;利用int 0x10 的0x06号功能实现清屏
 13     mov ax, 0x600
 14     mov bx, 0x700
 15     mov cx, 0
 16     mov dx, 0x184f
 17 
 18     int 0x10
 19     
 20     mov ah, 3
 21     mov bh, 0
 22 
 23     int 0x10
 24 ;输出字符串“HELLO MBR” A表示绿色背景闪烁,4表示前景色为红色
 25     mov byte [gs:0x00],'H'
 26     mov byte [gs:0x01],0xA4
 27     
 28     mov byte [gs:0x02],'E'
 29     mov byte [gs:0x03],0xA4
 30 
 31     mov byte [gs:0x04],'L'
 32     mov byte [gs:0x05],0xA4
 33         
 34     mov byte [gs:0x06],'L'
 35     mov byte [gs:0x07],0xA4
 36         
 37     mov byte [gs:0x08],'O'
 38     mov byte [gs:0x09],0xA4
 39 
 40     mov byte [gs:0x0A],' '
 41     mov byte [gs:0x0B],0xA4
 42 
 43     mov byte [gs:0x0C],'M'
 44     mov byte [gs:0x0D],0xA4
 45         
 46     mov byte [gs:0x0E],'B'
 47     mov byte [gs:0x0F],0xA4
 48         
 49     mov byte [gs:0x10],'R'
 50     mov byte [gs:0x11],0xA4
 51 
 52     mov eax, LOADER_START_SECTOR ;起始扇区lba的地址
 53     mov bx, LOADER_BASE_ADDR     ;loader将要被写入的内存地址
 54     mov cx, 4                    ;待读入的扇区数
 55     call rd_disk_m_16            ;调用函数,将loader写入到内存中
 56     
 57     jmp LOADER_BASE_ADDR + 0x206
 58 
 59 ;---------------------------------------
 60 ;功能:读取硬盘n个扇区
 61         rd_disk_m_16:  
 62                 mov esi, eax               ;备份eax,eax中存放了扇区号,这里为0x2
 63                 mov di, cx                 ;备份cx,cx中存放待读入的扇区数
 64 
 65         ;读写硬盘:
 66         ;第一步:设置要读取的扇区数
 67                 mov dx, 0x1f2
 68                 mov al, cl
 69                 out dx, al
 70                 
 71                 mov eax, esi
 72 
 73         ;第二步:将lba地址存入到0x1f3 ~ 0x1f6
 74                 ;lba地址7-0位写入端口0x1f3
 75                 mov dx, 0x1f3
 76                 out dx, al
 77                 
 78                 ;lba地址15-8位写入端口0x1f4
 79                 mov cl, 8
 80                 shr eax, cl
 81                 mov dx, 0x1f4
 82                 out dx, al
 83                 
 84                 ;lba地址23-16位写入端口0x1f5
 85                 shr eax, cl
 86                 mov dx, 0x1f5
 87                 out dx, al
 88                         
 89                 shr eax, cl
 90                 and al, 0x0f
 91                 or al, 0xe0
 92                 mov dx, 0x1f6
 93                 out dx, al
 94 
 95         ;第三步:向0x1f7端口写入读命令,0x20
 96                 mov dx, 0x1f7
 97                 mov al, 0x20
 98                 out dx, al
 99 
100         ;第四步:检测硬盘状态
101         .not_ready:
102                 nop
103                 in al, dx
104                 and al, 0x88
105                 cmp al, 0x08
106                 jnz .not_ready
107 
108         ;第五步:从0x1f0端口读数据
109                 mov ax, di
110                 mov dx, 256
111                 mul dx
112                 mov cx, ax
113         ;di为要读取的扇区数,一个扇区共有512字节,每次读入一个字,总共需要
114         ;di*512/2次,所以di*256
115                 mov dx, 0x1f0
116         .go_on_read:
117                 in ax, dx
118                 mov [bx], ax
119                 add bx,2
120                 loop .go_on_read
121                 ret
122 ;---------------------------------------
123 
124     times 510-($-$$) db 0
125     db 0x55, 0xaa
mbr.S
  1 %include "boot.inc"
  2 section loader vstart=LOADER_BASE_ADDR
  3 LOADER_STACK_TOP equ LOADER_BASE_ADDR
  4 ;构建gdt及其内部描述符
  5 GDT_BASE:        dd 0x00000000
  6                  dd 0x00000000
  7 CODE_DESC:       dd 0x0000FFFF
  8                  dd DESC_CODE_HIGH4
  9 DATA_STACK_DESC: dd 0x0000FFFF
 10                  dd DESC_DATA_HIGH4
 11 VIDEO_DESC:      dd 0x80000007
 12                  dd DESC_VIDEO_HIGH4
 13 
 14 GDT_SIZE  equ $-GDT_BASE
 15 GDT_LIMIT equ GDT_SIZE-1
 16 times 60 dq 0  ;此处预留60个描述符的空位
 17 
 18 SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0
 19 SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0
 20 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
 21 
 22 ;以下是gdt指针,前2个字节是gdt界限,后4个字节是gdt的起始地址
 23 gdt_ptr   dw GDT_LIMIT 
 24           dd GDT_BASE
 25 
 26 ;---------------------进入保护模式------------
 27 loader_start:
 28     ;一、打开A20地址线
 29     in al, 0x92
 30     or al, 0000_0010B
 31     out 0x92, al
 32     
 33     ;二、加载GDT
 34     lgdt [gdt_ptr]
 35 
 36     ;三、cr0第0位(pe)置1
 37     mov eax, cr0
 38     or eax, 0x00000001
 39     mov cr0, eax
 40     
 41     jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线
 42 
 43     [bits 32]
 44     p_mode_start:
 45             mov ax, SELECTOR_DATA
 46             mov ds, ax
 47             mov es, ax
 48             mov ss, ax
 49             mov esp, LOADER_STACK_TOP
 50             mov ax, SELECTOR_VIDEO
 51             mov gs, ax
 52             
 53             mov byte [gs:160], 'p'
 54 ;---------------------------------------           
 55 
 56 ;------------------开启分页机制-----------------
 57     ;一、创建页目录表并初始化页内存位图
 58     call setup_page
 59 
 60     ;将描述符表地址及偏移量写入内存gdt_ptr,一会儿用新地址重新加载
 61     sgdt [gdt_ptr]
 62     ;将gdt描述符中视频段描述符中的段基址+0xc0000000
 63     mov ebx, [gdt_ptr + 2]
 64     or dword [ebx + 0x18 + 4], 0xc0000000
 65             
 66     ;将gdt的基址加上0xc0000000使其成为内核所在的高地址
 67     add dword [gdt_ptr + 2], 0xc0000000
 68 
 69     add esp, 0xc0000000  ;将栈指针同样映射到内核地址
 70             
 71     ;二、将页目录表地址赋值给cr3
 72     mov eax, PAGE_DIR_TABLE_POS
 73     mov cr3, eax
 74             
 75     ;三、打开cr0的pg位
 76     mov eax, cr0
 77     or eax, 0x80000000
 78     mov cr0, eax
 79             
 80     ;在开启分页后,用gdt新的地址重新加载
 81     lgdt [gdt_ptr]
 82     mov byte [gs:160], 'H'
 83     mov byte [gs:162], 'E'
 84     mov byte [gs:164], 'L'
 85     mov byte [gs:166], 'L'
 86     mov byte [gs:168], 'O'
 87     mov byte [gs:170], ' '
 88     mov byte [gs:172], 'P'
 89     mov byte [gs:174], 'A'
 90     mov byte [gs:176], 'G'
 91     mov byte [gs:178], 'E'
 92 
 93 ;---------------------------------------------
 94 
 95 ;--------------------拷贝内核文件并进入kernel--------------------------
 96     mov eax, KERNEL_START_SECTOR              ;kernel.bin所在的扇区号 0x09
 97     mov ebx, KERNEL_BIN_BASE_ADDR             ;从磁盘读出后,写入到ebx指定的地址0x70000
 98     mov ecx, 200                              ;读入的扇区数
 99 
100     call rd_disk_m_32
101 
102     ;由于一直处在32位下,原则上不需要强制刷新,但是以防万一还是加上
103     ;跳转到kernel处
104     jmp SELECTOR_CODE:enter_kernel
105     
106     enter_kernel:
107         call kernel_init
108         mov esp, 0xc009f000               ;更新栈底指针
109         jmp KERNEL_ENTRY_POINT            ;内核地址0xc0001500
110         ;jmp $
111         ;---------------------将kernel.bin中的segment拷贝到指定的地址
112         kernel_init:
113             xor eax, eax
114             xor ebx, ebx   ;ebx记录程序头表地址
115             xor ecx, ecx    ;cx记录程序头表中的program header数量
116             xor edx, edx    ;dx记录program header 尺寸,即e_phentsize
117 
118             ;偏移文件42字节处的属性是e_phentsize, 表示program header大小
119             mov dx, [KERNEL_BIN_BASE_ADDR + 42]
120             
121             ;偏移文件28字节处的属性是e_phoff
122             mov ebx, [KERNEL_BIN_BASE_ADDR + 28]
123 
124             add ebx, KERNEL_BIN_BASE_ADDR
125             mov cx, [KERNEL_BIN_BASE_ADDR + 44]
126     
127             .each_segment: 
128                     cmp byte [ebx + 0], PT_NULL
129                     je .PTNULL
130 
131             ;为函数memcpy压入参数,参数是从右往左压入
132             push dword [ebx + 16]
133             mov eax, [ebx + 4]
134             add eax, KERNEL_BIN_BASE_ADDR
135             push eax
136             push dword [ebx + 8]
137             call mem_cpy
138             add esp, 12
139 
140             .PTNULL:
141                     add ebx, edx
142                     loop .each_segment
143             ret
144 
145             ;-----------逐字节拷贝mem_cpy(dst, src, size)
146             mem_cpy:
147                     cld
148                     push ebp
149                     mov ebp, esp
150                     push ecx
151                     mov edi, [ebp + 8]
152                     mov esi, [ebp + 12]
153                     mov ecx, [ebp + 16]
154                     rep movsb
155 
156                     pop ecx
157                     pop ebp
158                     ret 
159 ;---------------------------------------------------     
160 
161 
162 
163 
164 ;--------------函数声明------------------------
165     ;setup_page:(功能)设置分页------------
166     setup_page:
167         ;先把页目录占用的空间逐字节清0
168         mov ecx, 4096
169         mov esi, 0
170         .clear_page_dir:
171                 mov byte [PAGE_DIR_TABLE_POS + esi], 0
172                 inc esi
173         loop .clear_page_dir
174         
175         ;开始创建页目录项
176         .create_pde:
177                 mov eax, PAGE_DIR_TABLE_POS
178                 add eax, 0x1000             ;此时eax为第一个页表的位置
179                 mov ebx, eax
180         
181         ;下面将页目录项0和0xc00都存为第一个页表的地址,每个页表表示4MB内存
182         ;页目录表的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问
183         or eax, PG_US_U | PG_RW_W | PG_P
184         
185         ;在页目录表中的第1个目录项中写入第一个页表的地址(0x101000)和属性
186         mov [PAGE_DIR_TABLE_POS + 0x0], eax
187 
188         mov [PAGE_DIR_TABLE_POS + 0xc00], eax
189 
190         ;使最后一个目录项指向页目录表自己的地址
191         sub eax, 0x1000
192         mov [PAGE_DIR_TABLE_POS + 4092], eax
193 
194         ;下面创建页表项(PTE)
195         mov ecx, 256     ;1M低端内存/每页大小4K=256
196         mov esi, 0
197         mov edx, PG_US_U | PG_RW_W | PG_P
198         .create_pte:     ;创建page table entry
199                 mov [ebx + esi*4], edx
200                 add edx, 4096
201                 inc esi
202         loop .create_pte
203         
204         ;创建内核其他页表的PDE
205         mov eax, PAGE_DIR_TABLE_POS
206         add eax, 0x2000           ;此时eax为第二个页表的位置
207         or eax, PG_US_U | PG_RW_W | PG_P
208         mov ebx, PAGE_DIR_TABLE_POS
209         mov ecx, 254              ;范围为第769~1022的所有目录项数量
210         mov esi, 769 
211         .create_kernel_pde:
212                 mov [ebx + esi*4], eax
213                 inc esi
214                 add eax, 0x1000
215         loop .create_kernel_pde
216         ret
217 
218 
219     ;rd_disk_m_32:(功能)读取硬盘n个扇区------------
220     rd_disk_m_32:
221         mov esi,eax               ;备份eax,eax中存放了扇区号
222         mov di,cx                 ;备份cx,cx中存放待读入的扇区数
223 
224         ;读写硬盘:
225         ;第一步:设置要读取的扇区数
226         mov dx,0x1f2
227         mov al,cl
228         out dx,al
229         
230         mov eax,esi
231 
232         ;第二步:将lba地址存入到0x1f3 ~ 0x1f6
233                 ;lba地址7-0位写入端口0x1f3
234         mov dx,0x1f3
235         out dx,al
236         
237         ;lba地址15-8位写入端口0x1f4
238         mov cl,8
239         shr eax,cl
240         mov dx,0x1f4
241         out dx,al
242         
243         ;lba地址23-16位写入端口0x1f5
244         shr eax,cl
245         mov dx,0x1f5
246         out dx,al
247                 
248         shr eax,cl
249         and al,0x0f
250         or al,0xe0
251         mov dx,0x1f6
252         out dx,al
253 
254     ;第三步:向0x1f7端口写入读命令,0x20
255         mov dx,0x1f7
256         mov al,0x20
257         out dx,al
258 
259     ;第四步:检测硬盘状态
260         .not_ready:
261                 nop
262                 in al,dx
263                 and al,0x88
264                 cmp al,0x08
265                 jnz .not_ready
266 
267     ;第五步:从0x1f0端口读数据
268         mov ax,di
269         mov dx,256
270         mul dx
271         mov cx,ax
272     ;di为要读取的扇区数,一个扇区共有512字节,每次读入一个字,总共需要
273     ;di*512/2次,所以di*256
274         mov dx,0x1f0
275         .go_on_read:
276                 in ax,dx
277                 mov [ebx],ax
278                 add ebx,2
279                 loop .go_on_read
280                 ret
281 ;----------------------------------------------
loader.S

四、运行测试

  运行测试后,tss成功初始化。

  
  在bochs控制台输入info gdt可以看到GDT表的内容,可以看到现在有7个描述符,在GDT中第4个描述符是刚安装好的TSS段描述符,其显示为32-Bit TSS(Busy),说明TSS的B位被CPU置1了,TSS已经生效了。
  
  本回到此结束,预知后事如何,请看下回分解。

posted @ 2022-08-15 21:20  李知行  阅读(605)  评论(0编辑  收藏  举报