0%

BIOS内核载入的方式(引导实验)

之前花了点时间稍微研究了下系统内核载入,重新翻出来整理一下:

内核载入之前的工作


1981年IBM PC刚推出时,系统只带有640K的的RAM,内存寻址范围最大也只有1M。现如今基本的32位机都能寻址到4G,经过CPU的新特性更是可以支持到64G的物理内存。但是为了与原来的PC想兼容,系统1M一下物理内存使用分配上仍然保持与原来一致。ROM BIOS都在物理内存能寻址的最高端位置处,所以在大于1M内存的新计算机上,ROM BIOS最后会被映射到1M末尾的 Shadow 区域中。

内核

计算机上电初始化时,物理内存被设置成从地址0开始的连续区域。除了地址从0xA0000~0xFFFFF(640K~1M)存放显存和ROM BIOS和0xFFFE0000~0xFFFFFFFF(4G的最后64K)存放ROM BIOS以外,其他部分都可以用作主要内存。

后续内核载入之后会重新分配,以后再研究。

x86体系的PC启动之后由BIOS完成自检,然后开始搜索外存上的引导信息,查找设备进行系统启动。BIOS会把硬盘等设备的头512字节数据复制到内存,检查这512字节是不是以16进制的0x55和0xaa结尾,这是启动引导程序的标记。

用汇编写一个简易的引导程序


下面是一个简单的程序:

1
2
3
4
5
6
7
8
9
.global start
.code16
.text
start:
movw $0xb800,%ax
movw %ax,%ds
movb $0x41,(0)
movb $0x1f,(1)
hlt

.global start 这一句把start标签做全局符号导出,后面链接的时候会用到。

把代码保存为boot.s文件。然后用gas的as.exe和ld.exe两个程序真正把这段汇编代码处理成机器码

1
2
3
as -o boot.o boot.s

ld -Ttext 0x200 -s -o boot --entry start boot.o

-Ttext 0x200 的作用是让输出的代码设置为从boot输出文件的0x200位置开头,否则我们就很难确定我们得到的boot文件从哪开始才是代码,一会写入的时候也从这里写就行。

ld 命令里的 --entry start 这段就是我们之前的汇编文件中需要写 .global start 这句声明的原因。指定入口。

在保护模式启动之前,微机处于实地址模式,这个时候需要先设定数据段段寄存器的地址,然后就可以用一个16位的地址来访问内存开头的1M空间了。

.code16 这一句声明是为了让gas了解到这段程序是为实模式编写的,且限定为16位的汇编编译。

首先把数据段寄存器设置成0xb800,这样,地址0就会指向PC的CPU中用来显示屏幕的一段内存的开头。在这个CPU的最初的阶段,一般屏幕有80行,25列,而当我们把数据段寄存器赋予值0xb800,地址0所指向的字节就代表第一行第一个字符,这里是ascii码中的大写字母A,即0x41。地址1现在指向的字节代表了第一行第一个字符的字体颜色和背景颜色,格式与windows命令行的color命令一样。

详细如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
颜色设置
颜色属性由两个十六进制数字指定 -- 第一个为背景,第二个则为前景。每个数字可以为以下任何值之一:
0 = 黑色
1 = 蓝色
2 = 绿色
3 = 浅绿色
4 = 红色
5 = 紫色
6 = 黄色
7 = 白色
8 = 灰色
9 = 淡蓝色
A = 淡绿色
B = 淡浅绿色
C = 淡红色
D = 淡紫色
E = 淡黄色
F = 亮白色

如果把上面这段程序作为一个引导写进硬盘,则之后理论上将能够看到运行结果是屏幕上出现一个蓝底白字的A。

准备写入存储设备,了解一下BIOS启动的流程,主要是MBR


完成编译之后需要把这块写入硬盘/U盘等存储设备。

刚开始我是拿U盘来试的,结果差点把我的U盘给毁了……汗……后来想到在VMware虚拟机里面直接新建一块硬盘来做。

vmware1

此时这块新建的磁盘里面什么都没有,但是用winhex打开磁盘1,看到的是这样的:

vmware2

winhex是一个16进制的编辑器,可以直接用来查看文件或者读取硬盘,免费版只能查看不能修改,不过这个对我们来说已经够了,因为之后的写硬盘操作我们要用C语言直接完成。

不管硬盘上设定的一扇区有多大,第一个扇区的引导都是固定512B,可以看到前512B的最后两个字节是 55 AA,即引导结束标志。然后开始部分是一些初始程序和找不到操作系统的提示信息。

这些东西都是在创建硬盘的时候里面默认写着的,如果对此块硬盘进行引导的话,将返回这些错误信息。


下面解释一下MBR的相关知识:

MBR全称是Master Boot Record,硬盘主引导记录,它存在于硬盘的第一个物理扇区,也就是0面0磁道1扇区。

软盘上的引导扇区的作用是把软盘上存放的真正的操作系统内核读到内存中,然后跳到内核去执行。而MBR的作用却是找到硬盘上的活动分区,然后把活动分区上的引导记录读到内存中,然后再跳到此引导记录中去执行,后面的过程就与从软盘启动计算机一致了。

这里,可以把硬盘上的活动分区都看成一是个软盘。我们知道一个硬盘可以分成多个分区,也就是说一个硬盘可以视为由多个软盘组成,每一个分区都可以有自己的引导记录,这就如每个软盘上都有自己的引导记录一样。那么哪一个分区(软盘)用来真正引导计算机呢?这就由MBR来完成,MBR查看每个分区记录,当找到一个活动分区时,就把此活动分区的引导记录载入。

显然,必须有一个地方来存放硬盘的分区记录,这就是所谓的硬盘分区表。这个硬盘分区表也在MBR中,不过MBR只有一个扇区大小,既要有用来查找硬盘分区表的代码,还要包含硬盘分区表,所以这个硬盘分区表不可能很大。实际上,这个表只有四个表项,也就是说在一块硬盘里,最多有4个分区能用来引导电脑。

再来完整的看看MBR载入的流程,计算机启动的时候,MBR先把自己搬移到0x0600处,随后MBR开始查找内部的硬盘分区表表项,之后找到的引导程序会被BIOS载入到内存的0x7C00处,然后BIOS跳转到0x7C00处执行。

每个硬盘分区表表项的第一个字节只能是0x80或者0x00。如果是0×80表示这是一个活动分区,然后MBR再从这个表项中读出此分区在硬盘上的位置,将此分区的第一个扇区(含有此分区的引导记录)读入0x7C00处,然后跳到0x7C00处执行。如果表项的第一个字节是0x00,则查找下一个表项,直到四个表项都找完为止。如果此第一个字节既不是0x80又不是0x00,则打印一条错误信息。

MBR的整个结构,总长度上面说过了,一共512字节:

|地址|说明|长度(Byte)
|-|
|0x000(0)|MBR中的执行代码|440
|0x1B8(440)|磁盘商标(可选)|6
|0X1BE(446)|分区表(共4项,每项16字节)|4*16=64
|0X1FE(510)|引导记录标志(55AA)|2

分区表表项结构如图所示,总长度一共16字节:

|地址偏移量|说明|长度(Byte)
|-|
|0x0|状态码,0x80表示为活动分区,0x00表示为非活动分区,其他值无效|1
|0x1|分区的第一个扇区的CHS地址(磁头、柱面、扇区)|3
|0x4|分区类型(FAT、NTFS、ext2、ext3等)|1
|0x5|分区的最后一个扇区的CHS地址|3
|0x8|分区的第一个扇区的LBA地址|4
|0xC|分区的长度(多少个扇区)|4

所以写入前440个长度即可。后面的保持不变。

……之前拿U盘试的时候直接改了前面512B的所有内容,导致后来电脑直接读不出来U盘的东西了。因为分区表整个被破坏了。

把引导写入硬盘


写硬盘用C语言来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <string.h>
#include <windows.h>

int main()
{
FILE *fp = NULL;
DWORD length;
HANDLE handle = NULL;
byte buff[512],buf[512];
int i;

//读取硬盘,并把原始的引导信息保存下来
handle = CreateFile(TEXT("\\\\.\\PHYSICALDRIVE1"),GENERIC_READ,0,NULL,OPEN_EXISTING,0,0);
if (handle == INVALID_HANDLE_VALUE)
{
printf("Cannot open the Device~!\n");
return -1;
}
memset(buff,0,sizeof(buff));
ReadFile(handle,buff,sizeof(buff),&length,NULL);
CloseHandle(handle);

for (i=0;i<length;i++)
{
if (i%16==0) printf("\n");
printf("%02X ",buff[i]);
}
printf("\n");

//打开之前写好的引导文件
fp = fopen("boot","rb");
if (fp == NULL)
{
printf("Cannot open the File~!\n");
return -1;
}
memset(buf,0,sizeof(buf));
fseek(fp,0x200,SEEK_SET); //之前编译的时候指定的文件位置在这里用上
fread(buf,sizeof(buf),1,fp);
fclose(fp);
for (i=440;i<512;i++) buf[i]=buff[i]; //保留原有的引导的440以后的内容

//把整理好的引导信息写进硬盘
handle = CreateFile(TEXT("\\\\.\\PHYSICALDRIVE1"),GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,0);
if (handle == INVALID_HANDLE_VALUE)
{
printf("Cannot open the Device~!\n");
return -1;
}
if (!WriteFile(handle,buf,sizeof(buf),&length,NULL))
{
printf("Writing Error~!");
return -1;
} else printf("Writing Success~!");
CloseHandle(handle);

return 0;
}

写入完成之后,重启虚拟机,把硬盘1的启动项调到硬盘0前面去,然后启动!

vmware3

BIOS闪了一下之后,果然左上角出现了预先设计的A,至此引导程序实验成功。然后重新恢复启动项正常进去之后硬盘1也没有出什么问题。

最后…这篇文章给了我很大的帮助