您的位置:首页 > 其它

一个简单多任务内核实例的分析

2013-05-16 08:23 281 查看

一个简单多任务内核实例的分析

转载请注明出处:http://blog.csdn.net/rosetta


简介

Linux-0.00是由Linus Torvalds写的Linux最初版本(未发布),只是打印AAA和BBB而没有更多的功能,比如内存管理、文件系统、字符设备驱动程序等,而Linux-0.11是一个比较完整的内核,也包含上述内容。

先分析Linux-0.00而不是Linux-0.11是因为前者是后者的基础,它非常精简但又能涵括几乎所有操作系统的基础知识,在真实完全明白Linux-0.00后再分析Linux-0.11就相对容易很多。

Linux-0.00的原始源码在redhat9.0中无法编译,但在《Linux内核完全剖析》一书中其作者给出了可以在redhat9.0中编译通过的Linux-0.00版本。

在分析任何代码之前如果可以我一般会先搭建环境,搭建环境的过程就会对整个操作过程有大概映像,如何搭建环境文档如下。/article/1652331.html

Linux-0.00包含两个特权级3的用户任务和一个系统调用中断过程。其由两个文件组成:as86汇编语言写的boot.s(引导启动程序)和GNU as汇编写的head.s(多任务内核程序)。前者只是引导程序,把head.s代码加载进内存并把控制权转移到head.s中执行;后者实现两个特权级3上的任务在时钟中断控制下相互切换运行,并实现显示字符的系统调用。

任务A(0)不停的打印“AAA……”,当遇到时钟中断后切换到任务B(1)中运行打印“BBB……”,再遇时钟中断再打印“AAA……”,如此循环。

boot.s编译出的代码共544Bytes,由Makefile处理后剩余512B存放在软盘映像文件的第一扇区中(就是《Linux-0.00运行环境搭建》中的Image,这里使用的环境能用不用实际的软盘就不用,因为现在的计算机都没软区,以后考虑使用USB启动是挺不错的)。Image是由Makefile通过dd等命令把boot.s和head.s编译出来的二进制文件合起来的,在Linux-0.11中是由build.c完成合并操作的。

PC加电启动时,ROM BIOS中的程序会把启动盘(Image可被bochs直接启动,相当于启动盘)上第一个扇区加载到物理内存0x7c00处,并把程序执行权移到0x7c00处开始运行boot代码。boot的主要功能是把映像文件中的head内核代码先加载到内存的0x10000处,再把head搬到0x0处,并设置好临时GDT等信息后,把处理器设置成运行在保护模式下,再跳转到head中执行。这里有一个问题是boot为什么要把head先加载到0x10000再移到0x0处,而不直接加载到0x0处,这是因为把映像文件加载到内存中时需要使用BIOS INT13中断,而BIOS在初始化时会把中断向量表放在内存0x0处。

BOOT.S源码分析

完整的boot.s共66行,包括注释和空行,带行号。所有新增的分析都写在代码旁边,不带行号。

1 ! boot.s

2

3 BOOTSEG = 0x07c0

4 SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).

5 SYSLEN = 17 ! sectors occupied.

6

7 entry start

8 start:

9 jmpi go,#BOOTSEG !跳转到段BOOTSEG,段内偏移为go的地方执行代码,此时为实模式,所以BOOTSEG为段地址,后面在保护模式下会讲到,jmpi后面跟的是偏移和段选择符。

10 go: mov ax,cs !跳转到这里后,cs的值就为0x07c0(注意,段地址在实际转换过程是需要左移4位,即多一个零为0x07c00,以下出些段的地方类似)

11 mov ds,ax

12 mov ss,ax

13 mov sp,#0x400 ! arbitrary value >>512

14 !设置临时栈指针。在这个程序中好像没什么用。

!开始加载内核模块head到段0x1000处。这里主要利用BIOS 的int 13中断从第一扇区读取代码到内存。

使用BIOS int 13 02H功能时各寄存器含义,可查询《x86中断手册》。

功能描述:读扇区

入口参数:AH=02H

AL=扇区数

CH=柱面

CL=扇区

DH=磁头

DL=驱动器,00H~7FH:软盘;80H~0FFH:硬盘

ES:BX=缓冲区的地址

出口参数:CF=0——操作成功,AH=00H,AL=传输的扇区数,否则,AH=状态代码,参见功能号01H中的说明

15 ! ok, we've written the message, now

16 load_system:

17 mov dx,#0x0000 !DH=00H,驱动器号;DL=00H,磁头号。

18 mov cx,#0x0002 !CH=00H,0柱面;CL=02H,从第2扇区开始读。

19 mov ax,#SYSSEG !ES:BX 读入到此缓冲区地址处(0x1000:0x0000)。

20 mov es,ax

21 xor bx,bx

22 mov ax,#0x200+SYSLEN !AH=02H,读扇区;AL=17,需要读到第17个扇区。

23 int 0x13

24 jnc ok_load !CF=0时跳转。CF=0表明读扇区操作成功。

25 die: jmp die !如果读取失败进入死循环,此时代码无法解除错误,只能让代码重新运行。

26

27 ! now we want to move to protected mode ...

28 ok_load:

29 cli ! no interrupts allowed !关中断,为什么要关?怎么开?sti开中断

30 mov ax, #SYSSEG

31 mov ds, ax

32 xor ax, ax

33 mov es, ax

34 mov cx, #0x1000

35 sub si,si

36 sub di,di

37 rep

38 movw !ES:DI<-DS:SI(从DS段的SI偏移处移动CX个字到ES段的DI处)

!这里使用的是movw,每次移动的是双字节(一个字),一共移动cx=0x1000次=4K次,

!总共4K*2B=8192B=8KB。

!一个扇区为512B,8KB=16个扇区,所以这里一共从0x1000:0000开始处移动16个扇区大

!小到0x0000:0000处。

39 mov ax, #BOOTSEG

40 mov ds, ax!因为前面修改了数据段寄存器ds的值,所以需要设置回来为0x07c0。因为lidt和lgdt是从数据段寄存指向的段中取内存中的值,如果不设置,那么此时ds为0x1000,而lidt会读0x1000:0x69处的值到idtr寄存器中,那么读取的idt肯定是错的。

正常情况是读0x07c0:0x69处的值到idtr寄存器。



41 lidt idt_48 ! load idt with 0,0

!加载中断向量表idt的基地址和长度加载到IDTR寄存器中。

42 lgdt gdt_48 ! load gdt with whatever appropriate

!加载全局描述符表gdt的基地址和长度加载到IDTR寄存器中。

!基地址:0x7c00+gdt,长度:0x7ff(2047+1=2048字节,gdt中的每一项为段描述符,为8字节,所以一共有2048/8项=256项)。

43

44 ! absolute address 0x00000, in 32-bit protected mode.

45 mov ax,#0x0001 ! protected mode (PE) bit

46 lmsw ax ! This is it! 设置CR0保护模式标志PE(位0).

47 jmpi 0,8 ! jmp offset 0 of segment 8 (cs)

!因为此时已经进入到保护模式,所以这里的8是段选择符,8=1000b,TI为0表示GDT表,索引为1,表示GDT表中的第2项(索引为0表示第1项),即下面GDT表中的第二项——代码段。

48

49 gdt: .word 0,0,0,0 ! dummy 第一项为0,不用。

50

51 .word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb) !第1,2字节

52 .word 0x0000 ! base address=0x00000 !第3,4字节

53 .word 0x9A00 ! code read/exec !第5,6字节

54 .word 0x00C0 ! granularity=4096, 386 !第7,8字节

!段描述符结构如图所示。

!gdt中的第二项以二进制表示,高双字:0000000011000000 1001101000000000 (4 2

! 低双字:0000000000000000 0000011111111111 (1 0

!所以高双字的位23为颗粒度,置位表示单位是4KB,低双字中的位0~15为段限长=0x07FF+1

!=2KB,所以段限长最终为2KB*4KB=8MB。

!类型:1010,查P93表4-3知,为代码段,可执行/可读

! 高双字中的位12 S=1表示非系统段描述符(代码或数据段描述符)。





55

56 .word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)!第三项为数据段

57 .word 0x0000 ! base address=0x00000

58 .word 0x9200 ! data read/write

59 .word 0x00C0 ! granularity=4096, 386

60

61 idt_48: .word 0 ! idt limit=0

62 .word 0,0 ! idt base=0L

63 gdt_48: .word 0x7ff ! gdt limit=2048, 256 GDT entries

64 .word 0x7c00+gdt,0 ! gdt base = 07xxx

65 .org 510

66 .word 0xAA55

到此boot.s已跳转到head.s的代码中执行,控制权交给head.s处理,下面看head.s源码。

head.s源码分析

head.s文件一共256行,GNU汇编,此代码已经运行在保护模式下,代码分析总结写在此代码最后。

1 # head.s contains the 32-bit startup code.

2 # Two L3 task multitasking. The code of tasks are in kernel area,

3 # just like the Linux. The kernel code is located at 0x10000.





4 SCRN_SEL = 0x18!这些都为段选择符,如图所示。

!0x18=00011000b,表示GDT表中的第4条(因为第一条从0开始计数)

5 TSS0_SEL = 0x20

6 LDT0_SEL = 0x28

7 TSS1_SEL = 0X30

8 LDT1_SEL = 0x38

9 .global startup_32

10 .text

11 startup_32:

12 movl $0x10,%eax #因为进入保护模式,0x10为段选择符,GDT中的第二项,0x10指之前boot中设置的GDT第三项数据段。

13 mov %ax,%ds #数据段寄存器ds中被赋予了0x10段选择符。

14 lss init_stack,%esp #设置系统堆栈段,ss:esp<-init_stack

15

16

17 # setup base fields of descriptors.

18 call setup_idt !重新设置IDT表

19 call setup_gdt !重新设置GDT表

20 movl $0x10,%eax # reload all the segment registers#重新加载所有段寄存器。

21 mov %ax,%ds # after changing gdt.

22 mov %ax,%es

23 mov %ax,%fs

24 mov %ax,%gs

25 lss init_stack,%esp #重新设置系统堆栈段,ss:esp<-init_stack

26

27 # setup up timer 8253 chip.#设置8253定时芯片,这部分先不管,其作用是每隔10秒向中断控制器发送一个中断请求。

28 movb $0x36, %al

29 movl $0x43, %edx

30 outb %al, %dx #向8253芯片控制字寄存器写端口。

31 movl $11930, %eax # timer frequency 100 HZ #??

32 movl $0x40, %edx

33 outb %al, %dx

34 movb %ah, %al

35 outb %al, %dx

36

37 # setup timer & system call interrupt descriptors.

38 movl $0x00080000, %eax #看42、43行,这是中断门描述符,P95页。8表示段选择(1000,即之前设置好的GDT中的第2项,代码段),P104页,调用门描述符格式。

39 movw $timer_interrupt, %ax #

40 movw $0x8E00, %dx # 类型为E=1110,即中断门

41 movl $0x08, %ecx # The PC default timer int.

42 lea idt(,%ecx,8), %esi #加载idt的基地址到esi中。#用ecx的值批明IDT表中的第几项?后面的8代码是每一项八字节?

43 movl %eax,(%esi) #给IDT表中的第8项的低四节地址赋值。

44 movl %edx,4(%esi) #给IDT表中的第8项的高四节地址赋值。

45 movw $system_interrupt, %ax

46 movw $0xef00, %dx

47 movl $0x80, %ecx

48 lea idt(,%ecx,8), %esi

49 movl %eax,(%esi) #这里的eax高16位没有赋值啊?还是00008。

50 movl %edx,4(%esi)

51

52 # unmask the timer interrupt.

53 # movl $0x21, %edx

54 # inb %dx, %al

55 # andb $0xfe, %al

56 # outb %al, %dx

57

58 # Move to user mode (task 0)

59 pushfl

60 andl $0xffffbfff, (%esp) #?

61 popfl

62 movl $TSS0_SEL, %eax #把任务0的段选择符加载到任务寄存器TR中。

63 ltr %ax

64 movl $LDT0_SEL, %eax #把任务0的LDT段选择符加载到局部描述符表寄存器LDTR中。

65 lldt %ax

66 movl $0, current #当前任务号0保存在current 中。

67 sti #开中断

68 pushl $0x17

69 pushl $init_stack

70 pushfl

71 pushl $0x0f #1111

72 pushl $task0

73 iret #IP<-SS:[SP],SP<-SP+2;所以就开始执行task0。

74

75 /****************************************/

76 setup_gdt:

77 lgdt lgdt_opcode!加载gdt表的基地址和表限长到GDTR寄存中。

78 ret

79

80 setup_idt:

81 lea ignore_int,%edx!加载IDT表的基地址和表限长到IDTR寄存中。

82 movl $0x00080000,%eax

83 movw %dx,%ax /* selector = 0x0008 = cs */

84 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */

85 lea idt,%edi

86 mov $256,%ecx #设置中断描述符表中256项描述符表的默认值。

87 rp_sidt:

88 movl %eax,(%edi)

89 movl %edx,4(%edi)

90 addl $8,%edi

91 dec %ecx

92 jne rp_sidt

93 lidt lidt_opcode #加载内在中的IDT相关信息到IDTR寄存中。

94 ret

95

96 # -----------------------------------

97 write_char: #打印字符调用。这段不太明白。

98 push %gs

99 pushl %ebx

100 # pushl %eax

101 mov $SCRN_SEL, %ebx #内存显示段,不知道怎么用??或者哪里可以查到相关资料?

102 mov %bx, %gs #gs?

103 movl scr_loc, %ebx

104 shl $1, %ebx

105 movb %al, %gs:(%ebx)

106 shr $1, %ebx

107 incl %ebx

108 cmpl $2000, %ebx

109 jb 1f

110 movl $0, %ebx

111 1: movl %ebx, scr_loc

112 # popl %eax

113 popl %ebx

114 pop %gs

115 ret

116

117 /***********************************************/

118 /* This is the default interrupt "handler" :-) */

119 .align 2

120 ignore_int: #默认中断处理程序打印“C”。

121 push %ds

122 pushl %eax

123 movl $0x10, %eax

124 mov %ax, %ds

125 movl $67, %eax /* print 'C' */

126 call write_char

127 popl %eax

128 pop %ds

129 iret

130

131 /* Timer interrupt handler */

132 .align 2

133 timer_interrupt:

134 push %ds

135 pushl %eax

136 movl $0x10, %eax

137 mov %ax, %ds

138 movb $0x20, %al

139 outb %al, $0x20 #?向8259A控制寄存器引脚写命令。

140 movl $1, %eax

141 cmpl %eax, current #判断当前任务号是否是任务1,如果是任务1则切换到任务0.

142 je 1f

143 movl %eax, current

144 ljmp $TSS1_SEL, $0

145 jmp 2f

146 1: movl $0, current

147 ljmp $TSS0_SEL, $0

148 2: popl %eax

149 pop %ds

150 iret

151

152 /* system call handler */

153 .align 2

154 system_interrupt:

155 push %ds

156 pushl %edx

157 pushl %ecx

158 pushl %ebx

159 pushl %eax

160 movl $0x10, %edx

161 mov %dx, %ds

162 call write_char

163 popl %eax

164 popl %ebx

165 popl %ecx

166 popl %edx

167 pop %ds

168 iret

169

170 /*********************************************/

171 current:.long 0

172 scr_loc:.long 0

173

174 .align 2

175 lidt_opcode:

176 .word 256*8-1 # idt contains 256 entries!IDT表长度256项*8字节/项-1。

177 .long idt # This will be rewrite by code.!IDT表的基地址。

178 lgdt_opcode:

179 .word (end_gdt-gdt)-1 # so does gdt !gdt表的表限长,2字节长。

180 .long gdt # This will be rewrite by code. !gdt表基地址,4字节长。

181

182 .align 8

183 idt: .fill 256,8,0 # idt is uninitialized !默认未初始化的IDT表为256个8字节0。

184

185 gdt: .quad 0x0000000000000000 /* NULL descriptor */

186 .quad 0x00c09a00000007ff /* 8Mb 0x08, base = 0x00000 */

187 .quad 0x00c09200000007ff /* 8Mb 0x10 */

188 .quad 0x00c0920b80000002 /* screen 0x18 - for display */

189

190 .word 0x0068, tss0, 0xe900, 0x0 # TSS0 descr 0x20

191 .word 0x0040, ldt0, 0xe200, 0x0 # LDT0 descr 0x28

192 .word 0x0068, tss1, 0xe900, 0x0 # TSS1 descr 0x30

193 .word 0x0040, ldt1, 0xe200, 0x0 # LDT1 descr 0x38

194 end_gdt:

195 .fill 128,4,0 !这是栈空间,因为栈是向下增加的,前面的代码放在低内存处,所以高内存的栈顶的位置在后面。即init_stack标号表示栈顶位置。

196 init_stack: # Will be used as user stack for task0.

197 .long init_stack

198 .word 0x10 !0x10为段选择符。

199

200 /*************************************/

201 .align 8

202 ldt0: .quad 0x0000000000000000

203 .quad 0x00c0fa00000003ff # 0x0f, base = 0x00000

204 .quad 0x00c0f200000003ff # 0x17

205

206 tss0: .long 0 /* back link */

207 .long krn_stk0, 0x10 /* esp0, ss0 */

208 .long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */

209 .long 0, 0, 0, 0, 0 /* eip, eflags, eax, ecx, edx */

210 .long 0, 0, 0, 0, 0 /* ebx esp, ebp, esi, edi */

211 .long 0, 0, 0, 0, 0, 0 /* es, cs, ss, ds, fs, gs */

212 .long LDT0_SEL, 0x8000000 /* ldt, trace bitmap */

213

214 .fill 128,4,0

215 krn_stk0:

216 # .long 0

203 .quad 0x00c0fa00000003ff # 0x0f, base = 0x00000

204 .quad 0x00c0f200000003ff # 0x17

205

206 tss0: .long 0 /* back link */

207 .long krn_stk0, 0x10 /* esp0, ss0 */

208 .long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */

209 .long 0, 0, 0, 0, 0 /* eip, eflags, eax, ecx, edx */

210 .long 0, 0, 0, 0, 0 /* ebx esp, ebp, esi, edi */

211 .long 0, 0, 0, 0, 0, 0 /* es, cs, ss, ds, fs, gs */

212 .long LDT0_SEL, 0x8000000 /* ldt, trace bitmap */

213

214 .fill 128,4,0

215 krn_stk0:

216 # .long 0

217

218 /************************************/

219 .align 8

220 ldt1: .quad 0x0000000000000000

221 .quad 0x00c0fa00000003ff # 0x0f, base = 0x00000

222 .quad 0x00c0f200000003ff # 0x17

223

224 tss1: .long 0 /* back link */

225 .long krn_stk1, 0x10 /* esp0, ss0 */

226 .long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */

227 .long task1, 0x200 /* eip, eflags */

228 .long 0, 0, 0, 0 /* eax, ecx, edx, ebx */

229 .long usr_stk1, 0, 0, 0 /* esp, ebp, esi, edi */

230 .long 0x17,0x0f,0x17,0x17,0x17,0x17 /* es, cs, ss, ds, fs, gs */

231 .long LDT1_SEL, 0x8000000 /* ldt, trace bitmap */

232

233 .fill 128,4,0

234 krn_stk1:

235

236 /************************************/

237 task0:

238 movl $0x17, %eax

239 movw %ax, %ds

240 movb $68, %al /* print 'A' */

241 int $0x80

242 movl $0xfff, %ecx

243 1: loop 1b

244 jmp task0

245

246 task1:

247 movl $0x17, %eax

248 movw %ax, %ds

249 movb $69, %al /* print 'B' */

250 int $0x80

251 movl $0xfff, %ecx

252 1: loop 1b

253 jmp task1

254

255 .fill 128,4,0

256 usr_stk1:

head.s运行在32位保护模式下,主要包括初始设置的代码,时钟中断int 0x08的过程代码,系统调用中断int 0x80的过程代码以及任务A和任务B的代码和数据。初始设置主要包括:1,重新设置GDT表;2,设置系统定时器芯片;3,重新设置IDT表并设置时钟和系统调用中断门;4,移动到任务A中执行。

在虚拟地址空间中head.s程序的内核代码和任务代码分配如下图所示。在本示例中,所有代码和数据段都对应到物理内存同一区域上,即从物理内存0开始的区域。GDT中全局代码和数据段描述符的内容都设置为:基地址为0x0000;段限长为0x07ff。因为颗粒度为1,所以实际长度为8MB。而全局显示数据段被设置成:基地址0xb8000;段限长为0x0002,所以实际长度为8KB,对应到显示内存区域上。



两个任务在LDT中代码段和数据段描述符内容都设置为:基地址0x0000;段限长0x03ff,实际长度为4MB。因此在线性地址空间中这个“内核”的代码和数据段与任务代码和数据段都从线性地址0开始并且因为没有采用分页机制,所以它们都直接对应物理地址0开始处。在head程序编译出的目标文件中以及最终得到的软件映像文件Image中,代码和数据的组织形式如下图所示。



因为处理特权级0的代码不能直接把控制权转移到特权级3的代码中执行,但中断返回操作是可以的,因此当初始化GDT、IDT和定时芯片结束后,就可以利用中断中断返回指令IRET来启动运行第1个任务。具体的方法是在初始堆栈init_stack中人工设置一个返回环境。

即把任务0的TSS段选择符加载到任务寄存器LTR中、LDT段选择符加载到LDTR中后,把任务0的用户栈指针(0x17:init_stack)和代码指针(0x0f:task0)以及标志寄存器值压入栈中,然后执行中断返回指针IRET。该指令会弹出堆栈上的堆栈指针作为任务0用户栈指针,恢复假设的任务0的标志寄存器内容,并且弹出栈中代码指针放入CS:EIP寄存中,从而开始执行任务0的代码,完成了从特权级0到特权级3代码的控制转移。

为了每隔10毫秒切换运行的任务,head.s程序把定时器芯片8253的通道0设置成每经过10毫秒就向中断控制芯片8259A发送一个时钟中断请求信号。PC机的ROM 了BIOS开机时已经在8259A中把时钟中断请求信号设置成中断风和向量8,因为我们需要在中断8的处理过程中执行任务切换操作。任务切换的实现方法是查看current变量中当前运行任务号。如果current为0,就利用任务1的TSS选择符作为操作数执行远跳转指令,从而切换到任务1中执行,否则反之。

每个任务在执行时,会首先把一个字符的ASCII码放入寄存器AL中,然后调用系统中断调用int 0x80,而该系统调用处理过程会调用一个简单的字符写屏子程序,把寄存器AL中的字符显示在屏幕上,同时把字符显示的屏幕的下一个位置记录下来。在显示过一个字符后,任务代码使用循环语句延迟一段时间,然后又跳转到任务代码开始处继续循环执行,直到运行了10毫秒而发生了定时中断,从而代码会切换到另一个任务去运行。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐