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
上面三行是为了使EFLAGS
的NT
位=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:
- 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.
- The saved EFLAGS image is popped off of the stack and loaded into the EFLAGS register.
- The return-program stack pointer is popped off of the stack, loading both the SS register and ESP register with the saved values.
- 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的局部描述符表分析如下:
索引号 | 描述符类型 | 基地址 | 段界限 | 粒度 | P | DPL | 备注 | 选择子 |
---|---|---|---|---|---|---|---|---|
0 | 空描述符 | - | - | - | - | - | - | - |
1 | 代码段 | 0 | 0X3FF | 4KB | 1 | 3 | 代码段,非一致性,可读 | 0x0F |
2 | 数据段 | 0 | 0X3FF | 4KB | 1 | 3 | 数据段,向上扩展,可写 | 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中的描述符。
索引号 | 选择子 | 描述符类型 | 基地址 | 段界限 | 粒度 | P | DPL | 备注 |
---|---|---|---|---|---|---|---|---|
1 | 0x08 | 代码段 | 0 | 0X7FF | 4KB | 1 | 0 | 内核代码段,非一致性,可读 |
2 | 0x10 | 数据段 | 0 | 0X7FF | 4KB | 1 | 0 | 内核数据段,向上扩展,可写 |
3 | 0x18 | 数据段 | 0XB8000 | 0X2 | 4KB | 1 | 0 | 内核显存段,向上扩展,可写 |
4 | 0x20 | TSS段 | tss0 | 0X68 | 1B | 1 | 3 | 任务0的TSS段,不忙 |
5 | 0x28 | LDT段 | ldt0 | 0X40 | 1B | 1 | 3 | 任务0的LDT描述符 |
6 | 0x30 | TSS段 | tss1 | 0X68 | 1B | 1 | 3 | 任务1的TSS段,不忙 |
7 | 0x38 | LDT段 | ldt1 | 0X40 | 1B | 1 | 3 | 任务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). eip
和eflags
不是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 代码解析的主要内容,如果未能解决你的问题,请参考以下文章