要做一个能在裸机上跑的系统,一个简单的boot loader或者是支持grub之类的是必须的。目前并没有把babyos装到实体机上、支持多系统等的打算,所以还是自己写boot loader。
PC上电后,80x86 CPU自动进入实模式,并从0xFFFF0开始自动执行,这个地址是ROM-BIOS中的地址。BIOS会执行一些检测及初始化中断向量表等,之后它将启动设备第一个扇区512字节读入内存0x7c00,并跳转过去开始执行。这一段是硬件自动完成的,给出的这几个地址也是硬件决定的,跟boot loader无关,跟内核也无关。而一旦跳转到0x7c00处开始执行,就是自己写的代码在工作了。
因为硬件自动加载到内存的数据只有512自己,所以一般操作系统需要自己把自己加载到内存中去,这就是boot loader干的事情。
在boot阶段,开始时处于16位实模式,内存的使用也非常自由。但是自由的同时需要做好规划,比如当还需要用到BIOS中断的时候,不要破坏掉BIOS所占的内存区域。BIOS的内存分布如上图所示。
# the main function
main:xorw %ax, %axmovw %ax, %dsmovw %ax, %esmovw %ax, %ssmovw $STACK_BOOT, %spcall clear_screencall set_video_modecall get_memory_infocall load_kernelcall copy_gdt_and_video_infocall 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 screen
clear_screen:movb $0x06, %ahmovb $0x00, %al # roll up all rows, clear the screenmovb $0x00, %ch # row of left top cornermovb $0x00, %cl # col of left top cornermovb $0x18, %dh # row of right bottom cornermovb $0x4f, %dl # col of right bottom cornermovb $0x07, %bh # property of roll up rowsint $0x10
ret
清屛函数使用BIOS 0x10号中断,ah=0x06,al=0x00表示上滚所有行,即清屛。
2.set_video_mode
# function to set video mode
set_video_mode:xorw %ax, %axmovw %ax, %dsmovw %ax, %esmovw $0x800, %di # buffer# check vbemovw $0x4f00, %axint $0x10cmp $0x004f, %axjne set_vga_0x13movw 0x04(%di), %axcmp $0x0200, %ax # vbe version < 2.0jb set_vga_0x13# check vbe mode 0x118movw $0x118, %cxmovw $0x4f01, %axint $0x10cmpb $0x00, %ah # call failedjne set_vga_0x13cmpb $0x4f, %al # not support this modejne set_vga_0x13movw (%di), %axandw $0x0080, %ax # not support Linear Frame Buffer memory modeljz set_vga_0x13# save video infomovw $0x118, video_modemovw 0x12(%di), %axmovw %ax, screen_xmovw 0x14(%di), %axmovw %ax, screen_ymovb 0x19(%di), %almovb %al, bits_per_pixelmovb 0x1b(%di), %almovb %al, memory_modelmovl 0x28(%di), %eaxmovl %eax, video_ram#set vbe modemovw $0x118, %bxaddw $0x4000, %bxmovw $0x4f02, %axint $0x10retset_vga_0x13:movb $0, %ahmovb $0x13, %alint $0x10ret
该函数首先指定了一块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, %simovb $0x42, %ahmovb $0x80, %dlint $0x13ret# function to load the kernel
load_kernel:lea disk_addr_packet, %simovw $TMP_KERNEL_ADDR>>4,6(%si)xorw %cx, %cx1:call read_a_sect_hdlea disk_addr_packet, %simovl 8(%si), %eaxaddl $0x01, %eaxmovl %eax, (disk_addr_packet + 8)movl 6(%si), %eaxaddl $512>>4, %eaxmovl %eax, (disk_addr_packet + 6)incw %cxcmpw $KERNEL_SECT_NUM+1, %cxjne 1b# move first sector(the loader) to 0x0000cld # si, di incrementmovw $TMP_KERNEL_ADDR>>4, %axmovw %ax, %ds # DS:SI srcxorw %si, %simovw $0x00, %axmovw %ax, %es # ES:DI dstxorw %di, %dimovw $(LOADER_SECT_NUM*SECT_SIZE) >> 2, %cx # 512/4 times rep movsl # 4 bytes per timeret
该函数主要功能是利用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, %axmovw %ax, %ds # DS:SI srcleaw gdt, %simovw $BOOT_INFO_SEG, %axmovw %ax, %es # ES:DI dstxorw %di, %dimovw $(GDT_SIZE+VIDEO_INFO_SIZE),%cx # num of bytes to moverep movsbret
这个函数比较简单,主要功能是把前面保存下来的信息拷贝到一个安全的位置,babyos2把这些数据拷贝到了0x90000。内核启动后会用到这些数据,比如显存位置,内存信息等。
begin_protected_mode:
# function to begin protected mode
begin_protected_mode:cli
1:inb $0x64, %altestb $0x02, %aljnz 1bmovb $0xd1, %aloutb %al, $0x642:inb $0x64, %altestb $0x02, %aljnz 2bmovb $0xdf, %aloutb %al, $0x60lgdt gdt_ptrmovl %cr0, %eaxorl $CR0_PE, %eaxmovl %eax, %cr0ljmp $SEG_KCODE<<3, $0ret
这个函数将会跳转到保护模式。
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 mainmain:movl $DATA_SELECTOR, %eaxmovw %ax, %dsmovw %ax, %esmovw %ax, %fsmovw %ax, %gsmovw %ax, %ssmovl $STACK_PM_BOTTOM, %espcall 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的加载,后面再描述。