1 #include <inc/x86.h>
2 #include <inc/elf.h>
3
4 /**********************************************************************
5 * 这是一个非常简单的引导加载程序,它的唯一任务是从第一个IDE硬盘引导ELF内核映像。
6 *
7 * 磁盘布局
8 * * 引导加载程序由boot.S和main.C组成,这两个文件应该储存在磁盘的第一个扇区。
9 *
10 * * 第二个扇区是内核映像(kernel image)。
11 *
12 * * kernel image 必须是ELF格式。
13 *
14 * 启动步骤
15 * * 计算机启动时,CPU将BIOS加载到内存中并执行它。
16 *
17 * * BIOS初始化设备,初始化中断,将启动设备(磁盘或者CD)的第一个扇区(包含boot.S和main.C)读取进内存,
18 * 并跳转执行。
19 *
20 * * 首先执行的是boot.S,boot.S启动了保护模式,并且设置了一个堆栈(使得C代码可以运行),
21 * 然后调用了本文件也就是main.C中的bootmain函数。
22 *
23 * *之后由本文件的bootmain()接管,主要功能是从磁盘第一个扇区开始读入内核到内存0x10000中并跳转到它。
24 *
25 **********************************************************************/
26
27 //扇区的大小为512
28 #define SECTSIZE 512
29 //将内核加载到内存的起始地址
30 #define ELFHDR ((struct Elf *) 0x10000) // 定义一个名叫ELFHDER,起始位置为0x10000(暂存空间)的ELF(结构体代码见博客)类型的结构体,
31
32
33 //读取一个扇区
34 void readsect(void*, uint32_t);
35 //读取一个程序段
36 void readseg(uint32_t, uint32_t, uint32_t);
37
38 //读取执行ELF所需要的全部程序段到内存指定位置
39 void
40 bootmain(void)
41 {
42 //定义了两个程序头表项指针
43 struct Proghdr *ph, *eph;
44
45 //偏移量为0,从0+0处读取SECTSIZE*8个字节读取到ELFHDR处
46 //即将硬盘上开始的4096个字节读到内存中地址为0x10000处(也就是将磁盘的第一页读入扇区)
47 //先将ELF头文件从磁盘读到内存检查是否合法文件以及获取其PROGRAM HEADER TABLE
48 readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
49
50 //检查这是否是一个合法的ELF文件
51 if (ELFHDR->e_magic != ELF_MAGIC)
52 goto bad;
53
54 //程序头表项(PROGRAM HEADER TABLE)的起始地址
55 ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
56 //e_phum是段的数量
57 eph = ph + ELFHDR->e_phnum;
58
59 //将内核加载进入内存指定位置中
60 //有几个段就循环几次
61 for (; ph < eph; ph++)
62 //p_pa就是当前程序段应该被加载到内存中的物理位置
63 //p_memsz是当前程序段大小 p_offest表示读取该段时的偏移量
64 //每次读取一个程序段到内存中
65 readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
66
67 //开始执行内核
68 //// 跳转到程序入口处执行,即0x10000c处,可以查看ELF header的e_entry确认
69 ((void (*)(void)) (ELFHDR->e_entry))();
70
71 bad:
72 outw(0x8A00, 0x8A00);
73 outw(0x8A00, 0x8E00);
74 while (1)
75 /* do nothing */;
76 }
77
78 /*
79 这个函数的作用是根据offest找到段在磁盘中起始的扇区,根据count和pa计算出段的结束地址
80 从offest扇区开始,每次读一个扇区到内存pa处,每读取一个扇区,变量pa=pa+512,直到pa>end_pa
81 offest(相对于ELF文件起始处而言的偏移量,ELF文件起始处应该在磁盘开头,也就是0)
82 若offest为0,则从第一个扇区开始读取
83 */
84 void
85 readseg(uint32_t pa, uint32_t count, uint32_t offset)
86 {
87 //段的结束地址(kernel的截至地址)
88 uint32_t end_pa;
89
90 //计算段的结束地址
91 end_pa = pa + count;
92
93 //将pa设置为512字节对齐(因为扇区是磁盘读取的最小单位(512B))
94 //确保该地址指向所在扇区的第一个字节
95 //&(按位与) ~ (按位取反) pa = pa & ~(512-1) = pa & ~(111111111) = pa & 000000000
96 //就是将低 9 位全部变成0
97 pa &= ~(SECTSIZE - 1);
98
99 // 根据偏移, 计算kernel文件在硬盘哪个扇区开始存放
100 // 从现在开始, offset就变成要读的扇区号了, 不再是kernel文件的偏移量了
101 // 合理利用变量, 这代码真漂亮
102 offset = (offset / SECTSIZE) + 1;
103
104 //开始读取该程序段的内容、
105 while (pa < end_pa) {
106 //每次读取程序的一个节,即一个扇区
107 //也就是将offset扇区中的内容,读到物理地址为pa的地方
108 //(uint8_t*) 表示强制转换成uint8_t类型的指针
109 readsect((uint8_t*) pa, offset);
110 //将pa的值增加512字节
111 pa += SECTSIZE;
112 //读取下一个扇区
113 offset++;
114 }
115 }
116
117 //这个函数的作用是等待磁盘准备完毕
118 void
119 waitdisk(void)
120 {
121 // wait for disk reaady
122 while ((inb(0x1F7) & 0xC0) != 0x40)
123 /* do nothing */;
124 /*这段函数的功能从名字可以非常直观的看出来是等待磁盘就绪的,那么它的具体实现是先调用inb()函数从0x1F7
125 (0号硬盘状态寄存器放在这个里面)中读取数据。因为在0号硬盘状态寄存器中最高两位为01时表示硬盘就绪,
126 所以inb(0x1F7) & 0xC0的作用就是获取寄存器最高两位并且和0x40进行比较,
127 如果相等,说明就绪了,跳出循环,如果不相等,说明还没有就绪,就一直等待直到磁盘就绪为止
128 ps:16进制:2进制 0x40:1000000 0xC0:11000000*/
129
130 }
131
132 /*该函数的作用是读取整个扇区到内存指定位置dst去。
133 步骤如下;
134 1. waitdisk();等待磁盘就绪。
135 2. outb(0x1F2, 1);向 0x1F2 端口写入 1,表示要读取 1 个扇区。
136 3. outb(0x1F3, offset); „„分别向 0x1F3, 0x1F4, 0x1F5 端口写入 offset 的 0-7, 8-15, 16-23 位,
137 表示LBA(Logincal Block Addeessing)地址的低 24 位。
138 offest是32位的,outb(0x1F3,offest)命令表明向1F3端口输出8位,所以输出的不是整个offest,而是offest的最后8位
139 4. outb(0x1F6, (offset >> 24) | 0xE0); 向 0x1F6 端口写入 offset 的 24-27 位,
140 表示 LBA 地址的高 4 位(LBA 地址共 28 位,因此 offset 的高 4 位为 0), 并
141 置高 4 位为 0xE0=1110b,表示使用 0 号驱动器并使用 LBA 模式。
142 5. outb(0x1F7, 0x20);向 0x1F7 端口写入 0x20。0x1F7 处是命令寄存器,0x20 表示读取指定的扇区。
143 现在磁盘会将所请求的数据读入,并以 32 位为单位存放在端口 0x1F0的寄存器中。
144 6. insl(0x1F0, dst, SECTSIZE/4);等待磁盘就绪。然后调用 insl 函数从 0x1F0 端口读入一个扇区
145 (4 ×SECTSIZE / 4 = 512 个字节)的内容至 dst 处。这样readset()函数就算分析完了。*/
146 void
147 readsect(void *dst, uint32_t offset)
148 {
149 // 等待磁盘准备完毕
150 waitdisk();
151
152 outb(0x1F2, 1); // count = 1
153 outb(0x1F3, offset);
154 outb(0x1F4, offset >> 8);
155 outb(0x1F5, offset >> 16);
156 //e0(十六进制) = 11100000(二进制) 0|0=0, 0|1=1, 1|1=1
157 //所以这里是先移位,让offest原来的24-27位变成低四位,此时offest前四位为0,
158 //再与0XE0进行|运算,置offest高四位为0xE0,最后将offest传入0x1F6端口
159 //高 4 位 0xE0=1110b,表示使用 0 号驱动器并使用 LBA 模式,低4位作为LBA地址的高四位
160 outb(0x1F6, (offset >> 24) | 0xE0);
161 outb(0x1F7, 0x20); // cmd 0x20 - read sectors
162
163 // wait for disk to be ready
164 waitdisk();
165
166 // 从0x1F0读取SECTSIZE字节数到dst的位置,每次读四个字节(insl中l代表long,即4个字节),读取 SECTSIZE/ 4次
167 insl(0x1F0, dst, SECTSIZE/4);
168 }