写一简单kernel心得
接着bios开机自检(这个不需要了解,只需了解最后跳转到0x7c00处即可.对于写kernel的人来说也是透明的.除非你是写bios的).它将自动从0盘1扇区加载mbr(主引导程序,512字节必须是以0x55,0xaa结尾..如果不够,可以用times 510-($-$$) db 0.很多都是这样定义的.) 加载到0x7c00处.因为bios执行的最后一条指令时jmp 0x7c00处.
紧接着用汇编编写mbr.. Mbr的功能是也是从硬盘读取loader(加载内核的程序). Loader存放的位置跟加载到的内存地址由自己决定...mbr就是根据硬盘提供的接口(,0x1f2~0x1f7)读取内容.movsb之类的指令复制到内存.最后一个jmp指令跳转到loader加载的地址
(注:以上的地址都是物理地址)
紧接着编写loader程序(加载内核程序).在loader程序中.我打算通过bios中断获取整个计算机的物理内存.并且进入保护模式.因此在loader.S中..我要定义代码段,跟数据段.显示段(显示字符.一般是0xb8000位置.)还是定义页表(采用二级页表
--------------------------------------------------------------------
数据段的内容是定义
全局描述符表跟代码段数据段显示段描述符..还有定义各种段选择子.位置自己定义.(这里我深深体会到了intel为了跟前代保持兼容多么拼了..定义的描述符真的好恶心.....的说
代码段:
通过int 15h中断获取物理内存.保存在某一固定物理位置.紧接着打开A20 Gate.加载gdt(全局描述符表).将cpu的cr0的pe位置置为1(由此进入保护模式,cs,ds的值不再是段基址.而是段选择子)
紧接着将各种段选择子复制到段寄存器中(保护模式)................紧接着..就开始创建二级页表(位置.自己固定.不过我看知乎上..一般是在1m物理内存以下).. .创建好以后.用sgdt将描述符表地址(现在是物理地址)加载到寄存器中(注意在创建页目录表的时候...一般我看知乎是将地址映射到0xc000000处.(windows是采用这地址)........开启映射前后....内核的虚拟地址(一个原本的物理地址变成了线性地址),还有0xc000000..这2个线性地址都要映射到1m以下物理内存.等于768页目录跟0页目录(寻址方式.一个虚拟地址的前10位为页目录表索引.中间10位为页表索引.最后10位为页框偏移量)都指向一个页表.
紧接着栈指针也加上0xc000000处...为了映射).紧接着将cr0寄存器的31位置为1,表示开启分页机制..注意之前的加载的gdt是物理地址.现在改为线性地址.需要重新加载(其实就是原来的地址+0xc000000处..一个lgdt[gdt_ptr].ok...最后.......跳转到内核所在的地址(自己定义内核所在的地址).......内核编写需要了解elf(格式.推荐<<程序员的自我修养>>..我没看..只是网友推荐罢了).定义elf头.程序头表.节头表.内核大小偏移量.等这些定义好了后.就可以加载内核了.一jmp指令就搞定了.....其实加载的内核一开始就是int main(void){while(1); return 0}....没办法.啥系统调用都没实现.啥都要自己实现....没办法...轮子是一步一步造出来的嘛.
保护模式的话.主要体现在cpu的特权级...有了操作系统后.其实一个程序分为用户部分跟内核部分(权限不同..一般用户部分是3特权级,0是最高特权级.还有IO特权级这个就懒得解释了都一样.哪些端口可以访问权限).........主要从低特权级转移到高特权级(用户态到内核态转换)只能通过各种门(任务们,调用门.陷阱门.中断门等) 条件.访问门时 cpl<=dpl[门] &cpl>=dpl[段] 这个表示r3可以使用内核服务访问段max(cpl,rpl)<=dpl[段] 这样就可以访问段. 从高特权级向低特权级返回的话.用ret弹出ip.或者retf cs:ip.......
PS:门描述符也是定义在GDT中.Call 调用门选择子.参数就是选择子.选择子找到门描述符.结果门描述符定义了代码段的基址(段选择子)跟偏移量..接着..找到内核例程.............坑爹.....对了..从用户态到内核态.所用的代码段跟数据段权限是不同的.so.所对应的栈也不同.有个TSS(任务状态段就是用来保存上下文的..其实一般只保存ss:sp... TSS效率比较低.linux只用了一个TSS并不切换).
紧接着要了解函数调用约定.查了wiki.c语言用的是cdecl约定(eax,edx,ecx由调用者保存.其他是被调用者保存.返回值在eax中.由调用者清理栈空间(就是esp+xx.实际不能说清理),参数压栈从右到左.例子自己google查吧
紧接着...卧槽..总该用c语言了吧.....之前一直用汇编..开始实现打印hello os.然后就....想顺便学学内联汇编..结果我了个大去...内联汇编是.AT&T汇编语法跟x86汇编有很大不同.没办法.....google对比下.一个一个指令弄呗......接着..想实现一个printf函数..除了 bios中断....算了..自己实现..一查显卡端口控制..我勒个去..吓得半死..VGA寄存器..一大堆..从0懒得实现了......从github抄了别人实现可以光标跟踪的输出函数....不过也懂得其原理.一个读入写出寄存器一个控制寄存器...忘了....不过不重要.略过
紧接着实现中断......哥现在理解的所谓操作系统就是由中断驱动的.所谓的时间片不过是(时间中断产生的间隔时间罢了),等于躺着被调用的代码....中断根据网上资料cpu提供统一的接口为中断信号的公共路线..毕竟没则么多引脚.分为INTR跟NMI(灾难性错误).只管INTR就可以了.编写中断向量表(IDT).并且要保存在IDTR寄存器中.一般发出的信号就是中断向量号.这个号就是中断描述符表的索引,加上IDTR得到某中断描述符具体位置.接着.中断描述符保存中断处理程序的选择子跟偏移量..接着道GDT中找.到代码段的起始地址再加上偏移量(保存在IDT描述符中).最后还要了解8259A(外设的可屏蔽中断的代理.包括键盘.时间中断).编程设置它初始化.设置主片葱片级联方式........最后编写中断处理程序(参考 unix v6实现)..最后时间中断....网上一般采用计数器8253设置时间中断发生的频率.提速降速.随你便.2333333 接下来就是哥认为最兴奋的一笔.内存管理.通过位图映射来管理内存.位图的每一位映射一个4k大小的页.0表示该页未被使用.1表示已经被使用..位图相关处理函数写好后.正式开始规划内存了..
内存池的划分参考知乎某大牛说的答案.内存池分为虚拟内存池跟物理内存池.虚拟内存池管理虚拟地址.物理内存管理物理地址.......................我的话把物理内存一半给用户,一半给内核........虚拟内存池(每个用户进程都有一个).但内核虚拟地址只有一个(因为内核只有一个.对不对).有关位图的位置就是自己定义了.......虚拟地址映射物理地址.完成此操作就OK了(不过这里还没实现用户进程.so.只完成了内核相关虚拟地址映射.).其实malloc的..就是用链表管理更小的内存块.我这里最小的内存块...是4k...太大了..要很小(16字节.32字节.64字节.等).用内存块描述符管理更小相同规格的内存块.用块描述符链表将整个链接起来.
接下来实现线程..线程哎..说实话..也没啥.就一个执行流(有自己的栈.寄存器映像)..不过只能使用进程的虚拟地址(没资源).没啥很高深的..总之进程跟线程就是人为搞的代码块.各种状态也是人自己划分的.自圆其说.2333333333333.(注:我实现的是内核线程.不然..还要自己实现.用户线程调度..太麻烦了.不然这些都交给内核调度岂不美哉.
一开始呢.要实现PCB(不然..你的上下文保存在哪?.TSS,cpu只自动获取ss:sp,需要自己手动压入当前各种寄存器状态.因为如果用TSS(在内存)切换效率相当低.so.基本linux也不用TSS).TCB分为2部分(一个是被中断打断的栈空间.一个是线程自己空间,说明一点.一般发现线程基本都是个函数.函数的地址....嘿嘿没错就是当运行这个线程时cs:ip指向函数地址.就通过赋值语句.)忘说了.pcb需要占用一页大小内存.so.从内核内存池中申请一页大小.)...有关内核调度.只不过时间中断处理程序调用了scedule函数(开始要判断是否该线程时间片用完了没).........用链表管理就绪队列.跟总队列..其实我搞的这简单内核最复杂不过是用来链表罢了.所以写个简单内核..初中生差不多都可以完成.当然linus那种大神....不是一般人可以达到的.
最后说说锁...保证原子性(不能被打断).其实最基本的就是一条关中断cli汇编指令.实现的.只不过封装了一层..哎.....没想到这么简单吧
最后说说系统调用(参考linux最初版本,实现的这功能其实现在已经被弃用了)...mov eax,子功能号. Int 号码.调用syscall_table[eax].(其中system_table保存的是子功能的函数名(地址)........)...最后就是无聊的抄...系统调用实现的代码了.............
最后要实现用户进程.用户进程最主要区别就是特权级是最低特权级3.还有有自己的虚拟地址.流程(向内核申请一页大小.创建pcb.初始化pcb.创建管理虚拟地址用户位图...紧接着创建线程...线程中创建页目录表.添加到就绪队列中...等待调度......这里最关键是要让用户进程在特权级3下运行..so.其用户进程所使用的代码段跟数据段都必须是3特权级......完结.
参考<<一个操作系统的实现>>,<<x86从实模式到保护模式>>.<<现代操作系统>>
(上面其实已经写的省略了很多.这是我一口气写完的..可能忘了很多....如果有啥不对.请求指正)
文件系统跟shell部分.(...文件系统打算照搬unix v6的..我读都没读完..shell参考学院的教材(unix/linux编程实践)......
好了.很惭愧.只做了一点微小的工作.谢谢大家.