<pre name="code" class="plain">! boot.s
!
! It then loads the system at 0x10000, using BIOS interrupts. Thereafter
! it disables all interrupts, changes to protected mode, and calls the
!计算机在启动的时候会把磁盘里的第一个扇区的内容存到内存中0x7c00这个位置上
!这个时候计算机用的是实地址模式BOOTSET=0x07c0的原因是因为它是段地址,要左
!左移一位就变成了0x7c00了
BOOTSEG = 0x07c0
!引导程序的目的是把head.s编译出来的代码(二进制文件)先放到0x10000处,
!这里用SYSSEG=0x1000是因为这属于段地址,需要在十六进制下左移一位再加上偏
!地址。
SYSSEG = 0x1000 ! system loaded at 0x10000
!内核所占用的扇区数,每个扇区是512Byte,这个不是真正内核的大小,而是内核
!大小的一个上限。SYSLEN = 17 ! sectors occupied.
entry start
start:
!跳到go的地方,并且将BOOTSEG作为段地址,go表示的是偏移地址,go表示
!的是go所在的指令地址到程序开始地址的距离,然后加上BOOTSEG这个段
!地址后才能找到真正的地址,起初的cs=0,eip=0x7c00,如果直接执行下
!去的话就变成cs=0,eip=0x7c05了,但是用了这句jmpi后就变成cs=0x07c0
!eip=0x5了,主要是为了初始化cs,eip这两个寄存器。
jmpi go,#BOOTSEG
!通过ax将数据段寄存器赋值,和cs一样,因为这段代码的指令和数据是混
!合在一起的。
go: mov ax,cs
mov ds,ax
!将堆栈段寄存器ss也设置的和cs一样,同时将堆栈偏移地址sp中设成0x400
!0x400转换成十进制为1024,也就是说在第1024字节处,这个地址要大于
!512很多,因为IA-32的架构下堆栈是倒着长的
mov ss,ax
mov sp,#0x400 ! arbitrary value >>512
! ok, we've written the message, now
load_system:
!load_system这段代码是为了加载head.s
!dx cx 是为了告诉系统一些拷贝代码的参数
!dh=0x00 磁头号 dl=0x00 驱动器号
!cl(7:6)=00B 磁道号高2位 ch=0x00 磁道号低8位 组合出来磁道号也是0
!cl(5:0)=00010B 表示起始扇区号,扇区号从1开始算,前面第一个扇区就
!是boot.s也就是这份代码,紧接着从第二个扇区开始的就是head.s了
mov dx,#0x0000
mov cx,#0x0002
!es:bx 也是一组段地址加偏移地址表示地址的组合表示写入的起始位置
!为了把代码先复制到0x10000处,因此通过ax把SYSSEG=0X1000作为段地址
!放到es中,然后xor bx,bx将bx清零,从而组合成0x10000的地址
mov ax,#SYSSEG
mov es,ax
xor bx,bx
!ax也是作为一个传递参数的寄存器需要设置的
!ah 表示后面int 0x13系统中断需要执行的操作是从磁盘复制代码到内存
!al则是需要写的字节数,这个只可大于head.s的长度,保证代码完整
mov ax,#0x200+SYSLEN
int 0x13
!如果拷贝完成的话就跳到ok_load段,否则此处陷入死循环
jnc ok_load
die: jmp die
! now we want to move to protected mode ...
!这段代码将会把代码从0x10000移到0x0处,之所以不能直接把代码移到0x0处
!是因为计算机启动的时候会把bios的代码存到开头,比如int 0x13的代码,如果
!在复制的过程中把代码覆盖过去的话系统就出错了,因此把它放到内存0x10000处
!后在用cpu的指令把代码挪到0x0
ok_load:
!关闭中断
cli ! no interrupts allowed !
!移动需要源地址ds:si 目的地址 es:di
!通过ax把head.s代码的段地址0x1000放到数据段ds中
!将0x0送到es中,最后将si和di都变成0
!和loop一样cx表示循环次数,每次都自减1,0x2000=8K次(1K=2^10)
mov ax, #SYSSEG
mov ds, ax
xor ax, ax
mov es, ax
mov cx, #0x2000
sub si,si
sub di,di
!rep表示重复下面的代码知道cx变成0
!movw 中mov表示他是一个移动指令,就是将ds:si 移到 es:di处
!w表示每次移动一个字,每复制一次si和di就自动+1
!但是为什么移动的代码段明显大于head.s
!我现在能给出的解释就是为了保险起见,多复制总比少复制好
!但是在从磁盘中选定的长度是17个扇区 也就是相当于8704bytes
!而这里移动的次数相当于16384bytes,这两个不相等也是醉了。
rep
movw
!将ds恢复过来
mov ax, #BOOTSEG
mov ds, ax
!lidt 将idt的基地址和限制长度载入到idtr中
!lgdt 将gdt的基地址和限制长度载入到gdtr中,这边可以先下去看
! idt_48 和gdt_48的内容
lidt idt_48 ! load idt with 0,0
lgdt gdt_48 ! load gdt with whatever appropriate
! absolute address 0x00000, in 32-bit protected mode.
!设置机器状态字,开启保护模式
mov ax,#0x0001 ! protected mode (PE) bit
lmsw ax ! This is it!
!保护模式开启后 jmpi 偏移地址,段地址就和前面的使用方法不一样
!这里的段地址不再左移变成基地址,而是要从gdt表中取出基地址
!而后再加上偏移地址0,执行后的结果就是eip变成0,cs变成用8
!从gdt中取出的段地址了,这里可以看gdt表
!gdt表每4个字作为一项段描述符,从0开始计,但第0项无用,上面8是选择
!符也就是1000B,是4个字节,共32位,低16位有用,15:3 是索引,表示
!gdt表中的第几项,这里可以看到cs(15:3)是1B,表示第一项
!第0位为1表示执行程序以外的事件造成了异常(不明白什么意思???)
!第1位为1表示这是指向idt的
!第2位为1表示这是指向ldt的
!可见这三位都是0
!因此8表示gdt表中的第一项
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
!gdt表中的每项是4个字即64位
!查询一下数据段描述符 代码段描述符 系统段描述符
!这里用第一项为例子,我们先从中间找到段限长,也就最低字0x07ff中的0x07ff
!然后我们再拼出基地址,倒数第二低字节0x0000就是基地址的低16位,第二高字节
!0x9A00的低8位0x0 是基地址的23:16位,而最高字的最高8位就是基地址的高8位,
!至此我们可以完整地拼出了基地址0x0。
!剩下的就是最高双字的中间16位了,这里我们写出来
!C 0 9 A
!1100 0000 1001 1010 用23到8表示 中间19到16表示段限长的19:16位,这里得
!到了完整的段限长0x007ff
gdt: .word 0,0,0,0 ! dummy
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0x00000
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0x00000
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386
!这里可以看到这是两个部分的数据,idt_48有6个字节,最低位字节是 idt表的长度
!注意这个代码在上面的表示低位地址,高位的两个字节是idt的基地址,这里没写idt
!因此都默认成0,idt会在head.s中写出来,接着说字节问题,在书上画idtr的时候是
!从左到右是基地址 限制长度,地址从左到右是从高到低,而这里的代码是从下到
!上是对应的地址从高到低,因此有点违背常理,需要注意。
!gdt_48 可以看到0x7c00+gdt是上面gdt表的位置
idt_48: .word 0 ! idt limit=0
.word 0,0 ! idt base=0L
gdt_48: .word 0x7ff ! gdt limit=2048, 256 GDT entries
.word 0x7c00+gdt,0 ! gdt base = 07xxx
.org 510
.word 0xAA55
</pre><pre name="code" class="plain">
<pre name="code" class="plain"># 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
.global startup_32
.text
startup_32:
!通过ax将选择符0x10放到ds中,初始化数据段,0x10的低3位是000因此是在gdt表中
!的10B项,也就是第二项,而gdt表就是刚才boot.s中设置的,和第一项唯一的区别
!就是其中的A和C,这其中蕴含着数据段和代码段的属性区别
movl $0x10,%eax
mov %ax,%ds
!设置堆栈地址,转去init_stack代码,这里还包括了ss=ds
lss init_stack,%esp
# setup base fields of descriptors.
!调用设置idt和gdt的过程,跳转
call setup_idt
call setup_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 $11930, %eax
movl $0x40, %edx
outb %al, %dx
movb %ah, %al
outb %al, %dx
!设置内核段选择符是0x8
movl $0x00080000, %eax
!设置调用的代码是timer_interrupt
movw $timer_interrupt, %ax
!设置一些属性
movw $0x8E00, %dx
!系统的定时中断号是8,因此要用新的中断来覆盖它,下面就是把上面设置的写进去
movl $0x08, %ecx
lea idt(,%ecx,8), %esi
movl %eax,(%esi)
movl %edx,4(%esi)
!再设置一个80中断来处理
!可见是调用system_interrupt来处理
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)
!将标志寄存器压栈,然后在栈中把nt标志复位,完了之后弹出恢复
pushfl
andl $0xffffbfff, (%esp)
popfl
!设置tr的选择符是0x20,tr寄存器中的值就是0x20
movl $TSS0_SEL, %eax
ltr %ax
!将ldt装载进寄存器
movl $LDT0_SEL, %eax
lldt %ax
!current表示当前的任务号是0
movl $0, current
!打开中断
sti
!通过压栈,回弹,实现跳转
!iret的回弹顺序是eip cs eflag esp ss,让初始化,然后指针指向了任务0
!0x17是一个选择符,二进制展开是00011111,可见是ldt中的第三项
pushl $0x17
pushl $init_stack
pushfl
pushl $0x0f
pushl $task0
iret
/****************************************/
setup_gdt:
lgdt lgdt_opcode
ret
!这里是把所有的idt都设成一样的,也就是当有中断来的时候就调用ignore_int过程
!0xb5
!这里终端门设置高双字是edx,低双字是eax
!movl表示每次移动双字
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
!将idt的表基地址和段限长放入idtr表中
lidt lidt_opcode
ret
# -----------------------------------
!
write_char:
!保存要用到的寄存器
push %gs
pushl %ebx
# pushl %eax
!让gs指向显示内存段
mov $SCRN_SEL, %ebx
mov %bx, %gs
!字符显示位置值
movl scr_loc, %ebx
!因为还有一位属性值,故位置乘2
shl $1, %ebx
!显示字符
movb %al, %gs:(%ebx)
!恢复位置,然后再加一,如果等于2000则清0
shr $1, %ebx
incl %ebx
cmpl $2000, %ebx
jb 1f
movl $0, %ebx
!保存位置
1: movl %ebx, scr_loc
!恢复寄存器
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:
!保存寄存器,包括ax中的字符ASCII码,为了下次切换的时候返回,同时在tss中
!保存的是对应ljmp对应的下一个指令的地址
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 %eax, current
ljmp $TSS1_SEL, $0
jmp 2f
!切换到0任务,然后切换tss,此时连堆栈也切换了,因此后面弹出的不是上面push
!的,而是前一次要切换到1任务是进入这个过程初始压入的ds和eax,同理切换到1任
!务
1: 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
!数据段指向gdt 10项中的内核段
movl $0x10, %edx
mov %dx, %ds
!调用写字符过程
call write_char
!恢复寄存器的值0
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
end_gdt:
!这里一定要注意堆栈增长方向,栈底地址是0xbd8,用0初始化堆栈
.fill 128,4,0
init_stack: # Will be used as user stack for task0.
.long init_stack
.word 0x10
/*************************************/
.align 8
!对应的段,段限长都是0x3ff,偏移地址都是0x0对应任务0
ldt0: .quad 0x0000000000000000
.quad 0x00c0fa00000003ff # 代码段0x0f, base = 0x00000
.quad 0x00c0f200000003ff # 数据段0x17
!注意两个tss初始化不一样,第一次切到任务一是是从task1开始运行的,后面就不
!是了,其中task1处都改变了,这里的系统栈也用的init_stack
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
!对应的段,段限长都是0x3ff,偏移地址都是0x0对应任务1
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:
!这里我们去看一下ldt表中的17项,设置ds段
movl $0x17, %eax
movw %ax, %ds
!65对应A的ascii码
movb $65, %al /* print 'A' */
!调用80中断,这边我们再看设置80终端的代码
int $0x80
!延迟作用
movl $0xfff, %ecx
1: loop 1b
!重复task0
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: