上篇我们实现了时钟中断的响应,以及输出简单的字符功能。本篇我们实现系列文章的最终目的:任务切换。任务切换在x86上,硬件给提供了硬件支持,简单可行。
涉及到tss切换,并自动实现了ldt切换。非常简单。
1. 看代码head.s
TSS0_SEL = 0x20
LDT0_SEL = 0x28
TSS1_SEL = 0X30
LDT1_SEL = 0x38
TSS2_SEL = 0X40
LDT2_SEL = 0x48
.globl startup_32
.text
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%gs
mov %ax,%fs
lss init_stack,%esp
call setup_idt
call setup_gdt
# init again
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%gs
mov %ax,%fs
lss init_stack,%esp
# setup timer & system call interrupt descriptors.
movl $0x00080000, %eax
movw $timer_interrupt, %ax
movw $0x8E00, %dx
movl $0x08, %ecx
lea idt(,%ecx,8), %esi
movl %eax,(%esi)
movl %edx,4(%esi)
movw $system_interrupt, %ax
movw $0xef00, %dx
movl $0x80, %ecx
lea idt(,%ecx,8), %esi
movl %eax,(%esi)
movl %edx,4(%esi)
# Move to user mode (task 0)
pushfl
andl $0xffffbfff, (%esp)
popfl
movl $TSS0_SEL, %eax
ltr %ax
movl $LDT0_SEL, %eax
lldt %ax
movl $0, current
sti
pushl $0x17
pushl $usr_stk0
pushfl
pushl $0x0f
pushl $task0
iret
die:
jmp die
.align 2
ignore_int:
push %ds
pushl %eax
movl $0x10, %eax
mov %ax, %ds
mov $0x0c98, %ax /* print 'C' */
call write_char
popl %eax
pop %ds
iret
write_char:
push %gs
pushl %ebx
mov $SCRN_SEL,%ebx
mov %bx,%gs
mov src_loc,%bx
shl $1,%ebx
mov %ax,%gs:(%ebx)
shr $1,%ebx
incl %ebx
cmpl $2000,%ebx
jb 1f
movl $0,%ebx
1:
movl %ebx,src_loc
popl %ebx
pop %gs
ret
.align 2
timer_interrupt:
push %ds
pushl %edx
pushl %ecx
pushl %ebx
pushl %eax
movl $0x10, %eax
mov %ax, %ds
movb $0x20, %al
outb %al, $0x20
// is 1
movl $1, %eax
cmpl %eax, current
je 3f
// is 2
movl $2, %eax
cmpl %eax, current
je 1f
// is 0
movl $0, %eax
cmpl %eax, current
je 2f
1: movl $0, current
ljmp $TSS0_SEL, $0
jmp 4f
2: movl $1, current
ljmp $TSS1_SEL, $0
jmp 4f
3: movl $2, current
ljmp $TSS2_SEL, $0
jmp 4f
4: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %ds
iret
/* system call handler */
.align 2
system_interrupt:
push %ds
pushl %edx
pushl %ecx
pushl %ebx
pushl %eax
movl $0x10, %edx
mov %dx, %ds
call write_char
popl %eax
popl %ebx
popl %ecx
popl %edx
pop %ds
iret
/*********************************************/
current:
.long 0
setup_idt:
lea ignore_int,%edx
movl $0x00080000,%eax
movw %dx,%ax /* selector = 0x0008 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea idt,%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
lidt lidt_opcode
ret
setup_gdt:
lgdt lgdt_opcode
ret
.align 2
lidt_opcode:
.word 256*8-1
.long idt
lgdt_opcode:
.word (end_gdt-gdt)-1
.long gdt
src_loc:
.long 0
.align 2
idt:
.fill 256,8,0
gdt:
.quad 0x0000000000000000
.quad 0x00c09a00000007ff
.quad 0x00c09200000007ff
.quad 0x00c0920b80000002
.word 0x68,tss0,0xe900,0x0
.word 0x40,ldt0,0xe200,0x0
.word 0x68,tss1,0xe900,0x0
.word 0x40,ldt1,0xe200,0x0
.word 0x68,tss2,0xe900,0x0
.word 0x40,ldt2,0xe200,0x0
end_gdt:
.fill 128,4,0
init_stack:
.long init_stack
.word 0x10
.align 2
ldt0:
.quad 0x0000000000000000
.quad 0x00c0fa00000003ff # 0x0f, base = 0x10000
.quad 0x00c0f200000003ff # 0x17
tss0:
.long 0 /* back link */
.long stack0_krn_ptr, 0x10 /* esp0, ss0 */
.long 0, 0 /* esp1, ss1 */
.long 0, 0 /* esp2, ss2 */
.long 0 /* cr3 */
.long task0 /* eip */
.long 0x200 /* eflags */
.long 0, 0, 0, 0 /* eax, ecx, edx, ebx */
.long usr_stk0, 0, 0, 0 /* esp, ebp, esi, edi */
.long 0x17,0x0f,0x17,0x17,0x17,0x17 /* es, cs, ss, ds, fs, gs */
.long LDT0_SEL /* ldt */
.long 0x8000000 /* trace bitmap */
.fill 128,4,0
stack0_krn_ptr:
.long 0
/************************************/
.align 2
ldt1:
.quad 0x0000000000000000
.quad 0x00c0fa00000003ff # 0x0f, base = 0x10000
.quad 0x00c0f200000003ff # 0x17
tss1:
.long 0 /* back link */
.long stack1_krn_ptr, 0x10 /* esp0, ss0 */
.long 0, 0 /* esp1, ss1 */
.long 0, 0 /* esp2, ss2 */
.long 0 /* cr3 */
.long task1 /* eip */
.long 0x200 /* eflags */
.long 0, 0, 0, 0 /* eax, ecx, edx, ebx */
.long usr_stk1, 0, 0, 0 /* esp, ebp, esi, edi */
.long 0x17,0x0f,0x17,0x17,0x17,0x17 /* es, cs, ss, ds, fs, gs */
.long LDT1_SEL /* ldt */
.long 0x8000000 /* trace bitmap */
.fill 128,4,0
stack1_krn_ptr:
.long 0
/************************************/
.align 2
ldt2:
.quad 0x0000000000000000
.quad 0x00c0fa00000003ff # 0x0f, base = 0x10000
.quad 0x00c0f200000003ff # 0x17
tss2:
.long 0 /* back link */
.long stack2_krn_ptr, 0x10 /* esp0, ss0 */
.long 0, 0 /* esp1, ss1 */
.long 0, 0 /* esp2, ss2 */
.long 0 /* cr3 */
.long task2 /* eip */
.long 0x200 /* eflags */
.long 0, 0, 0, 0 /* eax, ecx, edx, ebx */
.long usr_stk2, 0, 0, 0 /* esp, ebp, esi, edi */
.long 0x17,0x0f,0x17,0x17,0x17,0x17 /* es, cs, ss, ds, fs, gs */
.long LDT2_SEL /* ldt */
.long 0x8000000 /* trace bitmap */
.fill 128,4,0
stack2_krn_ptr:
.long 0
.align 2
task0:
movw $0x0c61, %ax /* print 'a' */
int $0x80
movl $0xefff, %ecx
1: loop 1b
jmp task0
.fill 128,4,0
usr_stk0:
.long 0
task1:
movw $0x0d62, %ax /* print 'b' */
int $0x80
movl $0xffff, %ecx
1: loop 1b
jmp task1
.fill 128,4,0
usr_stk1:
.long 0
task2:
movw $0x0e63, %ax /* print 'c' */
int $0x80
movl $0xfff, %ecx
1: loop 1b
jmp task2
.fill 128,4,0
usr_stk2:
.long 0
2. 代码分析
SCRN_SEL = 0x18
TSS0_SEL = 0x20
LDT0_SEL = 0x28
TSS1_SEL = 0X30
LDT1_SEL = 0x38
TSS2_SEL = 0X40
LDT2_SEL = 0x48
.globl startup_32
.text
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%gs
mov %ax,%fs
lss init_stack,%esp
call setup_idt
call setup_gdt
# init again
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%gs
mov %ax,%fs
lss init_stack,%esp
以上设置环境,分析见以前文章,此处略过。
movl $0x00080000, %eax
movw $timer_interrupt, %ax
movw $0x8E00, %dx
movl $0x08, %ecx
lea idt(,%ecx,8), %esi
movl %eax,(%esi)
movl %edx,4(%esi)
movw $system_interrupt, %ax
movw $0xef00, %dx
movl $0x80, %ecx
lea idt(,%ecx,8), %esi
movl %eax,(%esi)
movl %edx,4(%esi)
以上设置了时钟中断和系统调用中断服务程序,时钟中断默认为0x08,系统调用中断默认为0x80.因为任务运行在特权级3,所以不能直接访问显存,需要通过中断门切换到特权级0完成输出字符功能。
src_loc:
.long 0
.align 2
idt:
.fill 256,8,0
gdt:
.quad 0x0000000000000000
.quad 0x00c09a00000007ff
.quad 0x00c09200000007ff
.quad 0x00c0920b80000002
.word 0x68,tss0,0xe900,0x0
.word 0x40,ldt0,0xe200,0x0
.word 0x68,tss1,0xe900,0x0
.word 0x40,ldt1,0xe200,0x0
.word 0x68,tss2,0xe900,0x0
.word 0x40,ldt2,0xe200,0x0
end_gdt:
以上设置了gdt表,分别是系统代码,系统数据,系统显存,任务0 tss,任务0 ldt,任务1 tss,任务1 ldt,任务2 tss,任务2 ldt。相关属性参照相关书籍即可。
.align 2
system_interrupt:
push %ds
pushl %edx
pushl %ecx
pushl %ebx
pushl %eax
movl $0x10, %edx
mov %dx, %ds
call write_char
popl %eax
popl %ebx
popl %ecx
popl %edx
pop %ds
iret
以上设置了系统调用中断服务程序,调用write_char输出一个字符,字符数据在ax中,由任务自己定义。
.align 2
timer_interrupt:
push %ds
pushl %edx
pushl %ecx
pushl %ebx
pushl %eax
movl $0x10, %eax
mov %ax, %ds
movb $0x20, %al
outb %al, $0x20
// is 1
movl $1, %eax
cmpl %eax, current
je 3f
// is 2
movl $2, %eax
cmpl %eax, current
je 1f
// is 0
movl $0, %eax
cmpl %eax, current
je 2f
1: movl $0, current
ljmp $TSS0_SEL, $0
jmp 4f
2: movl $1, current
ljmp $TSS1_SEL, $0
jmp 4f
3: movl $2, current
ljmp $TSS2_SEL, $0
jmp 4f
4: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %ds
iret
以上是时钟服务程序,比较了当前的任务,如果是0,则用ljmp $TSS1_SEL, $0跳到任务1,此句完成了任务切换,相当简单。如果是任务1,则跳到任务2,如果是任务2,则跳到任务0.
.align 2
ldt0:
.quad 0x0000000000000000
.quad 0x00c0fa00000003ff # 0x0f, base = 0x10000
.quad 0x00c0f200000003ff # 0x17
tss0:
.long 0 /* back link */
.long stack0_krn_ptr, 0x10 /* esp0, ss0 */
.long 0, 0 /* esp1, ss1 */
.long 0, 0 /* esp2, ss2 */
.long 0 /* cr3 */
.long task0 /* eip */
.long 0x200 /* eflags */
.long 0, 0, 0, 0 /* eax, ecx, edx, ebx */
.long usr_stk0, 0, 0, 0 /* esp, ebp, esi, edi */
.long 0x17,0x0f,0x17,0x17,0x17,0x17 /* es, cs, ss, ds, fs, gs */
.long LDT0_SEL /* ldt */
.long 0x8000000 /* trace bitmap */
.fill 128,4,0
stack0_krn_ptr:
.long 0
以上代码定义了任务0的tss和ldt,ldt描述了任务0的代码和数据段信息,tss定义了任务的各种属性,入口,用户堆栈,ldt,寄存器内容,内核栈等。
ljmp tss段选择符这样的语句会造成任务切换,cpu自动保存当前任务tss内容,并恢复需要切换到的任务的tss内容,造成任务切换。任务1和任务2的描述内容与任务0类似。
.align 2
task0:
movw $0x0c61, %ax /* print 'a' */
int $0x80
movl $0xefff, %ecx
1: loop 1b
jmp task0
.fill 128,4,0
usr_stk0:
.long 0
以上是任务0的代码,设置参数ax为0x0c61,并调用系统调用int 0x80,后边的语句实现延时,并循环执行代码。
pushfl
andl $0xffffbfff, (%esp)
popfl
movl $TSS0_SEL, %eax
ltr %ax
movl $LDT0_SEL, %eax
lldt %ax
movl $0, current
sti
pushl $0x17
pushl $usr_stk0
pushfl
pushl $0x0f
pushl $task0
iret
以上代码比较关键,为转移到任务0做准备,分别准备堆栈,加载任务0的内容进cpu,开中断,最后通过iret指令,让cpu觉得是中断返回,这样造成cpu特权级的切换,切换到了任务0中执行。
3. 编译执行
编过过程不再叙述,看运行截图:
通过一些列的文章,我们阐述了如何在保护模式下切换任务。同时知识包括:gdt,idt,ldt,tss,时钟中断服务,特权级切换,显存编程,boot和loader功能,bios调用等等。详细知识还要在实践中摸索学习,希望大家一起进步。
本文完.