要做一个能在裸机上跑的系统,一个简单的boot loader或者是支持grub之类的是必须的。目前并没有把babyos装到实体机上、支持多系统等的打算,所以还是自己写boot loader。

PC上电后,80x86 CPU自动进入实模式,并从0xFFFF0开始自动执行,这个地址是ROM-BIOS中的地址。BIOS会执行一些检测及初始化中断向量表等,之后它将启动设备第一个扇区512字节读入内存0x7c00,并跳转过去开始执行。这一段是硬件自动完成的,给出的这几个地址也是硬件决定的,跟boot loader无关,跟内核也无关。而一旦跳转到0x7c00处开始执行,就是自己写的代码在工作了。

因为硬件自动加载到内存的数据只有512自己,所以一般操作系统需要自己把自己加载到内存中去,这就是boot loader干的事情。

PS2BIOS文件是哪个 呆萌ps2bios文件怎么放置_PS2BIOS文件是哪个


在boot阶段,开始时处于16位实模式,内存的使用也非常自由。但是自由的同时需要做好规划,比如当还需要用到BIOS中断的时候,不要破坏掉BIOS所占的内存区域。BIOS的内存分布如上图所示。

# the main function
main:
    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_mode

1:
    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 screen
clear_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     $0x10
ret

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

2.set_video_mode

# function to set video mode
set_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
    ret

set_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 0x0000
disk_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 hd
read_a_sect_hd:
    lea     disk_addr_packet,   %si
    movb    $0x42,              %ah
    movb    $0x80,              %dl
    int     $0x13
    ret

# function to load the kernel
load_kernel:
    lea     disk_addr_packet,   %si
    movw    $TMP_KERNEL_ADDR>>4,6(%si)
    xorw    %cx,                %cx

1:
    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 position
copy_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 mode
begin_protected_mode:
    cli
1:
    inb     $0x64,         %al
    testb   $0x02,         %al
    jnz     1b

    movb    $0xd1,         %al
    outb    %al,            $0x64

2:
    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 2
gdt:
.quad   0x0000000000000000
.quad   0x00cf9a000000ffff
.quad   0x00cf92000000ffff
.quad   0x0000000000000000
.quad   0x0000000000000000
.quad   0x0000000000000000

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

load:

.org 0

_start:
    jmp     main


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

1:
    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格式的内核,但此时它还是一些测试代码,主要功能是绘制一条直线,表示顺利进入保护模式,并执行到了这里~

PS2BIOS文件是哪个 呆萌ps2bios文件怎么放置_PS2BIOS文件是哪个_02

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