最近在看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启动过程如下图所示:
在Makefile中也可以看出linux image由bootsect、setup、kernel.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)。下面这个图画的非常准确,内存分布在各个阶段很清楚:
最开头的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,也只作了最简单的初始化。细节部分以下这几篇文章分析的很清楚了。
- linux 0.11_boot启动详解1
- 一站式linux0.11内核head.s代码段图表详解
- bootsect.s 分析—— Linux-0.11 学习笔记(一)
- 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
lea idt,%edi # 取idt的偏移给edi |
在设置页目录与页表中有两个循环,如何理解?
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就可以读取。