Linux0.11 boot分析总结

最近在看linux启动过程,把看的过程中一些问题记了下来(有错误,麻烦大神指正!)推荐使用https://github.com/tinyclub/linux-0.11-lab.git可以一边运行,一边分析。该代码与原始代码有点不同,把system改成了kernel.bin,大部分是一样的。下载完之后运行make编译,然后make debug开始调试,调试的时候要打开另一个终端输入gdb src/kernel.sym。

初始断点设置需要修改目录下的.kernel_gdbinit.

target remote :1234
b startup_32
c
b main
c
bt
l

“b”是设置断点位置,startup_32就是head.s中的开头。“c”是继续运行,然后又停在main。

如果需要调试bootsect, 运行make debug DST=src/boot/bootsect.sym,打开另一个终端输入gdb src/boot/bootsect.sym,同样的设置初始断点位置编辑.boot_gdbinit。

linux启动过程如下图所示:

https://i-blog.csdnimg.cn/blog_migrate/5e59544f6bbf92e46d7e1d796f2ab367.png

在Makefile中也可以看出linux image由bootsectsetupkernel.bin组成,并且tools\build.sh合成,bootsect大小正是512byte

Image: boot/bootsect boot/setup kernel.bin FORCE

    $(BUILD) boot/bootsect boot/setup kernel.bin Image

    $(Q)rm -f kernel.bin

    $(Q)sync

目前所有的chip中bootloader都要放在第一个存储区,这样方便读取,也是芯片设计决定的,大小呢一般由第一块存储扇区决定(3.5英寸软区的扇区大小是512byte)。其实大小并不一定要是第一个扇区大小,现在有的芯片把bootloader长度信息放在最开头的128字节,只要芯片可以读到这个值就行。为什么bootsect正好是512?看下面代码:org 508+.word ROOT_DEV + .word 0xAA55=512

msg1:

    .byte 13,10

    .ascii "Loading system ..."

    .byte 13,10,13,10

 

    .org 508

root_dev:

    .word ROOT_DEV

boot_flag:

    .word 0xAA55

bootloader的主要作用就是加载kernel。上电之后bios就会将第一个扇区的内容加载到0x7c00的内存处(X86计算机启动流程分析之BIOS),若是芯片移植,这个地址不一定是0x7c00,要看一下芯片的datasheet)。下面这个图画的非常准确,内存分布在各个阶段很清楚:

https://i-blog.csdnimg.cn/blog_migrate/ae9ccbf70ee10ebb8e3b3badd6bb2579.jpeg

最开头的0地址由bios中断会使用,然后bootsect又将自已移动到0x90000。

为什么要移动?因为后面要把kernel移动到0x0开始的位置,kernel一般比较大,有可能将0x7c00这个地址覆盖掉,这个地址只需要满足后续读取kernel就可以了,并不一定要是0x90000。

然后就是读setup与kernel。这里有个问题为什么要分成setup与kernel,而不是一个bin,岂不更简单?

bootsect使用bios,bios是个非常简单的系统,可以理解为只有读取数据功能,读取完了之后将系统由bios切换到chip也就是setup接管之后将kernel移动到0位置,setup另一个工作便是从16位转到32位,也就是运行在保护模式,所以加载gdt与idt,也只作了最简单的初始化。细节部分以下这几篇文章分析的很清楚了。

  1. linux 0.11_boot启动详解1
  2. 一站式linux0.11内核head.s代码段图表详解
  3. bootsect.s 分析—— Linux-0.11 学习笔记(一)
  4. setup.s 分析—— Linux-0.11 学习笔记(二)

         我们接着说head.s ,kernel中的最开始部分是head.s也在boot目录。该工作就是初始化idt与gdt,与setup.s相比更详细一些。Linux0.1的时候就没有bootsect.s至setup.s切换,直接到了head.s( head.s 分析——Linux-0.11 学习笔记(三))。linux中的head.s是AT&T汇编,因为使用 GNU 的 as 和 ld 进行编译和连接。嵌入式移植应该都有toolchain,装好就行了。

为什么要初始化堆栈,setup.s中并没有,而且初始化时还作了两次?

startup_32:
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    mov %ax,%fs
    mov %ax,%gs
    lss stack_start,%esp
    call setup_idt
    call setup_gdt
    movl $0x10,%eax        # reload all the segment registers
    mov %ax,%ds        # after changing gdt. CS was already
    mov %ax,%es        # reloaded in 'setup_gdt'
    mov %ax,%fs
    mov %ax,%gs
    lss stack_start,%esp
    xorl %eax,%eax
1:    incl %eax        # check that A20 really IS enabled
    movl %eax,0x000000    # loop forever if it isn't
    cmpl %eax,0x100000
    je 1b

堆栈只在call,或者有push/pop时才会使用到,head.s是gun32位汇编,堆栈初始化用lss,setup是intel16位汇编,堆栈初始化在bootsect.s中:

! put stack at 0x9ff00.
    mov    ss,ax
    mov    sp,#0xFF00        ! arbitrary value >>512

后面一次是因为gdt与idt已经发生了变化,所以重新设置。

为什么要检测x87协处理器?为了弥补x86系列在进行浮点运算时的不足,Intel于1980年推出了x87系列数学协处理器,那时x87是一个外置的、可选的芯片。1989年,Intel发布了486处理器。从486开始,以后的CPU一般都内置了协处理器。这样,对于486以前的计算机而言,操作系统检测x87协处理器是否存在就非常必要了。

    movl %cr0,%eax          # check math chip
    andl $0x80000011,%eax  # Save PE ET PG
    orl $2,%eax                # set MP=1
    movl %eax,%cr0
    call check_x87
    jmp after_page_tables

函数调用有的用call,有的用jmb?两者并没有区别。在汇编程序中调用C函数,这里我再讲一上jmpi在实模式与保护模式下结果不同。实模式下jmpi go,INITSEG意思是转到INITSEG<<4+GO,在保护模式下jmpi    0,8就是段描述符在GDT中的偏移8,根据这个描述符中的段地址+GO。

after_page_tables:
    pushl $0       # These are the parameters to main :-)
    pushl $0
    pushl $0
    pushl $L6      # return address for main, if it decides to.
    pushl $main
    jmp setup_paging # 设置页目录和页表,并开启分页
L6:
    jmp L6          # main should never return here, but
                    # just in case, we know what happens.

call setup_idt



setup_idt:
    lea ignore_int,%edx
    movl $0x00080000,%eax
    movw %dx,%ax              /* selector = 0x0008 = cs */
    movw $0x8E00,%dx /* interrupt gate:dpl=0, present */

    lea idt,%edi      # 取idt的偏移给edi
    mov $256,%ecx     # 循环256次
rp_sidt:
    movl %eax,(%edi)     # eax -> [edi]
    movl %edx,4(%edi)    # edx -> [edi+4]
    addl $8,%edi         # edi + 8 -> edi
    dec %ecx
    jne rp_sidt
    lidt idt_descr       # 加载IDTR
    ret

        在设置页目录与页表中有两个循环,如何理解?

setup_paging:
    movl $1024*5,%ecx      # 每个页表占用1024个双字(4B),共5个页表
    xorl %eax,%eax          # eax = 0
    xorl %edi,%edi          # edi = 0
    cld
    rep;stosl               # eax -> es:[edi],edi每次增加4,重复ecx次
    movl $pg0+7,pg_dir     /* set present bit/user r/w */
    movl $pg1+7,pg_dir+4       
    movl $pg2+7,pg_dir+8       
    movl $pg3+7,pg_dir+12      
    movl $pg3+4092,%edi
    movl $0xfff007,%eax        /*  16Mb - 4096 + 7 (r/w user,p) */
    std
1:  stosl           /* fill pages backwards - more efficient :-) */
    subl $0x1000,%eax
    jge 1b
    xorl %eax,%eax      /* pg_dir is at 0x0000 */
    movl %eax,%cr3      /* cr3 - page directory start */
    movl %cr0,%eax
    orl $0x80000000,%eax
    movl %eax,%cr0      /* set paging (PG) bit */
    ret         /* this also flushes prefetch-queue */

清0的时候是从0x1000往0x5000递增,cld就是递增方向,std就是递减。rep是重复指令,中间有个";",没什么关系,没有也可以。

 

设置r/w,user,p时是递减,其实也可以写成递增方式,个人习惯而已,我并不明白为什么作者的注释说更有效,也可以将清0操作也改成递减

启动过程中还涉及到了信息的输出,台式电脑输出肯定是从显卡出来,bios中有最基本的显示字符操作(BIOS int 10H中断介绍),另个读取软区也是bios的工作(INT13中断详解)。现在的嵌入式一般从串口输出信息,比这简单多了,配置一下clk就行了,而数据一般存在sd卡或者flash,一般芯片配置boot pin,内部rom就可以读取。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值