浅析Linux源码:bootsect.s,setup.s,head.s

     最近在学习操作系统,就看了下Linux0.11版的源码解析,写一点自己的体会。

     PC开机之后,80X86进入实模式,并且从BIOS(RAM)里的某个地址开始,读取代码到CPU,并执行。BIOS对机器自检,并从内存0x0000处初始化中断向量(BIOS的),中断向量可以这样理解,子函数的入口地址,CPU调用BIOS的中断向量,可以获得一些硬盘等参数,后面会用到。这些子函数据猜测应该是在BIOS里面。中断向量表占用大概1M的空间(0x0000-0x1000)。

     然后,BIOS把磁盘或者硬盘的第一个扇区(磁盘引导区)读入内存的0x7c00处,BIOS完事了。

     这里,第一个扇区装的是bootsect.s,好了,下面是它的代码:

    .code16 # rewrite with AT&T syntax by falcon at 081012 # # SYS_SIZE is the number of clicks (16 bytes) to be loaded. # 0x3000 is 0x30000 bytes = 196kB, more than enough for current # versions of linux # .equ SYSSIZE, 0x3000 # # bootsect.s (C) 1991 Linus Torvalds # # bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves # iself out of the way to address 0x90000, and jumps there. # # It then loads 'setup' directly after itself (0x90200), and the system # at 0x10000, using BIOS interrupts. # # 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, especially as this doesn't contain the # buffer cache as in minix # # The loader has been made as simple as possible, 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. .global _start, begtext, begdata, begbss, endtext, enddata, endbss .text begtext: .data begdata: .bss begbss: .text .equ SETUPLEN, 4 # nr of setup-sectors .equ BOOTSEG, 0x07c0 # original address of boot-sector .equ INITSEG, 0x9000 # we move boot here - out of the way .equ SETUPSEG, 0x9020 # setup starts here .equ SYSSEG, 0x1000 # system loaded at 0x10000 (65536). .equ ENDSEG, SYSSEG + SYSSIZE # where to stop loading # ROOT_DEV: 0x000 - same type of floppy as boot. # 0x301 - first partition on first drive etc .equ ROOT_DEV, 0x301 ljmp $BOOTSEG, $_start _start: mov $BOOTSEG, %ax mov %ax, %ds mov $INITSEG, %ax mov %ax, %es mov $256, %cx sub %si, %si sub %di, %di rep movsw ljmp $INITSEG, $go go: mov %cs, %ax mov %ax, %ds mov %ax, %es # put stack at 0x9ff00. mov %ax, %ss mov $0xFF00, %sp # arbitrary value >>512 # load the setup-sectors directly after the bootblock. # Note that 'es' is already set up. load_setup: mov $0x0000, %dx # drive 0, head 0 mov $0x0002, %cx # sector 2, track 0 mov $0x0200, %bx # address = 512, in INITSEG .equ AX, 0x0200+SETUPLEN mov $AX, %ax # service 2, nr of sectors int $0x13 # read it jnc ok_load_setup # ok - continue mov $0x0000, %dx mov $0x0000, %ax # reset the diskette int $0x13 jmp load_setup ok_load_setup: # Get disk drive parameters, specifically nr of sectors/track mov $0x00, %dl mov $0x0800, %ax # AH=8 is get drive parameters int $0x13 mov $0x00, %ch #seg cs mov %cx, %cs:sectors+0 # %cs means sectors is in %cs mov $INITSEG, %ax mov %ax, %es # Print some inane message mov $0x03, %ah # read cursor pos xor %bh, %bh int $0x10 mov $24, %cx mov $0x0007, %bx # page 0, attribute 7 (normal) #lea msg1, %bp mov $msg1, %bp mov $0x1301, %ax # write string, move cursor int $0x10 # ok, we've written the message, now # we want to load the system (at 0x10000) mov $SYSSEG, %ax mov %ax, %es # segment of 0x010000 call read_it call kill_motor # After that we check which root-device to use. If the device is # defined (#= 0), nothing is done and the given device is used. # Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending # on the number of sectors that the BIOS reports currently. #seg cs mov %cs:root_dev+0, %ax cmp $0, %ax jne root_defined #seg cs mov %cs:sectors+0, %bx mov $0x0208, %ax # /dev/ps0 - 1.2Mb cmp $15, %bx je root_defined mov $0x021c, %ax # /dev/PS0 - 1.44Mb cmp $18, %bx je root_defined undef_root: jmp undef_root root_defined: #seg cs mov %ax, %cs:root_dev+0 # after that (everyting loaded), we jump to # the setup-routine loaded directly after # the bootblock: ljmp $SETUPSEG, $0 # 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) # sread: .word 1+ SETUPLEN # sectors read of current track head: .word 0 # current head track: .word 0 # current track read_it: mov %es, %ax test $0x0fff, %ax die: jne die # es must be at 64kB boundary xor %bx, %bx # bx is starting address within segment rp_read: mov %es, %ax cmp $ENDSEG, %ax # have we loaded all yet? jb ok1_read ret ok1_read: #seg cs mov %cs:sectors+0, %ax sub sread, %ax mov %ax, %cx shl $9, %cx add %bx, %cx jnc ok2_read je ok2_read xor %ax, %ax sub %bx, %ax shr $9, %ax ok2_read: call read_track mov %ax, %cx add sread, %ax #seg cs cmp %cs:sectors+0, %ax jne ok3_read mov $1, %ax sub head, %ax jne ok4_read incw track ok4_read: mov %ax, head xor %ax, %ax ok3_read: mov %ax, sread shl $9, %cx add %cx, %bx jnc rp_read mov %es, %ax add $0x1000, %ax mov %ax, %es xor %bx, %bx jmp rp_read read_track: push %ax push %bx push %cx push %dx mov track, %dx mov sread, %cx inc %cx mov %dl, %ch mov head, %dx mov %dl, %dh mov $0, %dl and $0x0100, %dx mov $2, %ah int $0x13 jc bad_rt pop %dx pop %cx pop %bx pop %ax ret bad_rt: mov $0, %ax mov $0, %dx int $0x13 pop %dx pop %cx pop %bx pop %ax jmp 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 $0x3f2, %dx mov $0, %al outsb pop %dx ret sectors: .word 0 msg1: .byte 13,10 .ascii "Loading system ..." .byte 13,10,13,10 .org 508 root_dev: .word ROOT_DEV boot_flag: .word 0xAA55 .text endtext: .data enddata: .bss endbss:

   这段代码很长很长,做的事情却不是很多。

   代码前面做了一些变量定义,每个代码段在物理内存的起始地址等。

   程序从start开始执行。

   它做的第一件事情就是把自己移动到0x9000处。ds:si=0x07C0:0x0000->es:di=0x9000:0x0000,这样做是要干嘛呢?是为了以后加载setup.s和system模块,因为在bootsect.s里面调用了0x0000内存处的中断向量,同样,setup.s也要调用,所以0x1000以下的地址要留给中断向量,而system又比较大(<512KB),8086实模式下寻址范围只有1M,只能把自己的代码移过来移过去了。

  好了,现在bootsect.s到达了0x9000处,紧接着,它调用BIOS的int 0x13中断,把磁盘的第二个扇区开始,读取四个扇区到它后面,

就是0x9020(setup.s)以上的地址。这样,bootsect.s的功能就完成了。具体中断调用的格式很无聊,没有仔细去研究。不过8086实模式下的编程倒是很有意思,cs,ds,es,ss,都可以随便玩,可以移动自身代码,在1M如此小的寻址范围内,应该是一种很常用的方法。

   下面就不贴setup.s的代码了,很长,功能也很单一。

    setup.s主要工作是利用BIOS中断向量表读取一些系统参数,比如光标位置,硬盘参数,等。然后,BIOS中断向量表就没啥用了,setup.s把system模块移动到0x0000处,这里用到了一个小的技巧。 mov $0x0000, %ax cld # 'direction'=0, movs moves forward do_move: mov %ax, %es # destination segment //目的地址es:di,初始0x0000:0000 32位 add $0x1000, %ax//要移动的代码地址+0x1000bit cmp $0x9000, %ax//是否移动完 jz end_move mov %ax, %ds # source segment //ax是当前已经移动到的地址 sub %di, %di sub %si, %si mov $0x8000, %cx //移动0x8000字=0x1000bit 0x8010bit rep //每次移动64K字节(0x1000bit)的代码...64*8=512K(内核代码长度) 0x1000->0x9000 || 0x0000->0x8000 movsw jmp do_move

    由system模块比较长,cx只能表示一个十六位的数字,最多就是0xFFFF。所以这里做了一个嵌套循环,每次只移动64K的代码,分为8次移动,刚刚好把0x1000的代码移动到了0x9000处。如果把cx表示移动的字节数(0x8000每次),显然0x8000*4超过了一个十六位数可以表示的范围。这个比较好玩。

    好了,下面,加载idt和gdt,idt是中断门描述符表,gdt是全局描述符表。这里,为了head里调用保护模式,所以先load下idt和gdt,具体的数值在该程序的末尾,idt_48,gdt_48里面。

   然后,刊A20地址线是否关闭(IBM的历史遗留问题,不管了),然后做了一件痛苦的事情:

   对俩中断芯片编程。这也是遗留问题吧貌似。

   中断芯片,顾名思义,就是终端到来时,发给CPU的信号,CPU在时钟的作用下,每隔10ms干一次活,终端可以在间隙里向CPU发送一些固定的信号,就是通过终端芯片发送(俩8259),比如,键盘,鼠标,异常等,这些中断发给CPU之后,CPU会响应他们或者不管他们(丢进队列),然后,idt表就神奇的起作用了:CPU根据发送过来的信号(int),找相应的中断函数去处理,idtr寄存器的值保存了当前idt表的内存位置,然后根据中断的数值找到处理中断的函数,做相应的处理,在后面还会看到,中断处理函数有个特殊的机制:需要保存当前寄存器的值之后,再调用中断函数。这用C显然是不可能实现的,汇编又大显身手了。

  

你可能感兴趣的:(浅析Linux源码:bootsect.s,setup.s,head.s)