Linux-0.00 代码解析

Posted 车子 chezi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux-0.00 代码解析相关的知识,希望对你有一定的参考价值。

6. 安装中断门和陷阱门

# 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)

中断门的格式是:

代码中edx是高32位,eax是低32位

movl $0x00080000, %eax 代码段的选择子就绪
movw $timer_interrupt, %ax 偏移15..0就绪
movw $0x8E00, %dx edx的低16位就绪

我的疑问是:edx的高16位呢?

movl $0x08, %ecx
从表格中可以看出Bios把8253的中断号设置为8.

lea idt(,%ecx,8), %esi
esi = idt + ecx * 8,计算出第8个中断门的位置,乘以8是因为一个中断门描述符占8字节。

movl %eax,(%esi) 
movl %edx,4(%esi)    # 安装中断门

此中断门用于切换任务,每隔10ms切换一次。

中断门timer_interrupt的代码是:

timer_interrupt:
    push %ds
    pushl %eax
    movl $0x10, %eax # 0x10是内核数据段的选择子
    mov %ax, %ds
    movb $0x20, %al
    outb %al, $0x20  # 向8259发送中断结束(EOI)命令,端口是0x20, 命令字是0x20,不用深究
    movl $1, %eax    # eax=1
    cmpl %eax, current
    je 1f            #相等跳转到1处
    movl %eax, current # current = 1
    ljmp $TSS1_SEL, $0 #切换到任务1
    jmp 2f
1:  movl $0, current   #切换到任务0
    ljmp $TSS0_SEL, $0
2:  popl %eax
    pop %ds
    iret

ljmp $TSS1_SEL, $0
当处理器执行这条指令时,首先用指令中给出的段选择子访问GDT或LDT(这里是GDT),分析它的描述符类型,这里发现是TSS描述符,于是执行任务切换,指令中的偏移量(这里是0)被忽略。

下面是安装陷阱门。
陷阱门的格式是:

movw $system_interrupt, %ax eax的低16位就绪,高16位在上面已经设置成了代码段的选择子
movw $0xef00, %dx DPL=3 的陷阱门

疑问:edx的高16位呢?
movl $0x80, %ecx 系统调用向量号是0x80,这个0x80应该是作者指定的

    lea idt(,%ecx,8), %esi #这三句同上,不赘述 
    movl %eax,(%esi) 
    movl %edx,4(%esi)

这个陷阱门其实是一个系统调用,用AL传参,把AL代表的字符打印到屏幕上。其内部调用了内核过程write_char

system_interrupt:       # 0x80系统调用,把AL中的字符打印到屏幕上
    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

7. 开始执行任务0

# 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

上面这段代码要想说清楚,就说来话长了。

    pushfl #把EFLAGS入栈
    andl $0xffffbfff, (%esp) #设NT=0
    popfl  #加载EFLAGS

上面三行是为了使EFLAGSNT位=0;为什么要这样做呢?因为后面要用iret指令返回,当返回的时候,如果NT=1,表示返回到前一个任务,而这种情况不是我们想要的。

    movl $TSS0_SEL, %eax #TSS0_SEL是任务0的TSS选择子
    ltr %ax

以上两行是把任务0的TSS选择子装入任务寄存器TR;
LTR指令的格式是

ltr r/m16

操作数是16位的通用寄存器或者是指向16位单元的内存地址。TSS选择子是16位的,所以没有用eax,而用ax; LLDT指令用法类似。

    movl $LDT0_SEL, %eax
    lldt %ax            #把任务0的LDT段选择子装入LDTR(局部描述符表寄存器)
    movl $0, current    #表示当前运行的是任务0
    sti                 #开中断

7.1 中断处理过程

咱们先复习一下异常或中断的处理过程。
当目标代码段描述符的DPL(可以用门描述符中的段选择子,从GDT或LDT中找到)在数值上<=CPL(当前特权级)时,才允许将控制转移到中断或异常处理程序。

如果 DPL < CPL,将发生栈切换。栈切换的过程:
(1)根据DPL,从当前任务的TSS中取得对应的SS和ESP,作为中断或异常处理过程使用的SS和ESP(新栈)。
(2)处理器把旧栈的选择子和栈指针压入新栈。
(3)把EFLAGS、CS、EIP压入新栈。
(4)对于有错误码的异常,还要把错误代码压入新栈(我们模拟返回的时候没有错误码)。

有了上面的铺垫,我们可以继续看代码了。

    pushl $0x17         #把任务0的局部空间数据段(也是栈段)选择子入栈
    pushl $init_stack   #把任务0的ESP入栈
    pushfl              #把EFLAGS入栈
    pushl $0x0f         #把任务0的代码段选择子入栈
    pushl $task0        #把任务0的EIP入栈
    iret                #环境已经设置完毕,这里模拟从中断返回,返回后开始执行任务0,其特权级为3

7.2 中断返回过程

先不管后面的操作数是怎么来的,总之压栈该压什么,顺序是什么我们理解了。接下来看看IRET指令。这个指令的含义,我首先看了Intel指令集手册,感觉说得很复杂,某些地方还有歧义。我不甘心,又搜了很多网上的资料,没有一个令我满意。最后,我打算用AMD的解释,因为很简明:

IRET, Less Privilege. If an IRET changes privilege levels, the return program must be at a lower privilege than the interrupt handler. The IRET in this case causes a stack switch to occur:

  1. The return pointer is popped off of the stack, loading both the CS register and EIP register with the saved values. The return code-segment RPL is read by the processor from the CS value stored on the stack to determine that a lower-privilege control transfer is occurring.
  2. The saved EFLAGS image is popped off of the stack and loaded into the EFLAGS register.
  3. The return-program stack pointer is popped off of the stack, loading both the SS register and ESP register with the saved values.
  4. Control is transferred to the return program at the target CS:EIP.

说到这里,文件head.s的主体就完了。

8. 任务0的LDT

ldt0:   
    .quad 0x0000000000000000
    .quad 0x00c0fa00000003ff    # 0x0f
    .quad 0x00c0f200000003ff    # 0x17

任务0的局部描述符表分析如下:

索引号描述符类型基地址段界限粒度PDPL备注选择子
0空描述符-------
1代码段00X3FF4KB13代码段,非一致性,可读0x0F
2数据段00X3FF4KB13数据段,向上扩展,可写0x17

为了少耗费点脑细胞,我写了一个C语言的小程序,专门用来分析各种类型的段描述符,拿走不谢。
http://blog.csdn.net/longintchar/article/details/78881396
运行截图如下:

9. 任务0的TSS

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:

以上填0的字段就不分析了,我们重点看看非0字段。
krn_stk0是ESP0,就在代码的最后一行。
0x10是内核数据段的选择子。
LDT0_SEL是任务0的LDT的选择子,数值上 = 0x28,在GDT中有定义。

复习一下,GDT中的描述符。

索引号选择子描述符类型基地址段界限粒度PDPL备注
10x08代码段00X7FF4KB10内核代码段,非一致性,可读
20x10数据段00X7FF4KB10内核数据段,向上扩展,可写
30x18数据段0XB80000X24KB10内核显存段,向上扩展,可写
40x20TSS段tss00X681B13任务0的TSS段,不忙
50x28LDT段ldt00X401B13任务0的LDT描述符
60x30TSS段tss10X681B13任务1的TSS段,不忙
70x38LDT段ldt10X401B13任务1的LDT描述符

0x8000000是I/O许可位串。如果这个值大于或者等于TSS的段界限(在TSS描述符中),则表明没有I/O许可位串。因为TSS的段界限是0x68,所以表示没有许可位串。

10. 任务1的TSS

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:

和任务0相比,任务1的TSS有几点不同。
(1). eipeflags不是0,而是task1和0x200,task1是任务1代码的入口点,当任务0首次被时钟中断打断时,将会切换到任务1,这时CPU把tss1作为任务1的EIP初始值。同理,0x200作为任务1的eflags初始值。为什么eflags初始值是0x200呢?根据下图,可以知道eflags中IF为1,表示允许中断。必须要允许中断,因为任务切换靠中断实现。

【System Flags in the EFLAGS Register】

(2). 任务0的esp=0,ss=0,任务1的esp=usr_stk1,ss=0x17。这是因为TSS中的esp和ss对应的特权级别是3,任务0的esp、ss的初始值通过iret指令从栈中获取,而任务1的要从TSS中获取。
(3). 任务0的es,cs,ds,fs,gs都为0,任务1的es=ds=fs=gs=0x17,cs=0x0f,道理同上,任务0的cs初始值从栈中获得,任务1的所有段寄存器的初始值都从TSS中获得。

下图是32位任务状态段(TSS)的格式,贴出来方便复习。

11. 任务0的代码段

task0:
    movl $0x17, %eax #0x17是任务0的数据段的选择子
    movw %ax, %ds    #因为任务0没有用到局部数据段,所以这两句可以不要
    movb $65, %al    # print 'A' 
    int $0x80        # 系统调用
    movl $0xfff, %ecx
1:  loop 1b          # 为了延时
    jmp task0        # 死循环

任务1的代码段类似,不再赘述。

用了3篇博文,基本把代码说完了。下篇文章打算修改几个地方并做实验。

【未完待续】

以上是关于Linux-0.00 代码解析的主要内容,如果未能解决你的问题,请参考以下文章

Ubuntu编译运行 Linux-0.00

全新Java入门到架构师课程之Java15编程基础-数组:数组声明初始化数组元素的界限和遍历

确定整数溢出是不是超过或低于界限

指令不解析初始值

C语言 "limits.h" 数据类型的界限值

Java虚拟机类加载机制--概述