boot.s head.s 学习说明

本文详细介绍了从实模式到保护模式的转换过程,包括BIOS中断的应用、内存地址的定位与调整、系统代码的加载及保护模式的开启。此外,还深入探讨了GDT、IDT的设置与应用,以及多任务间的切换机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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





                
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值