• Linux0.11内核代码浅读

    前言

    image-20241021155241557

    推荐书籍 《Linux 内核设计的艺术》、《Linux 内核完全注释》

    加载内存

  • BIOS 会将硬盘中启动区的 512 字节的数据,原封不动复制到内存中的 0x7c00 这个位置,并跳转到那个位置进行执行

    image-20241021155252738

  • 启动区:硬盘中的 0 盘 0 道 1 扇区的 512 个字节的最后两个字节分别是 0x55 和0xaa

    我们只需要把操作系统最开始的那段代码,编译并存储在硬盘的 0 盘 0 道 1 扇区即可

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

    image-20241021154530236

    mov ax,0x07c0
    mov ds,ax
    

    把 0x07c0 这个值复制到 ax 寄存器里,再将 ax 寄存器里的值复制到 ds 寄存器

    image-20241021154949574

    ds 是一个 16 位的段寄存器,具体表示数据段寄存器,在内存寻址时充当段基址的作用,作用为之后再写的代码,里面访问的数据的内存地址,都先默认加上 0x7c00,再去内存中寻址

    image-20241021155154706

    继续往下看

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

    image-20241028162709347

    其中 rep 表示重复执行后面的指令。

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

    重复执行次数: cx 寄存器中的值,也就是 256 次。

    复制路径:从 ds:si 处复制到 es:di 处。

    复制数量:复制一个字,16 位,也就是两个字节。

    作用:将内存地址 0x7c00 处开始往后的 512 字节的数据,原封不动复制到 0x90000 处

    image-20241101110337120

    再往后是一个跳转指令

    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 这行指令的位置

    image-20241101110527687

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

    image-20241101110906956

    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

    image-20241101154907090

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

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

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

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

    做好这些基础工作后,接下来就又该折腾了其他事了。

    总结拔高一下,这一部分其实就是把代码段寄存器 cs数据段寄存器 ds栈段寄存器 ss栈基址寄存器 sp 分别设置好了值,方便后续使用。

    再拔高一下,其实操作系统在做的事情,就是给如何访问代码,如何访问数据,如何访问栈进行了一下内存的初步规划。其中访问代码和访问数据的规划方式就是设置了一个基址而已,访问栈就是把栈顶指针指向了一个远离代码位置的地方而已。

    image-20241101155114676

    三种段寄存器的作用

    image-20241101155132130

    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 赋值都是作为这个中断程序的参数

    image-20241101164844517

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

    将硬盘的第 2 个扇区开始,把数据加载到内存 0x90200 处,共加载 4 个扇区

    image-20241101164940491

    如果复制成功,就跳转到 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 处

    整体流程

    image-20241204103923037

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

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

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

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