计算机系统启动流程
我们知道计算机启动是从BIOS开始,再由BIOS决定从哪个设备启动以及启动顺序,比如先从DVD启动再从硬盘启动等。计算机启动后,BIOS根据配置找到启动设备,并读取这个设备的第0个扇区,把这个扇区的内容加载到0x7c00,之后让CPU从0x7c00开始执行,这时BIOS已经交出了计算机的控制权,由被加载的扇区程序接管计算机。从电脑上电开始,到系统启动完毕,这之间隔着很多层,包括硬件的加载,软件加载,控制权的转移
物理结构
软盘
1.44软盘结构:2个盘面(0和1),一个盘面有80条磁道(或称磁柱),一个磁道有18个扇区,一个扇区大小为512Byte,于是软盘总容量:28018*512Byte=1474560Byte=1.44M;
相对扇区号={盘面(0~1)*每条磁道扇区数(18)} + {2*磁道(0~79)*每条磁道扇区数(18)} + {扇区(1-18)-1}
;
硬盘
硬盘多了一个磁头参数,相当于多块软盘叠起来了.
磁头数(Heads)表示硬盘总共有几个磁头,也就是有几面盘片, 最大为 255 (用 8 个二进制位存储);
柱面数(Cylinders) 表示硬盘每一面盘片上有几条磁道;
扇区数(Sectors) 表示每一条磁道上有几个扇区, 最大为 64(用 6个二进制位存储);
每个扇区一般是 512个字节,也有4096的
每个硬盘有128个盘片,每个盘片有两面,一共256个盘面(0-255);每个盘面有N个同心圆,就是N个柱面。每个盘面中的每个柱面叫磁道,每个磁道可以分成64个512字节的扇区。所以每个磁道一共32768 B。所以256个柱面的所有同心磁道一共内256*32768B=8388608B=8192KB=8MB。
MBR
硬盘的第一个扇区(sector)被称作MBR(Master Boot Record)。由于硬盘可以有多个分区,所以在MBR上,不仅放置着用于启动的可执行代码master boot,还放着磁盘分区表(DPT),占用66个字节,所以MBR中的可执行代码必须在512 - 66 = 446个字节以内。
MBR结构:
偏移 | 内容 | 大小(字节) |
---|---|---|
0h | 主引导程序 | 最大466 |
01BEh | 硬盘分区表(DPT) | 64 |
01FEh | 启动标志(0x55 0xAA) | 2 |
十六进制标记55 AA
,标志着一个有效的引导记录(boot
sector)的结尾。每一个分区的引导记录中都必须有这个标记。
硬盘分区表(DPT)结构:
偏移 | 内容 | 大小(字节) |
---|---|---|
01BEh | 分区1的分区数据表 | 16 |
01CEh | 分区2的分区数据表 | 16 |
01DEh | 分区3的分区数据表 | 16 |
01FEh | 分区4的分区数据表 | 16 |
分区数据表(Partition Data Table)结构:
偏移 | 内容 | 大小(字节) |
---|---|---|
00h | 引导ID标记(Boot indicator) | 00h:不可启动分区; 80h:可启动分区(只能有一个分区为此ID) |
01h | 起始扇区头号 | 1 |
02h | 起始扇区 | (柱面号的最高2位) 1 |
03h | 起始柱面号# | (柱面号的低位) 1 |
04h | 系统属性ID | 标记 1 |
05h | 结束扇区头号 | 1 |
06h | 结束扇区(柱面号的最高2位) | 1 |
07h | 结束柱面号# | (柱面号的低位) 1 |
08h | 此分区前的扇区总数目 | 4 |
0Bh | 此分区的扇区总数目 | 4 |
系统属性ID | 标记(System Indicator ) |
---|---|
00h | 未知操作系统 |
01h | DOS FAT12(16位扇区数) |
02h | XENIX |
04h | DOS FAT16(16位扇区数) |
05h | DOS 扩展分区(DOS 3.3+) |
06h | DOS 4.0 (Compaq 3.31), 32位扇区数 |
51h | Ontrack扩展分区 |
64h | Novell |
75h | PCIX |
DBh | CP/M |
FFh | BBT |
启动流程
BIOS
当机器被打开时,等电源稳定之后,电源会发送一个“加电成功信号”给芯片,以启动时钟生成器(8284);然后,CPU重新自设定为初始状态,开始准备运行。
当CPU最初被启动的时候,系统RAM中是空的,没有任何内容可供执行。当然CPU设计者也知道这一点,所以他们对CPU进行了预先编程,以让CPU在这个阶段总是去查找一个固定的位置FFFF0h,以启动系统BIOS ROM中的BIOS Boot Program,这个位置是统一内存架构 (Unified Memory Architeture简称UMA)临近结尾的位置。之所以选择这个位置是因为,不会引起由于ROM的大小改变而造成的兼容性问题。既然FFFF0h到UMA结束的位置之后16个字节,所以这里只放置着一个Jump指令,以进一步跳转到真正的BIOS startup program的位置。(不同的BIOS厂商可以将其放在不同的位置,只需要通过Jump指定就可以了)。
POST
然后BIOS开始实施Power-On Self Test(POST),在这个过程中,如果遇到任何错误,Booting处理就会结束,机器会被挂起。
然后BIOS开始查找显示卡。精确的说,是查找被内建在BIOS内部的显示卡程序,并执行它,它通常被放在C0000h的内存位置,它的作用是初始化显示卡。绝大多数的现代显示卡都能够在显示器上显示它的相关信息。这就是为什么当我们开机的时候,首先会在显示器的顶端会出现关于显示卡的信息。
然后BIOS会查看其它设备的ROM,看一看这些设备之中哪些存在着BIOS,通常能够在C8000h的位置找到IDE/ATA硬盘的BIOS,并执行它们。如果找到任何其它设备的BIOSes,它们也会被执行。然后BIOS显示它的启动屏幕。然后BIOS开始做进一步的检测,包括我们可以看到的内存容量检测。在这个阶段,如果BIOS遇到任何错误,BIOS将会在屏幕上显示它的错误信息。
然后BIOS会根据自己的"系统资源列表“,来对系统资源进行进一步的检测以确定究竟那些系统资源(设备)被安装在机器上。有些计算机会逐步显示这些被检测到的设备。如果BIOS支持Plug&Play标准,它将会检测和配置Plug&Play设备,并显示这些它找到的设备。等这一些检测结束之后,BIOS会在系统屏幕上列出一个检测总结。
总之,这个阶段有大量的事情要做,比如自检,初始化各种芯片,控制器,与端口;包括显示器,内存,键盘,软驱,串口等等;在这个过程中BIOS将检测到的数据放置于BIOS Data Area;同时还将中断向量以及BIOS程序运行所需要的Stack设置置于0到1K的RAM。
INT 19H
int 19h
是bios 开机自检以后第一条执行的指令,意思是找启动分区,在引导的时候按alt + ctrl + del的本质就是执行int 19h
如图所示,BIOS最后一条指令就是INT 19h
中断
INT 19会查看分区表,将被设为活动的分区的第一个Sector装入0X7C00的位置,正常的情况下,此Sector放置的就是boot sector程序. 可以通过配置BIOS来决定其搜索的顺序,这些设备包括Floppy Disk(A:),或者Hard Disk(C:),甚至还可以包括CD-ROM Driver或者其它设备。
当找到响应的启动设备之后,BIOS将会查找Boot信息以开始OS的启动过程。如果它找到了一个Hard Disk,它将会查找一个位于Cylinder 0, Head 0, Sector 1的Master Boot Record(硬盘的第一个扇区),如果它找到的是Floppy Disk,它也会读区软盘的第一个扇区。如果找不到任何启动设备,系统将会显示一条错误信息,然后冻结系统。
如果找到了响应的启动设备,BIOS会将读到的扇区放在内存0X7C00h
的位置,并跳转到那里将控制权交给OS的boot程序,从此以后,就由硬件启动阶段进入了OS启动阶段。
Boot
开机后,BIOS自检完毕以后加载COMS的参数,通过COMS的参数,BIOS程序加载启动磁盘的MBR到内存里运行,运行MBR的引导代码,这段代码会查找活动分区(BIOS不认识活动分区,但这段代码认识活动分区)的位置,加载并执行活动分区的PBR(另一段引导程序),与MBR类似,PBR在运行后加载操作系统的引导程序到内存运行,例如Windows的bootmgr或Linux的grub。当引导程序运行后,操作系统内核就被加载运行,完成从BIOS程序中接手的引导流程,整体流程如下图:
这第一个扇区的程序就叫Boot,由于Boot只能有一个扇区大小,即512字节,它所能做的工作很有限,因此它有可能不直接加载内核,而是加载一个叫Loader的程序,再由Loader加载内核。因为Loader不是BIOS直接加载的,所以它可以突破512字节的程序大小限制(在实模式下理论上可以达到1M)。如果Boot没有加载Loader而直接加载内核,我们可以把它叫做Bootloader。
Bootloader加载内核就要读取文件,在实模式下可以用BIOS的INT 13h中断。内核文件放在哪里,怎么查找读取,这里牵涉到文件系统,Bootloader要从硬盘(软盘)的文件系统中查找内核文件,因此Bootloader需要解析文件系统的能力。GRUB是一个专业的Bootloader,它对这些提供了很好的支持。
内核文件是什么格式的呢?跟Bootloader一样的当然可以。内核一般使用C语言编写,每次编译链接完成之后调用objcopy是可以的。
也可以手動支持通用的可执行文件格式,ELF(Executable and Linkable Format)。对于加载可执行文件,我们只需关注执行视图,即解析ELF文件,遍历Program Header Table中的每一项,把每个Program Header描述的Segment加载到对应的虚拟地址即可,然后从ELF header中取出Entry的地址,跳转过去就开始执行了。
对于ELF格式的内核文件来说,这个工作就需要由Bootloader完成。Bootloader支持ELF内核文件加载之后,用C语言编写的内核编译完成之后就不需要objcopy了。
多分区
Master Booter: 放置于Hard disk的第一个扇区(即MBR),用于装载boot block的程序,466字节。
Boot Sector:放置与Floppy的第一个扇区,或者Hard disk的某一分区的第一个扇区的用于装载Secondary boot,或其它程序的可运行程序,512字节。
Secondary Boot:放置于非Floppy/Hard disk的第一个扇区,以及Hard disk的任意分区的第一个扇区之外的任意其它位置,用于装载OS,或其它程序的可运行程序。 无大小限制。
Master booter最起码需要做这些事情:
- 检测MAGIC(Signature)是否为合法值(十六进制55 AA);
- 将自己移动到其它位置(一般是0x0600),将0x7C00到0x7c00+512的空间让出来,以备其后将boot sector程序装入这个位置,这样才能和直接从软盘直接装入boot sector程序相一致;
- 具体移动到什么位置,则根据设计而定,理论上,可以移动到任何非冲突位置(即没有被预留为其它程序所用的位置);
- 但一般情况下,都是在0X000800至0X0A0000之间寻找一端空间存放。
当用硬盘启动OS的时候,以上调用顺序为 MB -> BS -> SB -> OS;
当用软盘启动OS的时候,以上调用顺序为 BS -> SB -> OS
ORG 0X7C00
- org 指令影响的是 标号的绝对地址
- org 指令不会影响寄存器,说白了,不会影响CS 、DS、ES、SS
为什么是0x7c00
0x7C00这个地址来自Intel的第一代个人电脑芯片8088,以后的CPU为了保持兼容,一直使用这个地址。1981年8月,IBM公司最早的个人电脑IBM PC 5150上市,就用了这个芯片。当时,搭配的操作系统是86-DOS。这个操作系统需要的内存最少是32KB。
我们知道,内存地址从0x0000开始编号,32KB的内存就是0x0000~0x7FFF。8088芯片本身需要占用0x0000~0x03FF,用来保存各种中断处理程序的储存位置。所以,内存只剩下0x0400~0x7FFF可以使用。为了把尽量多的连续内存留给操作系统,主引导记录就被放到了内存地址的尾部。由于一个扇区是512字节,主引导记录本身也会产生数据,需要另外留出512字节保存。所以,它的预留位置就变成了:
0x7FFF - 512 - 512 + 1 = 0x7C00
为什么是ORG
- 首先,无论汇编代码头部加不加一句org 0x 7C00指令,主引导扇区代码都会被BIOS 自动加载到内存的0x0000:7C00处,自动加载这是焊死在硬件上面的规范,说白了,自动加载与org指令半点关系都没有;
- 其次,是不是主引导扇区代码,看的是你扇区最后两个字节是不是0x55aa,也和org指令没有半点关系;
- 然后,有些主引导扇区汇编代码不加org指令也可以正常运行,就是因为那些代码里面没有涉及到 标号label 相关的代码,不需要对 标号label 进行绝对地址的计算
- 接着,标号是标号,标号不是寄存器,标号是程序员可以自由任意随便取名字的,取的那个名字只是帮助你阅读代码用的,在代码经过汇编编译器之后标号会被解读成一个具体的数值,org影响的就是最后算标号的那个具体数值;
- 最后,这个标号代码的那个具体的数值是一个offset,即当前标号所在代码行距离代码开始处的字节数
为什么需要 org
- 假设,你的标号距离你的代码开始处是八个字节,那个标号的值原本应该就是0x08的,这个原本可以是不写org指令,也可以是写个org 0x0000,反正无论哪种这时标号会被解读成数值0x00000008;
- 由于你是主引导扇区的代码,你的代码被自动加载到内存0x0000:7C00处了,代码被加载到内存,实际上是说机器码被加载到内存,也就说是说无论你的代码被加载到0x0000:7C00还是被加载到0x1234:5678这里, 数值0x00000008还是数值0x00000008;
- 那么问题就来了?数值0x00000008是一个绝对地址,你现在代码全部在
0x0000:7C00
,你真正想要找的标号是被放到了从0x0000:7C00
开始后的八个字节的地方,它应该是0x00007C000+0x00000008 = 0x00007C08
而不是0x00000008
- 在代码开头使用org 0x7C00,代码还是写
mov ax ,bootmsg
但是它会悄悄地把bootmsg解读成 bootmsg+0x7C00
ELF格式
ELF在计算机科学中,是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件的文件格式。
是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的,也是Linux的主要可执行文件格式。
1999年,被86open项目选为x86架构上的类Unix操作系统的二进制文件标准格式,用来取代COFF。因其可扩展性与灵活性,也可应用在其它处理器、计算机系统架构的操作系统上
可以使用objcopy或者strip移除elf文件中的調試信息和符號表,變成純二進制文件
文件作用
ELF 文件参与程序的连接(建立一个程序)和程序的执行(运行一个程序),所以可以从不同的角度来看待 elf 格式的文件:
- 如果用于编译和链接(可重定位文件),则编译器和链接器将把 elf 文件看作是节头表描述的节的集合,程序头表可选。
- 如果用于加载执行(可执行文件),则加载器则将把 elf 文件看作是程序头表描述的段的集合,一个段可能包含多个节,节头表可选。
- 如果是共享文件,则两者都含有。
总体组成
elf 文件头描述 elf 文件的总体信息。包括: 系统相关,类型相关,加载相关,链接相关。
系统相关表示:elf 文件标识的魔术数,以及硬件和平台等相关信息,增加了 elf 文件的移植性,使交叉编译成为可能。 类型相关: 就是前面说的那个类型。 加载相关: 包括程序头表相关信息。 链接相关: 节头表相关信息。
链接视图通过Section Header Table描述,执行视图通过Program Header Table描述。
Section Header Table描述了所有Section的信息,包括所在的文件偏移和大小等;
Program Header Table描述了所有Segment的信息,即Text Segment, Data Segment和BSS Segment,每个Segment中包含了一个或多个Section。
readelf
readelf 命令可以查看ELF文件的详细信息
选项 | 含义 |
---|---|
-a | --all 显示全部信息,等价于 -h -l -S -s -r -d -V -A -I 。 |
-h | --file-header 显示 elf 文件开始的文件头信息. |
-l | --program-headers , --segments 显示程序头(段头)信息(如果有的话)。 |
-S | --section-headers , --sections 显示节头信息(如果有的话)。 |
-g | --section-groups 显示节组信息(如果有的话)。 |
-t | --section-details 显示节的详细信息( -S 的)。 |
-s | --syms , --symbols 显示符号表段中的项(如果有的话)。 |
-e | --headers 显示全部头信息,等价于: -h -l -S |
-n | --notes 显示 note 段(内核注释)的信息。 |
-r | --relocs 显示可重定位段的信息。 |
-u | --unwind 显示 unwind 段信息。当前只支持 IA64 ELF 的 unwind 段信息。 |
-d | --dynamic 显示动态段的信息。 |
-V | --version-info 显示版本段的信息。 |
-A | --arch-specific 显示 CPU 构架信息。 |
-D | --use-dynamic 使用动态段中的符号表显示符号,而不是使用符号段。 |
其他命令
objdump 查看目标文件或者可执行的目标文件
- objdump -d 查看每个段的汇编
- objdump -t 查看符号信息
nm 全称names,是linux下自带的特定文件分析工具,一般用来检查分析二进制文件、库文件、可执行文件中的符号表,返回二进制文件中各段的信息。 nm -s 輸出文件相關信息
输出符号 | 说明 | 擧個慄子 |
---|---|---|
A | 该符号的值是绝对的,在以后的链接过程中,不允许进行改变。 这样的符号值,常常出现在中断向量表中 |
用符号来表示各个中断向量函数在中断向量表中的位置。 |
B | 该符号的值出现在非初始化数据段(bss)中。 | 全局static int test,则符号test的类型为b,位于bss
section中。 其值表示该符号在bss段中的偏移。一般而言,bss段分配于RAM中 |
C | 该符号为common。common
symbol是未初始话数据段。 该符号没有包含于一个普通section中。只有在链接过程中才进行分配。 符号的值表示该符号需要的字节数 |
int test 并且该符号在别的地方会被引用,则该符号类型即为C。否则其类型为B |
D | 该符号位于初始话数据段中。一般来说,分配到data
section中。 全局int baud_table[5] = {9600, 19200, 38400, 57600, 115200},则会分配于初始化数据段中。 |
|
G | 该符号也位于初始化数据段中 | 主要用于small object提高访问small data object的一种方式。 |
I | 该符号是对另一个符号的间接引用。 | |
N | 该符号是一个debugging符号。 | |
R | 该符号位于只读数据区。 | 全局const int test[] = {123,
123} test就是一个只读数据区的符号 |
S | 符号位于非初始化数据区 | 用于small object。 |
T | 该符号位于代码区text section。 | |
U | 该符号在当前文件中是未定义的,即该符号的定义在别的文件中 | 当前文件调用另一个文件中定义的函数,在这个被调用的函数在当前就是未定义的 但是在定义它的文件中类型是T,但是对于全局变量来说 在定义它的文件中,其符号类型为C,在使用它的文件中,其类型为U。 |
V | 该符号是一个weak object。 | |
W | The symbol is a weak symbol that has not been specifically tagged as a weak object symbol. | |
- | 该符号是a.out格式文件中的stabs symbol。 | |
? | 该符号类型没有定义 |