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: