http://blog.chinaunix.net/uid-11319766-id-3445575.html
在进入保护模式后,head.s程序重新建立和设置IDT,GDT表的主要原因是为了让程序在结构上比较清晰,也是为了与后面的linux 0.11内核源代码中这两个表的设置方式保持一致,当然。就本程序来说我们完全可以直接使用boot.s中设置的IDT和GDT表位置,填入适当的描述符即可。
#
#
LATCH
= 11930
#定时器初始计数值
SCRN_SEL
= 0x18
#屏幕显示内存段选择符
TSS0_SEL
= 0x20
#任务0的TSS段选择符
LDT0_SEL
= 0x28
#任务0的LDT段选择符
TSS1_SEL
= 0x30
#任务1的TSS段选择符
LDT1_SEL
= 0x38
#任务1的LDT段选择符
.text
startup_32
#首先加载数据段寄存器DS,堆栈段寄存器SS和堆栈指针ESP。所有段的线性基地址都是0
movl $0x10, %eax
#0x10是GDT中数据段选择符
mov %ax, %ds
#将选择符赋值给ds。ds寄存器
lss init_stack, %esp
#lss指令把,段地址:偏移地址存到SS:esp。init_stack见后面
#设置IDT和GDT表
call
setup_idt
#设置IDT表
call
setup_gdt
#设置GDT表
movl $0x10, %eax
#在改变了GDT之后重新加载所有段寄存器,都加载为GDT第3个项内存数据段
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
lss init_stack, %esp
#设置8253定时芯片。把计数器通道0设置成每隔10毫秒向中断控制器发送一个中断请求信号
movb $0x36, %al
#控制字:设置通道0工作方式3,计数初值采用二进制
movl $0x43, %edx
#8253芯片控制字寄存器写端口
outb %al, %dx
#IO端口操作
movl $LATCH, %eax
#初始计数值设置为LATCH(1193180/100),即频率100Hz
movl $0x40, %edx
#通道0的端口
outb %al, %dx
#分两次把初始值写入通道0
movb %ah, %al
outb %al, %dx
#在IDT表第8个第128(0x80)项处分别设置定时中断门描述符和系统调用陷阱门描述符
movl $0x00080000, %eax
#中断程序属内核,即EAX高字是内核代码段选择符0x0008
movw $timer_interrupt, %ax
#设置定时中断门描述符
movw $0x8E00, %dx
#中断门类型是14
movl $0x08, %ecx
#将IDT描述符表向量8的地址放入ESI中
lea idt(,%ecx,8), %esi
#idt(,%ecx,8)表示idt表(可看成数组)中,每项长度为8,索引值为eax地址
movl %eax, (%esi)
movl %edx, 4(%esi)
movw $system_interrupt, %ax
#设置系统调用陷阱门描述符。取系统调用处理程序地址
movw $0xef00, %dx
#陷阱门类型是15,特权级3的程序可执行
movl $0x80, %ecx
#系统调用的向量号是0x80
lea idt(,%ecx,8), %esi
#将idt中向量为0x80的地址传给esi
movl %eax, (%esi)
#
movl %edx, 4(%esi)
#现在我们为移动到任务0中执行来操作堆栈内容,在堆栈中人工建立中断返回时的场景。
pushfl
#标志位寄存器入栈 ()
andl $0xffffbfff, (%esp)
popfl
movl $TSS0_SEL, %eax
#把任务0的TSS段选择符加载进eax
ltr
%ax
# LTR指令是专门用于装载任务状态段寄存器TR的指令
movl $LDT0_SEL, %eax
#把任务0的LDT段选择符加载到ax
lldt %ax
#转载LDTR寄存器。TR和LDTR只需要人工加载一次,以后CPU会自动处理
mov $0, current
#将任务号0装入current变量
sti
#开启中断
pushl $0x17
#把任务0当前局部空间数据段(堆栈段)选择符入栈
pushl $init_stack
#把堆栈指针入栈
pushfl
#标志寄存器入栈
pushl $0x0f
#把当前局部空间代码段选择符入栈
pushl $task0
#把任务0的代码地址入栈
iret
#执行中断返回指令,从而切换到特权级3的任务0中执行。
#以下是设置GDT和IDT中描述符项的子程序。
setup_gdt:
#使用6字节操作数lgdt_opcode设置GDT表位置和长度
lgdt
lgdt_opcode
ret
#这段代码暂时设置IDT表中所以256个中断门描述符都为同一个默认值,均使用默认的中断处理过程ignore_int.
#设置的具体方法是:首先在eax和edx寄存器对中分别设置好默认中断门描述符的0-3字节和4-7字节,然后利用该
#寄存器对循环往IDT表中填充默认中断门描述内容。
setup_idt:
lea
ignore_int, %edx
#将标号为ignore_int出的偏移地址,赋值给edx
movl
$0x00080000, %eax
#将0x00080000给eax,这里有用的是0x0008 对应GDT表内核代码段
movw
%dx,
%ax
#将dx(是edx的低16位)的值给ax,eax就是中断门描述符的0-3字节
movw
$0x8E00, %dx
#edx是中断门描述符的4-7字节
lea
idt, %edi
#让edi(通用变址指针寄存器)中是标号为idt的地址。
mov
$256, %ecx
#ecx保存要循环的次数
rp_idt:
movl
%eax, (%edi)
#将eax的值传给edi指向的内存。“()”是取地址
movl
%edx, 4(%edi)
#将edx的值传给edi+4指向的内存
addl
$8,
%edi
#地址edi加8
dec
%ecx
#次数ecx减一
jne
rp_idt
#循环
lidt
lidt_opcode
#装载lid表地址 到IDTR寄存器,lidt_opcode是6字节的内容包括基地址和限长
ret
#
#
#显示字符子程序。取当前光标位置并把AL中的字符显示在屏幕上,这个屏幕可显示80X25个字符
write_char:
push %gs
#入栈,保存此调用可能用到的寄存器,gs,ebx
push %ebx
mov $SCRN_SEL, %ebx
#将屏幕显示内存段的描述符存在ebx
mov %bx, %gs
movl scr_loc, %bx
#将变量scr_loc放入bx,变量定义见后面
shl $1, %ebx
#将ebx逻辑左移1 ,屏幕上每个字符还有一个属性字节,因此内存地址要乘2
movb %al, %gs:(%ebx)
#将al(其中保存字符码),传到gs段的ebx地址。用于显示。
shr $1, %ebx
#除2(左移1)
incl %ebx
#加1
cmp1 $2000, %ebx
#是否等于2000
jb
1f
#如果不等于 跳过到1
movl $0, %ebx
#ebx清零
1:
movl %ebx, scr_loc
#保存到变量scr_loc
popl %ebx
#出栈
pop
%gs
ret
#以下是3个中断处理程序,默认中断,定时中断和系统中断
.align 2
ignore_int:
push
%ds
pushl
%eax
movl $0x10, %eax
mov %ax, %ds
#让DS指向内核数据段,因为中断程序属于内核
movl $67, %eax
call write_char
#在AL中存放字符‘C’的代码,调用显示程序显示在屏幕上
popl %eax
pop %ds
iret
#定时中断处理程序,其中主要执行任务切换操作
.align 2
timer_interrupt:
push %ds
push %eax
movl $0x10, %eax
mov %ax, %ds
# DS寄存器指向内核数据段(0x10的段选择符)
movb $0x20, %al
#然后立刻允许其他硬件中断,即向8259A发送E01命令
outb %al $0x20
movl $1, %eax
#任务切换,若是任务1,则执行任务0,反之亦然
cmpl %eax, current
je 1f
movl %eax, current
#若是任务0
ljmp $TSS1_SEL, $0
# TSS1_SEL是GDT表中任务段tss0的段选择符,$0偏移地址无意义。
jmp 2f
1:
movl $0, current
#若当前任务是1,则把0存入current,并跳转到任务0
ljmp $TSS0_SEL, $0
2:
popl %eax
pop %ds
iret
#系统调用中断int 0x80处理程序。该示例只有一个字符显示功能
.align 2
system_interrupt:
push %ds
pushl %edx
pushl %ecx
pushl %ebx
pushl %eax
movl $0x10, %edx
#首先让DS指向内核数据段
mov %dx, %ds
call
write_char
pop1 %eax
pop1 %ebx
pop1 %ecx
popl %edx
pop %ds
iret
#32位变量
current: .long 0
#当前任务号 (0或1)
scr_loc: .long 0
#屏幕当前显示位置。按从左上到右下顺序
.align 2
lidt_opcode:
.word 256*8-1
.long idt
lgdt_opcode:
.word(end_gdt-gdt) – 1
.long gdt
.align 3
idt:
.fill 256,8,0
#IDT表空间,共256个门描述符。用fill指令分配了256×8个字节 = 2KB
gdt:
.quad 0x0000000000000000
#GDT 第一为空
.quad 0x00c09a00000007ff
#2内核代码段描述符。其选择符是0x08,基地址0,
.quad 0x00c09200000007ff
#3内核数据段描述符。选择符是0x10,基地址0,
.quad 0x00c0920b80000002
#4显示内存段描述符。选择符是0x18
.word 0x68, tss0, 0xe900, 0x0
#5 TSS0段的描述符。选择符是0x20,基地址tss0,限长0x68,参考4.3.6
.word 0x40, ldt0, 0xe200, 0x0
#6 LDT0段的描述符。选择符是0x28,基地址ldt0,限长0x40
.word 0x68, tss1, 0xe900, 0x0
#7 TSS1段的描述符。选择符是0x30,基地址tss1,限长0x68
.word 0x40, ldt0, 0xe900, 0x0
#8 LDT1段的描述符。选择符是0x38,基地址ldt0,限长0x40
end_gdt:
.fill 128, 4, 0
#初始内核堆栈空间
init_stack:
.long init_stack
#4字节偏移地址。init_stack标号是在fill分配的空间的尾部,是栈顶
.wor 0x10
#2 字节段选择符。对应GDT表中3 内核数据段描述符
#下面是任务0的LDT表段中的描述符
.align 3
ldt0:
.quad 0x0000000000000000
#空,不用
.quad 0x00c0fa00000003ff
#局部代码段描述符。选择符是0x0f;限长1024 基地址0
.quad 0x00c0f200000003ff
#局部数据段描述符选择符是0x17;限长1024 基地址0
#下面是任务0的TSS段的内容。注意其中的标号等字段在任务切换时不会改变。
tss0:
.long 0
#前一个任务的链接(TSS段选择符)
.long krn_stk0, 0x10
# krn_stk0(内核栈空间地址标号)作为ESP0;0x10作为SS0堆栈段,对应#GDT表中第3项
.long 0, 0, 0, 0, 0
#esp1, ss1, esp2, ss2, cr3
.long 0, 0, 0, 0, 0
#eip, eflags, eax, ecx, edx
.long 0, 0, 0, 0, 0
#ebx, esp, ebp, esi, edi
.long 0, 0, 0, 0, 0, 0
#es, cs, ss, ds, fs, gs
.long LDT0_SEL, 0x80000000
# ldt, LDT0_SEL = 0x28(LDT表的段选择符); trace bitmap(I\O位图基地址)
.fill 128, 4, 0
#分配任务0的内核栈空间
krn_stk0:
#下面是任务1的LDT表段内容和TSS段内容。
.align 3
ldt1:
.quad 0x0000000000000000
#第一个不用
.quad 0x00c0fa00000003ff
#代码段,选择符0x0f,基地址 =0x0
.quad 0x00c0f200000003ff
#数据段,选择符是0x17, 基地址 =0x0
tss0:
.long 0
#前一个任务的链接(TSS段选择符)
.long krn_stk1, 0x10
# krn_stk1(内核栈空间地址标号)作为ESP0;0x10作为SS0堆栈段,对应#GDT表中第3项
.long 0, 0, 0, 0, 0
#esp1, ss1, esp2, ss2, cr3
.long 0, 0, 0, 0, 0
#eip, eflags, eax, ecx, edx
.long 0, 0, 0, 0, 0
#ebx, esp, ebp, esi, edi
.long 0, 0, 0, 0, 0, 0
#es, cs, ss, ds, fs, gs
.long LDT1_SEL, 0x80000000
# ldt, LDT1_SEL = 0x38(LDT表的段选择符); trace bitmap(I\O位图基地址)
.fill 128,4,0
krn_stk1;
#下面是任务0和任务1的程序,它们分别循环显示字符A和B
task0:
movl $0x17, %eax
#首先让DS指向任务局部数据段
movw %ax, %ds
#因为任务没有使用局部数据,所以这两句可省略
movl $65, %al
#把需要显示的字符A放入AL寄存器中
int $0x80
#系统调用,显示字符
movl $0xfff, %ecx
#执行循环,起延时作用
1:
loop 1b
jmp task0
#跳转到任务开始处继续显示字符
task1:
movl $66, %al
#把需要显示的字符A放入AL寄存器中
int $0x80
#系统调用,显示字符
movl $0xfff, %ecx
#执行循环,起延时作用
1:
loop 1b
jmp task1
#跳转到任务开始处继续显示字符
.fill 128,4,0
#这是任务1的用户栈空间
usr_stk1: