linux0.00

;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:


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值