转自:http://blog.csdn.net/rosetta/article/details/8933228
一个简单多任务内核实例的分析
转载请注明出处: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版本。
在分析任何代码之前如果可以我一般会先搭建环境,搭建环境的过程就会对整个操作过程有大概映像,如何搭建环境文档如下。http://blog.csdn.net/rosetta/article/details/8933240
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毫秒而发生了定时中断,从而代码会切换到另一个任务去运行。