老僧非是爱花红

导航

Linux0.00内核学习

前言

这里的Linux0.00内核,指的是赵炯博士编著的《Linux内核完全剖析 -基于0.12内核》第四章末尾提供的一个简单多任务内核。

源码地址:http://www.oldlinux.org/Linux.old/bochs/linux-0.00-050613.zip

如在64位机器下编译此内核可能会遇到许多问题,本博客下的《64位系统下编译linux0.00内核 》记载了一种编译成功的案例,可供参考。

 

对照着源码动手写了一遍boot.s和head.s,过程中解开了许多疑惑,记录在此以供复习。

阅读源码(boot.s+head.s)

boot.s

! 加载内核镜像文件到指定的位置
! 设置临时的GDT表项  (不开启分页)
! 进入保护模式,跳转到head执行

!按照设计,先将head加载到0x1000:0位置处,然后再复制到0x0000:0处

!启动扇区在内存中的代码段
bootseg=0x07c0
!内核被临时加载位置
tmpseg=0x1000
!内核对应的扇区个数
kernel_len=17

entry start
start:

!MBR被加载到内存后,位于0x07c0:0处,由于cs的值被初始化为0,因此这里要手动设置cs=0x7c0
!用jmpi 段间跳转命令实现
!jmpi offset,cs
jmpi  realstart,#bootseg
realstart:

!jmpi之后,此时cs=0x7c0
!设置ds,ss,sp
!mov指令格式 mov dst,src
mov ax,cs
mov ds,ax
mov ss,ax

!设置栈顶指针,暂时不懂这里为什么要用到栈 :)
mov sp,#0x400

!接下来的主要任务是借助bios中断,从软盘镜像中加载内核到内存0x10000处
!涉及到具体的bios中断例程的使用,不做细纠
load_system:
mov dx,#0x0000
mov cx,#0x0002
mov ax,#tmpseg
mov es,ax
xor bx,bx
mov ax,#0x200+kernel_len
int 0x13
jnc load_success
!失败则死循环
die: jmp die

!到这里说明内核已经被加载到0x10000处
!接下来将内核模块移动到0x00000地址处
!0x1000:0开始,移动到0x0000:0,移动的内容共为8KB,
!ds:si=0x1000:0  es:di=0x0000:0
load_success:
cli
!首先将ds设置为0x1000,(前面被设置为了0x07c0) Q:前面难道一定要将ds设置为0x7c0吗?
mov ax,#tmpseg
mov ds,ax
!ax=0,用来设置es,di
xor ax,ax
mov es,ax
!cx用于指定循环的次数,共4K次
mov cx,#0x1000
sub si,si
sub di,di
!每次传输一个word,两个byte
rep
movw
!执行到这里,说明当前已经将kernel移动到0x00000了
!接下来为进入保护模式做准备

!目前还不太懂为什么要将ds设置为bootseg
mov ax,#bootseg
mov ds,ax


!?明明没有使用idt为什么这里要加载idtr呢? 不太懂
lidt idtr_
!设置gdtr和idtr
lgdt gdtr_


!接下来进行真正的开启保护模式操作:通过写CR0中的PG标志位来开启保护模式
mov ax,#0x0001
!lmsw指令的含义是加载源操作数到机器状态寄存器CR0中,且只加载源操作数的低四位
lmsw ax
!开启保护模式后,这里我们立刻使用jmp跳转到cs:0处,
!此时cs的含义是gdt选择子,8代表gdt表中的第一项,
!根据gdt表的设计可知,此时跳转到0x00000处执行,即内核所在处的第一条指令
!jmpi offset,gdt_selector
jmpi 0,8


!gdt表的内容,little-indian,先存低地址
!
gdt_table:
.word 0,0,0,0 !第一项为空
!代码段: 基地址:0  段限长:2047 粒度:4K 权限:可读/执行 因此总共的线性地址大小为8MB
.word 0x07ff
.word 0x0000
.word 0x9a00
.word 0x00c0

!数据段,数据段和代码段映射在相同的线性地址空间,唯一的区别在于访问权限的不同,
!数据段权限为可读写,不可执行
.word 0x07ff
.word 0x0000
.word 0x9200
.word 0x00c0

!用于设置idtr
idtr_:
!idt表的长度
.word 0 
!idt表的线性基地址,因为没有采用分页,所以线性基地址就等于物理地址
.word 0,0

!用于设置gdtr
gdtr_:
!gdt表的大小设置为2048Byte,每个表项8Byte,因此最多可以放256个表项
.word 0x07ff
!gdtr中需要保存gdt_table的线性地址,gdt_table为相对bootsect开始的偏移,
!因为bootsect被加载到0x7c0:0处,故实际gdt表的物理地址采用如下的方式计算得到
!由于没有采用分页,因此线性地址就等于物理地址
.word gdt_table+0x7c00,0

.org 510
.word 0xaa55

 

heas.s

#32位的head.s

#head.s 主要需要完成的工作
#重新设置GDT,添加tss0,cs0,ds0,tss1,cs1,ds1等几个描述符
#设置IDT,设置好几个必要的中断处理函数
#为task0,task1进程设置必要的数据结构:tss段,栈等
#实现task0和task1的业务逻辑
#实现进程调度逻辑
#实现由内核态通过切换到用户态的逻辑


#######定义几个基本的常量和选择子
#用于设置时钟硬件
latch=11930

#dgt中的几个选择子
screen_seg_selector= 0x18
tss0_selector =0x20
ldt0_selector  =0x28
tss1_selector =0x30
ldt1_selector  =0x38


#进入head后首先重新设置gdt和idt
.code32
.text
.globl startup_32
startup_32:
#重新设置32位下的ds,es,esp
movl $0x10,%eax
mov %ax,%ds

#设置ss=0x10,esp,init_stack为临时的内核栈
lss init_stack,%esp
call setup_idtr
call setup_gdtr

#设置好新的gdt表之后,重新设置相关段寄存器的值
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss init_stack,%esp

######################
##时钟中断硬件相关的设置,不做深入研究
movb $0x36,%al
movl $0x43,%edx
outb %al,%dx
movl $latch,%eax
movl $0x40,%edx
outb %al,%dx
movb %ah,%al
outb %al,%dx
######################


###接下来在idt表中设置时钟中段和系统调用相关的idt描述符
###这里的实现是 中断号8(0x08)对应定时器中断,中断号128(0x80)对应系统调用

##设置中断号为0x08的中断门描述符
#eax-段选择子:偏移值0-15
#edx-偏移值16-31:权限位
#eax对应0-3字节,dex对应4-7字节
#设置eax
movl $0x00080000,%eax
movw $timer_interrupt,%ax

#设置edx
movw $0x8e00, %dx

#接下来就是定位到idt表中的0x08位置,设置idt中断门描述符
movl $0x08, %ecx
#得到0x08项的地址,每项占用8Byte空间
lea idt_table(,%ecx,8),%esi

#下面开始设置idt时钟中断门描述符
    movl %eax,(%esi)
movl %edx,4(%esi)

##设置中断号为0x80的idt陷阱门描述符
#设置eax,高位已经设置为了0x0008,这里只需再设置低16位
    movw $system_call_interrupt,%ax

#设置edx,注意这里的陷阱门描述符和时钟中断中断门描述符的区别
#ef00=1110 1111 0000 0000
#8e00=1000 1110 0000 0000
#区别在于DPL位,以及第8位
    movw $0xef00, %dx

#接下来就是定位到idt表中的第0x80位置,设置idt陷阱门描述符
    movl $0x80, %ecx
#得到0x80项的地址,每项占用8Byte空间
    lea idt_table(,%ecx,8),%esi

#下面开始设置idt系统调用陷阱门描述符,同上面的时钟中断门描述符设置过程
    movl %eax,(%esi)
movl %edx,4(%esi)

###到这里,已经设置好了idt中的中断门描述符和系统调用陷阱门
###接下来就准备将系统模拟为task0的内核态的状态下
###然后通过iret指令模拟从task0的内核态返回到task0的用户态
###要模拟这个状态,至少需要进行如下设置:
###1、设置tss为tss0,表明当前进程为task0 
###2、设置ldt为ldt0,表明当前正处于task0
###3、内核栈的结构处于“用户态中断切换到内核态”的状态
###具体来说,在执行iret前,内核栈中存在如下的结构
###【空】【原ss】
###【  原esp  】
###【  EFLAGS 】
###【空】【原cs】
###【  原EIP  】<-------esp

    pushfl
#设置栈中EFLAGS中NT标志位为0,模拟的是陷阱门
andl $0xffffbfff,(%esp)
    popfl

##接下来设置tss为tss0
    movl $tss0_selector,%eax
    ltr %ax

##接下来设置ldt为ldt0
    movl $ldt0_selector,%eax
    lldt %ax

##设置current=0
    movl $0,current

##开中断,马上要返回到用户态了,在此之前必须打开中断
    sti

##接下来就是模拟中断发生时内核栈的结构,以便利用iret返回到task0的用户态
###具体来说,在执行iret前,内核栈中存在如下的结构
###【空】【原ss】
###【  原esp  】
###【  EFLAGS 】
###【空】【原cs】
###【  原EIP  】<-------esp
    pushl $0x17
    pushl $init_stack
    pushfl
    pushl $0x0f
    pushl $task0
    iret


    setup_gdtr:
    lgdt gdtr_value
    ret

#设置好中断服务例程后,挂载idt
#首先将256个idt_entry全部设置为default_interrupt例程
    setup_idtr:
##搞不明白lea指令的含义
#202007272324 困了

#20200728继续开始
#这里按照idt描述符的格式设置eax,edx
#eax-段选择子:偏移值0-15
#edx-偏移值16-31:权限位
#eax对应0-3字节,dex对应4-7字节

#得到default_interrupt例程的偏移地址
    lea default_interrupt,%edx
#eax选择子设置为0x0008,偏移值0-15位设置为0000
    movl $0x00080000,%eax
#将edx中的偏移地址的0-15位保存到eax的0-15位
    movw %dx,%ax 
#设置edx0-15位中的权限值,
#0x8e00=1(P)00(DPL)0 1110 000(空)0 0000(B)
    movw $0x8e00,%dx

#经过上面三条指令,eax和edx已经设置好了,接下来就用这些值填满256个idt表项
#获取idt表的地址
    lea idt_table,%edi
#总共循环256次
    mov $256,%ecx

    write_idt_item:
#接下来是循环体,依次设置每个idt表项的0-3字节,4-7字节
#%idt描述符的0-3字节
    movl %eax,(%edi)
movl %edx,4(%edi)
    addl $8,%edi
    dec %ecx
    jne write_idt_item
#设置idtr
    lidt idtr_value
    ret




#系统服务,打印字符
    write_char:

#需要加载显存段
#需要用到gs来指示显存段,所以先保存原值
    push %gs
#ebx用作“临时存储单元”
    pushl %ebx

#加载显存段
    mov $screen_seg_selector, %ebx
    mov %bx,%gs

#接下来就是具体的往显存写数据的过程,一般例程,不做重点研究

##写显存
##############
    movl scr_loc,%ebx
# %bx*2得到实际该写的位置
    shl $1,%ebx
    movb %al,%gs:(%ebx)
# %bx/2得到当前已经写的字符数
    shr $1,%ebx
# 得到下一个字符的位置
    incl %ebx
# 2000=25*80
    cmpl $2000,%ebx
    jb 1f
#>2000,则从第一行第一列重新开始写入
    movl $0,%ebx
    1:
#否则保存下一个字符的位置,下次直接从该位置写入字符
    movl %ebx,scr_loc
##############

#恢复寄存器值,返回
    popl %ebx
    pop %gs
    ret

#设置必要的中断处理函数,并挂载到idt
#当前的系统总共设置了针对如下三种情况的中断处理函数
#1、时钟中断,用于驱动任务切换操作
#2、系统调用,用于用户调用内核的打印字符功能
#3、其它的中断


#1、时钟中断,用户切换task
    .align 4
timer_interrupt:
push %ds  #(不懂为什么要保存用户态的ds)A:因为接下来使用的是内核段,所以需要保存用户态段值
    pushl %eax #(不懂为什么要保存eax)A:后面用到了eax,所以需要保存旧值

#切换到内核态的ds段选择子
    movl $0x10,%eax
    mov %ax,%ds

#设置中断控制器开中断,以允许后续的中断
    movb $0x20,%al
    outb %al,$0x20

#接下来就是具体的任务切换逻辑
#通过jmp到tss选择子,让cpu硬件完成任务切换的功能
    movl $1,%eax
    cmpl %eax,current
    je 1f

#current!=1 ,即当前任务号为0
#切换到task1
#首先更新current
    mov %eax,current
#切换到task1,通过jmp tss1的选择子:偏移(无用) 的形式切换到task1
#硬件会完成保存上下文,回复寄存器值的任务
    ljmp $tss1_selector,$0
    jmp 2f #Q:这里的jmp 2f能有机会执行吗?A:有的,因为需要等到iret指令,才会真正完成任务切换

#current==1
    1:
    movl $0,current
    ljmp $tss0_selector,$0

    2:
    popl %eax
    pop %ds
    iret


#2、系统调用,提供打印字符服务
    .align 4
    system_call_interrupt:

#首先系统服务过程中可能会使用的寄存器
    push %ds
    pushl %edx
    pushl %ecx
    pushl %ebx
    pushl %eax

#切换到内核数据段 Q:为什么要切换到内核数据段呢?这里需要访问内核数据段的内容吗?A:需要,write_char会用到内核数据段中的src_loc
#换句话说,内核态提供服务,理所当然可能会用到内核数据段中的数据,“没用到内核数据段中的数据”这种情况是一种特例。
    movl $0x10, %edx
    mov %dx,%ds

#调用内核服务,现在使用的是内核态栈
    call write_char

#调用完服务后,恢复原寄存器值
    popl %eax
    popl %ebx
    popl %ecx
    popl %edx
    pop %ds
    iret


#3、其它的中断
    .align 4
    default_interrupt:
#如果是其它的中断,本例的做法是往屏幕写一个字符
    push %ds
    pushl %eax

#指向内核数据段
    movl $0x10,%eax
    mov %ax,%ds

    movl $67,%eax
    call write_char

    popl %eax
    pop %ds
    iret


#######################
##这里存放了内核的主要的数据结构和数据
#######################
#gdtr
    current:.long 0
    scr_loc:.long 0

    .align 4
#标识idt表的大小和位置
    idtr_value:
#idt的大小:单位为字节
    .word 256*8-1
#idt表的线性地址
    .long idt_table

    gdtr_value:
    .word (new_gdt_table_end-new_gdt_table)-1
    .long new_gdt_table

    .align 8
#定义idt表的位置,预留了256*8大小的空间
    idt_table:
    .fill 256,8,0

    new_gdt_table:
# 空 内核代码 内核数据 
# task0代码 task0数据 task0tss task1代码 task1数据 task1tss(采用ldt的形式)
    .word 0,0,0,0 #第一项空
    .quad 0x00c09a00000007ff #同boot中的内核代码段的设置一样
    .quad 0x00c09200000007ff #同boot中的内核数据段的设置一样
    .quad 0x00c0920b80000002 #显存段,往此段中写的数据会显示在屏幕上

#接下来设置task0的tss段描述符
    .word 0x0068,tss0,0xe900,0x0000
#设置task0的ldt段描述符
    .word 0x0040,ldt0,0xe200,0x0000

#接下来设置task1的tss段描述符
    .word 0x0068,tss1,0xe900,0x0000
#设置task1的ldt段描述符
    .word 0x0040,ldt1,0xe200,0x0000
#可以看到和task1和task0的tss段和ldt段的主要区别在于offset的不同

    new_gdt_table_end:
    .fill 128,4,0

#这里设置的是刚刚进入32位保护模式时系统使用的栈,由于栈是向下增长,
#esp:ss
    init_stack: 
    .long init_stack
    .word 0x10


    .align 8
#ldt for task0
    ldt0:
    .quad 0x0000000000000000
    .quad 0x00c0fa00000003ff
    .quad 0x00c0f200000003ff
#由于task0和task1是以硬编码的形式嵌入在kernel中,
#因此需要我们提前设置好task0和task1的tss段的内容
#tss段格式参见p127,按照严格的格式设置tss段的内容
    tss0:
    .long 0 #前一任务tss选择子,本例不需要
    .long task0_krn_stack,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(暂时搞不懂为什么eip这些也为空?返回到哪里执行呢?)
#####这里可以为空的原因是,在模拟从task0内核态返回到task0的用户态过程中,会使用内核态栈中的值恢复相关寄存器的值,
#####所以这里的eip,cs,eflags,ss,esp等这些字段可以被设置为空-20200729
#接下来是基本段寄存器
    .long 0,0,0,0,0,0               #es,cs,ss,ds,fs,gs    
    .long ldt0_selector,0x8000000  #ldt选择子,trace bitmap

#从tss0的结尾到task0_krn_stack中间的这部分为task0的内核态堆栈空间
    .fill 128,4,0
    task0_krn_stack:

.align 8
ldt1:
.quad 0x0000000000000000
.quad 0x00c0fa00000003ff
.quad 0x00c0f200000003ff

#参考tss0,设置tss1
tss1:
.long 0 #前一任务tss选择子,本例不需要
    .long task1_krn_stack,0x10 #内核态堆栈esp0 ss0,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 task1_usr_stack,0,0,0  #esp(用户态堆栈空间),ebp,esi,edi
#接下来是基本段寄存器
.long 0x17,0x0f,0x17,0x17,0x17,0x17    #es,cs,ss,ds,fs,gs (ldt中的段选择子,0x0f:代码段选择子  0x17:数据段选择子)    
.long ldt1_selector,0x8000000  #ldt选择子,trace bitmap

#从tss0的结尾到task0_krn_stack中间的这部分为task0的内核态堆栈空间
    .fill 128,4,0
    task1_krn_stack:


#######实现task0和task1的业务
###task0:调用中断号为0x80的中断服务,往屏幕写一个字符
    task0:
    mov $65,%al
    int $0x80
    movl $0xffffff,%ecx
##loop每循环一次会递减%ecx,b的意思是向前跳转
    1:loop 1b
    jmp task0

###task1:调用中断号为0x80的中断服务,往屏幕写一个字符
    task1:
    mov $66,%al
    int $0x80
    movl $0xffffff,%ecx
##loop每循环一次会递减%ecx,b的意思是向前跳转
    1:loop 1b
    jmp task1

    .fill 128,4,0
    task1_usr_stack:

 

总结

遇到的理解困难主要来自于:

1、对进程切换的具体细节,比如切换前后栈,tr、ldtr的变化缺乏准确的把握

2、对模拟成“当前处于task0的内核态”并“通过iret返回到task0的用户态”的原理和实现理解不足。

3、对汇编指令的生疏

4、纸上得来终觉浅

 

参考链接

[1] https://www.cnblogs.com/hongzg1982/articles/2117263.html    linux0.11 head.s中lss相关说明

[2]https://www.cnblogs.com/SuperBlee/p/4095124.html    [Operating System Labs] 我对Linux0.00中 head.s 的理解和注释

posted on 2020-07-30 11:14  老僧非是爱花红  阅读(399)  评论(0编辑  收藏  举报