Featured image of post 《操作系统真象还原》第五章(二)—— 加载内核

《操作系统真象还原》第五章(二)—— 加载内核

本文介绍了程序加载到内存是如何开始工作的以及如何加载内核

本章节所有代码托管在miniOS_32

章节任务介绍

任务简介

上一节,我们介绍了如何开启分页机制,从而让内核能够在分页机制下工作

本节我们将在正式书写内核之前完成loader的最后一项任务——加载内核

本章的主要任务有:

  1. 将可执行内核程序从磁盘加载到内存
  2. 读取可执行内核程序的elf header,从中读取内核程序的代码段和数据段等内容,并将其加载到内存的指定位置

内存布局

本节我们的任务完成后,loader的使命也就告一段落了,以下是本节完成后目前的内存布局

image-20241215000251778

前置知识

目标文件的分类

  • 静态链接文件::这类文件通常是 .a.o 文件,包含已经静态链接的代码和数据,可以被其他程序引用并最终生成可执行文件。
  • 动态链接文件::这类文件通常是 .so 文件(共享对象文件),它们不包含完整的程序代码,而是依赖于运行时加载共享库(共享对象)。
  • 可执行文件:最终生成的可执行文件,通常是 .out 或类似的格式,包含了可以被操作系统加载和执行的程序代码。

ELF与可执行文件的组成

ELF:即Executable and Linkable Format,可执行文件和目标文件的一种标准格式。它仅仅代表一种文件格式,类似与windows的exe格式,在linux中,目标文件都按照ELF文件格式进行组织.

程序是由段(如代码段、数据段)组成的,而段是由节组成的(如.text节、.data节、.bss节等),因此在程序头中要有一个段头表(程序头表)和节头表来描述程序中各种段及节的信息,故

  • 程序头表:也称段头表,用于记录程序中的各个段的信息
  • 节头表:其内元素用于描述程序中的各个节

由于程序头(段头)和节头的数量不固定,因此程序头表和节头表的大小也就不固定,因此需要一个数据结构来说明程序头表和节头表的大小和位置信息,这个数据结构就是elf header,这就是elf header的由来

故,一个目标文件按照ELF格式进行组织,其主要组成为

  • ELF header
  • 程序体(代码和数据等)

ELF header就是文件头,记录了程序体的相关信息(如程序的入口地址、各种段的信息、符号信息等),并非代码也并非数据,是无法执行的,因此当一个程序加载到内存后,需要根据ELF header抽取出程序的代码和数据,然后将其放到指定内存处才能真正执行代码

elf header详解

以下是一个典型的elf可执行目标文件组成

image-20241214205230981

可以看到,本质上程序是由各种节组成的,其中.init、.text、.rodata组成了代码段,.data、.bss组成了数据段,还有其他的一些段

以下是这些节的概念

  • **.text:**已编译程序的机器代码。
  • .rodata只读数据,比如 printf 语句中的格式串和开关语句的跳转表。
  • .data已初始化的全局和静态 C 变量局部 C 变量在运行时被保存在栈中,既不岀现在 .data 节中,也不岀现在 .bss 节中。
  • .bss未初始化的全局和静态 C 变量,以及所有被初始化为 0 的全局或静态变量在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为 0。
  • .symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过 -g 选项来编译一个程序,才能得到符号表信息。实际上,每个可重定位目标文件在 .symtab 中都有一张符号表(除非程序员特意用 STRIP 命令去掉它)。然而,和编译器中的符号表不同,.symtab 符号表不包含局部变量的条目。
  • .rel.text:一个 .text 节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。
  • .rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
  • .debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的 C 源文件。只有以 - g 选项调用编译器驱动程序时,才 会得到这张表。
  • .line:**原始 C 源程序中的行号和 .text 节中机器指令之间的映射。只有以 -g 选项调用编译器驱动程序时,才会得到这张表。
  • .strtab:一个字符串表,其内容包括 .symtab 和 .debug 节中的符号表,以及节头部中的节名字。字符串表就是以 null 结尾的字符串的序列。

elf header组成

在程序中,elf header以结构体的形式存在,如下所示,elf header除了记录程序头表和节头表的信息外,还记录了程序的大小端、elf文件类型等信息

image-20241214211521585

  • e_ident[16]:16字节,用来表示elf字符等信息,开头的4个字节是固定不变的,是elf文件的魔数,它们分别是0x7f,以及字符串ELF的asc码:0x45,0x4c,0x46;有关e_ident[5]大小端字节序,,用 file 命令就能够查看到elf格式的可执行程序是LSB,还是MSB。 image-20241214211630394
  • e_type:2字节,指定elf目标文件的类型,可以看到,可执行文件、动态共享文件以及可重定位文件(静态链接文件)都属于ELF格式的文件,除此以外,代码出现core dump错误时生成的core文件也是一个ELF格式文件 image-20241214211756139
  • e_machine:2字节,描述elf目标文件要在那种硬件平台运行
  • e_version:4字节,版本信息
  • e_entry:占用4字节,用来指明操作系统运行该程序时,将控制权转交到的虚拟地址
  • e_phoff(program header table offset):4字节,程序头表在文件内的字节偏移量
  • e_shoff(section header table offset):4字节,节头表在文件内的偏移量
  • e_flags:4字节,指明与处理器相关的标志
  • e_ehsize:2字节,指明elf header字节大小
  • e_phentsize;2字节,指明程序头表中每个条目(entry)的字节大小,也就是每个用来描述段信息的数据结构的字节大小
  • e_phnum:2字节,程序头表中条目的数量
  • e_shentsize:2字节,节头表中每个条目的字节大小
  • e_shnum:2字节,节头表中条目的数量
  • e_shstrndx:2字节,指明string name table在节头表中的索引index

程序头表组成

同理,程序头表也是以结构体形式存在,记录了程序各段的信息

image-20241214212256256

  • p_type:4字节,程序中段的类型 image-20241214212345602
  • p_offset:4字节,本段在文件内的起始偏移地址
  • p_vaddr:4字节,本段在内存中的起始虚拟地址
  • p_paddr:4字节,暂且保留,未设定
  • p_filez:4字节,本段在文件中的大小
  • p_memsz:4字节,本段在内存中的大小
  • p_flags:4字节,指明与本段相关的标志,如本段的权限信息 image-20241214212451966
  • p_align:4字节,用来指明本段在文件和内存中的对齐方式。如果值为0或1,则表示不对齐。否则p_align应该是2的幂次数。

如何查看一个程序的elf header信息

如下,我们写一个简单的程序

1
2
3
4
5
6
int num1=10;
char c1='a';
int main(){
    int num2=20;
    char c2='b';
}

然后使用gcc编译它

1
gcc ./main.cc -o main

在Linux中,查看一个可执行文件的工具是objdump,如下我们使用objdump查看可执行程序main的头信息

1
objdump -x main

显示的信息比较多,我们分开来看

1
2
3
4
5
main:     file format elf64-x86-64
main
architecture: i386:x86-64, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x00000000000004f0

这段信息告诉我们这是一个64位的ELF文件,并且该文件是针对 64 位 x86 架构构建的,支持兼容Intel 32 位指令集,程序的入口地址是0x00000000000004f0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Program Header:
    PHDR off    0x0000000000000040 vaddr 0x0000000000000040 paddr 0x0000000000000040 align 2**3
         filesz 0x00000000000001f8 memsz 0x00000000000001f8 flags r--
  INTERP off    0x0000000000000238 vaddr 0x0000000000000238 paddr 0x0000000000000238 align 2**0
         filesz 0x000000000000001c memsz 0x000000000000001c flags r--
    LOAD off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**21
         filesz 0x00000000000007d8 memsz 0x00000000000007d8 flags r-x
    LOAD off    0x0000000000000df0 vaddr 0x0000000000200df0 paddr 0x0000000000200df0 align 2**21
         filesz 0x0000000000000225 memsz 0x0000000000000228 flags rw-
 DYNAMIC off    0x0000000000000e00 vaddr 0x0000000000200e00 paddr 0x0000000000200e00 align 2**3
         filesz 0x00000000000001c0 memsz 0x00000000000001c0 flags rw-
    NOTE off    0x0000000000000254 vaddr 0x0000000000000254 paddr 0x0000000000000254 align 2**2
         filesz 0x0000000000000044 memsz 0x0000000000000044 flags r--
EH_FRAME off    0x0000000000000694 vaddr 0x0000000000000694 paddr 0x0000000000000694 align 2**2
         filesz 0x000000000000003c memsz 0x000000000000003c flags r--
   STACK off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
         filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
   RELRO off    0x0000000000000df0 vaddr 0x0000000000200df0 paddr 0x0000000000200df0 align 2**0
         filesz 0x0000000000000210 memsz 0x0000000000000210 flags r--

我们主要看这段信息,这段信息就是我们说的elf header里的程序头表(Program Header table),再次强调

elf header里记录了程序头表和节头表的信息,程序头表记录了程序头(段)的信息,节头表记录了节的信息

观察上述程序头表的这段信息

这段信息起始就是代码段的相关信息,我们以这段信息为例:

  • off:该段在目标文件中的偏移
  • vaddr/paddr:该段在内存中的虚拟地址
  • align:对齐要求
  • filesz:目标文件中该段的大小
  • memsz:内存中该段的大小
  • flags:运行时该段的访问权限
1
2
    LOAD off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**21
         filesz 0x00000000000007d8 memsz 0x00000000000007d8 flags r-x

因此上述信息告诉我们,代码段具有可读、执行权限,开始于虚拟内存地址0x0000000000000000处,总共的内存大小是0x00000000000007d8字节

同理以下是程序的数据段信息,这段信息告诉我们,数据段具有读写权限,开始于虚拟内存地址0x0000000000200df0处,总共的内存大小是0x0000000000000228字节

1
2
    LOAD off    0x0000000000000df0 vaddr 0x0000000000200df0 paddr 0x0000000000200df0 align 2**21
         filesz 0x0000000000000225 memsz 0x0000000000000228 flags rw-

接下来我们再来看一下节的相关信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000000238  0000000000000238  00000238  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.ABI-tag 00000020  0000000000000254  0000000000000254  00000254  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .note.gnu.build-id 00000024  0000000000000274  0000000000000274  00000274  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .gnu.hash     0000001c  0000000000000298  0000000000000298  00000298  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .dynsym       00000090  00000000000002b8  00000000000002b8  000002b8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynstr       0000007d  0000000000000348  0000000000000348  00000348  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .gnu.version  0000000c  00000000000003c6  00000000000003c6  000003c6  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .gnu.version_r 00000020  00000000000003d8  00000000000003d8  000003d8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .rela.dyn     000000c0  00000000000003f8  00000000000003f8  000003f8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .init         00000017  00000000000004b8  00000000000004b8  000004b8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 10 .plt          00000010  00000000000004d0  00000000000004d0  000004d0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 11 .plt.got      00000008  00000000000004e0  00000000000004e0  000004e0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 12 .text         00000192  00000000000004f0  00000000000004f0  000004f0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 13 .fini         00000009  0000000000000684  0000000000000684  00000684  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .rodata       00000004  0000000000000690  0000000000000690  00000690  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 15 .eh_frame_hdr 0000003c  0000000000000694  0000000000000694  00000694  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .eh_frame     00000108  00000000000006d0  00000000000006d0  000006d0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 17 .init_array   00000008  0000000000200df0  0000000000200df0  00000df0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 18 .fini_array   00000008  0000000000200df8  0000000000200df8  00000df8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 19 .dynamic      000001c0  0000000000200e00  0000000000200e00  00000e00  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 20 .got          00000040  0000000000200fc0  0000000000200fc0  00000fc0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .data         00000015  0000000000201000  0000000000201000  00001000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .bss          00000003  0000000000201015  0000000000201015  00001015  2**0
                  ALLOC
 23 .comment      00000029  0000000000000000  0000000000000000  00001015  2**0
                  CONTENTS, READONLY

我们仍旧截取一些我们熟悉的节进行说明

1
2
3
 Idx Name          Size      VMA               LMA               File off  Algn
 12 .text         00000192  00000000000004f0  00000000000004f0  000004f0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE

这段信息表示,这是一个.text节,隶属于代码段,只读,在Linux程序中的索引是12(程序以索引标记一个节),节的大小是00000192

同理以下是.data节的相关信息

1
2
3
  Idx Name          Size      VMA               LMA               File off  Algn
 21 .data         00000015  0000000000201000  0000000000201000  00001000  2**3
                  CONTENTS, ALLOC, LOAD, DATA

除此以外,我们还需要知道,程序在链接的时候需要知道各种符号的信息,所谓符号就是函数名、全局变量名等,注意局部变量不是符合,因为局部变量本质上是在程序执行后存在于栈中,随着函数栈的开辟和销毁而生存和死亡

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
SYMBOL TABLE:
0000000000000238 l    d  .interp        0000000000000000              .interp
0000000000000254 l    d  .note.ABI-tag  0000000000000000              .note.ABI-tag
0000000000000274 l    d  .note.gnu.build-id     0000000000000000              .note.gnu.build-id
0000000000000298 l    d  .gnu.hash      0000000000000000              .gnu.hash
00000000000002b8 l    d  .dynsym        0000000000000000              .dynsym
0000000000000348 l    d  .dynstr        0000000000000000              .dynstr
00000000000003c6 l    d  .gnu.version   0000000000000000              .gnu.version
00000000000003d8 l    d  .gnu.version_r 0000000000000000              .gnu.version_r
00000000000003f8 l    d  .rela.dyn      0000000000000000              .rela.dyn
00000000000004b8 l    d  .init  0000000000000000              .init
00000000000004d0 l    d  .plt   0000000000000000              .plt
00000000000004e0 l    d  .plt.got       0000000000000000              .plt.got
00000000000004f0 l    d  .text  0000000000000000              .text
0000000000000684 l    d  .fini  0000000000000000              .fini
0000000000000690 l    d  .rodata        0000000000000000              .rodata
0000000000000694 l    d  .eh_frame_hdr  0000000000000000              .eh_frame_hdr
00000000000006d0 l    d  .eh_frame      0000000000000000              .eh_frame
0000000000200df0 l    d  .init_array    0000000000000000              .init_array
0000000000200df8 l    d  .fini_array    0000000000000000              .fini_array
0000000000200e00 l    d  .dynamic       0000000000000000              .dynamic
0000000000200fc0 l    d  .got   0000000000000000              .got
0000000000201000 l    d  .data  0000000000000000              .data
0000000000201015 l    d  .bss   0000000000000000              .bss
0000000000000000 l    d  .comment       0000000000000000              .comment
0000000000000000 l    df *ABS*  0000000000000000              crtstuff.c
0000000000000520 l     F .text  0000000000000000              deregister_tm_clones
0000000000000560 l     F .text  0000000000000000              register_tm_clones
00000000000005b0 l     F .text  0000000000000000              __do_global_dtors_aux
0000000000201015 l     O .bss   0000000000000001              completed.7698
0000000000200df8 l     O .fini_array    0000000000000000              __do_global_dtors_aux_fini_array_entry
00000000000005f0 l     F .text  0000000000000000              frame_dummy
0000000000200df0 l     O .init_array    0000000000000000              __frame_dummy_init_array_entry
0000000000000000 l    df *ABS*  0000000000000000              main.cc
0000000000000000 l    df *ABS*  0000000000000000              crtstuff.c
00000000000007d4 l     O .eh_frame      0000000000000000              __FRAME_END__
0000000000000000 l    df *ABS*  0000000000000000
0000000000200df8 l       .init_array    0000000000000000              __init_array_end
0000000000200e00 l     O .dynamic       0000000000000000              _DYNAMIC
0000000000200df0 l       .init_array    0000000000000000              __init_array_start
0000000000000694 l       .eh_frame_hdr  0000000000000000              __GNU_EH_FRAME_HDR
0000000000200fc0 l     O .got   0000000000000000              _GLOBAL_OFFSET_TABLE_
0000000000000680 g     F .text  0000000000000002              __libc_csu_fini
0000000000000000  w      *UND*  0000000000000000              _ITM_deregisterTMCloneTable
0000000000201000  w      .data  0000000000000000              data_start
0000000000201015 g       .data  0000000000000000              _edata
0000000000000684 g     F .fini  0000000000000000              _fini
0000000000000000       F *UND*  0000000000000000              __libc_start_main@@GLIBC_2.2.5
0000000000201000 g       .data  0000000000000000              __data_start
0000000000000000  w      *UND*  0000000000000000              __gmon_start__
0000000000201008 g     O .data  0000000000000000              .hidden __dso_handle
0000000000000690 g     O .rodata        0000000000000004              _IO_stdin_used
0000000000000610 g     F .text  0000000000000065              __libc_csu_init
0000000000201010 g     O .data  0000000000000004              num1
0000000000201018 g       .bss   0000000000000000              _end
00000000000004f0 g     F .text  000000000000002b              _start
0000000000201014 g     O .data  0000000000000001              c1
0000000000201015 g       .bss   0000000000000000              __bss_start
00000000000005fa g     F .text  0000000000000016              main
0000000000201018 g     O .data  0000000000000000              .hidden __TMC_END__
0000000000000000  w      *UND*  0000000000000000              _ITM_registerTMCloneTable
0000000000000000  w    F *UND*  0000000000000000              __cxa_finalize@@GLIBC_2.2.5
00000000000004b8 g     F .init  0000000000000000              _init

以上是我们程序的符号信息,这些符号信息记录在符号表中,我们还截取一些我们知道的说明

1
00000000000005fa g     F .text  0000000000000016              main
  1. 00000000000005fa:这是符号在内存中的地址,表示 main 函数在程序内存中的位置。

  2. g符号类型g 表示符号是 全局符号

  3. F符号类型(进一步说明)F 表示这个符号是一个 函数。因此,main 符号代表的是一个函数的入口。

  4. .text节区(Section).text 是二进制文件中的代码段(text section)。在 ELF 文件中,.text 节区存放的是可执行代码

  5. 0000000000000016大小:这表示 main 函数的大小是 16 字节(十六进制表示 0x16,即 22 字节)。

  6. main:这是符号的名称。这里表示的是程序的入口函数 main

同理,我们也可以找到全局变量num1的符号信息,其中O 表示符号是 对象,也就是说,num1 是一个数据对象,而不是一个函数。这通常指的是变量或常量,而不是执行代码的函数

1
0000000000201010 g     O .data  0000000000000004              num1

观察上述符号信息,我们发现是没有num2这个局部变量的符号信息的,这个局部变量的生命周期由栈控制的,不参与链接过程

加载内核

代码目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.
├── boot
│   ├── include
│   │   └── boot.inc
│   ├── loader.S
│   └── mbr.S
├── lib
│   └── kernel
│       └── main.c
├── Makefile
└── start.sh

将内核载入内存

回忆我们之前加载mbr程序和loader程序的过程

  • mbr程序固定在磁盘0块0扇区,大小固定512B,最后两个字节固定是0x55,0xaa,由BIOS开启后加载到内存的0x7c00位置处执行
  • loader程序由我们自定义在磁盘0块2号扇区,同时由我们写的mbr程序将其加载到0x900位置处(可执行代码的位置是0xc00,中间是512字节的GDT和256字节的物理内存容量信息,这段0x300字节的信息是保存在内存中加载内核的必要信息,是不可执行的)

因此,本节我们加载内核文件也是同样的过程,也需要由loader将内核文件从磁盘的某个位置加载到内存的某个位置,于是我们定义

  • 内核文件在磁盘的位置是0块9号扇区
  • 加载到内存的物理位置是0x70000

另外我们的内核加载代码有以下特点

  • 在保护模式下运行,保护模式是32位的,因此需要修改之前我们加载loader的16位程序
  • 在分页机制开启之前进行加载,这一点并非固定,只是在分页机制开启之前加载,0x70000直接就是物理地址,我们可以直接内核加载到该物理地址处

要加载内核,我们首先需要写一个简单的内核代码

/lib/kernel/main.c

1
2
3
4
int main(void){
    while(1);
    return 0;
}

这里我们只是先写一段简单的代码以作测试,真正的内核代码我们后续再进行补充

编译内核

1
gcc-4.4 $(pwd)/lib/kernel/main.c -c -m32 -o $(pwd)/bin/main.o

链接内核

1
ld -m elf_i386 -Ttext 0x00001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o

这里有一个小插曲,由于目前我们使用的操作系统一般都是64位的,因此使用默认的编译器编译出来的结果都是64位的

故而在最后的编译阶段需要降低gcc版本至gcc-4.4,同时使用-m32选项使编译出来的结果是32位,否则便会报错

具体操作如下:

1.打开apt-get源

1
 vim /etc/apt/sources.list

增加如下内容

1
2
deb http://dk.archive.ubuntu.com/ubuntu/ trusty main universe
deb http://dk.archive.ubuntu.com/ubuntu/ trusty-updates main universe

2.更新apt源

1
sudo apt-get update

3.安装gcc-4.4

1
sudo apt-get install g++-4.4

4.如果最后仍旧报错,则执行

1
sudo apt-get install libc6-dev-i386

接下来我们开始正式将上述编译成功的内核文件kernel.bin加载到内存,以下是加载内核的代码

/boot/loader.S

1
2
3
4
5
6
;------------- 加载内核 ----------------
;将内核从磁盘的9号扇区加载的内存的KERNEL_BIN_BASE_ADDR地址处
    mov eax,KERNEL_START_SECTOR
    mov ebx,KERNEL_BIN_BASE_ADDR
    mov ecx,200
    call rd_disk_m_32

代码中相关的宏如下所示

/boot/include/boot.inc

1
2
3
4
5
6
;----- 定义内核相关宏 -----
    KERNEL_BIN_BASE_ADDR equ 0x70000                ;内核文件在内存中的位置,也就是包含文件头和程序数据的原始内核文件暂时存储在内存中的位置
                                                    ;注意内核加载是在开启分页机制之前进行的,因此该地址并不是虚拟地址,而是物理地址

    ;0号扇区存放mbr,2号~4号扇区存放loader,为避免将来扩大loader,将内核存放在9号扇区
    KERNEL_START_SECTOR equ 0x9                     ;内核在磁盘的起始扇区

同mbr加载loader一样,我们使用写好的函数将内核文件从磁盘加载到内存指定位置,只是这里是在32位保护模式下运行,因此需要简单修改一下代码,如下所示

/boot/loader.S

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
; ------ 32位模式下从磁盘中加载文件到内存 ------
rd_disk_m_32:
    ;参数:
        ; eax=LBA扇区号
        ; ebx=将数据写入的内存地址
        ; ecx=读入的扇区数
    mov esi,eax	                                        ;备份eax
    mov di,cx		                                    ;备份cx
                                                        ;读写硬盘:
                                                        ;第1步:选择特定通道的寄存器,设置要读取的扇区数
    mov dx,0x1f2
    mov al,cl
    out dx,al                                           ;读取的扇区数
 
    mov eax,esi	                                        ;恢复ax
 
                                                        ;第2步:在特定通道寄存器中放入要读取扇区的地址,将LBA地址存入0x1f3 ~ 0x1f6
                                                        ;LBA地址7~0位写入端口0x1f3
    mov dx,0x1f3                       
    out dx,al                          
 
                                                        ;LBA地址15~8位写入端口0x1f4
    mov cl,8
    shr eax,cl
    mov dx,0x1f4
    out dx,al
 
                                                        ;LBA地址23~16位写入端口0x1f5
    shr eax,cl
    mov dx,0x1f5
    out dx,al
 
    shr eax,cl
    and al,0x0f	                                        ;lba第24~27位
    or al,0xe0	                                        ; 设置7~4位为1110,表示lba模式
    mov dx,0x1f6
    out dx,al
 
                                                        ;第3步:向0x1f7端口写入读命令,0x20 
    mov dx,0x1f7
    mov al,0x20                        
    out dx,al
 
                                                        ;第4步:检测硬盘状态
.not_ready:
                                                        ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
    nop
    in al,dx
    and al,0x88	                                        ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
    cmp al,0x08
    jnz .not_ready	                                    ;若未准备好,继续等。
 
                                                        ;第5步:从0x1f0端口读数据
    mov ax, di                                          ;di当中存储的是要读取的扇区数
    mov dx, 256                                         ;每个扇区512字节,一次读取两个字节,所以一个扇区就要读取256次,与扇区数相乘,就等得到总读取次数
    mul dx                                              ;8位乘法与16位乘法知识查看书p133,注意:16位乘法会改变dx的值!!!!
    mov cx, ax	                                        ; 得到了要读取的总次数,然后将这个数字放入cx中
    mov dx, 0x1f0
.go_on_read:
    in ax,dx
    mov [ebx],ax                                        ;与rd_disk_m_16相比,就是把这两句的bx改成了ebx
    add ebx,2		        
                                                        ; 由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。
                                                        ; loader的栈指针为0x900,bx为指向的数据输出缓冲区,且为16位,
                                                        ; 超过0xffff后,bx部分会从0开始,所以当要读取的扇区数过大,待写入的地址超过bx的范围时,
                                                        ; 从硬盘上读出的数据会把0x0000~0xffff的覆盖,
                                                        ; 造成栈被破坏,所以ret返回时,返回地址被破坏了,已经不是之前正确的地址,
                                                        ; 故程序出会错,不知道会跑到哪里去。
                                                        ; 所以改为ebx代替bx指向缓冲区,这样生成的机器码前面会有0x66和0x67来反转。
                                                        ; 0X66用于反转默认的操作数大小! 0X67用于反转默认的寻址方式.
                                                        ; cpu处于16位模式时,会理所当然的认为操作数和寻址都是16位,处于32位模式时,
                                                        ; 也会认为要执行的指令是32位.
                                                        ; 当我们在其中任意模式下用了另外模式的寻址方式或操作数大小(姑且认为16位模式用16位字节操作数,
                                                        ; 32位模式下用32字节的操作数)时,编译器会在指令前帮我们加上0x66或0x67,
                                                        ; 临时改变当前cpu模式到另外的模式下.
                                                        ; 假设当前运行在16位模式,遇到0X66时,操作数大小变为32位.
                                                        ; 假设当前运行在32位模式,遇到0X66时,操作数大小变为16位.
                                                        ; 假设当前运行在16位模式,遇到0X67时,寻址方式变为32位寻址
                                                        ; 假设当前运行在32位模式,遇到0X67时,寻址方式变为16位寻址.
    loop .go_on_read
    ret

解析内核头文件,加载内核映像

内核映像,其实就是解析内核的elf头之后,将内核文件的程序代码和程序数据抽出来,然后放到内存的指定位置处执行,之所以这样做是因为elf头文件只是记录程序信息的结构化信息,不能执行,但是计算机不知道这一点,需要我们执行这一步操作

注意,解析内核头文件的工作是在开启分页机制后进行的,因为内核将来要做分页机制下工作,因此我们直接在分页机制下加载内核映像

那么问题来了,我们要将内核映像加载到内存的哪里呢?

  • 关于这个问题,我们知道,将来我们的内核会越来越大,而我们的内核物理空间在低1M字节这块区域内,因此我们尽量让我们的内核映像放置到内存低字节位置,这样将来的内核才有可余空间增长
  • 另一方面,我们知道,我们的loader程序已经占据在了内存的0x900位置,其中0x900 ~ 0xb00是GDT的信息,0xb00 ~ 0xc00是存放的物理内存容量信息,这些信息都不能被覆盖,因此我们的内核映像不能覆盖掉loader
  • 于是乎,假设我们loader大小最大是2000B,则0x900+2000=0x10d0,向上取整就将内核的物理加载地址选为0x1500
  • 读者会发现,之前我们给内核文件加载的位置的0x70000,而我们的内核映像是在0x1500,如果内核映像覆盖掉原始内核文件怎么办,这一点不必担心,因为内核文件在被抽离出来内核映像之后就没用了,因此即使覆盖掉也没关系

综上,我们有如下定义

/boot/include/boot.inc

1
2
3
4
5
    ;loader的物理加载地址是0x900,假设loader的文件大小最大为2000B,则0x900+2000=0x10d0,
    ;向上取整就将内核的物理加载地址选为0x1500
    ;由于我们将操作系统的虚拟地址空间划分为3GB~4GB
    ;因此内核的虚拟加载地址就成了0xc0000000+0x1500=0xc0001500
    KERNEL_ENTRY_POINT equ 0xc0001500               ;内核可执行代码(内核映像)的虚拟入口地址

注意,0xc0001500这个内核映像的虚拟入口地址是由gcc-4.4编译器用选项-Ttext 0x00001500指定的

另外,我们还需要添加一段宏定义信息,在提取elf头信息时使用

1
2
;-----  程序段的类型定义  ---------
    PT_NULL equ 0

有了以上准备后,我们就可以正式加载内核映像了

/boot/loader.S

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
;------------- 进入内核函数 ----------------
enter_kernel:
    call kernel_init
    mov esp,0xc009f000
    jmp KERNEL_ENTRY_POINT

kernel_init:
    xor eax,eax
    xor ebx,ebx                             ;记录程序头表(段)地址
    xor ecx,ecx                             ;记录程序头表(段头表)中程序头数量
    xor edx,edx                             ;记录程序头表中每个条目的大小

    mov dx,[KERNEL_BIN_BASE_ADDR+42]        ;42字节处是e_phentsize,即程序头中每个条目的大小
    mov ebx,[KERNEL_BIN_BASE_ADDR+28]       ;28字节处是e_phoff,即程序头表的偏移
    add ebx,KERNEL_BIN_BASE_ADDR            ;程序头表的偏移加上内核在内存中的起始地址,就是程序头表的起始物理地址
    mov cx,[KERNEL_BIN_BASE_ADDR+44]        ;44字节处是e_phnum,即程序头表(段头表)中程序头数量

;遍历段头表,取出程序中的每一个段,将其拷贝到对应的虚拟地址空间中
.each_segment:
;检查段头表中的段头是会否是空段(PT_NULL),如果不是就将该段拷贝到对应区域,否则就继续遍历下一个段头
    cmp byte [ebx+0],PT_NULL                ;比较p_type是否等于PT_NULL,若相等说明程序头未使用
    je .PTNULL                              ;若相等则跳转到.PTNULL

    ;为函数memcpy(dst,src,size)压入参数,参数依次从右向左压入,这样函数就可以从左到右取出参数
    push dword [ebx+16]                     ;实参size,程序头表偏移16字节的地方p_filesz,本段在文件内的大小
    
.PTNULL:
    add ebx,edx                             ;指向下一个段头
    loop .each_segment                      ;继续遍历段头表
    ret

;逐字节拷贝函数,将esi指向的内存区域的size个字节拷贝到edi指向的区域
mem_cpy:
    cld                                    ;指明拷贝时esi与edi的增长方向是向上的
    push ebp                               ;保存ebp
    mov ebp,esp                            ;将esp指向ebp
    push ecx                               ;rep指令会用到ecx的循环计数,而外层函数也用到了ecx的值,
                                           ;因此此处需要将外层函数的ecx的值进行备份

    mov edi,[ebp+8]                        ;参数dst
    mov esi,[ebp+12]                       ;参数src
    mov ecx,[ebp+16]                       ;参数size
    rep movsb                              ;rep(repeat)指令,重复执行movsb指令
                                           ;movsb指令,s表示string,b表示byte,
                                           ;即将esi指向的内存拷贝一个字节给edi指向的内存
                                           ;因此本条指令表示逐字节拷贝,拷贝的字节个数为ecx的值

    pop ecx                                ;取出备份的值
    pop ebp                                ;返回上层函数
    ret

代码逻辑很简单:

  1. 从elf头中读取程序头表的信息
    • 程序头表的初始偏移
    • 程序头表中条目(记录段的信息)的数量
    • 程序头表中每个条目的大小
  2. 读取到程序头表的信息后,我们就可以像遍历数组一样遍历程序头表,取出程序头表中的每个程序头(也就是段头)的信息
    • 本段在文件内的大小
    • 本段在文件内的起始偏移
    • 本段在内存中的起始虚拟地址
  3. 将段复制到内存指定的虚拟地址处

以下是完整代码

/boot/include/boot.inc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
;----- loader and kernel -----
    LOADER_BASE_ADDR equ 0x900                      ;loader在内存中位置
    LOADER_START_SECTOR equ 0x2                     ;loader在磁盘中的逻辑扇区地址,即LBA地址

    LOADER_STACK_TOP equ LOADER_BASE_ADDR           ;初始化栈指针地址

;----- 定义内核相关宏 -----
    KERNEL_BIN_BASE_ADDR equ 0x70000                ;内核文件在内存中的位置,也就是包含文件头和程序数据的原始内核文件暂时存储在内存中的位置
                                                    ;注意内核加载是在开启分页机制之前进行的,因此该地址并不是虚拟地址,而是物理地址

    ;0号扇区存放mbr,2号~4号扇区存放loader,为避免将来扩大loader,将内核存放在9号扇区
    KERNEL_START_SECTOR equ 0x9                     ;内核在磁盘的起始扇区
    ;loader的物理加载地址是0x900,假设loader的文件大小最大为2000B,则0x900+2000=0x10d0,
    ;向上取整就将内核的物理加载地址选为0x1500
    ;由于我们将操作系统的虚拟地址空间划分为3GB~4GB
    ;因此内核的虚拟加载地址就成了0xc0000000+0x1500=0xc0001500
    KERNEL_ENTRY_POINT equ 0xc0001500               ;内核可执行代码(内核映像)的虚拟入口地址

;----- 定义页目录表与页表相关宏 -----
    PAGE_DIR_TABLE_POS equ 0x100000                 ;页目录表基址

;-----  gdt描述符属性  ---------
    DESC_G_4K   equ   1_00000000000000000000000b    ;设置段界限的单位为4KB
    DESC_D_32   equ    1_0000000000000000000000b    ;设置代码段/数据段的有效地址(段内偏移地址)及操作数大小为32位
    DESC_L    equ       0_000000000000000000000b    ;64位代码段标记位,现在是32位操作系统,因此标记为0即可。
    DESC_AVL  equ        0_00000000000000000000b

;定义段界限位
    ;段界限的第2部分,即描述符的高32位中的第16~19位,最终的代码段段界限为0xFFFFF
    DESC_LIMIT_CODE2 equ 1111_0000000000000000b     ;定义代码段要用的段描述符高32位中16~19段界限为全1
    DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2           ;定义数据段要用的段描述符高32位中16~19段界限为全1
    DESC_LIMIT_VIDEO2  equ 0000_000000000000000b    ;定义我们要操作显存时对应的段描述符的高32位中16~19段界限为全0
    DESC_P  equ 1_000000000000000b                  ;定义了段描述符中的P标志位,表示该段描述符指向的段是否在内存中

;定义描述符的特权级别位
    DESC_DPL_0 equ 00_0000000000000b
    DESC_DPL_1 equ 01_0000000000000b
    DESC_DPL_2 equ 10_0000000000000b
    DESC_DPL_3 equ 11_0000000000000b
;定义类型位
    DESC_S_CODE equ 1_000000000000b                 ;代码段和数据段都是非系统段,故类型字段s设置为1
    DESC_S_DATA equ DESC_S_CODE                     ;代码段和数据段都是非系统段,故类型字段s设置为1
    DESC_S_sys equ 0_000000000000b                  ;系统段的类型字段设置为0
;定义子类型位
    DESC_TYPE_CODE equ 1000_00000000b               ;x=1,c=0,r=0,a=0 代码段是可执行的,非一致性,不可读,已访问位a清0
    DESC_TYPE_DATA equ 0010_00000000b               ;x=0,e=0,w=1,a=0,数据段不可执行,向上扩展,可写,已访问位a清0

;拼接代码段的描述符
DESC_CODE_HIGH4 equ (0x00<<24) + DESC_G_4K \
                    + DESC_D_32 + DESC_L + \
                    DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + \
                    DESC_S_CODE + DESC_TYPE_CODE + 0x00
;拼接数据段的描述符
DESC_DATA_HIGH4 equ (0x00<<24) + DESC_G_4K \
                    + DESC_D_32 + DESC_L + \
                    DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + \
                    DESC_S_DATA + DESC_TYPE_DATA + 0x00

;拼接显存段的描述符位                    
DESC_VIDEO_HIGH4 equ (0x00<<24) + DESC_G_4K \
                    + DESC_D_32 + DESC_L + \
                    DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + \
                    DESC_S_DATA + DESC_TYPE_DATA + 0x0b

;-----  选择子属性  ---------
    RPL0 equ 00b
    RPL1 equ 01b
    RPL2 equ 10b
    RPL3 equ 11b

    TI_GDT equ 000b
    TI_LDT equ 100b


;-----  模块化的页目录表字段  ---------
    PG_P equ 1b
    PG_RW_R equ 00b
    PG_RW_W equ 10b
    PG_US_S equ 000b
    PG_US_U equ 100b

;-----  程序段的类型定义  ---------
    PT_NULL equ 0

/boot/loader.S

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR

;------------- 构建gdt及其内部的描述符 -------------
    GDT_BASE: 
        dd 0x00000000
        dd 0x00000000

    ;代码段描述符的低4字节部分,其中高两个字节表示段基址的0~15位,在这里定义为0x0000
    ;低两个字节表示段界限的0~15位,由于使用的是平坦模型,因此是0xFFFF
    CODE_DESC:  
        dd 0x0000FFFF
        dd DESC_CODE_HIGH4;段描述符的高4字节部分
    DATA_STACK_DESC: 
        dd 0x0000FFFF
        dd DESC_DATA_HIGH4

    ;定义显存段的描述符
    ;文本模式下的适配器地址为0xb8000~0xbffff,为了方便显存操作,显存段不使用平坦模型
    ;因此段基址为0xb8000,段大小为0xbffff-0xb8000=0x7fff,
    ;段粒度位4k,因此段界限的值为0x7fff/4k=7
    VIDEO_DESC: 
        dd 0x80000007
        dd DESC_VIDEO_HIGH4

    GDT_SIZE equ $-GDT_BASE
    GDT_LIMIT equ GDT_SIZE-1
    times 60 dq 0 ;此处预留60个描述符的空位

;------------- 构建选择子 -------------
    SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
    SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
    SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

    total_mem_bytes dd 0    ;total_mem_bytes用于保存最终获取到的内存容量,为4个字节
                            ;由于loader程序的加载地址为0x900,而loader.bin的文件头大小为0x200
                            ;(4个gdt段描述符(8B)加上60个dp(8B)填充字,故64*8=512B),
                            ;故total_mem_bytes在内存中的地址为0x900+0x200=0xb00
                            ;该地址将来在内核中会被用到


;------------- 定义gdtr(指向GDT的寄存器) -------------
    gdt_ptr dw GDT_LIMIT
            dd GDT_BASE

;------------- 定义记录ARDS结构体信息的变量 -------------
    ards_buf times 244 db 0 ;开辟一块缓冲区,用于记录返回的ARDS结构体,
                            ;该定义语句事实上是定义了一个数组ards_buf[244]
                            ;244是因为total_mem_bytes(4)+gdt_ptr(6)+244+ards_nr(2)=256,即0x100
                            ;这样loader_start的在文件内的偏移地址就是0x100+0x200=0x300

    ards_nr dw 0            ;用于记录ards结构体数量

;------------------------------------------
;INT 0x15 功能号:0xe820 功能描述:获取内存容量,检测内存
;------------------------------------------
;输入:
    ;EAX:功能号,0xE820,但调用返回时eax会被填入一串ASCII值
    ;EBX:ARDS后续值
    ;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
    ;ECX:ARDS结构的字节大小,20
    ;EDX:固定为签名标记,0x534d4150

;返回值
    ;CF:若cf为0,表示未出错,cf为1,表示调用出错
    ;EAX:字符串SMAP的ASCII码值,0x534d4150
    ;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
    ;ECX:ARDS结构的字节大小,20
    ;EBX:ARDS后续值,即下一个ARDS的位置。
    ;每次BIOS中断返回后,BIOS会更新此值,BIOS会通过此值找到下一个待返回的ARDS结构。
    ;在cf位为0的情况下,若返回后的EBX值为0,表示这是最后一个ARDS结构

loader_start:
    xor ebx,ebx             ;第一次调用时,要将ebx清空置为0,此处使用的是异或运算置0
    mov edx,0x534d4150
    mov di,ards_buf         ;di存储缓冲区地址,即指向缓冲区首地址

.e820_mem_get_loop:
    mov eax,0x0000e820
    mov ecx,20              ;一个ards结构体的大小
    int 0x15                ;调用0x15中断函数,返回的ards结构体被返回给di指向的缓冲区中
    add di,cx               ;使di增加20字节指向缓冲区中下一个的ARDS结构位置
    inc word [ards_nr]      ;inc(increment增加)指令表示将内存中的操作数增加一,此处用于记录返回的ARDS数量
    cmp ebx,0               ;比较ebx中的值是否为0
    jnz .e820_mem_get_loop  ;若ebx不为0,则继续进行循环获取ARDS,
                            ;若为0说明已经获取到最后一个ards,则退出循环

    mov cx,[ards_nr]        ;cx存储遍历到的ards结构体个数
    mov ebx,ards_buf        ;ebx指向缓冲区地址
    xor edx,edx             ;EDX用于保存BaseAddrLow+LengthLow最大值,此处初始化为0

.find_max_mem_area:
    mov eax,[ebx]           ;eax用于遍历缓冲区中的每一个ards的BaseAddrLow
    add eax,[ebx+8]         ;ebx+8获取的是LengthLow,故该代码计算的是BaseAddrLow+LengthLow
    add ebx,20              ;遍历下一个ards
    cmp edx,eax             ;分支语句,如果edx大于等于eax,则跳转到.next_ards,也就是进入循环
    jge .next_ards
    mov edx,eax             ;否则就是更新edx
.next_ards:
    loop .find_max_mem_area

    mov [total_mem_bytes],edx   ;将最终结果保存到total_mem_bytes


;------------- 准备进入保护模式 -------------
;1.打开A20
;2.加载gdt
;3.置cr0的PE位为1

    ;------------- 打开A20 -------------
    in al,0x92
    or al,0000_0010B
    out 0x92,al
    
    ;------------- 加载gdt -------------
    lgdt [gdt_ptr]

    ;------------- 置cr0的PE位为1 -------------
    mov eax,cr0
    or eax,0x00000001
    mov cr0,eax

    jmp dword SELECTOR_CODE:p_mode_start;刷新流水线

.error_hlt:
    hlt                     ;出错则挂起


[bits 32]
p_mode_start:
    mov ax,SELECTOR_DATA                    ;初始化段寄存器,将数据段的选择子分别放入各段寄存器
    mov ds,ax
    mov es,ax                      
    mov ss,ax

    mov esp,LOADER_STACK_TOP                ;初始化栈指针,将栈指针地址放入bsp寄存器
    mov ax,SELECTOR_VIDEO                   ;初始化显存段寄存器,显存段的选择子放入gs寄存器
    mov gs,ax

;------------- 加载内核 ----------------
;将内核从磁盘的9号扇区加载的内存的KERNEL_BIN_BASE_ADDR地址处
    mov eax,KERNEL_START_SECTOR
    mov ebx,KERNEL_BIN_BASE_ADDR
    mov ecx,200 
    call rd_disk_m_32

;------------- 开启分页机制 -------------
;1.构建页表和页目录表
;2.将页目录表的位置加载到控制寄存器cr3
;3.置cr0寄存器的PG位为1,打开分页机制

    call setup_page                         ;创建页目录表和页表,并初始化页内存位图
    ;以下将各段描述符的基址迁移到虚拟内存空间中                                
    mov ebx,[gdt_ptr+2]                     ;gdt_ptr+2表示GDT_BASE,也就是GDT的起始地址
    or dword [ebx+0x18+4],0xc0000000        ;ebx中保存着GDT_BASE,0x18=24,故ebx+0x18表示取出显存段的起始地址
                                            ;+4表示取出段描述符的高32位,之后和0xc0000000进行或操作
                                            ;表示将显存段的起始地址增加了3G

    add dword [gdt_ptr+2],0xc0000000        ;同理将GDT_BASE的起始地址也增加3G
    add esp,0xc0000000                      ;同理将esp栈指针的起始地址也增加3G

    mov eax,PAGE_DIR_TABLE_POS              ;将页目录表的位置加载到控制寄存器cr3
    mov cr3,eax

    mov eax,cr0                             ;打开cr0的PG位,开启分页机制
    or eax,0x80000000
    mov cr0,eax

    lgdt [gdt_ptr]                          ;开启分页后,用gdt的新地址重新加载

;------------- 进入内核函数 ----------------
enter_kernel:
    call kernel_init
    mov esp,0xc009f000
    jmp KERNEL_ENTRY_POINT

kernel_init:
    xor eax,eax
    xor ebx,ebx                             ;记录程序头表(段)地址
    xor ecx,ecx                             ;记录程序头表(段头表)中程序头数量
    xor edx,edx                             ;记录程序头表中每个条目的大小
 
    mov dx,[KERNEL_BIN_BASE_ADDR+42]        ;42字节处是e_phentsize,即程序头中每个条目的大小
    mov ebx,[KERNEL_BIN_BASE_ADDR+28]       ;28字节处是e_phoff,即程序头表的偏移
    add ebx,KERNEL_BIN_BASE_ADDR            ;程序头表的偏移加上内核在内存中的起始地址,就是程序头表的起始物理地址
    mov cx,[KERNEL_BIN_BASE_ADDR+44]        ;44字节处是e_phnum,即程序头表(段头表)中程序头数量
 
;遍历段头表,取出程序中的每一个段,将其拷贝到对应的虚拟地址空间中
.each_segment:
;检查段头表中的段头是会否是空段(PT_NULL),如果不是就将该段拷贝到对应区域,否则就继续遍历下一个段头
    cmp byte [ebx+0],PT_NULL                ;比较p_type是否等于PT_NULL,若相等说明程序头未使用
    je .PTNULL                              ;若相等则跳转到.PTNULL
 
    ;为函数memcpy(dst,src,size)压入参数,参数依次从右向左压入,这样函数就可以从左到右取出参数
    push dword [ebx+16]                     ;实参size,程序头表偏移16字节的地方p_filesz,本段在文件内的大小
 
    mov eax,[ebx+4]                         ;程序头表偏移4字节的地方p_offset,本段在文件内的起始偏移
    add eax,KERNEL_BIN_BASE_ADDR            ;本段的偏移加上内核在内存中的起始地址,就是本段的物理地址
    push eax                                ;实参src
 
    push dword [ebx+8]                      ;实参dst,p_vaddr,本段在内存中的起始虚拟地址
 
    call mem_cpy
    add esp,12                              ;回收mem_cpy的栈帧空间
 
.PTNULL:
    add ebx,edx                             ;指向下一个段头
    loop .each_segment                      ;继续遍历段头表
    ret

;逐字节拷贝函数,将esi指向的内存区域的size个字节拷贝到edi指向的区域
mem_cpy:
    cld                                    ;指明拷贝时esi与edi的增长方向是向上的
    push ebp                               ;保存ebp
    mov ebp,esp                            ;将esp指向ebp
    push ecx                               ;rep指令会用到ecx的循环计数,而外层函数也用到了ecx的值,
                                           ;因此此处需要将外层函数的ecx的值进行备份

    mov edi,[ebp+8]                        ;参数dst
    mov esi,[ebp+12]                       ;参数src
    mov ecx,[ebp+16]                       ;参数size
    rep movsb                              ;rep(repeat)指令,重复执行movsb指令
                                           ;movsb指令,s表示string,b表示byte,
                                           ;即将esi指向的内存拷贝一个字节给edi指向的内存
                                           ;因此本条指令表示逐字节拷贝,拷贝的字节个数为ecx的值
 
    pop ecx                                ;取出备份的值
    pop ebp                                ;返回上层函数
    ret

;------------- 创建页目录表和页表 -------------
;初始化页目录表和页表

;逐字节清空页目录表
setup_page:
    mov ecx,4096                                    ;页目录表的大小为4KB,ecx是loop指令的循环计数器
                                                    ;由于初始化页表是逐字节置0的,因此ecx的值为4096
    mov esi,0                                       ;页目录表的偏移量
.clear_page_dir:
    mov byte [PAGE_DIR_TABLE_POS+esi],0             ;逐字节清空页目录表
                                                    ;其中PAGE_DIR_TABLE_POS为页目录表初始地址的宏
    inc esi                                         ;递增偏移量,清空下一个字节
    loop .clear_page_dir

;初始化创建页目录表
.create_pde:
    mov eax,PAGE_DIR_TABLE_POS                      ;eax保存页目录表的起始地址
    add eax,0x1000                                  ;0x1000为1k,故该代码的计算结果是将eax指向第一张页表的起始地址
    mov ebx,eax                                     ;ebx保存第一张页表的起始地址,后续会用到
    or eax,PG_US_U|PG_RW_W|PG_P                     ;eax已经有了第一张页表的起始地址
                                                    ;此处再加上属性,即可表示为页目录表的一个表项,
                                                    ;该表项代表的是第一张页表的物理地址及其相关属性

    mov [PAGE_DIR_TABLE_POS+0x0],eax                ;页目录表的第一个表项指向第一张页表
    mov [PAGE_DIR_TABLE_POS+0xc00],eax              ;0xc0000000即为3GB,由于我们划分的虚拟地址空间3GB以上为os地址空间
                                                    ;因此该语句是将3GB的虚拟空间映射到内核空间 
                                                    ;而0xc00/4=768,也就是说页目录表的768号表项映射为物理内核空间
    
    sub eax,0x1000          
    mov [PAGE_DIR_TABLE_POS+4092],eax               ;最后一个页表项指向自己,为将来动态操作页表做准备


;创建第一张页表的页表项,由于os的物理内存不会超过1M,故页表项个数的最大值为1M/4k=256
    mov ecx,256                                     ;循环计数器
    mov esi,0                                       ;偏移量
    xor edx,edx                                     ;第一个页表项指向物理空间的0~4KB物理页框
                                                    ;故记录的起始地址就是第一个页框的起始地址,也就是0
    mov edx,PG_US_S|PG_RW_W|PG_P                    ;此时的edx表示拥有属性PG_US_S|PG_RW_W|PG_P
                                                    ;且物理地址为0的物理页的页表项
.create_pte:
    mov [ebx+esi*4],edx                             ;此前ebx已经保存了第一张页表的起始地址
    add edx,4096                                    ;edx指向下一个物理页(一个物理页4KB)
    inc esi                                         ;esi指向页表的下一个偏移
    loop .create_pte

; -------------------初始化页目录表769号-1022号项,769号项指向第二个页表的地址(此页表紧挨着上面的第一个页表),770号指向第三个,以此类推
    mov eax,PAGE_DIR_TABLE_POS
    add eax,0x2000                                  ;此时的eax表示第二张页表的起始地址
    or eax,PG_US_U|PG_RW_W|PG_P                     ;为eax表项添加属性

    mov ebx,PAGE_DIR_TABLE_POS
    mov ecx,254                                     ;要设置254个页表项
    mov esi,769                                     ;从第769个页表项开始设置
.create_kernel_pde:
    mov [ebx+esi*4],eax                             ; 设置页目录表项
    inc esi                                         ; 增加要设置的页目录表项的偏移
    add eax,0x1000                                  ; eax指向下一个页表的位置,由于之前设定了属性,所以eax是一个完整的指向下一个页表的页目录表项
    loop .create_kernel_pde                         ; 循环设定254个页目录表项
    ret


; ------ 32位模式下从磁盘中加载文件到内存 ------
rd_disk_m_32:
    ;参数:
        ; eax=LBA扇区号
        ; ebx=将数据写入的内存地址
        ; ecx=读入的扇区数
    mov esi,eax	                                        ;备份eax
    mov di,cx		                                    ;备份cx
                                                        ;读写硬盘:
                                                        ;第1步:选择特定通道的寄存器,设置要读取的扇区数
    mov dx,0x1f2
    mov al,cl
    out dx,al                                           ;读取的扇区数
 
    mov eax,esi	                                        ;恢复ax
 
                                                        ;第2步:在特定通道寄存器中放入要读取扇区的地址,将LBA地址存入0x1f3 ~ 0x1f6
                                                        ;LBA地址7~0位写入端口0x1f3
    mov dx,0x1f3                       
    out dx,al                          
 
                                                        ;LBA地址15~8位写入端口0x1f4
    mov cl,8
    shr eax,cl
    mov dx,0x1f4
    out dx,al
 
                                                        ;LBA地址23~16位写入端口0x1f5
    shr eax,cl
    mov dx,0x1f5
    out dx,al
 
    shr eax,cl
    and al,0x0f	                                        ;lba第24~27位
    or al,0xe0	                                        ; 设置7~4位为1110,表示lba模式
    mov dx,0x1f6
    out dx,al
 
                                                        ;第3步:向0x1f7端口写入读命令,0x20 
    mov dx,0x1f7
    mov al,0x20                        
    out dx,al
 
                                                        ;第4步:检测硬盘状态
.not_ready:
                                                        ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
    nop
    in al,dx
    and al,0x88	                                        ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
    cmp al,0x08
    jnz .not_ready	                                    ;若未准备好,继续等。
 
                                                        ;第5步:从0x1f0端口读数据
    mov ax, di                                          ;di当中存储的是要读取的扇区数
    mov dx, 256                                         ;每个扇区512字节,一次读取两个字节,所以一个扇区就要读取256次,与扇区数相乘,就等得到总读取次数
    mul dx                                              ;8位乘法与16位乘法知识查看书p133,注意:16位乘法会改变dx的值!!!!
    mov cx, ax	                                        ; 得到了要读取的总次数,然后将这个数字放入cx中
    mov dx, 0x1f0
.go_on_read:
    in ax,dx
    mov [ebx],ax                                        ;与rd_disk_m_16相比,就是把这两句的bx改成了ebx
    add ebx,2		        
                                                        ; 由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。
                                                        ; loader的栈指针为0x900,bx为指向的数据输出缓冲区,且为16位,
                                                        ; 超过0xffff后,bx部分会从0开始,所以当要读取的扇区数过大,待写入的地址超过bx的范围时,
                                                        ; 从硬盘上读出的数据会把0x0000~0xffff的覆盖,
                                                        ; 造成栈被破坏,所以ret返回时,返回地址被破坏了,已经不是之前正确的地址,
                                                        ; 故程序出会错,不知道会跑到哪里去。
                                                        ; 所以改为ebx代替bx指向缓冲区,这样生成的机器码前面会有0x66和0x67来反转。
                                                        ; 0X66用于反转默认的操作数大小! 0X67用于反转默认的寻址方式.
                                                        ; cpu处于16位模式时,会理所当然的认为操作数和寻址都是16位,处于32位模式时,
                                                        ; 也会认为要执行的指令是32位.
                                                        ; 当我们在其中任意模式下用了另外模式的寻址方式或操作数大小(姑且认为16位模式用16位字节操作数,
                                                        ; 32位模式下用32字节的操作数)时,编译器会在指令前帮我们加上0x66或0x67,
                                                        ; 临时改变当前cpu模式到另外的模式下.
                                                        ; 假设当前运行在16位模式,遇到0X66时,操作数大小变为32位.
                                                        ; 假设当前运行在32位模式,遇到0X66时,操作数大小变为16位.
                                                        ; 假设当前运行在16位模式,遇到0X67时,寻址方式变为32位寻址
                                                        ; 假设当前运行在32位模式,遇到0X67时,寻址方式变为16位寻址.
    loop .go_on_read
    ret

编译运行

/start.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash

# 设置工作目录和工具
BIN_DIR=$(pwd)/bin
BOOT_DIR=$(pwd)/boot
BOOT_INCLUDE_DIR=$BOOT_DIR/include/
KERNEL_DIR=$(pwd)/lib/kernel
IMG=/home/minios/bochs/hd60M.img

NASM=nasm
GCC=gcc-4.4
LD=ld
DD=dd

# 目标文件
MBR_SRC=$BOOT_DIR/mbr.S
LOADER_SRC=$BOOT_DIR/loader.S
KERNEL_SRC=$KERNEL_DIR/main.c

MBR_BIN=$BIN_DIR/mbr.bin
LOADER_BIN=$BIN_DIR/loader.bin
KERNEL_OBJ=$BIN_DIR/main.o
KERNEL_BIN=$BIN_DIR/kernel.bin

# 创建目录
echo "创建目标目录..."
mkdir -p $BIN_DIR
mkdir -p $BOOT_DIR

# 编译 MBR
echo "编译 MBR..."
$NASM -I $BOOT_INCLUDE_DIR $MBR_SRC -o $MBR_BIN

# 编译 Loader
echo "编译 Loader..."
$NASM -I $BOOT_INCLUDE_DIR $LOADER_SRC -o $LOADER_BIN

# 编译内核
echo "编译内核..."
$GCC $KERNEL_SRC -c -m32 -o $KERNEL_OBJ

# 链接内核
echo "链接内核..."
$LD -m elf_i386 -Ttext 0x00001500 -e main -o $KERNEL_BIN $KERNEL_OBJ

# 创建启动镜像
echo "创建启动镜像..."
$DD if=$MBR_BIN of=$IMG bs=512 count=1 conv=notrunc
$DD if=$LOADER_BIN of=$IMG bs=512 count=4 seek=2 conv=notrunc
$DD if=$KERNEL_BIN of=$IMG bs=512 count=200 seek=9 conv=notrunc

# 完成
echo "构建完成!镜像文件已创建:$IMG"

# 退出脚本
exit 0
网站已运行
发表了17篇文章 · 总计 118,129字