;2013.10.10.
;文件名: boot.asm
;这是引导扇区代码,Linus也是使用Intel的汇编语法写的,这里使用了NASM的编译器,所以遵循NASM的语法,
; 而且也努力把后面的 HEAD.ASM 代码也写成NASM的,而不象Linus那样用难懂的AT&T语法!
; 重写这些代码仅仅是为了学习和自己动手调试,希望这里是个好开头!:)
BOOTSEG equ 07c0H ;引导扇区(本程序)被BIOS加载到内存0x7c00处。
SYSSEG equ 01000H ;内核(head)先加载到0x10000处,然后移动到0x0处。
SYSLEN equ 17 ;内核占用的最大磁盘扇区数。(不明白这个大小是怎么确定的)
start:
jmp BOOTSEG:go ;段间跳转至0x7c0:go处。当本程序刚运行时所有段寄存器值
;均为0。该跳转语句会把CS寄存器加载为0x7c0(原为0)。
go:
;让DS、ES和SS都指向0x7c0段。
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov sp,0x400 ;设置临时栈指针。其值需大于程序末端并有一定空间即可。
;加载内核代码到内存0x10000开始处。
;本程序是假设引导代码和程序都放在软盘上
;读取软盘的第2个扇区开始的[SYSLEN]个扇区的数据,读到[SYSSEG]:0处,第1个扇区是引导扇区!
load_system:
mov dx,0x80 ;利用BIOS中断int 0x13功能2从启动盘读取head代码。
mov cx,2 ;DH - 磁头号;DL - 驱动器号;CH - 10位磁道号低8位;
;CL - 位7、6是磁道号高2位,位5~0起始扇区号(从1计)。
mov ax,SYSSEG
mov es,ax
mov bx,0 ;ES:BX - 读入缓冲区位置(0x1000:0x0000)。
mov ax,0x200+SYSLEN ;AH - 读扇区功能号;AL - 需读的扇区数(17)。
int 0x13
jnc ok_load ;若没有发生错误则跳转继续运行,否则死循环。
die: jmp die
;把内核代码移动到内存0开始处。共移动8KB(内核长度不超过8KB)。
ok_load:
cli ;关中断
mov ax,SYSSEG ;移动开始位置DS:SI = 0x1000:0;目的位置ES:DI=0:0。
mov ds,ax
xor ax,ax
mov es,ax
mov cx,0x1000 ;设置共移动4K次,每次移动一个字(word)。
sub si,si
sub di,di
rep movsw ;执行重复移动指令。
;加载IDT和GDT基地址寄存器IDTR和GDTR。
mov ax,BOOTSEG
mov ds,ax ;让DS重新指向0x7c0段。
lidt [idt_48] ;加载IDTR。6字节操作数:2字节表长度,4字节线性基地址
lgdt [gdt_48] ;加载GDTR。6字节操作数:2字节表长度,4字节线性基地址。
;设置控制寄存器CR0(即机器状态字),进入保护模式。段选择符值8对应GDT表中第2个段描述符。
mov ax,0x0001 ;在CR0中设置保护模式标志PE(位0)。
lmsw ax
jmp 8:0 ;然后跳转至段选择符值指定的段中,偏移0处。
;注意此时段值已是段选择符。该段的线性基地址是0。
;下面是全局描述符表GDT的内容。其中包含3个段描述符。第1个不用,另2个是代码和数据段描述符。
gdt:
;每个描述符占8个字节,也就是4个字
dw 0,0,0,0 ;段描述符0,不用。每个描述符项占8字节。
;第二个描述符
dw 0x07FF ;段限长值=2047 (2048*4096=8MB)。
dw 0x0000 ;段基地址=0x00000。
dw 0x9A00 ;是代码段,可读/执行。
dw 0x00C0 ;段属性颗粒度=4KB,80386。
;第三个描述符,与代码段重合,这就是传说中的“别名”技术
dw 0x07FF ;段限长值=2047 (2048*4096=8MB)。
dw 0x0000 ;段基地址=0x00000。
dw 0x9200 ;是数据段,可读写。
dw 0x00C0 ;段属性颗粒度=4KB,80386
;下面分别是LIDT和LGDT指令的6字节操作数。
idt_48:
;将要赋值给IDT寄存器的6个字节,指明了中断描述符表的基址和限长
dw 0x0000 ;IDT表长度是0。
dw 0x0000,0x0000 ;IDT表的线性基地址也是0。
;在这里这个中断描述符表指向0x0000:0000处,长度也为0,不要奇怪,没有初始化而已,占个位置先!:)
gdt_48:
;将要赋值给GDT寄存器的6个字节,指明了全局描述符表的基址和限长
dw 0x07FF ;GDT表长度是2KB,可容纳256个描述符项。
dw 0x7C00+gdt,0x0000 ;GDT表的线性基地址在0x7c0段的偏移gdt处。
times 510-($-$$) db 0 ;填充不用的空间为0,凑够510个字节
dw 0xAA55 ;引导扇区有效标志。必须处于引导扇区最后2字节处。
;2013.10.10
;文件名:head.asm
;说明:实现多任务内核程序
;这是被引导扇区代码读取到物理地址0x10000H处然后又移动到物理地址0x00000的代码,他被编译后写在第2个扇区开始的地方,
;在这里写代码就不用顾忌512字节的限制了,随便写几个扇区,嘿嘿!
;从这时候开始已经是在保护模式下了。
;Linus从这里开始就使用了AT&T的汇编语法,我就比照着还是用NASM的语法实现吧!
;在进入保护模式后,head.s程序重新建立和设置IDT、GDT表的主要原因是为了让程序在结构上比较清晰,也为了与
;后面Linux 0.12内核源代码中这两个表的设置方式保持一致。当然,就本程序来说我们完全可以直接使用boot.asm中
;设置的IDT和GDT表位置,填入适当的描述符项即可。
;head.asm 包含32位保护模式初始化设置代码、时钟中断代码、系统调用中断代码和两个任务的代码。
;在初始化完成之后程序移动到任务0开始执行,并在时钟中断控制下进行任务0和1之间的切换操作。
;=======================================================================================
LATCH equ 11930 ;定时器初始计数值,即每隔10ms发送一次中断请求。
SCRN_SEL equ 0x18 ;屏幕显示内存段选择符。
TSS0_SEL equ 0x20 ;任务0的TSS段选择符。
LDT0_SEL equ 0x28 ;任务0的LDT段选择符。
TSS1_SEL equ 0x30 ;任务1的TSS段选择符。
LDT1_SEL equ 0x38 ;任务1的LDT段选择符。
;全局描述符表在引导扇区的实现中只有三项,第一项当然是空选择符,第二项是基址为0x00000、长度为8M的代码段,
;第三项是基址为0x00000、长度为8M的数据段,也就是别名技术的代码段了!
;而在这里的代码中重新设置了全局描述符表,里面留了两个TSS和LDT的位置,
;现在这里的TSS0_SEL就将要指向新的全局描述符表中的偏移位置为0x20也就是第4个描述符!
;同样地,LDT0_SEL指向了第5个描述符,随后就是TSS1和LDT1了
[BITS 32]
;=======================================================================================
startup_32:
;首先加载数据段寄存器DS、堆栈段寄存器SS和堆栈指针ESP。所有段的线性基地址都是0。
mov eax,0x10
mov ds,ax
lss esp,[init_stack]
;在新的位置重新设置IDT和GDT表。
call setup_idt
call setup_gdt
;在改变了GDT之后重新加载所有段寄存器。
mov eax,0x10
mov ds,eax
mov es,eax
mov fs,eax
mov gs,eax
lss esp,[init_stack]
;设置8253定时芯片。把计数器通道0设置成每隔10ms向中断控制器发送一个中断请求信号。
mov al,0x36 ;控制字:设置通道0工作在方式3、计数初值采用二进制。
mov edx,0x43 ;8253芯片控制字寄存器写端口
out dx,al
mov eax,LATCH ;初始计数值设置为LATCH(1193180/100),即频率100Hz。
mov edx,0x40 ;通道0的端口
out dx,al ;分两次把初始计数值写入通道0
mov al,ah
out dx,al
;在IDT表第8和第128(0x80)项处分别设置定时中断门描述符和系统调用陷阱门描述符。
mov eax,0x00080000 ;中断程序属内核,即EAX高字是内核代码段选择符
mov ax,timer_interrupt ;设置定时中断门描述符。取定时中断处理程序地址。
mov dx,0x8e00 ;中断门类型是14(屏蔽中断),特权级0或硬件使用。
mov ecx,0x08 ;开机时BIOS设置的时钟中断向量号8.这里直接使用它。
lea esi,[idt+ecx*8]
mov [esi],eax
mov [esi+4],edx
mov ax,system_interrupt ;设置系统调用陷阱门描述符。取系统调用处理程序地址。
mov dx,0xef00 ;陷阱门类型是15,特权级3的程序可执行
mov ecx,0x80 ;系统调用向量号是0x80
lea esi,[idt+ecx*8] ;把IDT描述符项0x80地址放入esi中,然后设置该描述符
mov [esi],eax
mov [esi+4],edx
push edx
push ds
push eax
mov edx,0x10 ;首先让DS指向内核数据段
mov ds,dx
mov eax,'W'
call write_char ;然后调用显示字符子程序write_char,显示AL中的字符。
pop eax
pop ds
pop edx
;好了,现在我们为移动到任务0(任务A)中执行来操作堆栈内容,在堆栈中人工建立中断返回时的场景。
pushf ;复位标志寄存器EFLAGS中的嵌套任务标志
and dword [esp],0xffffbfff
popf
mov eax,TSS0_SEL ;把任务0的TSS段选择符加载到任务寄存器TR
ltr ax
mov eax,LDT0_SEL ;把任务0的LDT段选择符加载到局部描述符表寄存器LDTR
lldt ax ;TR和LDTR只需人工加载一次,以后CPU会自动处理。
mov dword [current],0 ;把当前任务号0保存在current变量中
sti ;现在开启中断,并在栈中营造中断返回时的场景。
;假装是从中断程序返回,从而实现从特权级0的内核代码切换到特权级3的用户代码中去
push 0x17 ;把任务0当前局部空间数据段(堆栈段)选择符入栈。
push krn_stk0 ;把堆栈指针入栈(也可以直接把esp入栈)。
pushf ;把标志寄存器入栈。
push 0x0f ;把当前局部空间代码段选择符入栈。
push task0 ;把代码指针入栈
iret ;执行中断返回指令,从而切换到特权级3的任务0中执行。
;=======================================================================================
;以下是设置GDT和IDT中描述符项的子程序。
setup_gdt:
;使用6字节操作数lgdt_opcode设置GDT表位置和长度。
lgdt [lgdt_opcode]
ret
;---------------------------------------------------------------------------------------
;这段代码暂时设置IDT表中所有256个中断门描述符都为同一个默认值,均使用默认的中断处理过程
;ignore_int。设置的具体方法是:首先在eax和edx寄存器对中分别设置好默认中断门描述符的0~3
;字节和4~7字节的内容,然后利用该寄存器对循环往IDT表中填充默认中断门描述符内容。
setup_idt: ;把所有256个中断门描述符设置为使用默认处理过程。
lea edx,[ignore_int] ;取ignore_int的物理地址
mov eax,0x00080000 ;选择符为0x0008。
mov ax,dx
;EAX寄存器中高16位存放了下面要用到的段选择子0x0008,也就是指向GDT中的第1个代码段描述符,
;基址为0x10000,限长为8M
;EAX寄存器中低16位存放了下面要用到的偏移地址 ignore_int 。
mov dx,0x8e00 ;这里是下面要用到的属性字节,0x8E00的意思是"有效的386中断门,特权级为0"。
lea edi,[idt]
mov ecx,256 ;循环设置所有256个门描述符
rp_idt:
;中断描述符表中的每一个描述符的格式是:
;1、每个描述符占8个字节;
;2、第0、1个字节是偏移地址的低16位;
;3、第2、3个字节是段选择子;
;4、第4、5个字节是属性字节;
;5、第6、7个字节是偏移地址的高16位。
mov [edi],eax
mov [edi+4],edx
add edi,8
dec ecx
jne rp_idt
lidt [lidt_opcode] ;最后用6字节操作数加载IDTR寄存器。
ret
;=======================================================================================
;显示字符子程序。取当前光标位置并把AL中的字符显示在屏幕上。整屏可显示80×25个字符。
write_char:
push gs ;首先保存要用到的寄存器,EAX由调用者负责保存。
push ebx
mov ebx,SCRN_SEL ;然后让GS指向显示内存段(0xb8000)。
mov gs,ebx
mov ebx,[scr_loc] ;再从变量scr_loc中取目前字符显示位置值。
shl ebx,1 ;因为在屏幕上每个字符还有一个属性字节,因此字符
;实际显示位置对应的显示内存偏移地址要乘以2
mov [gs:ebx],al
shr ebx,1 ;把字符放到显示内存后把位置值除2加1,此时位置值对
inc ebx ;应下一个显示位置。如果该位置大于2000,则复位成
cmp ebx,2000 ;0
jb x1
mov ebx,0
x1:
mov [scr_loc],ebx ;最后把这个位置值保存起来(scr_loc),
pop ebx ;并弹出保存的寄存器内容,返回。
pop gs
ret
;=======================================================================================
;以下是3个中断处理程序:默认中断、定时中断和系统调用中断。
align 4 ;双字对齐
ignore_int: ;ignore_int是默认的中断处理程序,若系统产生了其他中断,则会在屏幕上显示一个字符“C”。
push ds
push eax
mov eax,0x10 ;首先让DS指向内核数据段,因为中断程序属于内核。
mov ds,eax
mov eax,67 ;在AL中存放"C"的代码,调用显示程序显示在屏幕上
call write_char
pop eax
pop ds
iret
;---------------------------------------------------------------------------------------
;这是定时中断处理程序。其中主要执行任务切换操作。
align 4 ;双字对齐
timer_interrupt:
push ds
push eax
mov eax,0x10 ;首先让DS指向内核数据段
mov ds,ax
mov al,0x20 ;然后立刻允许其他硬件中断,即向8259A发送EOI命令
out 0x20,al
mov eax,1 ;接着判断当前任务,若是任务1则去执行任务0,或反之
cmp dword [current],eax
je x2
mov dword [current],eax ;若当前任务是0,则把1存入current,并跳转到任务1
jmp TSS1_SEL:0 ;去执行。注意跳转的偏移值无用,但需要写上。
jmp x3
x2:
mov dword [current],0 ;若当前任务是1,则把0存入current,并跳转到任务0
jmp TSS0_SEL:0 ;去执行
x3:
pop eax
pop ds
iret
;---------------------------------------------------------------------------------------
;系统调用中断int 0x80处理程序。该示例只有一个显示字符功能。
align 4 ;双字对齐
system_interrupt:
push ds
push edx
push ecx
push ebx
push eax
mov edx,0x10 ;首先让DS指向内核数据段
mov ds,dx
call write_char ;然后调用显示字符子程序write_char,显示AL中的字符。
pop eax
pop ebx
pop ecx
pop edx
pop ds
iret
;=======================================================================================
current:
dd 0 ;当前任务号(0或1)。
scr_loc:
dd 0 ;屏幕当前显示位置。按从左上角到右下角顺序显示。
align 4
lidt_opcode: ;这里定义的6个字节是给 IDT寄存器 用的,使用的指令是 lidt lidt_opcode
dw 256*8-1 ;加载IDTR寄存器的6字节操作数:表长度和基地址。
dd idt
lgdt_opcode:
dw (end_gdt-gdt-1) ;加载GDTR寄存器的6字节操作数:表长度和基地址
dd gdt
align 8
idt: ;IDT空间。共256个门描述符,每个8字节,占用2KB。
times 256 dd 0 ;dd=double data word 双字也就是4字节
times 256 dd 0
gdt:
dw 0x0000,0x0000,0x0000,0x0000;空选择子
dw 0x07FF,0x0000,0x9A00,0x00C0;基址 0x00000 限长 (0x7ff+1)*4KB=8M 的代码段,其选择符是0x08。
dw 0x07FF,0x0000,0x9200,0x00C0;基址 0x00000 限长 8M 的数据段,其选择符是0x10。
dw 0x0002,0x8000,0x920B,0x00C0;基址 0xB8000 限长 12K 的显存数据段,其选择符是0x18。
dw 0x0068,tss0,0xE900,0x0000;对应于TSS0的描述符,基址暂定0x00000,但会被设置为指向 tss0 处,限长为0x68,即102个字节,其选择符是0x20。
dw 0x0040,ldt0,0xE200,0x0000;对应于LDT0的描述符,基址暂定0x00000,但会被设置为指向 ldt0 处,限长为0x40,即64个字节,其选择符是0x28。
dw 0x0068,tss1,0xE900,0x0000;对应于TSS1的描述符,基址暂定0x00000,但会被设置为指向 tss1 处,限长为0x68,即102个字节,其选择符是0x30。
dw 0x0040,ldt1,0xE200,0x0000;对应于LDT1的描述符,基址暂定0x00000,但会被设置为指向 ldt1 处,限长为0x40,即64个字节,其选择符是0x38。
end_gdt:
times 128 dd 0 ;初始内核堆栈空间。
init_stack: ;刚进入保护模式时用于加载SS:ESP堆栈指针值。
dd init_stack ;堆栈段偏移位置。
dw 0x10 ;堆栈段同内核数据段。
;下面是任务0的LDT表段中的局部段描述符。
align 8
ldt0:
dw 0x0000,0x0000,0x0000,0x0000;第1个描述符,不用。
dw 0x03FF,0x0000,0xFA00,0x00C0;基址为0x00000、限长为4M字节、DPL为3的代码段、对应的选择符是0x08
dw 0x03FF,0x0000,0xF200,0x00C0;基址为0x00000、限长为4M字节、DPL为3的数据段、对应的选择符是0x10
;下面是任务0的TSS段的内容。注意其中标号等字段在任务切换时不会改变。
tss0:
dd 0 ;back link
dd krn_stk0, 0x10 ;esp0,ss0
dd 0,0,0,0,0 ;esp1, ss1, esp2, ss2, cr3
dd task0 ;确保第一次切换到任务0的时候EIP从这里取值,所以就从 task0 处开始运行,
;因为任务0的基址为 0x00000,和核心一样,不指定这个的话,
;第一次切换进来就会跑去执行 0x00000处的核心代码了,
;除非ldt0中的代码段描述符中的基址改成 task0 处的线性地址,
;那这里就可以设为0.
dd 0x200 ;EFLAGS的IF标志位为1,使得中断开放
dd 0,0,0 ;eax, ecx, edx
dd 0,0,0,0,0 ;ebx esp, ebp, esi, edi
dd 0x17,0x0f,0x17,0x17,0x17,0x17 ;es, cs, ss, ds, fs, gs
dd LDT0_SEL,0x08000000 ;ldt,trace bitmap
times 128 dd 0 ;任务0的内核栈空间
krn_stk0:
;下面是任务1的LDT表段内容和TSS段内容。
ldt1:
dw 0x0000,0x0000,0x0000,0x0000;第1个描述符,不用。
dw 0x03FF,0x0000,0xFA00,0x00C0
dw 0x03FF,0x0000,0xF200,0x00C0
tss1:
dd 0 ;back link
dd krn_stk1, 0x10 ;esp0,ss0
dd 0,0,0,0,0 ;esp1, ss1, esp2, ss2, cr3
dd task1 ;确保第一次切换到任务0的时候EIP从这里取值,所以就从 task0 处开始运行,
;因为任务0的基址为 0x00000,和核心一样,不指定这个的话,
;第一次切换进来就会跑去执行 0x00000处的核心代码了,
;除非ldt0中的代码段描述符中的基址改成 task0 处的线性地址,
;那这里就可以设为0.
dd 0x200 ;EFLAGS的IF标志位为1,使得中断开放
dd 0,0,0 ;eax, ecx, edx
dd 0,0,0,0,0 ;ebx esp, ebp, esi, edi
dd 0x17,0x0f,0x17,0x17,0x17,0x17 ;es, cs, ss, ds, fs, gs
dd LDT1_SEL,0x08000000 ;ldt,trace bitmap
times 128 dd 0 ;任务1的n内核栈空间
krn_stk1:
;下面是任务0和任务1的程序,它们分别循环显示字符“A”和“B”。
task0:
mov eax,0x17 ;首先让DS指向任务的局部数据段
mov ds,ax ;因为任务没有使用局部数据,所以这两句可省略
mov al,65 ;把需要显示的字符"A"放入AL寄存器中
int 0x80 ;执行系统调用,显示字符
mov ecx,0xfff ;执行循环,起延时作用
y1:
loop y1
jmp task0 ;跳转到任务代码开始处继续显示字符
task1:
mov al,66 ;把需要显示的字符"B"放入AL寄存器中
int 0x80 ;执行系统调用,显示字符
mov ecx,0xfff ;延时一段时间,并跳转到开始处循环显示
y2:
loop y2
jmp task1
times 128 dd 0 ;这是任务1的用户栈空间
usr_stk1: