手写一个X86操作系统实战:从零开始构建一个U盘启动的自制操作系统(一)
这个标题可能有点大了:-)
一个操作系统至少应该有自己的文件系统和进程机制,不过我们的最终目标应该是这个~
无论如何,看完本文,你应该可以手写一段通过U盘启动的在PC上运行的不需要其他软件来协助的自启动的代码,这无疑是一个完整的操作系统的基础。
这篇文章本来是基于《[操作系统]从零开始的OS》这里的资料:
http://www.cnblogs.com/huqingyu/archive/2005/02/28/110186.html
我也是刚刚开始学这几篇教程。
网上还有几篇不错的几乎相同类型的教程,不过,可能是因为教程都比较年代久远了,全部都是将操作系统放到软盘上执行。
本人菜鸟一个,搞清楚怎么正确的将文件写到U盘上都耗了我不少时间,于是产生了将自己的经验写成博客与同道中人共享的念头。
另外这些教程中编码的方式几乎都是比较适合UNIX\linux类的系统的,而本人如果长时间不用windows会身体不适(主要是用不惯Ubuntu上的那种拼音输入法,呵呵),
所以这篇教程会同时讲解linux和windows两个操作系统中的做法,一般来说读者只需要掌握其中一个操作系统中的情形就足够了。
另外考虑到linux程序员一般对我们需要讲到的概念比较熟悉,因此相应的,windows版本的讲解可能会多讲一些基础。
学习实践的过程中我也遇到不少困难,并被我一一化解,相信笔者的经验一定能让对这个主题有兴趣的初学者有一定的帮助。
如果本文有任何较难理解的部分,那一定是笔者的技术水平不足,或是语言功底不好,还请读者留言指正与交流。
那好,废话就说到这里,Let's go!
---------------------------------------------我是正文开始的分割线----------------------------------------
导言
我们的PC使用的x86体系,总是在计算机启动后从BIOS中开始执行指令,然后查找设备来启动。
如果是我们平日里的那种正常从硬盘启动,BIOS会把硬盘的头512字节的数据复制到内存,检查这512字节是不是以16进制的55 aa结尾,
如果是,就猜想这是一个启动程序,并跳转到这段程序的开头执行指令。
以上这个过程是操作系统的载入器部分负责的。换句话说,操作系统载入器就是硬盘的头512字节中含有的代码。
正如上文所言,载入器是一个完整的操作系统中首先被执行的代码,我们现在就来编写这一段代码。
在载入器这个阶段,基本的硬件是准备就绪了,但是C语言等高级语言所编成的程序需要的堆栈,系统函数等资源却还远远没有准备好,
所以在最初的这段载入器代码,我们必须用能直接生成不依赖操作系统的机器代码的语言,汇编语言来实现。
------------------------------------我是步骤一开始的分割线-----------------------------------
步骤一:编写汇编程序(linux版)
在linux下,我使用as86和ld86这2个程序来产生代码。(我这里以linux的Ubuntu发行版为例)。
as86对于我们这个任务来说足够简单,因此也比较适合我们。
另外一个原因恐怕是在1990年的时候,linux系统的创始者,被奉为神明的linus大牛就是使用as86来编写linux的启动代码的。
无论如何,在PC机上凡是类UNIX的系统都有这套汇编器,而且一般是在bin86软件包内。
在Ubuntu下要安装bin86软件包,只需要在终端中输入命令 sudo apt-get install bin86 即可。
下面是我们的代码:
2 start:
3 mov ax,#0xb800
4 mov ds,ax
5 mov byte[0],#0x41
6 mov byte[1],#0x1f
7 hlt
8
上面这段就是我们需要的as86汇编代码。在linux下将它保存为boot.s文件。
entry start 这一句正如字面意思所言,我们的程序就从start这里开始。
代码的意义等我们讲完windows版本的汇编代码再在步骤二解释,我们先在linux终端中输入命令把代码汇编成机器指令:
as86 -o boot.o boot.s
ld86 -o boot boot.o
这样我们就在linux中得到了我们接下来要写入U盘的文件boot,
而且我们希望PC从U盘启动的时候就运行我们的boot小程序(而不是试图在U盘上启动别的什么操作系统)。
步骤一:编写汇编程序(windows版)
在windows中我没找到可以用的bin86软件包,我下载了一个cygwin版本的,不过它的ld86总是报错。
而我平时一直喜欢用MinGW做C\C++的编译器,索性我们就使用这个windows版的gcc中自带的汇编程序gas。
在linux中当然也可以使用gas,不过这反而增加了点麻烦(待会会解释)。
如果你在windows里一直用VC来编程,那么你可能需要安装一个Dev-C++或者直接安装MinGW。
我们在windows下安装Dev-C++,然后使用它自带的gas程序就可以了(也就是Dev-C++安装目录下,Bin文件夹里的as.exe执行文件)。
如果你没用过Dev-C++,我推荐你试试,安装完后你就有了一个windows下的完整的编程环境。不过它的代码编辑器我觉得还不够完善。
Dev-C++下载地址:
http://downloads.sourceforge.net/sourceforge/dev-cpp/devcpp-4.9.9.2_setup.exe
下载完这个安装文件之后,一路点确定就可以了。
也可以直接安装MinGW,它有一个安装器,安装器的下载地址如下:
http://sourceforge.net/projects/mingw/
在页面上点那个绿色的Download Now!按钮,稍等几秒下载就开始了。
下载完安装器之后,双击运行,需要选择下载哪些部分,一般来说全部下载就可以了。
下载完后,把需要的部分例如你下载到的可能是一个类似于gcc-core-3.4.5-20060117-3.tar.gz的压缩文件,
解压出来放到一个文件夹里(不提倡放到桌面,实际上路径中不应该有空格,而在几个windows操作系统中桌面所在的路径常常有空格)。
不管是装MinGW还是用Dev-C++一键安装,我们可能都需要设定windows的环境变量才好让我们接下来继续工作。
右键单击`我的电脑(计算机)`->`属性`->`高级系统设置`在`高级`选项卡里找到“环境变量”这个按钮,
点击“环境变量”后弹出的对话框中,下面一部分是“系统变量”,可以看到里面有各种变量和它的值。
我们找到PATH这个变量,你看到的也可能是Path等等,总之大小写不限,给它的开头加上你的汇编器所在的目录,以半角的分号`;`结束。
可能是在Dev-C++安装文件夹下的Bin文件夹,也可能是你安装MinGW的目录下的Bin文件夹。
我在自己的电脑上把Dev-C++直接装在C:\这个目录,因此我这里就是C:\Dev-CPP\Bin;
设置完环境变量后我们就可以在命令行解释器里使用gas汇编器了。
如果你还不熟悉windows的命令行解释器,我建议你在继续看这篇教程之前先打开命令行解释器行试试手。
你可以在C:\Windows\system32文件夹里找到名为cmd.exe的命令行解释器。
一般来说还有更方便的方法,你可以在windows桌面的`开始`->`运行`处输入cmd,这样就打开了cmd.exe程序。
在windows vista\win7下,开始菜单上还有一个搜索框,你可以在搜索框中输入cmd三个字母,稍等1秒钟,可以看到开始菜单里出现了cmd.exe程序,
点击后就可以运行。
在win7中你可以在浏览文件时,取消选择任何文件(如果已经选了你需要在空白处点一下鼠标——或许是点2下),
然后按住键盘Shift键再点鼠标右键,可以在弹出的右键菜单里看到`在此处打开命令窗口`,点击后就进入了命令解释器程序。
(这也是我喜欢上win7的第一个原因,:-)
如果你一直像我一样在命令行下通过gcc来编译C语言程序,你可能对命令行的使用已经驾轻就熟了:-)
如果不是,打开命令行窗口后可以试试输入help命令,里面列出了绝大多数系统默认的命令,
然后你可以试试dir命令,会打印出当前目录中的所有文件和文件夹。
你可以通过cd命令跳到别的目录。
比如我在C:\Windows\system32里双击cmd.exe来运行命令行解释器,会得到一个当前目录为C:\Windows\system32的新的命令行解释器。
我可以通过输入以下命令来跳到桌面上,并查看桌面有哪些文件:(以win7为例,其他windows系统的桌面所在目录并不相同)
cd \
cd User
cd bombless
cd Desktop
dir
输入的时候有一些小技巧:
比如cd目录来说,如果你想打目录cd User,你可以先打出cd U,这时再按下键盘左边的Tab按钮,就会自动补全为cd User
(也可能是当前目录里其他文件夹或文件的名字,这种情况下你可以继续按Tab键来查看其他的文件名或文件夹名)。
输入到一半的时候你还可以注意下键盘右边常有的Home键和End键,可以跳到你正输入的命令的最前边或最后边。按Esc键会清空已经输入的命令。
特别是你可以试试color命令,输入color /? 可以看到color命令的用法,
这里卖个关子,不过熟用这条命令后你绝对会比通过linux来使用这个教程的读者多了不少优势:-)
最重要的,在设置完前面的环境变量后你应该试试gcc命令。你可能会得到gcc: no input files的输出,不过你可以试试gcc --help可以看到打印出来的一串长长的单子。
如果gcc命令无效,也就是如果命令行解释器给出以下提示:
'gcc' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
那你就需要重新检查你前面是否安装成功,或者你的环境变量是不是设置对了。
这里说的太多了,不过会使用windows命令行的读者可以接着尝试编写我们的载入器程序了。
打开一个文本编辑器,这可能是记事本程序(如果你在开始菜单找不到还可以直接去C:\Windows\system32找,没错它就和cmd.exe放在一起的),
不过我推荐EditPlus或者notepad++,在编程中一个好的编辑器很重要。(如果你坚持只用VC那可能就有点不同了:-)
在编辑器中输入以下代码:(空格的时候你可以试试用Tab键空格,很过瘾。
2 .code16
3 .text
4 start:
5 movw $0xb800,%ax
6 movw %ax,%ds
7 movb $0x41,(0)
8 movb $0x1f,(1)
9 hlt
10
.global start 这一句把start标签做符号导出,这个是因为我们下一步在gas的ld程序中会用到。
.text 标明下面的这一段是代码(换句话说对于处理这段代码的gas程序来说这里可以放代码之外的东西)
剩下的我们放到步骤二解释。
把代码保存为boot.s文件。下面我们用gas的as.exe和ld.exe两个程序真正把这段汇编代码处理成机器码。
打开命令行解释器后跳到文件boot.s所在的目录,然后给命令行解释器敲入目录:
as -o boot.o boot.s
ld -Ttext 0x200 -s -o boot --entry start boot.o
这里我们让输出的代码设置为从boot输出文件的0x200位置开头,否则我们就很难确定我们得到的boot文件从哪开始才是代码。
这就是-Ttext 0x200 的作用。另外可以注意下ld命令中的-s参数,它使输出文件减小了一半。(在我这里是从3k减小到1.5k)
ld命令里的--entry start 这段就是我们之前的汇编文件中需要写.global start这句声明的原因。
不指定entry的话ld程序会报错。(不过这个报错是可以忽略的)
这里需要对某些使用过gas的读者解释一下。
用gas相比用as86来做这个汇编来说,麻烦之处就在于它无法让机器代码从输出文件的最开头开始。
我们需要指定代码从文件的0x200这个位置开始,之后我们需要从boot文件的0x200这个位置开始读取文件。
网上有人说可以用--oformat binary选项来解决这个问题,这个在我的MinGW里用并没有效果。
-Ttext 0x0这样,试图让代码从0x0就开头是不行的,实际上任何低于0x200的位置在我这里都没有效果。
在windows下编程时我还安装了nasm汇编程序,它的ndisasm用做反汇编时特别好用。
------------------------------------我是步骤二开始的分割线------------------------------------
步骤二:理解汇编代码
现在让我们分别看看gas格式与as86格式的汇编代码(其实都很像),然后理解一下:
2 .code16 ; start:
3 .text ; mov ax,#0xb800
4 start: ; mov ds,ax
5 movw $0xb800,%ax ; mov byte[0],#0x41
6 movw %ax,%ds ; mov byte[1],#0x1f
7 movb $0x41,(0) ; hlt
8 movb $0x1f,(1)
9 hlt
10
我们在前面的导言中说了,现阶段编写的代码是用于让BIOS从U盘中读出,然后放入内存中执行的。
这个阶段,CPU并没有发挥它最大的威力。
这个一方面是CPU的内存还处于未管理的状态,还可以由我们自己自由使用,还没有确定堆栈的位置等等。
另一方面,更强大,而且也是windows xp等系统和linux系统共同使用的,CPU的保护模式还有待我们启动。
(你可能已经猜到,启动保护模式这部分内容将会出现在未来的几篇教程中。)
而在保护模式启动之前我们处于所谓的实地址模式,这个时候我们总是先设定数据段段寄存器的地址,
然后我们的地址就可以用一个16位的地址来访问内存开头的1M空间了。
这里的gas版本的汇编程序中,.code16这一句声明就是为了让gas了解到这段程序是为实模式编写的(也即是此时还不能使用保护模式下专有的指令)
我们把数据段寄存器设置成0xb800,这样,地址0就会指向PC的CPU中用来显示屏幕的一段内存的开头。
写入这些地址,屏幕上就会输出latin-1字符(也就是我们常说的ascii字符的8位拓展)。
在这个CPU的最初的阶段,一般屏幕有80行,25列,而当我们把数据段寄存器赋予值0xb800,
地址0所指向的字节就代表第一行第一个字符,这里是ascii码中的大写字母A,0x41。
地址1现在指向的字节代表了第一行第一个字符的字体颜色和背景颜色,格式与windows命令行解释器的color命令一样。
具体如下:
前景。每个数字可以为以下任何值之一:
0 = 黑色 8 = 灰色
1 = 蓝色 9 = 淡蓝色
2 = 绿色 A = 淡绿色
3 = 浅绿色 B = 淡浅绿色
4 = 红色 C = 淡红色
5 = 紫色 D = 淡紫色
6 = 黄色 E = 淡黄色
7 = 白色 F = 亮白色
这里我们用0x1f这个值,也就是字符是白色的而背景颜色是蓝色的。
按我的电脑的情况举例,我的电脑主板使用的是Award公司出产的BIOS,它在运行载入器程序的前一刻显示的是计算机中已经接入的磁盘的列表,
以及一个框显示了USB控制器等各种设备的状态。而且它的文字是白色的而背景是黑色的(注意,也就是颜色属性是0f)。并且可以看出可以容纳80个文字一行共25行。
我们的程序就将在BIOS这最后一个画面的基础上输出显示画面。
可以看出在这个时候,段0xb800开头的这段内存有80x25x2字节的内存用于设置屏幕的文字。
也就是内存地址160的字节的值就代表第2行第一个字符。
在我们上面的汇编程序中,我们把0x41也就是大写字母A的ascii值写入内存地址0,将颜色属性0x1f写入内存地址1。
运行起来之后,我们就会看到屏幕的左上角是一个蓝色底白色字的大写字母A!
最后的hlt指令让CPU停住不动,这样我们就可以静静观察屏幕上的变化了~
不到10行的程序就是我们将来完善的操作系统的第一个版本,不赖吧~~
不过这个时候我们只是把程序的汇编出来了,还没有写入U盘并运行,下面我们就来完成这一步。
教程第三步我们也是分成了linux版和windows版。
可惜的是,无论是linux还是windows,都没有常见程序可以帮我们把文件写入磁盘,所以我们这里还需要写C程序自己实现这个功能。
---------------------------------我是步骤三开始的分割线----------------------------------
步骤三:将程序写入U盘并运行(linux版)
想要在linux中把数据写入U盘,首先要知道linux中怎表示U盘。
linux中,U盘的表示方法和硬盘是一致的,它们都是 /dev/sd开头,接着用小写字母做标号。
第一块磁盘就是/dev/sda,第二块磁盘就是/dev/sdb
我的电脑上装了2块硬盘,这时候再插上一个U盘,那就是总共3个磁盘了。
这个时候U盘就表示为/dev/sdc
在Ubuntu中,可以在菜单中找一找,有一项叫“磁盘实用工具”的,可以查看U盘和硬盘设备的设备名。
再不确定,可以使用linux中的fdisk -l命令,在某些系统中要访问设备可能需要超级权限。
以Ubuntu为例,需要使用sudo超级权限。
输入sudo fdisk -l /dev/sda可以查看第一块磁盘的信息。(-l代表list,列表)
插入U盘之后,分别查看/dev/sdb,/dev/sdc等磁盘,通过磁盘的大小和分区情况可以判断哪个是你的U盘。
一般来说命令得出的U盘的分区情况是混乱的,那么对U盘做fdisk命令通常提示不合理的磁盘分区,
这也可以帮助判断哪个设备是U盘。
这个一定要判断正确了,至少你不能把一个总容量几百G或者1T,2T大小的磁盘设备辨认成U盘吧。(这么大U盘,你哪买的?)
下面这个C程序把我们前面在linux下用as86处理出的程序写入设备/dev/sdc(如果你的U盘是设备/dev/sdb或是其他的,那么就需要相应修改程序)
2 #include <stdio.h>
3 int main(int argc,char*argv[]){
4 int dev_desc,file_desc;
5 unsigned char buffer[512];
6 file_desc = open("./boot",O_RDONLY);
7 if(file_desc ==-1){
8 perror("failed to read file boot");
9 return-1;
10 }
11 read(file_desc,buffer,510);
12 close(file_desc);
13 buffer[510] =0x55;
14 buffer[511] =0xaa;
15 dev_desc = open("/dev/sdc",O_RDWR);
16 if(dev_desc ==-1){
17 perror("failed to open device /dev/sdc");
18 return-1;
19 }
20 write(dev_desc,buffer,512);
21 close(dev_desc);
22 puts("done.");
23 return0
24 }
这里我们只是简单的用linux下的api,声明在fcntl.h里的open函数和close函数读取文件,并把内容写入U盘的起始数据。
这里我们并不是直接写入512个字节,而是写510个字节,并填上55和aa这2个字节,才把510字节的文件内容和2字节的结尾标识一起写入U盘。
我们把它保存成linux-write-mbr.c,然后编译:
cc -o linux-write-mbr linux-write-mbr.c
插入U盘,然后用超级权限执行我们的linux-write-mbr程序。(其他linux系统可能需要把sudo改成别的什么。)
这是因为访问设备还是需要比较高权限才能做的:-)
sudo ./linux-write-mbr
这下大功告成!
完事之后可以重现启动电脑,启动过程中通过设定BIOS从U盘启动,你可以看到效果了!
(从U盘启动的过程中记得盯住屏幕最左上角那个位置,你可以看着它从别的什么文字变为蓝底白字的大写字母A~
我这里是USB的U字变成了字母A。
步骤三:将程序写入U盘并运行(windows版)
在windows中,和linux一样,也还是自己写软件来写入U盘比较方便。
windows中我们使用CreateFile函数打开U盘。
我们先要学习windows中怎样表示我们的U盘。
windows中,和linux一样,它把U盘和普通硬盘等同来看待。
U盘和硬盘在内的这些磁盘怎么表示呢?
windows用\\.\PhysicalDrive加一个序号来表示磁盘。这里的大小写可以随意。
第一块磁盘就是0号,第2块磁盘就是1号。由于U盘是开机后才插上去的,那么最后一块磁盘就是U盘无疑了。
那么我这里的U盘就用的是\\.\PhysicalDrive2,因为我有2块硬盘,第3块磁盘才是U盘。
我写了一个小程序,查看所有的磁盘的开头,这样也方便确定最后一块磁盘,也就是U盘,到底是哪块。
2 #include <windows.h>
3 void ReadDevice(HANDLE hDeviceHandle,int DriverNumber){
4 BYTE buffer[512];
5 DWORD nBytes;
6 int iCount;
7 if(hDeviceHandle != INVALID_HANDLE_VALUE){
8 printf("正在尝试读取物理驱动器%d ...",DriverNumber);
9 ReadFile(hDeviceHandle,buffer,sizeof buffer,&nBytes,NULL);
10 }else{
11 perror("读取设备时发生错误");
12 puts("请检查您是否正以管理员账户运行此程序,");
13 printf("或许物理驱动器%d不存在.\n",DriverNumber);
14 exit(-1);
15 }
16 if(nBytes >0){
17 printf("\n读取了%d字节的数据:\n",nBytes);
18 for(iCount =0; iCount <sizeof buffer; ++iCount){
19 if(iCount %16==0){
20 printf("\n %04X\t|",iCount);
21 }
22 printf(" %02X",buffer[iCount]);
23 }
24 printf("\n ----\t ");
25 for(iCount =0; iCount <16; ++iCount){
26 printf(" --");
27 }
28 printf("\n\n");
29 }else{
30 perror("失败。");
31 exit(-1);
32 }
33 }
34 int main(int argc,char*argv[]){
35 HANDLE hDeviceHandle = NULL;
36 int iCount;
37 TCHAR DriverNameBuffer[MAX_PATH] = TEXT("");
38 PTSTR DriverNamePrefix = TEXT("\\\\.\\PHYSICALDRIVE");
39
40 for(iCount =0; iCount <256; ++iCount){
41 wsprintf(DriverNameBuffer,TEXT("%s%d"),DriverNamePrefix,iCount);
42 hDeviceHandle = CreateFile(
43 DriverNameBuffer,
44 GENERIC_READ,FILE_SHARE_READ,
45 NULL,OPEN_EXISTING,0,0);
46 ReadDevice(hDeviceHandle,iCount);
47 CloseHandle(hDeviceHandle);
48 }
49 return0;
50 }
51
这里要注意,反斜杠`\`在C语言的字符串中需要转义,所以在C字符串"\\\\.\\PHYSICALDRIVE"前有4个反斜杠。
我这里,如果某个驱动器不存在,例如本来只有3个磁盘,那么打印PhysicalDrive3的时候就会显示
读取设备时发生错误: No error
请检查您是否正以管理员账户运行此程序,
或许物理驱动器3不存在.
这样就知道总共只有3个磁盘,第3个磁盘也就是PhysicalDrive2就是我们最后插入的U盘的标识了。
我们编译这个程序。当然我们可以用VC来编译,但是既然我们现在已经会用命令行了,
再加上gcc命令实际上比整个VC编译过程方便的多,何不试试用gcc命令编译?
把文件保存成pdev.c后,在命令行编译:
gcc -o pdev pdev.c
然后我们需要用管理员账户启动我们的pdev.exe程序~
如果不是vista\win7\win server 2008等系统,那么你需要用管理员账户登录。(一般情况下可能很多人本身就是这样用windows的所以不需要太在意)
如果是vista\win7系统,需要在C:\Windows\system32目录找到我们的cmd.exe,(win7下那个方便的多的打开命令行的方法恐怕不适用了)。
然后跳到我们的pdev.exe所在的目录,执行pdev命令运行我们的程序。
这个时候还可以注意下程序打印出的你的电脑的主硬盘里的数据。它也是55 aa结束的,不是吗?这可不是巧合~你的windows也是靠这段程序来载入的!
确定了我们的U盘是第几号磁盘后,我们就可以把我们之前在windows中用gas汇编出的程序写入U盘了。
把下面代码敲击到编辑器(如记事本),写这段代码的时候记得把我的"\\\\.\\PHYSICALDRIVE2"字符串改写为你如今系统中U盘的标识!
然后保存为windows-write-mbr.c。
2 #include <string.h>
3 #include <windows.h>
4 void ShowError(){
5 void*pError;
6 FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|
7 FORMAT_MESSAGE_FROM_SYSTEM|
8 FORMAT_MESSAGE_IGNORE_INSERTS,
9 NULL,GetLastError(),
10 MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),
11 (LPTSTR)&pError,0,NULL);
12 MessageBox(0,(PTSTR)pError,0,0);
13 }
14 int main(int argc, char*argv[])
15 {
16 FILE *fp = NULL;
17 BYTE buf[512];
18 DWORD nBytes;
19 HANDLE hDeviceHandle = NULL;
20 if(argc ==1){
21 printf(
22 "Usage:\n%s [文件名]\n将文件从0x200开始写510字节到物理驱动器2",
23 argv[0]);
24 return0;
25 }
26 fp = fopen(argv[1],"rb");
27
28 if(fp == NULL){
29 perror("无法读取指定的文件");
30 return-1;
31 }else{
32 memset(buf,0x90,sizeof buf);
33 fseek(fp,0x200,SEEK_SET);
34 fread(buf,sizeof buf,1,fp);
35 fclose(fp);
36 buf[510] =0x55;
37 buf[511] =0xaa;
38 }
39 hDeviceHandle = CreateFile(
40 TEXT("\\\\.\\PHYSICALDRIVE2"),
41 GENERIC_WRITE ,0,
42 NULL,OPEN_EXISTING,0,0);
43 if(hDeviceHandle == INVALID_HANDLE_VALUE){
44 perror("读取设备时发生错误");
45 ShowError();
46 return-1;
47 }
48 WriteFile(hDeviceHandle,buf,sizeof buf,&nBytes,NULL);
49 if(nBytes >0){
50 printf("已将%d字节写入到物理驱动器2.\n",nBytes);
51 }else{
52 ShowError();
53 return-1;
54 }
55 CloseHandle(hDeviceHandle);
56 return0;
57 }
58
这里我们用C语言的fopen函数打开命令行中给出的文件名,并把这个文件从0x200这个位置开始读,
并复制到U盘中。
编译用命令:
gcc -o windows-write-mbr windows-write-mbr.c
还是如刚刚运行检查磁盘的pdev.exe程序那样,用管理员权限运行我们的程序windows-write-mbr.exe。
运行时记得带上参数boot!
也就是windows-write-mbr boot
如果提示说已将512字节写入到物理驱动器*.那么程序运作正常,你可以重启后从U盘启动电脑并查看效果了!
看到这里的物理驱动器*了吗?编译并运行你自己的程序时务必把磁盘设备的标识设置成你实际上的值,我猜多半都是PhysicalDrive1?
嗯,如果你用一块硬盘,然后开机后插上了U盘,那么就应该是PhysicalDrive1了,那么程序就该是:
hDeviceHandle = CreateFile(
TEXT("\\\\.\\PHYSICALDRIVE1"),
GENERIC_WRITE,0,
NULL,OPEN_EXISTING,0,0);
记得要在自己理解的基础上才编译运行程序!你懂得微软为啥要求你用管理员权限来运行这程序吧~
-------------------------------我是步骤四的分割线------------------------------------
步骤四:排错
如果前面你做的不错,那么重启后选择从U盘启动,启动后你应该看到电脑画面最终停在了BIOS程序的最后一个画面上,
并且屏幕的最左上角有一个蓝色底色的闪亮亮的大写字母A!
什么?你说你没看到?还是说你根本没有机会走到步骤三?
这下可以总结下为什么出错了。
你确定你的程序和我写出来的没有差错吗?如果编译错误了,你有好好检查编译器给出的提示吗,即使你英语并不好?
你确定你是在理解了我的代码之后,自己敲打的代码吗?
如果你实在找不出错误,你有试过推翻前面的工作,重新查看我的说明并重新操作包括重新敲打代码吗?
如果你不属于上面3种情况,但是仍然有疑问,可以在本文后留言,或者给我发邮件: bombless [ at ] 126.com
关于汇编程序方面有其他什么疑问的,可以期待后续的教程!完毕~~