babyos2(1)——boot

来源:互联网 发布:手机淘宝如何联系卖家 编辑:程序博客网 时间:2024/05/20 01:13

要做一个能在裸机上跑的系统,一个简单的boot loader或者是支持grub之类的是必须的。目前并没有把babyos装到实体机上、支持多系统等的打算,所以还是自己写boot loader。
PC上电后,80x86 CPU自动进入实模式,并从0xFFFF0开始自动执行,这个地址是ROM-BIOS中的地址。BIOS会执行一些检测及初始化中断向量表等,之后它将启动设备第一个扇区512字节读入内存0x7c00,并跳转过去开始执行。这一段是硬件自动完成的,给出的这几个地址也是硬件决定的,跟boot loader无关,跟内核也无关。而一旦跳转到0x7c00处开始执行,就是自己写的代码在工作了。
因为硬件自动加载到内存的数据只有512自己,所以一般操作系统需要自己把自己加载到内存中去,这就是boot loader干的事情。
BIOS内存分布
在boot阶段,开始时处于16位实模式,内存的使用也非常自由。但是自由的同时需要做好规划,比如当还需要用到BIOS中断的时候,不要破坏掉BIOS所占的内存区域。BIOS的内存分布如上图所示。

# the main functionmain:    xorw    %ax,            %ax    movw    %ax,            %ds    movw    %ax,            %es    movw    %ax,            %ss    movw    $STACK_BOOT,   %sp    call    clear_screen    call    set_video_mode    call    get_memory_info    call    load_kernel    call    copy_gdt_and_video_info    call    begin_protected_mode1:    jmp     1b

babyos2的boot主要做了下面几件事情:
1.清屛,主要功能是清除掉虚拟机启动时自动打印的一些信息
2.设置显示模式,虽然这次决定不再只做一些花哨的显示相关的东西,但终究没舍得放弃1024*768的显示模式,毕竟比单纯的字符模式有趣的多。这个函数将会设置VBE 模式为0x118,即1024*768,24位的显示模式。
3.获取内存信息,这个暂时不在这里说,后面再描述。
4.加载内核。这是最主要的功能之一,把内核加载到一个临时的地方,因为一旦跳转到保护模式,实模式的BIOS中断就不能或者不容易使用了。因为babyos2从硬盘启动,这个函数主要功能是读硬盘。
5.将gdt和显示模式,及内存相关的一些信息拷贝到一个安全的地址。
6.进入保护模式。

1.clear_screen

# function to clear the screenclear_screen:    movb    $0x06, %ah    movb    $0x00, %al     # roll up all rows, clear the screen    movb    $0x00, %ch     # row of left top corner    movb    $0x00, %cl     # col of left top corner    movb    $0x18, %dh     # row of right bottom corner    movb    $0x4f, %dl     # col of right bottom corner    movb    $0x07, %bh     # property of roll up rows    int     $0x10ret

清屛函数使用BIOS 0x10号中断,ah=0x06,al=0x00表示上滚所有行,即清屛。

2.set_video_mode

# function to set video modeset_video_mode:    xorw    %ax,        %ax    movw    %ax,        %ds    movw    %ax,        %es    movw    $0x800,        %di # buffer    # check vbe    movw    $0x4f00,   %ax    int     $0x10    cmp     $0x004f,   %ax    jne     set_vga_0x13    movw    0x04(%di),  %ax    cmp     $0x0200,   %ax # vbe version < 2.0    jb      set_vga_0x13    # check vbe mode 0x118    movw    $0x118,        %cx    movw    $0x4f01,   %ax    int     $0x10    cmpb    $0x00,     %ah # call failed    jne     set_vga_0x13    cmpb    $0x4f,     %al # not support this mode    jne     set_vga_0x13    movw    (%di),      %ax    andw    $0x0080,   %ax # not support Linear Frame Buffer memory model    jz      set_vga_0x13    # save video info    movw    $0x118,        video_mode    movw    0x12(%di),  %ax    movw    %ax,        screen_x    movw    0x14(%di),  %ax    movw    %ax,        screen_y    movb    0x19(%di),  %al    movb    %al,        bits_per_pixel    movb    0x1b(%di),  %al    movb    %al,        memory_model    movl    0x28(%di),  %eax    movl    %eax,       video_ram    #set vbe mode    movw    $0x118,        %bx    addw    $0x4000,   %bx    movw    $0x4f02,   %ax    int     $0x10    retset_vga_0x13:    movb    $0,            %ah    movb    $0x13,     %al    int     $0x10    ret

该函数首先指定了一块buffer,用来存放调用vbe相关函数后返回的结果,这里放到了es:0x800,从上面BIOS内存布局那个图可以看出,这个地址位于BIOS数据区的上面,boot sector的下面,暂时没有人使用,比较安全。
然后会检测是否支持vbe及vbe的版本,若不支持或者版本<2.0则设置显示模式位0x13;
然后检测是否支持0x118显示模式,如果不支持该模式,或者不支持Linear Frame Buffer memory model,还是设置为0x13模式;
然后把0x118显示模式下的一些信息,如width, height, bits per pixel,memory model等信息记录下来;
然后设置显示模式为0x118。至此显示模式设置完毕,可以发现此时虚拟机的窗口大小变为1024*768.

load_kernel:

# read kernel from hd# and put loader to 0x0000disk_addr_packet:    .byte   0x10                        # [0] size of packet 16 bytes    .byte   0x00                        # [1] reserved always 0    .word   0x01                        # [2] blocks to read    .word   0x00                        # [4] transfer buffer(16 bit offset)    .word   0x00                        # [6] transfer buffer(16 bit segment)    .long   0x01                        # [8] starting LBA    .long   0x00                        # [12]used for upper part of 48 bit LBAs# function to read a sect from hdread_a_sect_hd:    lea     disk_addr_packet,   %si    movb    $0x42,              %ah    movb    $0x80,              %dl    int     $0x13    ret# function to load the kernelload_kernel:    lea     disk_addr_packet,   %si    movw    $TMP_KERNEL_ADDR>>4,6(%si)    xorw    %cx,                %cx1:    call    read_a_sect_hd    lea     disk_addr_packet,   %si    movl    8(%si),             %eax    addl    $0x01,              %eax    movl    %eax,               (disk_addr_packet + 8)    movl    6(%si),             %eax    addl    $512>>4,            %eax    movl    %eax,               (disk_addr_packet + 6)    incw    %cx    cmpw    $KERNEL_SECT_NUM+1,    %cx    jne     1b    # move first sector(the loader) to 0x0000    cld                                                 # si, di increment    movw    $TMP_KERNEL_ADDR>>4,               %ax    movw    %ax,                                %ds     # DS:SI src    xorw    %si,                                %si    movw    $0x00,                             %ax    movw    %ax,                                %es     # ES:DI dst    xorw    %di,                                %di    movw    $(LOADER_SECT_NUM*SECT_SIZE) >> 2, %cx     # 512/4 times     rep     movsl                                       # 4 bytes per time    ret

该函数主要功能是利用BIOS中断读硬盘。
首先定义了一个数据结构disk_addr_packet,该结构指定了要读取硬盘的位置(LBA),及存放数据的buffer位置。然后每次增加LBA及buffer,并利用0x13号中断(ah=0x42)来读取数据。
babyos2将内核临时加载到0x10000。
然后将内核的第一个扇区拷贝到0x0000位置。硬盘中第一个扇区是boot,第二个扇区是loader,这里所说的内核第一个扇区是loader。
为了方便调试,babyos2的kernel采用elf格式,所以loader的作用是从临时内核的位置,按elf格式解析并加载内核各个段到指定位置。

copy_info:

# function to copy gdt and video info to a safe positioncopy_gdt_and_video_info:    xorw    %ax,                        %ax    movw    %ax,                        %ds     # DS:SI src    leaw    gdt,                        %si    movw    $BOOT_INFO_SEG,                %ax    movw    %ax,                        %es     # ES:DI dst    xorw    %di,                        %di    movw    $(GDT_SIZE+VIDEO_INFO_SIZE),%cx        # num of bytes to move    rep     movsb    ret

这个函数比较简单,主要功能是把前面保存下来的信息拷贝到一个安全的位置,babyos2把这些数据拷贝到了0x90000。内核启动后会用到这些数据,比如显存位置,内存信息等。

begin_protected_mode:

# function to begin protected modebegin_protected_mode:    cli1:    inb     $0x64,         %al    testb   $0x02,         %al    jnz     1b    movb    $0xd1,         %al    outb    %al,            $0x642:    inb     $0x64,         %al    testb   $0x02,         %al    jnz     2b    movb    $0xdf,         %al    outb    %al,            $0x60    lgdt    gdt_ptr    movl    %cr0,           %eax    orl     $CR0_PE,       %eax    movl    %eax,           %cr0    ljmp    $SEG_KCODE<<3,$0    ret

这个函数将会跳转到保护模式。
8086只有20位地址总线(A19-A0),为了访问超过1M的内存,需要打开存储器的A20地址线,IBM使用了8042键盘控制器上的一根线来控制,通过访问0x64端口可以打开A20.
然后需要加载gdt;
然后就是cr0寄存器的CR0_PE位需要置1;
最后执行一个ljmp,跳转到loader执行。loader的加载地址会设置为0,所以这里跳转到SEG_KCODE段,偏移地址为0的位置执行。

.p2align 2gdt:.quad   0x0000000000000000.quad   0x00cf9a000000ffff.quad   0x00cf92000000ffff.quad   0x0000000000000000.quad   0x0000000000000000.quad   0x0000000000000000

gdt是个表,每个表项描述一个段。而SEG_KCODE是一个索引,去寻找一个段,这里指的就是内核代码段。关于这些的知识在Intel的文档上都能找到,不再赘述。

load:

.org 0_start:    jmp     mainmain:    movl    $DATA_SELECTOR,            %eax    movw    %ax,                    %ds    movw    %ax,                    %es    movw    %ax,                    %fs    movw    %ax,                    %gs    movw    %ax,                    %ss    movl    $STACK_PM_BOTTOM,      %esp    call    loadmain1:    jmp     1b

loadmain:

/* GDT和IDT内存地址和大小 */#define IDT_ADDR            (0x90000)   #define IDT_SIZE            (256*8)#define GDT_ADDR            (IDT_ADDR + IDT_SIZE)#define GDT_LEN             (5)#define GDT_SIZE            (8 * GDT_LEN)/* 显示模式的一些信息的内存地址 */#define VIDEO_INFO_ADDR     (GDT_ADDR + GDT_SIZE)typedef struct vidoe_info_s {    uint16 video_mode;    uint16 cx_screen;    uint16 cy_screen;    uint8  n_bits_per_pixel;    uint8  n_memory_model;    uint8* p_vram_base_addr;} video_info_t;video_info_t* p_video_info = (video_info_t *)VIDEO_INFO_ADDR;uint8* p_vram_base_addr = (uint8 *)0xe0000000;uint32 cx_screen = 1024;uint32 cy_screen = 768;uint32 n_bytes_per_pixel = 3;static bool is_pixel_valid(int32 x, int32 y){    if (x < 0 || y < 0 || (uint32)x >= cx_screen || (uint32)y >= cy_screen) {        return false;    }    return true;}bool set_pixel(int32 x, int32 y, uint8 r, uint8 g, uint8 b){    uint8* pvram = NULL;    if (!is_pixel_valid(x, y)) {        return false;    }    pvram = p_vram_base_addr + n_bytes_per_pixel*y*cx_screen + n_bytes_per_pixel*x;    pvram[0] = b;    pvram[1] = g;    pvram[2] = r;    return true;}void test(){    for (int i = 100; i < 1024-100; i++) {        set_pixel(i, 200, 0xff, 34, 89);    }}void loadmain(void){    p_vram_base_addr = p_video_info->p_vram_base_addr;    test();}

loadmain主要目的是加载elf格式的内核,但此时它还是一些测试代码,主要功能是绘制一条直线,表示顺利进入保护模式,并执行到了这里~

这里写图片描述

elf格式kernel的加载,后面再描述。

原创粉丝点击