内核程序
head.s
# head.s contains the 32-bit startup code.
# Two L3 task multitasking. The code of tasks are in kernel area,
# just like the Linux. The kernel code is located at 0x10000.
.code32
SCRN_SEL = 0x18
TSS0_SEL = 0x20
LDT0_SEL = 0x28
TSS1_SEL = 0X30
LDT1_SEL = 0x38
TSS2_SEL = 0X40
LDT2_SEL = 0x48
.global startup_32
.text
startup_32:
movl $0x10,%eax
mov %ax,%ds
# mov %ax,%es
lss init_stack,%esp
# setup base fields of descriptors.
call setup_idt
call setup_gdt
movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt.
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss init_stack,%esp
# setup up timer 8253 chip.
movb $0x36, %al
movl $0x43, %edx
outb %al, %dx
movl $11930, %eax # timer frequency 100 HZ
movl $0x40, %edx
outb %al, %dx
movb %ah, %al
outb %al, %dx
# setup timer & system call interrupt descriptors.
movl $0x00080000, %eax
movw timer_interrupt, %ax
movw $0x8E00, %dx
movl $0x08, %ecx # The PC default timer int.
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)
# unmask the timer interrupt.
# movl $0x21, %edx
# inb %dx, %al
# andb $0xfe, %al
# outb %al, %dx
# 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 $init_stack
pushfl
pushl $0x0f
pushl $task0
iret
/****************************************/
setup_gdt:
lgdt lgdt_opcode
ret
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
# -----------------------------------
write_char:
push %gs
pushl %ebx
# pushl %eax
mov $SCRN_SEL, %ebx
mov %bx, %gs
movl scr_loc, %ebx
shl $1, %ebx
movb %al, %gs:(%ebx)
shr $1, %ebx
incl %ebx
cmpl $2000, %ebx
jb 1f
movl $0, %ebx
1: movl %ebx, scr_loc
# popl %eax
popl %ebx
pop %gs
ret
/***********************************************/
/* This is the default interrupt "handler" :-) */
.align 2
ignore_int:
push %ds
pushl %eax
movl $0x10, %eax
mov %ax, %ds
movl $67, %eax /* print 'C' */
call write_char
popl %eax
pop %ds
iret
/* Timer interrupt handler */
.align 2
timer_interrupt:
push %ds
pushl %eax
movl $0x10, %eax
mov %ax, %ds
movb $0x20, %al
outb %al, $0x20
movl $1, %eax
cmpl %eax, current
je 1f
movl $2,%eax
cmpl %eax,current
je 3f
movl $1, current
ljmp $TSS1_SEL, $0
jmp 2f
1: movl $2, current
ljmp $TSS2_SEL,$0
jmp 2f
3: movl $0,current
ljmp $TSS0_SEL,$0
2: popl %eax
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
scr_loc:.long 0
.align 2
lidt_opcode:
.word 256*8-1 # idt contains 256 entries
.long idt # This will be rewrite by code.
lgdt_opcode:
.word (end_gdt-gdt)-1 # so does gdt
.long gdt # This will be rewrite by code.
.align 8
idt: .fill 256,8,0 # idt is uninitialized
gdt: .quad 0x0000000000000000 /* NULL descriptor */
.quad 0x00c09a00000007ff /* 8Mb 0x08, base = 0x00000 */
.quad 0x00c09200000007ff /* 8Mb 0x10 */
.quad 0x00c0920b80000002 /* screen 0x18 - for display */
.word 0x0068, tss0, 0xe900, 0x0 # TSS0 descr 0x20
.word 0x0040, ldt0, 0xe200, 0x0 # LDT0 descr 0x28
.word 0x0068, tss1, 0xe900, 0x0 # TSS1 descr 0x30
.word 0x0040, ldt1, 0xe200, 0x0 # LDT1 descr 0x38
.word 0x0068, tss2, 0xe900, 0x0 # TSS2 descr 0x40
.word 0x0040, ldt2, 0xe200, 0x0 # LDT2 descr 0x48
end_gdt:
.fill 128,4,0
init_stack: # Will be used as user stack for task0.
.long init_stack
.word 0x10
/*************************************/
.align 8
ldt0:
.quad 0x0000000000000000
.quad 0x00c0fa00000003ff # 0x0f, base = 0x00000
.quad 0x00c0f200000003ff # 0x17
tss0: .long 0 /* back link */
.long krn_stk0, 0x10 /* esp0, ss0 */
.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, 0x8000000 /* ldt, trace bitmap */
.fill 128,4,0
krn_stk0:
# .long 0
/************************************/
.align 8
ldt1: .quad 0x0000000000000000
.quad 0x00c0fa00000003ff # 0x0f, base = 0x00000
.quad 0x00c0f200000003ff # 0x17
tss1: .long 0 /* back link */
.org 510
.word 0xAA55
.long krn_stk1, 0x10 /* esp0, ss0 */
.long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */
.long task1, 0x200 /* eip, 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, 0x8000000 /* ldt, trace bitmap */
.fill 128,4,0
krn_stk1:
.align 8
ldt2: .quad 0x0000000000000000
.quad 0x00c0fa00000003ff # 0x0f, base = 0x00000
.quad 0x00c0f200000003ff # 0x17
tss2: .long 0 /* back link */
.long krn_stk2, 0x10 /* esp0, ss0 */
.long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */
.long task2, 0x200 /* eip, 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, 0x8000000 /* ldt, trace bitmap */
.fill 128,4,0
krn_stk2:
/************************************/
task0:
movl $0x17, %eax
movw %ax, %ds
movb $65, %al /* print 'A' */
int $0x80
movl $0xfff, %ecx
1: loop 1b
jmp task0
task1:
movl $0x17, %eax
movw %ax, %ds
movb $66, %al /* print 'B' */
int $0x80
movl $0xfff, %ecx
1: loop 1b
jmp task1
.fill 128,4,0
usr_stk1:
task2:
movl $0x17, %eax
movw %ax, %ds
movb $67, %al /* print 'C' */
int $0x80
movl $0xfff, %ecx
1: loop 1b
jmp task2
.fill 128,4,0
usr_stk2:
内核代码的工作原理浅析
-
初始化并跳转到第一个任务
-
初始化数据段寄存器ds,idtr,gdtr和esp寄存器.其中idt表由程序动态初始化,gdt表的内容硬编码在内存中;这是的栈顶是内核栈栈顶.
-
设置定时器,以100Hz的频率产生时钟中断.
-
在idt表中的0x08号中断中注册时钟中断处理程序(执行任务切换);在0x80号中注册系统中断处理程序(打印字符)
-
初始化TR值为GDT表中的task0任务门描述符的选择子(0x20);初始化 LDTR为GDT表中ldt0项的段选择子(0x28).
-
开启中断,并通过在栈上伪造iret指令返回前的状态来执行任务切换,从ring0切换到ring3的task0任务继续执行.
-
-
在任务执行过程中通过时钟中断实现3任务切换
- 处理器在task0的上下文中运行,接收到时钟中断之后进入时钟中断处理程序(默认就是0x08号中断)。
在时钟中断处理程序中,将会判断当前任务是task0,task1还是task2,其中task0打印"A",task2打印"B",task3打印"C":
- 如果是task0,通过长转移(ljmp)的方式跳转到task1的上下文(用任务切换的方式),在task1的循环打印中继续等待下次时钟中断;
- 如果是task1,则通过ljmp切换到task0执行;如果是task2,则切换到task0执行。
最终的效果就是交替打印ABC。
内核代码的内存分布情况
memrange.png如何由内核态切换到用户态
伪造一个iret前的堆栈,并调用iret指令执行高优先级(ring0)向低优先级(ring3)的切换.这个关键的iret在代码的第76行。
至于为什么要伪造一个iret来切换到用户态,这是因为ia32不允许从高优先级直接切换到低优先级,但是我们知道,中断服务程序是运行在内核态的,所以中断服务程序返回到用户程序过程中一定发生了从内核态到用户态的切换,因此才模仿中断返回时调用iret来切换。
压栈之后(iret指令执行之前),栈中的结构是这样的
beforeiret.pngiret执行后,栈变成了这样的:
afteriret.png可以看到之前的0xbc4~0xbd4的内容已经被弹出栈(这里也可以看出task0和内核使用的是同一个栈)。
关注关键寄存器的值,已经被更改为之前栈中设定的值:
eip_eflags.png cs.pngiret执行后,将会将栈顶的eip和cs 弹出到eip和cs中,这样就能找到下一条指令的地址。
内核态和用户态的相互切换过程
本程序向屏幕中打印字符,必须要调用系统调用来实现。调用int 0x80 从用户态陷入内核态以及从内核态返回的过程中究竟发生了哪些事?
进入int 0x80前的栈:
beforeint.png进入int 0x80后的栈:
afterint.png执行system_interrupt中的iret之前,栈的情况是这样的:
beforeintiret.png执行system_interrupt中的iret之后,栈的情况是这样的:
afterintiret.png进入和退出system_interrupt时,都发生了模式切换(从用户模式到内核模式的切换和从内核模式用iret回到用户模式)。
分析
-
进入system_interrupt时进行的模式切换:
调用int 0x80时,CPU会查询idt的第0x80项,提取之前存入的值:
intdata.png
中断们描述符r格式如下:
![interruptgate.png](https://img.haomeiwen.com/i19793687/c4df7c71cbb5af9e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
可以看到段选择符的位置的值被置为0x0008,因此这中断门的RPL字段值为0,因此调用int 0x80后,将会从用户态(ring3)切换到内核态(ring0),查看程序跳转到system_interrupt之后的第一句时cs的值也可以看到CPL的确变为了0:
![showcpl.png](https://img.haomeiwen.com/i19793687/7b1499a44404d76e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
同时,进入system_interrupt之后,还将会发生栈切换(这一点从进入int 0x80前后的栈比较中可以看出,栈底从0xbd8变为了0xe5c)。
已经知道,从用户态切换到内核态,栈空间将会从用户栈切换到内核栈,并由硬件自动将切换前的现场(SS,ESP,EFALGS,EIP,CS这五个值)压入内核栈中保存,那么问题是,int 0x80如何知道内核栈的地址呢?
因为每个任务都被分配了一个用户栈和一个内核栈,如下图:
tss.png因此,在调试进入int 0x80后的程序时,查看TSS段中的对应字段和当前寄存器和段寄存器值,可以看到这些值都是吻合的(其中esp的值由于已经push了5个参数,所以会比TSS中存储的地址值小5*4 = 20字节):
taskchanged.png因此,可以小结一下,调用int 0x80从用户态切换到内核态时,CPU做了以下工作:
- 读取TSS中ss:esp(0)字段的值
- 将当前ss,esp,eflags,eip,cs压入内核栈
- 更新esp和ss的值,并根据idt表的0x80号偏移找到对应的段描述符和偏移,修改eip和CPL,实现跳转和特权级切换。
从system_interrupt返回时的模式切换
从调用int 0x80后的栈中的值可以看到,栈中将要弹出到cs的值被设置为0x17,也就是最低两位特权级位为十进制的3,也就是说,中断返回时将会返回到特权级为3的用户程序中继续执行。
而返回时还会从内核栈切换回用户栈,这直接通过弹栈即可实现,从上面执行system_interrupt中的iret前后的栈情况可以看到,这样的且换是确实发生了的。
任务切换过程详解
这里就记录task0 和 task1 之间的切换,3任务切换原理是一样的。
taskana.png-
task1执行到某处(大概率是loop指令,因为大部分时间task0都在loop),接收到时钟中断,进入中断响应程序timer_interrupt中执行。
-
执行判断current,由于初始值是0,因此会执行ljmp ‘0‘这个语句,将当前寄存器的值保存到当前tss(task0的tss)中;再将目标tss(task1的tss)的值更新到CPU的各个寄存器中,由于修改了eip和cs,因此跳转到task1代码的第一行开始执行,开始打印”B”。
-
10ms后,task1接收到时钟中断,执行中断响应程序timer_interrupt,此时current的值已经被改成1,因此将会执行
ljmp $TSS0_SEL,$0
指令,这时做的基本上是和2步中一样的操作:将会先把task1的当前寄存器值存储到task1的tss中去;再把2中存储在tss0中的值更新到CPU寄存器中,注意,由于2中tss0的eip和cs字段存储的是task0执行任务切换指令的下一条指令的地址,因此,更新之后将会回到步骤
2中跳转指令ljmp $TSS1_SEL,$0
的下一条语句jmp 2f开始继续执行。
-
执行到中断返回指令iret时,将会回到task0的代码段被时钟中断断下的指令的下一条指令继续执行,即继续打印’A’.
-
又经过10ms后,收到时钟中断的task0将会重复步骤2,但是
ljmp $TSS1_SEL,$0
的执行结果不再是调到task1的代码的第一行,而是根据第3步中记录的tss1中的返回地址来决定继续执行的指令(大概率是loop指令)。
网友评论