文章目录

  • 最开始的两行代码
  • 自己给自己挪个地
  • 做好最基础的准备工作
  • 硬盘里其他部分也放到内存
  • 资料

最开始的两行代码

话不多说,直奔主题。当你按下开机键的那一刻,在主板上提前写死的固件程序 BIOS 会将硬盘中启动区的 512 字节的数据,原封不动复制到内存中的 0x7c00 这个位置,并跳转到那个位置进行执行。

启动区的定义非常简单,只要硬盘中的 0 盘 0 道 1 扇区的 512 个字节的最后两个字节分别是 0x550xaa,那么 BIOS 就会认为它是个启动区。

所以对于我们理解操作系统而言,此时的 BIOS 仅仅就是个代码搬运工,把 512 字节的二进制数据从硬盘搬运到了内存中而已。所以作为操作系统的开发人员,仅仅需要把操作系统最开始的那段代码,编译并存储在硬盘的 0 盘 0 道 1 扇区即可。之后 BIOS 会帮我们把它放到内存里,并且跳过去执行。

而 Linux-0.11 的最开始的代码,就是这个用汇编语言写的 bootsect.s,位于 boot 文件夹下。

通过编译,这个bootsect.s会被编译成二进制文件,存放在启动区的第一扇区。

随后就会如刚刚所说,由 BIOS 搬运到内存的 0x7c00 这个位置,而 CPU 也会从这个位置开始,不断往后一条一条语句无脑地执行下去。

那我们的梦幻之旅,就从这个文件的前两行代码开始啦!

mov ax,0x07c0
mov ds,ax

这段代码是用汇编语言写的,含义是把 0x07c0 这个值复制到 ax寄存器里,再将ax寄存器里的值复制到 ds寄存器里。那其实这一番折腾的结果就是,让ds这个寄存器里的值变成了0x07c0。

ds是一个 16 位的段寄存器,具体表示数据段寄存器,在内存寻址时充当段基址的作用。啥意思呢?就是当我们之后用汇编语言写一个内存地址时,实际上仅仅是写了偏移地址,比如:

mov ax, [0x0001]

实际上相当于:

mov ax, [ds:0x0001]

ds 是默认加上的,表示在 ds 这个段基址处,往后再偏移 0x0001 单位,将这个位置的内存数据,复制到 ax 寄存器中。

形象地比喻一下就是,你和朋友商量去哪玩比较好,你说天安门、南锣鼓巷、颐和园等等,实际上都是偏移地址,省略了北京市这个基址。当然你完全可以说北京天安门、北京南锣鼓巷这样,每次都加上北京这个前缀。不过如果你事先和朋友说好,以下我说的地方都是北京市里的哈,之后你就不用每次都带着北京市这个词了,是不是很方便?

那 ds 这个数据段寄存器的作用就是如此,方便了描述一个内存地址时,可以省略一个基址,没什么神奇之处。

再看,这个 ds 被赋值为了 0x07c0,由于 x86 为了让自己在 16 位这个实模式下能访问到 20 位的地址线这个历史因素(不了解这个的就先别纠结为啥了),所以段基址要先左移四位。那 0x07c0 左移四位就是 0x7c00,那这就刚好和这段代码被 BIOS 加载到的内存地址 0x7c00 一样了。

也就是说,之后再写的代码,里面访问的数据的内存地址,都先默认加上 0x7c00,再去内存中寻址。

为啥统一加上 0x7c00 这个数呢?这很好解释,BIOS 规定死了把操作系统代码加载到内存 0x7c00,那么里面的各种数据自然就全都被偏移了这么多,所以把数据段寄存器 ds 设置为这个值,方便了以后通过这种基址的方式访问内存里的数据。

自己给自己挪个地

带着上文两行代码继续看几行:

mov ax,0x07c0
mov ds,ax
mov ax,0x9000
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep movw

此时 ds 寄存器的值已经是 0x07c0 了,然后又通过同样的方式将 es 寄存器的值变成 0x9000,接着又把 cx 寄存器的值变成 256(代码里确实是用十进制表示的,与其他地方有些不一致,不过无所谓)。

再往下看有两个sub指令,这个sub指令很简单,比如sub a,b表示a = a - b,那么代码中的sub si,si就表示si = si - si,所以如果sub后面两个寄存器一模一样,就相当于把这个寄存器里的值清零。那就非常简单了,经过这些指令后,以下几个寄存器分别被附上了指定的值,我们梳理一下:

ds = 0x07c0
es = 0x9000
cx = 256
si = 0
di = 0

上节画的 CPU 寄存器的总图此时就是这样了:

干嘛要给这些毫不相干的寄存器赋值呢?其实就是为下一条指令服务的,就是

rep movw

其中 rep 表示重复执行后面的指令。而后面的指令 movw 表示复制一个(word 16位),那其实就是不断重复地复制一个字

那下面自然就有三连问:

重复执行多少次呢 是 cx 寄存器中的值,也就是 256 次。

从哪复制到哪呢 是从 ds:si 处复制到 es:di 处。

一次复制多少呢 刚刚说过了,复制一个字,16 位,也就是两个字节。

上面是直译,那把这段话翻译成大白话就是,将内存地址 0x7c00 处开始往后的 512 字节的数据,原封不动复制到 0x90000 处。就是下图的第二步。

没错,就是这么折腾了一下。现在,操作系统最开头的代码,已经被挪到了 0x90000 这个位置了。

再往后是一个跳转指令。

jmpi go,0x9000
go: mov ax,csmov ds,ax

仔细想想或许你能猜到它想干嘛。

jmpi 是一个段间跳转指令,表示跳转到 0x9000:go 处执行。

还记得上文说的 段基址 : 偏移地址 这种格式的内存地址要如何计算吧?段基址仍然要先左移四位,因此结论就是跳转到 0x90000 + go 这个内存地址处执行。忘记的赶紧回去看看,这才过了一回哦,要稳扎稳打。

再说 go,go 就是一个标签,最终编译成机器码的时候会被翻译成一个值,这个值就是 go 这个标签在文件内的偏移地址。这个偏移地址再加上 0x90000,就刚好是 go 标签后面那段代码 mov ax,cs 此时所在的内存地址了。

那假如 mov ax,cx 这行代码位于最终编译好后的二进制文件的 0x08 处,那 go 就等于 0x08,而最终 CPU 跳转到的地址就是 0x90008 处。

所以到此为止,本篇内容其实就是一段 512 字节的代码和数据,从硬盘的启动区先是被移动到了内存 0x7c00 处,然后又立刻被移动到 0x90000 处,并且跳转到此处往后再稍稍偏移 go 这个标签所代表的偏移地址处,也就是 mov ax,cs 这行指令的位置。

做好最基础的准备工作

上节说到,操作系统的代码最开头的 512 字节的数据,从硬盘的启动区先是被移动到了内存 0x7c00 处,然后又立刻被移动到 0x90000 处,并且跳转到此处往后再稍稍偏移 go 这个标签所代表的偏移地址处。接下来,就继续把我们的目光放在 go 这个标签的位置,跟着 CPU 的步伐往后看。

go: mov ax,csmov ds,axmov es,axmov ss,axmov sp,#0xFF00

这段代码的直接意思很容易理解,就是把 cs 寄存器的值分别复制给 dsesss 寄存器,然后又把 0xFF00 给了 sp 寄存器。

回顾下 CPU 寄存器图。

cs 寄存器表示代码段寄存器,CPU 当前正在执行的代码在内存中的位置,就是由 cs:ip 这组寄存器配合指向的,其中 cs 是基址,ip 是偏移地址。由于之前执行过一个段间跳转指令,还记得不?

jmpi go,0x9000

所以现在 cs 寄存器里的值就是 0x9000,ip 寄存器里的值是 go 这个标签的偏移地址。那这三个 mov 指令就分别给 ds、es 和 ss 寄存器赋值为了 0x9000。

ds 为数据段寄存器,之前我们说过了,当时它被复制为 0x07c0,是因为之前的代码在 0x7c00 处,现在代码已经被挪到了 0x90000 处,所以现在自然又该赋值为 0x9000 了。

es 是扩展段寄存器,仅仅是个扩展,不是主角,先不用理它。

ss 为栈段寄存器,后面要配合栈基址寄存器 sp 来表示此时的栈顶地址。而此时 sp 寄存器被赋值为了 0xFF00 了,所以目前的栈顶地址就是 ss:sp 所指向的地址 0x9FF00 处。

其实到这里,操作系统的一些最最最最基础的准备工作,就做好了。都做了些啥事呢?

第一,代码从硬盘移到内存,又从内存挪了个地方,放在了 0x90000 处。

第二数据段寄存器 ds代码段寄存器 cs 此时都被设置为了 0x9000,也就为跳转代码和访问内存数据,奠定了同一个内存的基址地址,方便了跳转和内存访问,因为仅仅需要指定偏移地址即可了。

第三,栈顶地址被设置为了 0x9FF00,具体表现为栈段寄存器 ss 为 0x9000,栈基址寄存器 sp 为 0xFF00。栈是向下发展的,这个栈顶地址 0x9FF00 要远远大于此时代码所在的位置 0x90000,所以栈向下发展就很难撞见代码所在的位置,也就比较安全。这也是为什么给栈顶地址设置为这个值的原因,其实只需要离代码的位置远远的即可。

总结一下,这一部分其实操作系统准备阶段在做的事情,就是设置如何访问代码的代码段寄存器cs,如何访问数据的数据段寄存器ds,如何访问栈的栈顶指针(栈段寄存器ss和栈基址寄存器sp),即做了一次内存的初步规划。其中访问代码和访问数据的规划方式就是设置了一个基址而已,访问栈就是把栈顶指针指向了一个远离代码位置的地方而已。

硬盘里其他部分也放到内存

做好基础工作后,又该新的一番折腾,接着往下看:

load_setup:mov dx,#0x0000      ; drive 0, head 0mov cx,#0x0002      ; sector 2, track 0mov bx,#0x0200      ; address = 512, in 0x9000mov ax,#0x0200+4    ; service 2, nr of sectorsint 0x13            ; read itjnc ok_load_setup       ; ok - continuemov dx,#0x0000mov ax,#0x0000      ; reset the disketteint 0x13jmp load_setupok_load_setup:...

这里有两个int指令我们还没见过。注意这个int是汇编指令,可不是高级语言的整型变量哟。int 0x13 表示发起 0x13 号中断,这条指令上面给 dx、cx、bx、ax 赋值都是作为这个中断程序的参数。中断是啥如果你不理解,先不要管,如果你就是放不下,那可以看认真聊聊中断,里面讲得非常细致。

总之这个中断发起后,CPU 会通过这个中断号,去寻找对应的中断处理程序的入口地址,并跳转过去执行,逻辑上就相当于执行了一个函数。而 0x13 号中断的处理程序是 BIOS 提前给我们写好的,是读取磁盘的相关功能的函数。

之后真正进入操作系统内核后,中断处理程序是需要我们自己去重新写的,这个在后面的章节中,你会不断看到各个模块注册自己相关的中断处理程序,所以不要急。此时为了方便就先用 BIOS 提前给我们写好的程序了。可见即便是操作系统的源码,有时也需要去调用现成的函数方便自己,并不是造轮子的人就非得完全从头造。

本段代码的注释已经写的很明确了,直接说最终的作用吧,就是将硬盘的第 2 个扇区开始,把数据加载到内存 0x90200 处,共加载 4 个扇区,图示其实就是这样。

注: 为了图片清晰表达意思,可能比例就不那么严谨了,大家不必纠结。

可以看到,如果复制成功,就跳转到ok_load_setup这个标签,如果失败,则会不断重复执行这段代码,也就是重试。那我们就别管重试逻辑了,直接看成功后跳转的ok_load_setup这个标签后的代码。

ok_load_setup:...mov ax,#0x1000mov es,ax       ; segment of 0x10000call read_it...jmpi 0,0x9020

这段代码省略了很多非主逻辑的代码,比如在屏幕上输出 Loading system … 这个字符串以防止用户等烦了。

剩下的主要代码就都写在这里了,就这么几行,其作用是把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处,和之前的从硬盘捣腾到内存是一个道理。

至此,整个操作系统的全部代码,就已经全部从硬盘中,被搬迁到内存来了。然后又通过一个熟悉的段间跳转指令jmpi 0,0x9020,跳转到 0x90200 处,就是硬盘第二个扇区开始处的内容。

那这里的内容是什么呢?先不急,我们借这个机会把整个操作系统的编译过程说下。整个编译过程,就是通过 Makefilebuild.c 配合完成的,最终会:

1. 把 bootsect.s 编译成 bootsect 放在硬盘的 1 扇区。

2. 把 setup.s 编译成 setup 放在硬盘的 2~5 扇区。

3. 把剩下的全部代码(head.s 作为开头)编译成 system 放在硬盘的随后 240 个扇区。

所以整个路径就是这样的。

所以,我们即将跳转到的内存中的 0x90200 处的代码,就是从硬盘第二个扇区开始处加载到内存的。第二个扇区的最开始处,那也就是 setup.s 文件的第一行代码咯。

那这个代码是什么呢?我们后面再说,不过先打开 setup.s 这个文件看看吧。

start:mov ax,#0x9000  ; this is done in bootsect already, but...mov ds,axmov ah,#0x03    ; read cursor posxor bh,bhint 0x10        ; save it in known place, con_init fetchesmov [0],dx      ; it from 0x90000....

好了,到目前为止,你是不是觉得,我去,这前面编译放在硬盘的位置,和后面代码写死的跳转地址,竟然如此地强耦合?那万一整错了咋办。

是啊,就是这样,你以为呢?在操作系统刚刚开始建立的时候,那是完全自己安排前前后后的关系,一个字节都不能偏,就是这么强耦合,需要小心翼翼,需要大脑时刻保持清醒,规划好自己写的代码被编译并存储在硬盘的哪个位置,而随后又会被加载到内存的哪个位置,不能错乱。

但这也是很有好处的,那就是在这个阶段,你完完全全知道每一步跳转,每一步数据访问都是怎么设计和规划的,不存在黑盒。

不像我们在写高级语言的时候,完全不知道是怎么底层帮我们做了多少工作。虽然这解脱了程序员关心底层细节的烦恼,但在遇到问题或者想知道原理的时候,就显得很讨厌了。所以珍惜这个阶段吧!而且,你在上层之所以能那么随心所欲,很多底层细节完全不用考虑,很省心,正是因为像今天这样以及之后每一章的各种底层代码小心翼翼的做了很多铺垫。

好了,本文的内容就结束了。这也标志着我们走完了第一个操作系统源码文件 bootsect.s,开始向下一个文件 setup.s 进发了!

资料

[1] 有关寄存器的详细信息,可以参考 Intel 手册:Volume 1 Chapter 3.2 OVERVIEW OF THE BASIC EXECUTION ENVIRONMEN。有关段寄存器的详细信息,可以参考 Intel 手册:Volume 1 Chapter 3.4.2 Segment Registers,其中有一张图清晰地描述了三种段寄存器的作用。

正如我们本文所涉及到的讲述一样,CS 是代码段寄存器,就是执行代码的时候带着这里存的基地址。DS 是数据段寄存器,就是访问数据的时候带着这里的基地址。SS 是栈段寄存器,就是访问栈时带着这里的基地址。

[2] 有关计算机启动部分的原理如果还不清楚,可以看我之前的一篇文章了解一下计算机的启动过程

[3] 如果想了解计算机启动时详细的初始化过程,还是得参考 Intel 手册:Volume 3A Chapter 9 PROCESSOR MANAGEMENT AND INITIALIZATION

[4] 本系列的扩展资料看这本系列资料仓

[5] 如果想了解汇编指令的信息,可以参考 Intel 手册:Volume 2 Chapter 3 ~ Chapter 5
比如本文出现的 sub 指令,你完全没必要去百度它的用法,直接看手册。

Intel 手册对于理解底层知识非常直接有效,但却没有很好的中文翻译版本,因此让许多人望而生畏,只能去看一些错误百出的中文二手资料和博客。这里有一个 Intel 手册翻译计划,感兴趣的也可以参与进去。

[6] 本系列全局视角图:

你管这叫操作系统源码(一)相关推荐

  1. 第6章 RTX 操作系统源码方式移植

    以下内容转载自安富莱电子: http://forum.armfly.com/forum.php 本章教程为大家将介绍 RTX 操作系统源码方式移植,移植工作比较简单,只需要用户添加需要的源码文件即可, ...

  2. 【RTX操作系统教程】第6章 RTX操作系统源码方式移植

    原文来源:http://forum.armfly.com/forum.php?mod=viewthread&tid=16616&highlight=RTX%B2%D9%D7%F7%CF ...

  3. 你管这叫操作系统源码(五)

    你管这叫操作系统源码之五 控制台初始化tty_init 时间初始化time_init 控制台初始化tty_init 按下键盘后为什么屏幕上就会有输出? void main(void) {...mem_ ...

  4. 你管这叫操作系统源码(二)

    文章目录 保护模式前的最后一次折腾内存 段寄存器的历史包袱 进入保护模式 资料 保护模式前的最后一次折腾内存 上篇品读完第一个操作系统源码文件bootsect.s,之后便跳转到0x90200地址开始执 ...

  5. 你管这叫操作系统源码(七)

    你管这叫操作系统源码之七 新进程诞生全局概述 move_to_user_mode fork init pause 从内核态到用户态 让进程无法逃出用户态 内核态与用户态的本质-特权级 特权级转换方式 ...

  6. 你管这叫操作系统源码(九)

    你管这叫操作系统源码之九 通过fork看一次系统调用 小结 fork中进程基本信息的复制 通过fork看一次系统调用 有了前两篇文章的铺垫,我们终于可以回到主流程看看fork函数了.这个fork函数稍 ...

  7. 敢写操作系统源码系列?我就等着看你笑话!

    闪客同学告诉我说,他要在公众号搞一个系列,带着大家像读小说一样品读Linux 0.11的核心代码,我立马给他泼了一盆冷水: 操作系统这么枯燥的东西,怎么可能写成小说那样? 写起来吃力又不讨好,你哼哧哼 ...

  8. 基于51单片机运行RTX51 Tiny操作系统源码模板之1.LED闪烁

    什么是RTX51 Tiny: RTX51 Tiny是一种实时操作系统(RTOS),可以用它来建立多个任务(函数)同时执行的应用.能灵活的调度系统资源,像CPU和存储器,并且提供任务间的通信.RTX51 ...

  9. 恒宝番外-闪客破玩意儿系列-操作系统源码

      自从出来工作了,写技术博客就很少聊番外了,其实之前上学的时候写了好多,同学们爱看,也爱调侃我"当初你退出文坛我是极力反对的"之类的话.后来逐渐觉得不重要,也就不写了.这两天我和 ...

最新文章

  1. stm32 高级定时器产生PWM
  2. python装饰器-Python 装饰器
  3. ajax无刷新评论示例
  4. 转帖Masonry介绍与使用实践(快速上手Autolayout)
  5. 前端开发人员需知——浏览器详解
  6. nginx 禁止某个IP访问
  7. 海量数据挖掘MMDS week2: 局部敏感哈希Locality-Sensitive Hashing, LSH
  8. ios icon尺寸问题
  9. 计算机设备如何巡检,计算机机房巡检管理制度
  10. 选用计算机教材的理由原因,教材选用及管理规定
  11. 【金融手册】什么是本票、汇票、支票、银行承兑汇票、系统性、非系统性风险、巴塞尔协议、QDII...
  12. 突破某些网站限制只能由微信打开的尴尬场景
  13. 有两个空酒桶,分别能舀3斤和7斤酒,还有1个装满了10斤的酒桶,我要卖5斤酒给剑客,这该如何是好?
  14. 古典音乐入门的常见问题
  15. Springboot毕业设计毕设作品,汽车租赁系统 开题报告
  16. TCP的分片和IP分片的区别
  17. 喜马拉雅忙着上市,蜻蜓FM忙着融资
  18. kafka中AR、ISR、OSR以及HW、LEO的区别
  19. 【机器学习自学笔记4】朴素贝叶斯分类器
  20. 计算机组成运算器原理实验报告,计算机组成原理运算器实验报告

热门文章

  1. 滚雪球学 Python 第三轮,Python Web 之 Django 的世界
  2. Compound原理
  3. Compound学习(一) README.md
  4. git(7)自定义 Git
  5. PS制作复古彩色画人像照片、创意分割照片
  6. 光学分辨率光声显微镜中基于深度学习的运动校正算法
  7. arm汇编的学习笔记,对比x86和arm(1)-从最简单的函数谈起
  8. MySQL树形结构设计
  9. 使用栈实现中缀表达式转换成后缀表达式并计算结果(逆波兰计算器)
  10. Elsevier模板(1)