main.C

  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 }

 

posted @ 2021-11-09 11:51  Pril  阅读(280)  评论(0)    收藏  举报