操作系统如何把自己从硬盘搬运到内存
前文提要
栈顶地址被设置为了 0x9FF00,具体表现为栈段寄存器 ss 为 0x9000,栈指针寄存器 sp 为 0xFF00。
本节问题
- 操作系统是怎么把在硬盘中的自己搬运到内存中来的
访问内存
在上文中,操作系统将寄存器ds和cs设置为了0x9000,并且将栈顶地址ss:sp设置在了离代码的位置0x9000足够遥远的0x9FF00,保证栈向下发展不会轻易覆盖掉已有的代码。
| 寄存器 | 寄存器名 | 寄存器值 | 作用 |
|---|---|---|---|
| 数据段寄存器 | ds | 0x9000 | 访问数据 |
| 代码段寄存器 | cs | 0x9000 | 访问代码 |
| 栈顶代码 | ss:sp | 0x9FF00 | 访问栈 |
简单来说,设置了如何访问数据的数据段、如何访问代码的代码段、如何访问栈的栈顶指针——>初步做了一次内存规划
小结:硬盘中最开始的512字节加载到了内存中,但还有很多代码仍然在硬盘的其他扇区里
把剩下的操作系统代码从硬盘请到内存
load_setup:
mov dx,#0x0000 ; drive 0, head 0
mov cx,#0x0002 ; sector 2, track 0
mov bx,#0x0200 ; address = 512, in 0x9000
mov ax,#0x0200+4 ; service 2, nr of sectors
int 0x13 ; read it
jnc ok_load_setup ; ok - continue
mov dx,#0x0000
mov ax,#0x0000 ; reset the diskette
int 0x13
jmp load_setup
ok_load_setup:
...
Tips :这里有两个int指令,注意,它们不是高级语言中的整型变量,而是汇编指令
int指令
INT 指令是汇编语言中的中断指令。它允许程序在运行时调用系统服务或驱动程序。INT 指令的格式为 INT imm8,其中 imm8 是一个8位立即数,它指示哪个中断号。例如,INT 0x21 指示调用功能 0x21 的中断服务程序。
INT 指令会将程序的控制权转移到中断服务程序。中断服务程序执行完后,程序会恢复执行。
INT 指令在 DOS 系统中是常用的系统调用方式,在 Windows 和 Linux 系统中则不常用。
寄存器传参
寄存器传参是指在汇编语言中使用寄存器来传递参数给函数或子程序。这种方式不需要在内存中开辟空间来存储参数,而是直接使用寄存器来存储参数。
寄存器传参通常使用 CPU 内部的寄存器来传递参数。常用的寄存器有 EAX, EBX, ECX, EDX 等。不同的编译器和操作系统有不同的寄存器用法。
寄存器传参的优点是速度快,因为数据不需要在内存和寄存器之间来回复制。缺点是需要更多的寄存器来存储参数,如果参数过多可能会导致寄存器不够用。
中断
在汇编语言中,中断一般通过 INT 指令来实现。INT 指令会将程序的控制权转移到对应的中断服务程序(ISR)。中断服务程序执行完后会返回到原来的程序继续执行。
每个中断都有一个独特的中断号,在汇编中通过 INT 指令来指定中断号来调用不同的中断服务程序。例如,INT 0x21 会调用功能号为 0x21 的中断服务程序。
在汇编语言中,中断服务程序通常使用 IRET 指令来结束中断处理并返回到原来的程序。
在汇编语言中还可以使用 CLI 和 STI 指令来控制中断,CLI 指令用来禁止中断,STI 指令用来恢复中断。
示例 以上面的代码为例,int 0x13 表示发起 0x13 号中断,,这条指令上面的各种 mov 指令,用来给 dx、cx、bx、ax 赋值,这四个寄存器都是作为这个中断程序的参数。
扩展 :与寄存器传参对应的是栈传参,在C语言中应用很广。
伪*执行函数
发起中断后,CPU 就会通过这个中断号 0x13,去寻找对应的中断处理程序的入口地址,并跳转过去执行,逻辑上就相当于执行了一个函数。
Tips: 而 0x13 号中断的处理程序,是 BIOS 提前给我们写好的,具体就是读取磁盘的相关功能的函数。
Linux 在此处用这个 0x13 号中断干了什么?
load_setup:
mov dx,#0x0000 ; drive 0, head 0
mov cx,#0x0002 ; sector 2, track 0
mov bx,#0x0200 ; address = 512, in 0x9000
mov ax,#0x0200+4 ; service 2, nr of sectors
int 0x13 ; read it
...
这段代码的作用:从硬盘的第 2 个扇区开始,把数据加载到内存 0x90200 处,共加载 4 个扇区。
load_setup:
...
jnc ok_load_setup ; ok - continue
...
jmp load_setup
ok_load_setup:
...
此处的jnc和jmp指令表示成功和失败分别跳转到哪个标签处,相当于高级语言的if else,如果复制成功,就跳转到 ok_load_setup 这个标签;如果失败,则会不断重复执行这段代码。
ok_load_setup之后的代码
ok_load_setup:
...
mov ax,#0x1000
mov es,ax ; segment of 0x10000
call read_it
...
jmpi 0,0x9020
这段代码的作用:把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处
到这里,整个操作系统的代码已经全部从硬盘加载到内存中了。这些代码,又通过段间跳转指令 jmpi 0,0x9020,跳转到 0x90200 处,就是硬盘第二个扇区开始处的内容。
操作系统的编译过程
编译操作系统的过程通常包括以下几个步骤:
- 配置:使用配置文件或命令行工具来配置系统的各种参数和选项。
- 编译:使用编译器将源代码编译成机器码。这一步可能需要花费很长时间。
- 链接:将编译生成的机器码和其他库文件连接在一起,形成可执行文件。
- 安装:将可执行文件和其他必要的文件安装到系统中,以便启动和运行。
- 测试:对编译好的系统进行测试,以确保其功能正确且稳定。
对于目前已经阅读的代码,整个编译过程,就是通过 Makefile 和 build.c 配合完成的,最终达到这样一个效果:
- 把 bootsect.s 编译成 bootsect 放在硬盘的 1 扇区;
- 把 setup.s 编译成 setup 放在硬盘的 2~5 扇区;
- 把剩下的全部代码(head.s 作为开头,与各种 .c 和其他 .s 等文件一起)编译并链接成 system,放在硬盘的随后 240 个扇区。
同时,0x90200 处的代码,也就是我们即将跳转到的内存地址处的代码,就是从硬盘第二个扇区加载过来的。而第二个扇区的最开始处,也就是 setup 二进制文件的内容,是由 setup.s 源代码文件编译后形成的。
总结
了解了操作系统如何把自己从硬盘加载到内存,研究了 Linux 0.11 整个编译和加载的简要过程,操作系统的代码已经完全从硬盘被搬到内存中。
最后
笔记整理:千石
内容来源:极客时间《Linux源码趣读》学习笔记 Day 4
支持:点赞、评论、收藏