[Operating System Labs] 我对Linux0.00中 head.s 的理解和注释

?21,
#  head.s contains the 32-bit startup code.
#  head.s 是32位的启动代码

#  Two L3 task multitasking. The code of tasks are in kernel area,
# 有两个L3(Level 3,即第三特权级,IA32提供给用户0-3,四个特权级,但是Linux0.11只使用了
# 0和3这两个特权级,表示用户态程序和内核态程序,内核-kernel)的多任务

#  just like the Linux. The kernel code is located at 0x10000.
# ……内核代码在0x10000,在boot.s中已经确定了呦~

#大家可能发现boot.s和head.s的代码有一点点不一样,这是因为boot.s使用的是as86汇编器
#而head.s使用的是GNU as汇编器,具体表现在
#    1~movl等操作的出现:当然还是mov的意思,l表示双字,w表示单字,b表示字节
#    2~movl等操作的源操作数和目的操作数位置,源在前而目的在后,与boot.s中的mov操作是相反的
#    3~立即数前必须加$,寄存器前必须加%
#    4~待续

.code32
# CODE32伪指令通知编译器,其后的指令序列为 32 位的 ARM 指令
SCRN_SEL    = 0x18
TSS0_SEL    = 0x20
LDT0_SEL    = 0x28
TSS1_SEL    = 0X30
LDT1_SEL    = 0x38

.global startup_32
.text
#.text表示文本段,通常包含可执行代码
startup_32:
    movl $0x10,%eax
    mov %ax,%ds
    # 将0x0010赋给ds
#    mov %ax,%es
    # 将0x0010赋给es,可是为啥被注释掉了呢……
    lss init_stack,%esp
    # lss mem,reg( mem低字->reg,mem高字->ss),这句的意思就是指让ss:esp指向init_stack
    # 我们不妨去看看init_stack到底存了一些什么东西:
    #    .long init_stack 表示了init_stack这个数据的地址
    #    .word 0x10 给ss赋值了
# 那么现在问题来了0x10是什么,是段选择符:0x10000,右移三位后正好选定gdt中的第2项。内核数据段。
# setup base fields of descriptors.
# 装载基本的域和描述符
    call setup_idt
        # 跳转到setup_idt去执行,请直接去setup_idt那里看后续注释
        # 普及一下call 和 jmp 两条指令的差别吧,call相当于这三条指令的连续作用“push cs”,“push eip” 和“jmp XXX”
        # 所以由call跳转过去执行的代码最后一定要用ret的方式回来
    call setup_gdt
        # 好的,我们回到这里了!
        # idt设置完了,我们去设置gdt,显然我们也要类比一下idt的设置方法来讨论gdt的设置方法
        # 我们猜测gdt需要做这几件事:1~ 填满gdt,2~ 构建gdtr应该有的值,然后一个lgdt指令,完事儿!
        # 好,我们进入setup_idt看看
    movl $0x10,%eax        # reload all the segment registers
        # 在改变gdt后,重新设置全部的段寄存器
    mov %ax,%ds        # after changing gdt.
    mov %ax,%es
    mov %ax,%fs
    mov %ax,%gs
    lss init_stack,%esp   
        # 把初始栈地址放给 ss:esp

# setup up timer 8253 chip.
    # 设置8253芯片,这个芯片具体的工作原理请看这里:http://baike.baidu.com/view/1684875.htm
    # 这个芯片就像我们小学期设计的CPU里的那个节拍发生器
    # 这一段的作用是让8253芯片每隔10ms就向CPU发送一个时钟中断请求
    movb $0x36, %al
    movl $0x43, %edx
    outb %al, %dx
    # 向%dx所示端口输出一个字节(b),值为%al中数据
    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
        # 这两条指令就把 %eax设置成了 0x0008~timer_interrupt(请把“~”看成是地址的链接符……)的地址,
        # 还是代码段中偏移为timer_interrupt的地方
        # 但是究竟0x0008和0x0010有什么区别呢?
        # 显然我们知道0x0008和0x0010分别是代码段和数据段的段选择符,可是代码段和数据段分别在哪里?
        # 去gdt看看,跳到gdt去~
        # ok可以回来了!现在我们继续,去看看timer_interrupt是干什么的?
    movw $0x8E00, %dx
        # 将%dx置为0x8E00
    movl $0x08, %ecx              # The PC default timer int.
        # 将%ecx置为0x08,Linus注释说是PC的默认时钟中断
        # 还不清楚是啥意思……等一会再看……
    lea idt(,%ecx,8), %esi
        # 这条指令是什么意思呢,是将%ecx*8+idt的地址放入%esi中去
    movl %eax,(%esi)
        # 再把%eax的值放入%esi所指的内存区域
        # %eax里面是什么呢?是0x0008~timer_interrupt的地址
    movl %edx,4(%esi)
        # 把%edx放在这个中断描述符的高八位
    movw $system_interrupt, %ax
        # 设置完了时钟中断之后,我们再去设置系统中断
    movw $0xef00, %dx
    movl $0x80, %ecx
        # 0x80是什么!?系统调用的中断号……
    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
        # pushfl是push flags long的简写,将标志寄存器压栈,双字四字节
    andl $0xffffbfff, (%esp)
        # 1111111111111111 1011 1111 1111 1111
        # andl指令专门用来清零特定的位
        # 这里它的用处我猜不透啊……在完全剖析那本书上也没写……
    popfl
        # 然后又pop了标志位,难道andl操作会改变标志位?
    movl $TSS0_SEL, %eax
        # $TSS0_SEL是在一开始就设定好的,它的值是0x20
    ltr %ax
        # ltr--load task register,装载任务寄存器
        # 那么现在是装载了任务0的,段选择符0x20,右移三位变成二进制的100,表示gdt表的第4项
    movl $LDT0_SEL, %eax
        # 想执行任务0,除了要设置tr以外还要设置ldt
    lldt %ax
        # 好的,这里就设置完成了。
    movl $0, current
        # 把一个叫做current的变量设置成0
    sti
        # 在head.s中关闭的中断终于可以打开了
        # 现在栈里有什么?什么都没有。
    pushl $0x17
    pushl $init_stack
    pushfl
    pushl $0x0f
    pushl $task0
        # 好,这里我们在栈中push了很多东西,分别来看一看
        # 栈顶项 $task0的地址
        # 第二项 0x0f(这是什么?)
        # 第三项 全部标志寄存器
        # 第四项 初始栈的地址
        # 第五项 0x17(这又是什么?)
    iret
        # 借鉴一个网上的博客对这个指令的解释,他先引用了IA32手册上对IRET指令的解释:
            # the IRET instruction pops the return instruction pointer, return code segment selector,
            # 译:IRET指令一一对应地弹出IP指令指针和CS代码段选择符
            # and EFLAGS image from the stack to the EIP, CS, and EFLAGS registers, respectively,
            # 译:以及EFLAGS的值到EIP,CS和EFLAGS寄存器中
            # and then resumes execution of the interrupted program or procedure.
            # 译:然后继续执行中断的程序
            # If the return is to another privilege level, the IRET instruction also pops the stack pointer and SS from the stack,
            # 译:如果返回到另一个特权级,那么这个指令再继续执行前还要弹出栈指针和SS寄存器
            # before resuming program execution.
        # 显然,这里要转换特权级,就要弹出五项,一一对应
        # EIP -> $task0的地址
        # CS -> 0x0f (00001111),特权级3的ldt的第一项,任务0
        # EFLAGS -> EFLAGS 这个不用变
        # ESP -> $init_stack的地址
        # SS -> 0x17 (00010111),特权级3的ldt的第二项,数据段,也做堆栈段的选择符
        # 到这里,就跳到任务0去执行了……我们直接去任务0!
/****************************************/
setup_gdt:
    # 惊呆了有木有,好短啊……然后我们看看这个lgdt_opcode可能会提供给我们什么信息!
    # 我们刚刚的猜测第二步已经有了,lgdt指令就在这里。我们还是先猜测lgdt_opcode里有啥?
    # 无非就是长度,基址呗~ 去看看。
    lgdt lgdt_opcode
    ret

setup_idt:
    lea ignore_int,%edx
        # lea:load effective address,将变量的地址从内存中取出并放入寄存器
    movl $0x00080000,%eax
    movw %dx,%ax        /* selector = 0x0008 = cs */
        #将%eax设置为0x0008-ignore_int地址
    movw $0x8E00,%dx    /* interrupt gate - dpl=0, present */
        #中断门类型,dpl设置为0
    lea idt,%edi
        # 将idt的地址存入%edi中去
    mov $256,%ecx
        # %ecx通常在程序中充当循环语句执行次数计数器的角色,
        # 比如 loop指令和rep指令每次执行时都要%ecx减一,%ecx为零时停止循环
        # 所以当遇到%ecx时,可以考虑它是不是又来当循环计数器了,结果在dec那一句可以看到
rp_sidt:
    movl %eax,(%edi)
        # 将%edi地址中的数据改为%eax中的数据,就是这个ignore_int代码标记的地址,
        # 也就是每一个idt项的低四位
    movl %edx,4(%edi)
        # 首先说一下4(%edi)的意思是[%edi+4]这个内存地址里的数据,
        # 其次,这表示与上面同一个idt描述项的高四位。
        # 这一通循环的目的是把256个idt描述项都设置成一样的,即都是由ignore_int来处理。
        # 剧透,怎么处理?看名字可以知道:ignore_int——通过ignore的方式来处理……
    addl $8,%edi
        # 然后%edi加8,进入下一个idt描述符表去修改
    dec %ecx
        # 每执行一次都要修改%ecx,dec的意思是%ecx减一
    jne rp_sidt
        # %ecx 若不为零,则跳回rp_sidt继续执行,rp_sidt(repeat setup idt)
    lidt lidt_opcode
        # 当256项都执行完了,所有中断处理程序全部指向ignore_int了之后,高高兴兴地把lidt_opcode置成idtr
        # lidt_opcode把长度啊,基地址啥的都设好了
    ret
        # 执行完了这一通,可以回去了到call setup_idt这条之后的那一条去执行了
        # 那么回去之前,我们考虑一下究竟这个setup_idt都做了些什么呢?
        # 1~ 256个entries的表统统都填写上了这个ignore_int的处理函数
        # 2~ 把lidt_opcode置给了idtr,然后高高兴兴地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
        # 将%ds设置为0x10
    movb $0x20, %al
        # 将%al设置为0x20
    outb %al, $0x20
    movl $1, %eax
        # 将%eax置为1
    cmpl %eax, current
        # 将current与%eax进行比较,如果相等则跳转到1去执行
    je 1f
    movl %eax, current
        # 若不相等,证明目前current为0,那么就去跳转到TSS0那里去执行
    ljmp $TSS1_SEL, $0
    jmp 2f
1:    movl $0, current
        #在这里将current设置为0
    ljmp $TSS0_SEL, $0
        # ljmp的意思是跳转到段选择子,段内偏移
        # jmpi的意思是跳转到段内偏移,段选择子
        # 这里的意思是跳转到TSS0那里去执行
2:    popl %eax
    pop %ds
        # 最后无论如何都会pop,完事儿~
    iret
        # 这里才是真的完事儿了~
        # 用一个最可爱的IRET指令标志注释的结束,再见观众朋友们~

/* system call handler */
.align 2
system_interrupt:
# 系统中断
    push %ds
    pushl %edx
    pushl %ecx
    pushl %ebx
    pushl %eax
        # 保存寄存器
    movl $0x10, %edx
    mov %dx, %ds
        # 将%ds置0x10
    call write_char
        # 调用这个write_char函数,我们不去分析write_char是干啥的了,
        # 根据名称可知,是写一个字符
    popl %eax
    popl %ebx
    popl %ecx
    popl %edx
    pop %ds
        # 弹出各个寄存器
    iret
        # 中断返回
# 可见,这个系统中断做的事情就是把当前任务对应的字符打印出来,
# task0打印“A”,task1打印“B”
# 但是一开始我们看到的是task0是一个死循环,task1还没有执行过?
# 似乎……只有时钟中断可以让task1得到执行了!

/*********************************************/
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:
    # 长度16位,在这里,end_gdt标签位置减去gdt标签位置再加1。
    .word (end_gdt-gdt)-1    # so does gdt
    # 基址32位,gdt的基址就叫gdt。好,我们去看看写了什么……
    .long gdt        # This will be rewrite by code.

.align 8
    # 这个指令我们汇编课上学过,忘记的去百度一下哦~
idt:    .fill 256,8,0        # idt is uninitialized
# fill伪指令:.fill repeat,size,value 本命令生成size个字节的repeat个副本。
# 各个副本中的内容取自一个8字节长的数。最高4个字节为零,最低的4个字节是value,
# 那么这个指令的意思就是复制256个8字节大小的量,每项都填充成为0
# 看完后面那通代码可知,这里的fill指令纯粹就是为了占位置的,嗯对一定是这样。

gdt:    .quad 0x0000000000000000    /* NULL descriptor */
        # 出现了一个我们没有见过的伪指令,但是它的意思跟 .word是一类的,表示定义一个多长的空间
        # quad是多长呢?联想“quadra kill”……肯定跟四什么什么有关,
        # 再看后面串的长度,可见这个四是四个字的意思。.quad就是规定了后面的八个字节(四个字)的数据的伪指令
    .quad 0x00c09a00000007ff    /* 8Mb 0x08, base = 0x00000 */
        # 这个0x00c09a00000007ff是怎么组成的呢?
        # 显然从左到右是从高到低,我们按照每二位16进制数一拆分
        # 0x  00               c09a            00
        #     基地址的31-24位  一些状态标识位  基地址的23-16位
        #     0000               07ff
        #     基地址的15-0位   段限长,2047个字节
        # 可见这个基地址是0x00000000,与下面这个表项所示一样,说明代码段和数据段是重叠的~
    .quad 0x00c09200000007ff    /* 8Mb 0x10 */
    .quad 0x00c0920b80000002    /* screen 0x18 - for display */
        # 上面一共有四项,gdt第一项还是照例空缺
        # 第四项是显示内存段的描述符,段选择符是0x18……怎么算的就不讲了~
    .word 0x0068, tss0, 0xe900, 0x0    # TSS0 descr 0x20
        # 任务0的TSS段,段选择符0x20
    .word 0x0040, ldt0, 0xe200, 0x0    # LDT0 descr 0x28
        # 任务0的LDT段,段选择符0x28
    .word 0x0068, tss1, 0xe900, 0x0    # TSS1 descr 0x30
        # 任务1的TSS段,段选择符0x30
    .word 0x0040, ldt1, 0xe200, 0x0    # LDT1 descr 0x38
        # 任务1的LDT段,段选择符0x38
end_gdt:
    .fill 128,4,0
        # 哦最后这个是做啥的?
        # 猜~
    # 好的看到这里我们可以回去了,回到call setup_gdt那里!
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 */
    .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:

/************************************/
task0:
    movl $0x17, %eax
    movw %ax, %ds
        # %ds置为0x17,
    movb $65, %al              /* print 'A' */
        # %al置为ASCII码的65号,“A”
    int $0x80
        # 调用int $0x80,这个中断在之前就已经写好了,
        # 我们可以去看看system_interrupt,这个就是0x80中断的处理程序,
        # 为什么?在上面写了,请好好的找一找~
        # 然后我们就去这个处理程序看看去,我们目前知道的是,ds为0x17,al为65
        #???
    movl $0xfff, %ecx
        # 将%ecx改为0xfff,意思是一直执行这个task0,因为loop没执行一次会减ecx的值
1:    loop 1b
        # b和f的意思分别是向前跳转和向后跳转的意思
    jmp task0
# 先在我们只看到task0的工作,那么task1何时工作呢?
# 在前面我们记得时钟中断在每10ms时8253芯片会发送一个时钟中断,
# 我们还没有分析这个时钟中断是做什么的。
task1:
    movl $0x17, %eax
    movw %ax, %ds
    movb $66, %al              /* print 'B' */
    int $0x80
    movl $0xfff, %ecx
1:    loop 1b
    jmp task1
    # 不解释……跟task0差不多……
    .fill 128,4,0
usr_stk1:

上一篇:linux vim 配置文件(高亮+自动缩进+行号+折叠+优化)


下一篇:DOCKER 学习笔记4 认识DockerCompose 多容器编排