在虚拟机中运行简单内核

目录

第一部分:kernel.cpp

1. printf函数的实现

 2. callConstructors函数

3. kernelMain函数 

4. 完整代码

第二部分:loader.s

1. 设置变量

2. loader

3. _stop

4. 数据段和栈

5. 完整代码 

第三部分:linker.ld

第四部分:makefile

1. 变量定义

2. 编译规则

3. 链接并生成内核二进制文件

4. 生成 ISO 镜像

5. 运行虚拟机

6. 清理规则

7. 完整代码

运行结果


内核(kernel)

        内核是操作系统的核心。内核是操作系统执行的第一道程序,被率先加载到内存中开始系统行为。内核始终保持在主内存中直到系统被关闭。内核将用户输入的命令转换成计算机硬件能理解的机器语言。在这个内核从实模式转向保护模式,内核运行在保护模式下。


第一部分:kernel.cpp

1. printf函数的实现

        现在,我想在操作系统中实现简单的输出:

printf("Hello MyOS world!");

         然而,正常C++会include<iostream>等,但是现在写操作系统将没办法使用库函数。所以pirntf函数要自己实现。

        我们现在要做的工作是在屏幕上输出字符。内存的固定的一块会对应屏幕的输出,就是说,我们如果在对应屏幕的内存上写一个字符,这个字符就会输出在屏幕上。

        代码中的0xB8000就是VGA文本模式的起始地址,指向屏幕的字符缓冲区。

static short * VideoMemory = (short*)0xB8000;

         屏幕中,一行是80个字符,一共25行,y表示行数,x表示列数。

        80 * y + x 就是在表示这个字符要写在哪个位置(即字符要显示的位置)。

        在这段显示内存中,表示屏幕上的一个字符,需要两个字节:第一个字节表示这个字符的前景、背景、颜色(属性);第二个字节就是这个字节的内容。

        & 0xFF00:这个部分保留当前字符位置的前景和背景颜色(即高字节),用位于操作(&)清除低字节(字符部分)。FF表示黑底白字的属性设置。

        |str[i]使用位或操作(|)将新字符str[i]加入到当前字符位置,即低地址(表示要输出的字符)。

for(int i = 0; str[i] != '\0'; ++i)
{
    VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00) | str[i];
    ++x;
}

       完整的printf函数的实现如下:

void printf(const char * str)
{   
    static short * VideoMemory = (short*)0xB8000;
    static char x = 0, y = 0;
    for(int i = 0; str[i] != '\0'; ++i)
    {
        VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00) | str[i];
        ++x;
    }
}

2. callConstructors函数

         constructor 是一个函数指针类型,指向没有参数和返回值的函数。

          start_ctors 和 end_ctors 是外部变量,其实是在汇编定义的,所以用extern。(在链接文件linker中写的)功能是指向构造函数的起始和结束位置。在编译时,链接器会将构造函数放在这两个标记之间,所以说其实是在构造函数的范围。读下面callConstructors函数就会更加清楚。

typedef void(*constructor)();
extern constructor start_ctors;
extern constructor end_ctors;

        如果不加extern “C”,系统会自动给他改名(如_Z61callConstructors)。因为要确保跟loader.o里面的函数的名字是对应上的,才可以正常调用,所以要加个extern “C”,不让他改名。

         callConstructors函数遍历构造函数并调用它们,通常用于初始化全局变量。这里需要调用一些函数,这些函数要用一些函数指针。

extern "C" void callConstructors()
{
    for(constructor * i = &start_ctors; i != &end_ctors; ++i)
        (*i)();
}

3. kernelMain函数 

        这里不做太多的工作,就输出一段话。传递参数multiboo_structure 和 magicnumber,通常用于多重引导协议。(这两个参数我不是很懂)        

        这里不能return,因为内核程序会一直运行,关机的时候才会结束。所以要无限循环,使内核保持运行,防止程序结束。

extern "C" void kernelMain(void * multiboo_structure, int magicnumber)
{
    printf("Hello MyOS world!");
    while(1);
}

4. 完整代码

void printf(const char * str)
{   
    static short * VideoMemory = (short*)0xB8000;
    static char x = 0, y = 0;
    for(int i = 0; str[i] != '\0'; ++i)
    {
        VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00) | str[i];
        ++x;
    }
}

typedef void(*constructor)();
extern constructor start_ctors;
extern constructor end_ctors;

extern "C" void callConstructors()
{
    for(constructor * i = &start_ctors; i != &end_ctors; ++i)
        (*i)();
}

extern "C" void kernelMain(void * multiboo_structure, int magicnumber)
{
    printf("Hello MyOS world!");
    while(1);
}

第二部分:loader.s

1. 设置变量

        首先,设置MAGIC变量,这是grub定义的,grub要认识这几个字节才能引导程序。MAGIC的数据是固定的,用于指示内核是以多重引导的方式加载。

.set MAGIC, 0x1badb002

        FLAGS代表多重引导标志,这些标志制定了内核的加载选项,如是否使用内存信息、是否支持模块等。        
        1<<0表示将第0位设置成1,1<<1表示将第1位设置成1,其他位保持不变

.set FLAGS, (1 << 0 | 1 << 1)

        CHECKNUM用作检查数字,用于验证加载程序的有效性或完整性。grub会检查CHECKNUM、MAGIC、FLAGS加起来等于0,所以把CHECKNUM设为MAGIC和FLAGS加起来的负数。

.set CHECKNUM, -(MAGIC + FLAGS)

        定义了一个新的段,名为.multiboot,这是多重引导协议所需的一个特定段。multiboot是指多重引导。

.section .multiboot
    .long MAGIC
    .long FLAGS
    .long CHECKNUM

        text section表示代码段。

.section .text

        extern是外部符号,意味着kernelMain和callConstructors函数在其他模块中定义,引导程序会调用这些函数。要在callConstructors进行全局变量的初始化。下面是对这两个函数进行声明:

.extern kernelMain
.extern callConstructors

        说明指示链接器loader符号是全局的,意味着可以被其他模块访问。程序运行的时候是从loader开始运行的。

.global loader

        完整代码如下:

.set MAGIC, 0x1badb002
.set FLAGS, (1 << 0 | 1 << 1)
.set CHECKNUM, -(MAGIC + FLAGS)

.section .multiboot
    .long MAGIC
    .long FLAGS
    .long CHECKNUM

.section .text

.extern kernelMain
.extern callConstructors

.global loader

2. loader

        首先,初始化。

        将内核栈的地址传递到esp栈指针寄存器,即初始化栈。

mov $kernel_stack, %esp

        调用callConstructors函数,对全局变量进行初始化,这是在第一部分的kernel.cpp的函数。

call callConstructors

        将%eax和%ebx寄存器的值压入栈,以便在调用kernelMain之后可以恢复这些值。

push %eax
push %ebx

        之后,需要调用内核的开始函数,这也是在第一部分的kernel.cpp的函数。

call kernelMain

3. _stop

        这是停止程序,定义了一个无限循环。

        cli表示禁用中断。

        执行hlt指令使CPU进入休眠状态,直到接收到中断。

        jmp _stop 表示继续循环,确保程序不会意外地继续执行。

_stop:
    cli
    hlt
    jmp _stop

4. 数据段和栈

        表示定义一个数据段(这里没有初始化,通常用于分配位),并且将大小定为2M(两兆)。

.section .bss
.space 2*1024*1024

        栈是在这里,使用栈时,从高地址向低地址使用,所以栈空间在上面,栈的开始地址在下面。栈是从高地址向低地址增长,通常在高地址部分分配占空间。

kernel_stack: 

5. 完整代码 

.set MAGIC, 0x1badb002
.set FLAGS, (1 << 0 | 1 << 1)
.set CHECKNUM, -(MAGIC + FLAGS)

.section .multiboot
    .long MAGIC
    .long FLAGS
    .long CHECKNUM

.section .text

.extern kernelMain
.extern callConstructors

.global loader

loader:
    mov $kernel_stack, %esp
    
    call callConstructors

    push %eax
    push %ebx

    call kernelMain

_stop:
    cli
    hlt
    jmp _stop

.section .bss
.space 2*1024*1024

kernel_stack: 

第三部分:linker.ld

        编译出来是obj文件,kernel.cpp文件编译出来也是obj文件,我们要将这两个文件连接起来,所以需要linker.ld。

1. ENTRY

        首先,指定程序的开始的地方是loader。

ENTRY(loader)

 2. OUTPUT      

         指定输出文件的格式为 ELF(Executable and Linkable Format),并且是 32 位 x86 格式。

OUTPUT_FORMAT(elf32-i386)

        第一个 i386 表示生成的代码将用于 x86 32 位架构。第二个 i386 表示输出文件的目标架构同样是 x86 32 位。

OUTPUT_ARCH(i386:i386)

3. 段布局(SECTIONS)         

        下面是段定义SECTIONS,定义一个段的集合。

3.1 加载地址

        首先,将当前位置设置在内存的1M的位置,这是内核加载的起始地址。

. = 0x100000; 

3.2 .text段

        *(.multiboot) 表示在loader.s中定义的multiboot。

        *(.text*)表示.text开头的段合并到这里。

        *(.rodata)表示其他的只读数据段合并到这里。

.text :
{
    *(.multiboot)  
    *(.text*)   
    *(.rodata) 
}

3.3 .data段

        start_ctros的值为当前地址(此时指向 .data段的起始位置),用于标记构造函数的开始。(kernel.cpp里出现过)

start_ctors = .;  

        下面代码表示保持所有输入文件中的 .init_array 段。这个段通常包含在程序启动时需要执行的初始化函数指针。(所有的目标文件中的有关全局变量的数据)

KEEP(*(.init_array))

        保持所有以 .init_array. 开头的段,并按初始化优先级排序。(全局变量需要默认初始化的方式)

KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*)))

        end_ctros的值为当前地址(此时指向 .data 段的结束位置),用于标记构造函数的结束。(同样,kernel.cpp里出现过)

end_ctors = .;

        将所有输入文件中的 .data 段合并到此处。(其他的数据段)

*(.data)

        .data数据段的完整代码:

.data :
    {
        start_ctors = .;  
        KEEP(*(.init_array))
        KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*)))
        end_ctors = .;
        
        *(.data)
    }

3.4 .bss段

        下面定义一个未初始化的数据段,包含未初始化的全局和静态变量。*(.bss) 表示将所有输入文件中的 .bss 段合并到此处。

.bss :
    {
        *(.bss) 
    }

 3.5 /DISCARD/段 

        最后定义一个丢弃段,这里的内容在链接时会被丢弃(抛弃一些段)。

        所有以 .fini_array 开头的段将被丢弃。这个段通常包含在程序退出时需要执行的清理函数指针。

        所有输入文件中的 .comment 段也会被丢弃。通常用于存储版本信息等非必要的注释。

/DISCARD/ :
    {
        *(.fini_array*)
        *(.comment)
    }

4. 完整代码

ENTRY(loader)

OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386:i386)

SECTIONS
{
    . = 0x100000;   

    .text :
    {
        *(.multiboot)   
        *(.text*)  
        *(.rodata) 
    }

    .data :
    {
        start_ctors = .;  
        
        KEEP(*(.init_array))
        KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*)))
        
        end_ctors = .;
        
        *(.data)
    }

    .bss :
    {
        *(.bss) 
    }

    /DISCARD/ :
    {
        *(.fini_array*)
        *(.comment)
    }
}

第四部分:makefile

        ubuntu编译时需要用到makefile文件。

1. 变量定义

        以下是传递给 g++ 编译器的参数,主要用于确保生成 32 位代码并禁用一些 C++ 特性。
        -m32: 指定生成 32 位代码
        -fno-use-cxa-atexit: 禁用 C++ 的 atexit 功能。
        -nostdlib: 不链接标准库。
        -fno-builtin: 禁用内置函数。
        -fno-rtti: 禁用运行时类型识别。
        -fno-exceptions: 禁用异常处理。
        -fno-leading-underscore: 禁用在符号前添加下划线的行为。

GPPPARAMS = -m32 -fno-use-cxa-atexit -nostdlib -fno-builtin -fno-rtti -fno-exceptions -fno-leading-underscore

       传递给汇编器的参数,用于确保生成 32 位代码。

ASMPARAMS = --32

        这是传递给链接器的参数,用于指定输出格式。
        -melf_i386: 指定生成 ELF 格式,适用于 32 位 x86 架构。

LDPARAMS = -melf_i386

2. 编译规则

        这行定义了一个目标文件列表,包含kernel.o 和 loader.o,这两个文件是编译后生成的目标文件。

objects = kernel.o loader.o

        %是来替换输入,就是说因为我们有kernel.cpp,所以当编译kernel.cpp的时候%会变替换成kernel
        使用g++编译源文件$< 生成目标文件$@, -c参数表示只编译而不链接
        $(GPPPARAMS): 使用之前定义的编译参数。
        $<表示输入文件,是依赖于上面写的%.cpp。

%.o: %.cpp
	g++ $(GPPPARAMS) -o $@ -c $<

        这条规则表示如何从.s文件生成.o文件。
        使用汇编器as将汇编源文件$< 编译为目标文件$@。
        因为我的ubuntu是64位机,所以默认编译成64位,编译的时候需要参数ASMPARAMS。

%.o: %.s
	as $(ASMPARAMS) -o $@ $<

3. 链接并生成内核二进制文件

        mykernel.bin是目标文件(可执行文件)。
        linker.ld是链接脚本文件,定义了内存布局和如何将目标文件链接在一起。
        $(objects)之前定义的目标文件列表。
        ld: 使用链接器。
        -T $<: 指定链接脚本文件($< 代表依赖文件)。
        -o $@: 指定输出文件为 mykernel.bin($@ 代表目标文件)。
        mykernel.bin需要依赖于linker.ld和objects。

mykernel.bin: linker.ld $(objects)
	ld $(LDPARAMS) -T $< -o $@ $(objects)

4. 生成 ISO 镜像

        创建光盘镜像文件。
        定义一个目标,表示要生成的iso文件。这会依赖于mykernel.bin,意味着在生成iso文件之前,必须先生成mykernel.bin文件。

mykernel.iso: mykernel.bin

        创建一个名为iso的目录,用于存放iso文件结构。

mkdir iso

         在iso目录下创建一个boot子目录,通常这个目录包含启动加载器和内核文件。

mkdir iso/boot

        在 boot 目录下创建一个 grub 子目录,用于存放 GRUB 启动加载器的配置文件。

mkdir iso/boot/grub

        将 mykernel.bin 复制到 iso/boot 目录中。$< 表示依赖文件,这里是 mykernel.bin。

cp $< iso/boot

        向 GRUB 配置文件中写入一行,设置启动菜单的超时时间为 0 秒。意味着如果没有选择,系统会立即启动默认项。

        iso/boot/grub/grub.cfg: 将这一行(timeout=0)追加到 grub.cfg 文件中。如果文件不存在,则会创建它。

echo 'set timeout = 0' >> iso/boot/grub/grub.cfg

        设置 GRUB 启动菜单的默认项为第一个条目(索引为 0),这通常是内核。(因为我们只有一个系统)

echo 'set default = 0' >> iso/boot/grub/grub.cfg

        向 grub.cfg 文件中写入一个空行,增加可读性。

echo '' >> iso/boot/grub/grub.cfg

        定义一个启动菜单项,菜单项的名称为 "My Operating System ..."。

        {: 表示这个菜单项的内容将从这里开始。

echo 'menuentry "My Operating System ..." {' >> iso/boot/grub/grub.cfg

        指示 GRUB 启动操作系统。

        同样前面有制表符: 继续属于上面定义的菜单项。

echo '	boot' >> iso/boot/grub/grub.cfg

        echo '}': 结束之前定义的菜单项内容。

echo '}' >> iso/boot/grub/grub.cfg

        grub-mkrescue: 使用 GRUB 工具创建 ISO 映像。

        --output=$@: 指定输出文件为目标文件(这里是 mykernel.iso)。

        iso: 指定输入的目录结构,即包含内核和 GRUB 配置的目录。

grub-mkrescue --output=$@ iso

        清理临时创建的 iso 目录,删除它及其所有内容,以避免后续编译时的混淆。

rm -rf iso

5. 运行虚拟机

        run: 目标,用于运行生成的 ISO 文件。

        依赖于: mykernel.iso,确保在运行前生成该文件。

        virtualboxvm: 启动 VirtualBox 虚拟机。这里的Tuitorial是我创建的虚拟机的名字。

        这段命令只不过是为了方便更快速启动虚拟机(毕竟运行一行代码就能启动虚拟机了)

run: mykernel.iso
	virtualboxvm --startvm "Tuitorial" &

6. 清理规则

        .phony声明clean是一个虚拟目标,make clean的时候会执行下面的代码。

        因为如果没有及时的删除已经导出过的.bin或者.iso,会显示不用更新,还要手动删除。为了防止这个,还是通过make clean命令来直接删除会比较方便。

.phony: clean
clean:
	rm -rf $(objects) mykernel.bin mykernel.iso

7. 完整代码

GPPPARAMS = -m32 -fno-use-cxa-atexit -nostdlib -fno-builtin -fno-rtti -fno-exceptions -fno-leading-underscore
ASMPARAMS = --32
LDPARAMS = -melf_i386

objects = kernel.o loader.o

%.o: %.cpp
	g++ $(GPPPARAMS) -o $@ -c $<
%.o: %.s
	as $(ASMPARAMS) -o $@ $<

mykernel.bin: linker.ld $(objects)
	ld $(LDPARAMS) -T $< -o $@ $(objects)

mykernel.iso: mykernel.bin
	echo 'set timeout = 0' >> iso/boot/grub/grub.cfg
	echo 'set default = 0' >> iso/boot/grub/grub.cfg
	echo '' >> iso/boot/grub/grub.cfg
	echo 'menuentry "My Operating System ..." {' >> iso/boot/grub/grub.cfg
	echo '	multiboot /boot/mykernel.bin' >> iso/boot/grub/grub.cfg
	echo '	boot' >> iso/boot/grub/grub.cfg
	echo '}' >> iso/boot/grub/grub.cfg
	grub-mkrescue --output=$@ iso
	rm -rf iso

run: mykernel.iso
	virtualboxvm --startvm "Tuitorial" &

.phony: clean
clean:
	rm -rf $(objects) mykernel.bin mykernel.iso


运行结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值