文档章节

跟我一起写操作系统(二)——史上最简单的内核

wangxuwei
 wangxuwei
发布于 2017/08/01 23:32
字数 2399
阅读 43
收藏 2

  项目地址:https://github.com/lucasysfeng/lucasOS

  上一讲我们介绍了计算机的启动流程,并给出了一份简单的主引导记录代码,此份代码仅仅是显示几个字符,并没有做它本应该做的事--启动内核。本讲我们首先看下内核是如何被启动的,然后写一个简单的内核,用已经实现的主引导记录配合GRUB启动它。

如何启动内核

  前一讲我们说到,计算机读取"主引导记录"前面446字节的机器码之后,会运行事先安装的“启动管理器”bootloader,由用户选择启动哪个内核,之后就会载入内核,将控制权交给内核。GNU GRUB(GRand Unified Bootloader)就是一种bootloader,满足多重引导规范(The Multiboot Specification),GRUB可选择操作系统分区上的不同内核,下图就是GRUB的图形界面:

图 GRUB界面

  能够被GRUB启动的内核需要满足两个的条件:

(1) 内核的前8K字节内必须要包含多重引导规范的头信息(Multiboot Header);
(2) 内核要加载在内存地址的1MB以上。

  那么Multiboot Header是什么样子的呢?它必须包含4字节对齐的3个域(还有其他非必须域,我们不讨论),如下:

魔数域(magic):标志头的魔数,必须等于 0x1BADB002。。
标志域(flag):是否需要引导程序支持某些特性,我们不关心这些特性,这个标志置为0。
校验域(checksum):校验等式是否成立(magic + flags + checksum = 0)

     本文不讨论GRUB的实现,我们会用前人已经写好的GRUB(笔者会给出),我们要做的是完成符合GRUB启动规范的内核。为了完成这个内核,我们需要写少量的汇编用来在内核中加入Multiboot Header,然后用C语言写内核入口,最后将汇编目标代码和C语言目标代码链接起来生成真正的内核。下面就让我们一步步地完成这些吧!

第一步 汇编入口

  1. 汇编代码如下:

MBOOT_MAGIC  equ 0x1BADB002  ; multiboot magic域,必须为此值
MBOOT_FLAGS  equ 0x00        ; multiboot flag域, GRUB启动时是否要做一些特殊操作
MBOOT_CHECKSUM  equ -(MBOOT_MAGIC + MBOOT_FLAGS) ; multiboot checksum域,校验上面两个域是否正确

[BITS 32]                    ; 以32位编译

section .text
  dd  MBOOT_MAGIC
  dd  MBOOT_FLAGS
  dd  MBOOT_CHECKSUM
  dd  start

[GLOBAL start]
[EXTERN kernel_main]         ; 内核入口函数, EXTERN表明此符号在外部定义

start:
  cli                        ; 禁用中断 
  call kernel_main           ; 调用内核入口函数
  jmp $                      ; 无限循环

  在上面汇编中,我们定义了GRUB启动需要的域MBOOT_MAGIC、MBOOT_FLAGS和MBOOT_CHECKSUM,并调用了内核入口函数kernel_main, kernel_main下一节实现。

2. 编译生成目标文件boot.o

  $nasm -f elf boot.asm -o boot.o

运行上面命令后会生成目标文件boot.o,-f  elf的意思是生成ELF格式的目标代码。

第二步 内核入口

  1. 内核代码如下:

/****************************************************
# File        : kernel.c
# Blog        : www.cnblogs.com/lucasysfeng
# Author      : lucasysfeng
# Description : 内核入口函数
****************************************************/

int kernel_main()
{ 
    // 显存开始地址 
    char *display_buf = (char*)0xb8000;         

    // 清屏 
    unsigned int i = 0;
    const unsigned int total = 80 * 25 * 2;      // 一屏25行,每行80个字符,每个字符2个字节
    while(i < total) 
    {
        display_buf[i++] = ' ';
        display_buf[i++] = 0x04;                 // 颜色 
    }

    // 显示字符
    const char *str = "Hello World, welcome to mykernel!";
    for (i = 0; '\0' != *str;)
    {
        display_buf[i++] = *(str++);
        display_buf[i++] = 0x04;
    }

    return 0;
}

 0xb8000h是显存开始的地址,读者可以看第一讲(http://www.cnblogs.com/lucasysfeng/p/4846119.html)“实模式内存地址空间分布”那张图,找到0xb8000h这个地址。从0xb8000h这个地址开始,每2个字节表示一个字符,前一个字节是字符的ASCII码,后一个字节是这个字符的颜色和属性,颜色和属性此处先不用关心。这段C代码的其余部分相信读者都能看得懂,我就不过多解释了。 

2. 编译生成目标文件kernel.o

  $gcc -m32 -c -o kernel.o kernel.c  

运行上面命令后,目标文件kernel.o就生成了。

 

第三步 生成内核

 上面讲到了能被GRUB启动的内核需要满足的条件:

(1) 内核的前8K字节内必须要包含多重引导规范的头信息(Multiboot Header);
(2) 内核要加载在内存地址的1MB以上。

  我们将头信息放在了汇编生成的目标文件boot.o中,因此我们需要将boot.o和kernel.o链接到一起生成真正的kernel,并且这个真正的内核要加载到1MB内存上,为此,我们需要下面的链接脚本和命令(关于链接脚本的使用自行google,笔者的另一篇文章《链接到底干了什么》可以参考):

/***************************
* 文件名: link.ld   
***************************/

ENTRY(start)
SECTIONS
{
	. = 0x100000;

	.text :
	{
		*(.text)
		. = ALIGN(4096);
	}
	.data :
	{
		*(.data)
		*(.rodata)
		. = ALIGN(4096);
	}
}

  我们用ld命令链接目标文件boot.o和kernel.o,指明使用链接脚本link.ld:

$ ld -T link.ld -m elf_i386 -nostdlib boot.o kernel.o -o kernel

  运行上面命令后,会生成我们要启动的真正的内核kernel,那么这个kernel是否满足GRUB启动规范呢?我们可以通过反汇编来看一下:

$ objdump -d  kernel | head -n30

  结果如下图所示,我们看到100000了吗,这个就是起始的地址即1M,看到02 b0 ad 1b 00 00了吗,这个就是GRUB魔数域1b ad b0 02(大小端问题,反向存储)

$ objdump -d  kernel | head -n30

kernel:     文件格式 elf32-i386


Disassembly of section .text:

00100000 <start-0x10>:
  100000:	02 b0 ad 1b 00 00    	add    0x1bad(%eax),%dh
  100006:	00 00                	add    %al,(%eax)
  100008:	fe 4f 52             	decb   0x52(%edi)
  10000b:	e4 10                	in     $0x10,%al
  10000d:	00 10                	add    %dl,(%eax)
	...

00100010 <start>:
  100010:	fa                   	cli    
  100011:	bc 03 80 00 00       	mov    $0x8003,%esp
  100016:	bd 00 00 00 00       	mov    $0x0,%ebp
  10001b:	83 e4 f0             	and    $0xfffffff0,%esp
  10001e:	89 1d 00 a0 10 00    	mov    %ebx,0x10a000
  100024:	e8 03 00 00 00       	call   10002c <kernel_main>

00100029 <stop>:
  100029:	f4                   	hlt    
  10002a:	eb fd                	jmp    100029 <stop>

0010002c <kernel_main>:
  10002c:	55                   	push   %ebp
  10002d:	89 e5                	mov    %esp,%ebp
  10002f:	83 ec 08             	sub    $0x8,%esp

 

第四步 将内核拷贝到软盘镜像

  我们这里不制作软盘镜像,而是使用已经制作好的软盘镜像,镜像名称lucasOS.img,已经放在github上了。我们也无需制作GRUB,这个软盘镜像已经包含了GRUB.我们要做的是把内核文件kernel拷贝到软盘镜像lucasOS.img中。

1. 获取lucasOS.img软盘镜像。lucasOS目录下的lucasOS.img就是我们要的软盘镜像。

$ git clone https://github.com/lucasysfeng/lucasOS.git

$cd lucasOS/code/chapter2

2. 创建挂载点。

    mkdir lucasOS

3. 挂载软盘镜像。注意把lucasOS.img改为你的lucasOS.img所在路径。

   sudo mount lucasOS.img lucasOS     

4. 把内核文件拷贝到软盘镜像中。注意把kernel改为你的kernel所在路径。

   sudo cp kernel lucasOS/kernel          

5. 卸载软盘镜像。

    sudo umount mnt/lucasOS

上面步骤可写成Makefile(your-rootpasswd改为你的sudo密码)如下:

CC = gcc
ASM = nasm
LD = ld
# 这里挂载点采用相对路径,即当前目录下的lucasOS
MOUNT_POINT = lucasOS

CC_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-builtin -fno-stack-protector
LD_FLAGS = -T link.ld -m elf_i386 -nostdlib

all: boot.o kernel.o link update_kernel

boot.o: boot.asm
	@echo '编译boot, 生成GRUB需要的信息..'
	$(ASM) -f elf boot.asm

kernel.o: kernel.c
	@echo '编译kernel..'
	$(CC) $(CC_FLAGS) kernel.c -o kernel.o

.PHONY: kernel
link:
	@echo '链接boot和kernel, 生成最终kernel..'
	$(LD) $(LD_FLAGS) boot.o  kernel.o -o kernel

.PHONY: update_kernel
update_kernel:
	@echo '将kernel拷贝到软盘镜像..'
	@if [ ! -d $(MOUNT_POINT) ]; then mkdir $(MOUNT_POINT); fi  # 挂载点不存在则创建
	echo "your-rootpasswd" | sudo -S mount lucasOS.img $(MOUNT_POINT)
	sudo cp kernel $(MOUNT_POINT)/kernel
	sleep 1
	sudo umount $(MOUNT_POINT)

.PHONY: clean
clean:
	rm *.o kernel

第五步 启动内核

   用bochs,bochsrc配置如下(需要根据bochs安装路径改变):

###############################################################
# Configuration file for Bochs
###############################################################

# how much memory the emulated machine will have
 megs: 32

# filename of ROM images
 romimage: file=/opt/local/share/bochs/BIOS-bochs-latest #/opt/share/bochs/BIOS-bochs-latest
 vgaromimage: file=/opt/local/share/bochs/VGABIOS-lgpl-latest #/usr/share/vgabios/vgabios.bin

# what disk images will be used
 floppya: 1_44=lucasOS.img, status=inserted

# choose the boot disk.
 boot: floppy

# where do we send log messages?
# log: bochsout.txt

# disable the mouse
 mouse: enabled=0

# enable key mapping, using US layout as default.
# keyboard_mapping: enabled=1, map=/opt/local/share/bochs/keymaps/x11-pc-us.map
  keyboard: keymap=/opt/local/share/bochs/keymaps/x11-pc-us.map

也可用vbox来启动:

创建的虚拟机lucasOS需要添加软驱控制器如下图:

 

然后添加虚拟软盘选中lucasOS.img,这样就可以启动了,并且有GRUB效果:

 

代码获取

  本系列GitHub地址 https://github.com/lucasysfeng/lucasOS,用下面命令获取代码:

git clone https://github.com/lucasysfeng/lucasOS.git

  本讲的代码是code/chapter2,笔者已经将上面的命令集成到Makefile中了,读者只需进入目录,按ReadMe.txt说明执行即可,有问题请留言。

类似项目hurlex

 git clone https://github.com/hurley25/hurlex-doc.git

下载后Makefile修改如下,bochsrc如前面

#!Makefile
#
# --------------------------------------------------------
#
#    hurlex 这个小内核的 Makefile
#    默认使用的C语言编译器是 GCC、汇编语言编译器是 nasm
#
# --------------------------------------------------------
#

# patsubst 处理所有在 C_SOURCES 字列中的字(一列文件名),如果它的 结尾是 '.c',就用 '.o' 把 '.c' 取代
C_SOURCES = $(shell find . -name "*.c")
C_OBJECTS = $(patsubst %.c, %.o, $(C_SOURCES))
S_SOURCES = $(shell find . -name "*.s")
S_OBJECTS = $(patsubst %.s, %.o, $(S_SOURCES))

CC = gcc
LD = ld
ASM = nasm
MOUNT_POINT = ../kernel
C_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-builtin -fno-stack-protector -I include
LD_FLAGS = -T scripts/kernel.ld -m elf_i386 -nostdlib
ASM_FLAGS = -f elf -g -F stabs

all: $(S_OBJECTS) $(C_OBJECTS) link update_image

# The automatic variable `$<' is just the first prerequisite
.c.o:
	@echo 编译代码文件 $< ...
	$(CC) $(C_FLAGS) $< -o $@

.s.o:
	@echo 编译汇编文件 $< ...
	$(ASM) $(ASM_FLAGS) $<

link:
	@echo 链接内核文件...
	$(LD) $(LD_FLAGS) $(S_OBJECTS) $(C_OBJECTS) -o hx_kernel

.PHONY:clean
clean:
	$(RM) $(S_OBJECTS) $(C_OBJECTS) hx_kernel

.PHONY:update_image
update_image:
	@echo '将kernel拷贝到软盘镜像..'
	@if [ ! -d $(MOUNT_POINT) ]; then mkdir $(MOUNT_POINT); fi  # 挂载点不存在则创建
	echo "wxwpxh" | sudo -S mount floppy.img $(MOUNT_POINT)
	sudo cp hx_kernel $(MOUNT_POINT)/hx_kernel
	sleep 1
	sudo umount $(MOUNT_POINT)

.PHONY:mount_image
mount_image:
	sudo mount floppy.img $(MOUNT_POINT)

.PHONY:umount_image
umount_image:
	sudo umount $(MOUNT_POINT)

.PHONY:qemu
qemu:
	qemu -fda floppy.img -boot a

.PHONY:bochs
bochs:
	bochs -f scripts/bochsrc.txt

.PHONY:debug
debug:
	qemu -S -s -fda floppy.img -boot a &
	sleep 1
	cgdb -x scripts/gdbinit

参考

 1. http://www.jamesmolloy.co.uk/tutorial_html/2.-Genesis.html

本文转载自:http://blog.csdn.net/judyge/article/details/52281490

共有 人打赏支持
wangxuwei
粉丝 25
博文 335
码字总数 117394
作品 0
杭州
其他
私信 提问
史上最简单的 Mybatis 教程 · demo 代码

如题, 博主已经将 “史上最简单的 Mybatis 教程”系列博文重新整理了一遍,并且连同代码一起上传到 GitHub 中啦! 欢迎大家在 GitHub 上 Follow 博主,以及 Fork、Star、Watch 该项目! 当然...

qq_35246620
2017/04/09
0
0
史上最简单的 Spring MVC 教程

1 前言   spring MVC 属于 SpringFrameWork 的后续产品,已经融合在 Spring Web Flow 里面。Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块,而 Spring MVC 就是其中最优秀的 MVC ...

qq_35246620
2017/01/25
0
0
史上最简单的 Spring MVC 教程 · demo 代码

如题, 博主已经将 “史上最简单的 Spring MVC 教程”系列博文重新整理了一遍,并且连同代码一起上传到 GitHub 中啦! 欢迎大家在 GitHub 上 Follow 博主,以及 Fork、Star、Watch 该项目! ...

qq_35246620
2017/04/23
0
0
史上最简单的 MyBatis 教程(五)

1 前言 在史上最简单的 MyBatis 教程(一、二、三、四)中,咱们已经把 MyBatis 框架的基本内容了解的差不多啦,然而美中不足的是:在前四篇博文的示例中,咱们仅仅演示了一对一(1:1)的映射...

qq_35246620
2017/03/07
0
0
史上最简单的 Spring MVC 教程(三)

1 前言 在史上最简单的 Spring MVC 教程(二)中,咱们讲解了常见的处理器映射(handlerMapping),并给出了应用示例。在本篇博客中,咱们讲解常见的控制器(Controller),在这里有一点需要...

qq_35246620
2017/01/26
0
0

没有更多内容

加载失败,请刷新页面

加载更多

day11

architect刘源源
今天
6
0
论学好Linux系统的超级重要性

不知道各位在日常的工作生活中有没有接触过“rm -rf /*”这个命令,因为这个命令搞出来的事情可还不少呢!前段时间就在一个群里看到了有个小伙子,老板让他去维护一下服务器,这小伙也不太懂...

Linux就该这么学
昨天
6
0
git 使用

1,首先在github配置好信息和仓库,然后在本地进行操作 git init git config user.name 'zhangwuer' git config user.email '56789053@qq.com' 2,与远程分支建立连接 git checkout -b test......

天王盖地虎626
昨天
3
0
git checkout 命令详解

在日常的git操作中,git checkout——检出,是我们的常用命令。最为常用的两种情形是创建分支和切换分支。 在下面的命令中,使用了一些简写,在这里说明一下: git st # git statusgit ci ...

shzwork
昨天
10
0
【Nginx】Nginx多级代理,获取客户端真实请求IP以及每级代理IP

Nginx多级代理,获取客户端真实请求IP以及每级代理IP 如图所示,每一级nginx里的location配置里需要加上对应的配置,最后一级nginx是直接到应用,测试时为了方便,直接用echo模块去测试,打印...

薛定谔的旺
昨天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部