Linux-0.01 引导代码分析-boot.s

Linux-0.01 的引导部分主要由两个源代码完成:boot.s 与 head.s 。boot.s 由 BIOS 加载执行,head.s 是 32 位的引导代码,在最后会调用 main() 函数,完成系统的引导。

boot.s 代码:

;
;	boot.s
;
; boot.s is loaded at 0x7c00 by the bios-startup routines, and moves itself
; out of the way to address 0x90000, and jumps there.
;
; It then loads the system at 0x10000, using BIOS interrupts. Thereafter
; it disables all interrupts, moves the system down to 0x0000, changes
; to protected mode, and calls the start of system. System then must
; RE-initialize the protected mode in it's own tables, and enable
; interrupts as needed.
;
; NOTE! currently system is at most 8*65536 bytes long. This should be no
; problem, even in the future. I want to keep it simple. This 512 kB
; kernel size should be enough - in fact more would mean we'd have to move
; not just these start-up routines, but also do something about the cache-
; memory (block IO devices). The area left over in the lower 640 kB is meant
; for these. No other memory is assumed to be "physical", ie all memory
; over 1Mb is demand-paging. All addresses under 1Mb are guaranteed to match
; their physical addresses.
;
; NOTE1 abouve is no longer valid in it's entirety. cache-memory is allocated
; above the 1Mb mark as well as below. Otherwise it is mainly correct.
;
; NOTE 2! The boot disk type must be set at compile-time, by setting
; the following equ. Having the boot-up procedure hunt for the right
; disk type is severe brain-damage.
; The loader has been made as simple as possible (had to, to get it
; in 512 bytes with the code to move to protected mode), and continuos
; read errors will result in a unbreakable loop. Reboot by hand. It
; loads pretty fast by getting whole sectors at a time whenever possible.

; 1.44Mb disks:
sectors = 18
; 1.2Mb disks:
; sectors = 15
; 720kB disks:
; sectors = 9

.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

BOOTSEG = 0x07c0;引导程序被加载到的地址
INITSEG = 0x9000;引导程序将自己移动到的目标地址
SYSSEG  = 0x1000;系统核心被加载到的地址
ENDSEG	= SYSSEG + SYSSIZE;系统代码的结束地址,其中 SYSSIZE 在 Makefile 中定义

entry start;程序入口标识
start:
	mov	ax,#BOOTSEG;将引导程序被加载的地址移动到 AX 中
	mov	ds,ax;将数据段的基址设置为引导程序的起始地址
	mov	ax,#INITSEG;将上文提到的目标地址设置到 AX 中
	mov	es,ax;将 AX 中的值设置到 ES 中,使用基址加偏移的方式移动地址
	mov	cx,#256;将 CX 计数器的值设置为 256
	sub	si,si;清空 SI
	sub	di,di;清空 DI
	rep;重复执行 movw 256 次,实际上是将 512K 的数据(引导程序自身)搬移到INITSEG 处
	movw;搬移指令,数据寻址方式,源:DS:SI,目标:ES:DI
	jmpi	go,INITSEG;jmpi 段间跳转指令,由于当前代码已经被复制到 INITSEG ,在不同的段。64K 一个段
go:	mov	ax,cs;将代码段的地址移动到 AX
	mov	ds,ax;将 AX 的值移动到 DS
	mov	es,ax;将 AX 的值移动到 ES
	mov	ss,ax;将 AX 的值移动到 SS,堆栈段的基址
	mov	sp,#0x400;栈顶指针,堆栈段的大小设置为 512K。堆栈可能是向下发展,否则会覆盖 INITSEG 的代码

	mov	ah,#0x03;BIOS 10H 中断的 03H 服务,读取当前光标位置,dh:行,dl:列
	int	0x10;执行 10H 中断
	
	mov	cx,#24;将 CX 的值设置为 24,需要显示字符的个数
	mov	bx,#0x0007;设置显示属性	; page 0, attribute 7 (normal)
	mov	bp,#msg1;将要显示字符的起始地址加载到 BP
	mov	ax,#0x1301;13H显示字符串(ES:BP=显示串地址)AL=显示输出方式(1:字符串中只含显示字符,其显示属性在BL中,显示后,光标位置改变),综合起来功能是:向屏幕写字符串并移动光标
	int	0x10;执行中断

; ok, we've written the message, now
; we want to load the system (at 0x10000)

	mov	ax,#SYSSEG;将 AX 的值设置为:SYSSEG
	mov	es,ax;将 ES 的值设置为 SYSSEG
	call	read_it;调用 read_it,call 指令会使用堆栈寄存器
	call	kill_motor;调用 kill_motor,关闭软驱

; if the read went well we get current cursor position and save it for
; posterity.

	mov	ah,#0x03	; read cursor pos
	xor	bh,bh
	int	0x10		; save it in known place, con_init fetches
	mov	[510],dx	; it from 0x90510.将当前光标位置存储到系统代码最后一个段的最后两个字节
		
; now we want to move to protected mode ...

	cli			; 关闭中断

; first we move the system to it's rightful place

	mov	ax,#0x0000;AX 清零
	cld			; DF 被置为 0,SI 与 DI 增加
do_move:
	mov	es,ax		; 设置目标代码段索引:0
	add	ax,#0x1000   ;将代码段索引加一
	cmp	ax,#0x9000   ;将目标代码段索引与结束段索引进行比较
	jz	end_move     ;如果相等则完成复制,跳转到 end_move
	mov	ds,ax		;复制时的源代码段
	sub	di,di;DI 清零
	sub	si,si;SI 清零
	mov 	cx,#0x8000;复制的字节数
	rep
	movsw;按 word(16bit) 的方式移动
	j	do_move;循环执行,直到复制完成

; then we load the segment descriptors

end_move:

	mov	ax,cs		; 在完成加载系统代码后,代码还在当前段执行,由于没有使用数据段,数据都存放在当前代码段,因此需要恢复 DS 到当前段基址
	mov	ds,ax        ;将 DS 设置为正确的值
	lidt	idt_48		;加载中断描述符表 load idt with 0,0
	lgdt	gdt_48		;加载全局描述符表 load gdt with whatever appropriate

; that was painless, now we enable A20 参考 Intel 8042 芯片资料

	call	empty_8042;调用empty_8042
	mov	al,#0xD1		; 控制码
	out	#0x64,al           ; 输出控制码
	call	empty_8042
	mov	al,#0xDF		; A20 on
	out	#0x60,al
	call	empty_8042

; well, that went ok, I hope. Now we have to reprogram the interrupts :-(
; we put them right after the intel-reserved hardware interrupts, at
; int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
; messed this up with the original PC, and they haven't been able to
; rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
; which is used for the internal hardware interrupts as well. We just
; have to reprogram the 8259's, and it isn't fun.参考 Intel 8259 中断控制器的芯片资料

	mov	al,#0x11		; 初始化控制码
	out	#0x20,al		; 输出控制码,send it to 8259A-1
	.word	0x00eb,0x00eb	; jmp $+2, jmp $+2 机器码,由于当前在代码段,会被执行,延迟作用
	out	#0xA0,al		; and to 8259A-2
	.word	0x00eb,0x00eb
	mov	al,#0x20		; start of hardware int's (0x20)
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x28		; start of hardware int's 2 (0x28)
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0x04		; 8259-1 is master
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x02		; 8259-2 is slave
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0x01		; 8086 mode for both
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0xFF		; 设置中断控制器的标志位,关闭全部中断 mask off all interrupts for now
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al

; well, that certainly wasn't fun :-(. Hopefully it works, and we don't
; need no steenking BIOS anyway (except for the initial loading :-).
; The BIOS-routine wants lots of unnecessary data, and it's less
; "interesting" anyway. This is how REAL programmers do it.
;
; Well, now's the time to actually move into protected mode. To make
; things as simple as possible, we do no register set-up or anything,
; we let the gnu-compiled 32-bit programs do that. We just jump to
; absolute address 0x00000, in 32-bit protected mode.

	mov	ax,#0x0001	; protected mode (PE) bit PE 将被设置为 1,将要进入保护模式
	lmsw	ax		; lmsw是指装入机器状态字,即实际进入保护模式

            ;进入保护模式后,段寄存器就变成了选择子,选择子的结构:
            ;        16            2    1  0
            ;        ———————————————
            ;        |    索引      | TI    | RDL |
            ;        ———————————————
            ;其中TI=0时是从GDT中找描述符

	jmpi	0,8		; jmp offset 0 of segment 8 (cs)选择子=8,跳到GDT中的索引号为1的描述符,即代码段,参考 GDT 的定义

; This routine checks that the keyboard command queue is empty
; No timeout is used - if this hangs there is something wrong with
; the machine, and we probably couldn't proceed anyway.
empty_8042:;8042 是键盘控制器
	.word	0x00eb,0x00eb
	in	al,#0x64	;将#0x64端口的状态值读取到 AL中, 8042 status port
	test	al,#2		;测试 AL 的第二位是否为 1, is input buffer full?
	jnz	empty_8042	;如果输入缓冲区没有满,就循环执行,如果满了,函数返回
	ret

; This routine loads the system at address 0x10000, making sure
; no 64kB boundaries are crossed. We try to load it as fast as
; possible, loading whole tracks whenever we can.
;
; in:	es - starting address segment (normally 0x1000)
; 读取过程:先读一个磁道,再读同磁道的另一个盘面,然后重复上面过程读取下一个磁道
; This routine has to be recompiled to fit another drive type,
; just change the "sectors" variable at the start of the file
; (originally 18, for a 1.44Mb drive)
;1.44Mb 的软盘结构:2面、80道/面、18扇区/道、512字节/扇区,2880扇区,512字节/扇区X 2880扇区 = 1440 KB ,每个磁道 9K,每个段64K(7个磁道+2个扇区),注意下面的溢出处理
sread:	.word 1			; 当前磁道已经读取的扇区数
head:	.word 0			; 当前磁头号
track:	.word 0			; 当前磁道号
read_it:
	mov ax,es;ES 是系统代码的段基址(当前段读满后 ES 会被加一)
	test ax,#0x0fff;相当于 AX 高 4 位清零,低 4 位保持原来的值,0x0FFF 是 64
die:	jne die	;按扇区复制,要求对齐
	xor bx,bx;BX 清零,用来标记起始地址,后面读取数据后 BX 会变化
rp_read:
	mov ax,es;将 ES 的值加载到 AX 中
	cmp ax,#ENDSEG;将 AX 的值与结束地址进行比较,判断是否完成加载
	jb ok1_read;如果小于#ENDSEG,跳转到 ok1_read
	ret;过程结束,返回,通过跳转到存储在堆栈中的返回地址继续执行
ok1_read:;在没有填满数据到 #ENDSEG 时执行
	mov ax,#sectors;将扇区数加载到 AX 中,实际为 AL
	sub ax,sread;将 AX 的值减一,实际为 AL,在下面读磁盘时 AL 代表扇区数
	mov cx,ax;将 CX 的值设置为 AX 的值,此时 AX 中存放的上次读取的扇区数
	shl cx,#9;2 的 9 次方是 512,剩余扇区数乘以 512Byte 等于已经读取的字节数
	add cx,bx;CX 为 16 位寄存器,最多可以表示 64K 数据,如果已经读取的字节数加上基址(BX)超过当前段需要溢出处理
	jnc ok2_read;若CF没有置位则跳转,即:没有溢出就继续执行,此处表示如果读取的数据少于 64K 跳转执行
	je ok2_read;利用零标志ZF 作跳转判断条件,此处表示如果读取的数据等于 64K 跳转执行
	xor ax,ax;AX 清零,如果执行此处代码,表示读取发生了溢出即:当前磁道的剩余字节数超过了当前段需要的字节
	sub ax,bx;发生溢出,相当于0xFFFF-BX,等于还需要填写的字节数
	shr ax,#9;AX/512;每个扇区 512 Byte,此时 AX 中是还需要读取的扇区数
ok2_read:;在没有填满当前段时执行
	call read_track;调用 read_track
	mov cx,ax;将 CX 设置为 AX,AX 中存放的是上次读取的扇区数
	add ax,sread;将 AX 的值设置为 AX + sread = Total Sectors
	cmp ax,#sectors;判断是否已经读完
	jne ok3_read;如果没有读完,跳转到 ok3_read
	mov ax,#1;将 AX 设置为 1,运行到此处是代表当前磁道的扇区已经读完
	sub ax,head;AX = AX - head
	jne ok4_read;如果不为 0,当前盘面已经读完,跳转到 ok4_read
	inc track;如果为 0,当前盘面没有读完,增加磁道号,继续读下一个磁道
ok4_read:;在当前盘面读完时执行
	mov head,ax;将 AX 的值保存到 head 中
	xor ax,ax;将 AX 清零,继续执行
ok3_read:;在当前磁道没有读完时执行
	mov sread,ax;将下一个需要读取的磁道号保存到 sread 中
	shl cx,#9;将 CX 乘以 512,CX 代表剩余读取的扇区数,此时 CX 代表剩余读取的字节数
	add bx,cx;BX = BX + CX,此时 BX 为下次存放数据的起始地址
	jnc rp_read;如果没有超过 64K 继续读
	mov ax,es;如果超过 64K,将 ES(段基址)的值设置到 AX 中
	add ax,#0x1000;AH 加一
	mov es,ax;将 ES 设置为 AX,移动到下一个段
	xor bx,bx;清空 BX
	jmp rp_read;继续读当前磁道

read_track:
        ;BIOS 13H的02H功能:将从磁盘上把一个或更多的扇区内容读进存贮器。因为这是一个低级功能,在一个操作中读取的全部扇区必须在同一条磁道上(磁头号和磁道号相同)
        ;入口参数: AH=02H ;功能号
        ; AL=扇区数
        ; CH、CL=磁道号的低8位数、位7-6表示磁道号的高2位,低6位放入所读起始扇区号
        ; DH、DL=磁头号、驱动器号
        ; ES:BX=数据缓冲区地址
        ;返回: AH=0:成功,AL=读取的扇区数;
        ; 如果CF=1,AX中存放出错状态:AH=错误码。
        ;注意: 寄存器DS、BX、CX、DX不变。
        ;磁头号: 软盘A面=0;软盘B面=1。
        ;驱动器号:软驱A=0;软驱B=1;硬驱=80H
	push ax
	push bx
	push cx
	push dx;保护现场,此时 AL 等于剩余读取的扇区数,注意:下面的代码中 DX 作为临时数据存放饿寄存器
	mov dx,track;将 DX 设置为 track(磁道号),此时 DL 为磁道号,DX 中是临时数据
	mov cx,sread;将 CX 设置为 sread(扇区号),此时 CL 为扇区号
	inc cx;CX 加 1,加一的原因:磁盘的第一个扇区(512K)存放的是引导代码,不是系统代码,系统代码从第二个扇区开始,除了第一次其它从 1 开始读取 
	mov ch,dl;将 CH 设置为 DL,即磁道号,此时 CH 为磁道号-磁道号与扇区号已经设置完成
	mov dx,head;DX 设置为磁头号,此处为 DL,此时 DL 为磁头号,DL 中是临时数据
	mov dh,dl;将 DH 同样设置为磁头号,此时 DH 为磁头号
	mov dl,#0;将 DL 设置为 0,软驱A=0,DL 为驱动器号
	and dx,#0x0100;DX中的值除了 DH 的最后一位被保留,其它清零(上面那句代码可以去掉?),DH为磁头号
	mov ah,#2;将 AH 设置为 2,功能号
	int 0x13;调用 BIOS 13H 中断
	jc bad_rt
	pop dx
	pop cx
	pop bx
	pop ax;恢复现场
	ret
bad_rt:	mov ax,#0
	mov dx,#0
	int 0x13;软盘复位,13H的00H号功能——软盘系统复位,AH=00H 功能号,DL=驱动器号,总之:将软驱A复位
	pop dx
	pop cx
	pop bx
	pop ax
	jmp read_track;跳转到 read_track,即:如果读取当前扇区出现异常,继续读

/*
 * This procedure turns off the floppy drive motor, so
 * that we enter the kernel in a known state, and
 * don't have to worry about it later.
 */
kill_motor:
	push dx
	mov dx,#0x3f2
	mov al,#0
	outb
	pop dx
	ret

gdt:
	.word	0,0,0,0		; dummy

	.word	0x07FF		; 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		; base address=0
	.word	0x9A00		; code read/exec
	.word	0x00C0		; granularity=4096, 386

	.word	0x07FF		; 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		; base address=0
	.word	0x9200		; data read/write
	.word	0x00C0		; granularity=4096, 386

idt_48:;
	.word	0			; idt limit=0
	.word	0,0			; idt base=0L

gdt_48:
	.word	0x800		; gdt limit=2048, 256 GDT entries
	.word	gdt,0x9		; gdt base = 0X9xxxx
	
msg1:
	.byte 13,10
	.ascii "Loading system ..."
	.byte 13,10,13,10

.text
endtext:
.data
enddata:
.bss
endbss:

你可能感兴趣的:(Linux-0.01 引导代码分析-boot.s)