Linux0.11内核代码浅读
Linux0.11内核代码浅读
前言
推荐书籍 《Linux 内核设计的艺术》、《Linux 内核完全注释》
加载内存
BIOS 会将硬盘中启动区的 512 字节的数据,原封不动复制到内存中的 0x7c00 这个位置,并跳转到那个位置进行执行
启动区:硬盘中的 0 盘 0 道 1 扇区的 512 个字节的最后两个字节分别是 0x55 和0xaa
我们只需要把操作系统最开始的那段代码,编译并存储在硬盘的 0 盘 0 道 1 扇区即可
Linux-0.11 的最开始的代码,就是这个用汇编语言写的 bootsect.s,位于boot 文件夹下
mov ax,0x07c0 mov ds,ax
把 0x07c0 这个值复制到 ax 寄存器里,再将 ax 寄存器里的值复制到 ds 寄存器里
ds 是一个 16 位的段寄存器,具体表示数据段寄存器,在内存寻址时充当段基址的作用,作用为之后再写的代码,里面访问的数据的内存地址,都先默认加上 0x7c00,再去内存中寻址
继续往下看
mov ax,0x9000 mov es,ax mov cx,#256 sub si,si sub di,di rep movw
其中 rep 表示重复执行后面的指令。
而后面的指令 movw 表示复制一个字(word 16位),那其实就是不断重复地复制一个字。
重复执行次数: cx 寄存器中的值,也就是 256 次。
复制路径:从 ds:si 处复制到 es:di 处。
复制数量:复制一个字,16 位,也就是两个字节。
作用:将内存地址 0x7c00 处开始往后的 512 字节的数据,原封不动复制到 0x90000 处
再往后是一个跳转指令
jmpi go,0x9000 go: mov ax,cs mov ds,ax
jmpi 是一个段间跳转指令,表示跳转到 0x9000:go 处执行
0x9000:go 段基址 : 偏移地址
go 就是一个标签,最终编译成机器码的时候会被翻译成一个值,这个值就是 go 这个标签在文件内的偏移地址
假如 mov ax,cx 这行代码位于最终编译好后的二进制文件的 0x08 处,那 go 就等于 0x08,而最终 CPU 跳转到的地址就是 0x90008
总结:一段 512 字节的代码和数据,从硬盘的启动区先是被移动到了内存 0x7c00 处,然后又立刻被移动到 0x90000 处,并且跳转到此处往后再稍稍偏移 go 这个标签所代表的偏移地址处,也就是 mov ax,cs 这行指令的位置
go: mov ax,cs mov ds,ax mov es,ax mov ss,ax mov sp,#0xFF00
ax: 通常用来执行加法,函数调用的返回值一般也放在这里面 bx: 数据存取 cx: 通常用来作为计数器,比如for循环 dx: 读写I/O端口时,edx用来存放端口号 sp: 栈顶指针,指向栈的顶部 bp: 栈底指针,指向栈的底部,通常用`ebp+偏移量`的形式来定位函数存放在栈中的局部变量 si: 字符串操作时,用于存放数据源的地址 di: 字符串操作时,用于存放目的地址的,和esi两个经常搭配一起使用,执行字符串的复制等操作 cs: 代码段 ds: 数据段 ss: 栈段 es: 扩展段 fs: 数据段 gs: 数据段
参考
cs 寄存器里的值就是 0x9000,ip 寄存器里的值是 go 这个标签的偏移地址。那这三个 mov 指令就分别给 ds、es 和 ss 寄存器赋值为了 0x9000
ds 为数据段寄存器,之前我们说过了,当时它被复制为 0x07c0,是因为之前的代码在 0x7c00 处,现在代码已经被挪到了 0x90000 处,所以现在自然又改赋值为 0x9000 了
其实到这里,操作系统的一些最最最最基础的准备工作,就做好了。都做了些啥事呢?
第一,代码从硬盘移到内存,又从内存挪了个地方,放在了 0x90000 处。
第二,数据段寄存器 ds 和代码段寄存器 cs 此时都被设置为了 0x9000,也就为跳转代码和访问内存数据,奠定了同一个内存的基址地址,方便了跳转和内存访问,因为仅仅需要指定偏移地址即可了。
第三,栈顶地址被设置为了 0x9FF00,具体表现为栈段寄存器 ss 为 0x9000,栈基址寄存器 sp 为 0xFF00。栈是向下发展的,这个栈顶地址 0x9FF00 要远远大于此时代码所在的位置 0x90000,所以栈向下发展就很难撞见代码所在的位置,也就比较安全。这也是为什么给栈顶地址设置为这个值的原因,其实只需要离代码的位置远远的即可。
做好这些基础工作后,接下来就又该折腾了其他事了。
总结拔高一下,这一部分其实就是把代码段寄存器 cs,数据段寄存器 ds,栈段寄存器 ss 和栈基址寄存器 sp 分别设置好了值,方便后续使用。
再拔高一下,其实操作系统在做的事情,就是给如何访问代码,如何访问数据,如何访问栈进行了一下内存的初步规划。其中访问代码和访问数据的规划方式就是设置了一个基址而已,访问栈就是把栈顶指针指向了一个远离代码位置的地方而已。
三种段寄存器的作用
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: ...
int 0x13 表示发起 0x13 号中断,这条指令上面给 dx、cx、bx、ax 赋值都是作为这个中断程序的参数
CPU 会通过这个中断号,去寻找对应的中断处理程序的入口地址,并跳转过去执行,逻辑上就相当于执行了一个函数。而 0x13 号中断的处理程序是 BIOS 提前给我们写好的,是读取磁盘的相关功能的函数。
将硬盘的第 2 个扇区开始,把数据加载到内存 0x90200 处,共加载 4 个扇区
如果复制成功,就跳转到 ok_load_setup 这个标签,如果失败,则会不断重复执行这段代码,也就是重试
ok_load_setup: ... mov ax,#0x1000 mov es,ax ; segment of 0x10000 call read_it ... jmpi 0,0x9020
这段代码省略了很多非主逻辑的代码,比如获取硬盘部分信息,在屏幕上输出 Loading system等
剩下的代码作用是把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处
整体流程
所以,我们即将跳转到的内存中的 0x90200 处的代码,就是从硬盘第二个扇区开始处加载到内存的。第二个扇区的最开始处,那也就是 setup.s 文件的第一行代码
把 bootsect.s 编译成 bootsect 放在硬盘的 1 扇区。
把 setup.s 编译成 setup 放在硬盘的 2~5 扇区。
把剩下的全部代码(head.s 作为开头)编译成 system 放在硬盘的随后 240 个扇区。