标签:实现 默认 编写 基础 二进制 好的 bio cti 固定
2020-09-06 15:30:21 hawk
这里填补一下计算机基础方面缺失的基础知识,为之后的信息安全学习打基础。这部分主要是阅读和学习《操作系统——真象还原》这本书,按照书籍内容完成相关实验,从而对于计算机操作系统有一个更为完成的了解。所有相关的代码都会放在我的仓库中。
这一篇博客主要用来总结一下书中的主要内容,并且将书中给出的样例进行具体实现。
根据我们的常识,计算机的启动过程,主要是载入数据(既可以是单纯的数据,也可以是引用到这些数据的程序指令等)至内存,然后通过CPU进行执行,然后成功运行了操作系统,这样就基本相当于完成了计算机的启动。
但是说起来虽然简单,但实际上细究起来还有有一些疑问的——为什么要载入数据到内存中去执行,直接在硬盘中执行不行么?实际上这是由CPU决定的,因为CPU的硬件电路就是被设计成只能运行处于内存中的程序,因此如果我们想要CPU运行相关的程序,必须首先将程序将载入内存中,然后才能让CPU执行。另一方面,第一个程序是什么,怎么被加载的?实际上第一个被加载的程序是BIOS,即Basic Input & Output System系统。
对于其他程序来说,其加载往往直接依赖于操作系统,由操作系统自动的帮助我们完成程序的加载功能。但是对于第一个被启动的程序来说,自然不可能这样子完成加载,否则就变成了一个鸡生蛋、蛋生鸡的问题。因此第一个程序的加载,即BIOS程序的加载不同于其他程序的加载。下面我们具体分析一下BIOS程序的加载。
我们知道,计算机方面一直有兼容性的优良传统——而在早期的时候,Intel 8086处理器有20条地址线(1MB内存空间),但是寄存器都仅仅是16位的。因此对于其内存,会进行一些特别的布置。而当前虽然处理器已经有了很大的进步,其地址线从32位到64位等,但是遵循于兼容性,计算机开机时CPU仍然处于实模式,即仅仅能访问1MB内存,寄存器也是16位的。而由于实模式下的内存实在有限,因此往往其各个部分的用途都是约定好的,如下所示
起始地址 | 结束地址 | 大小 | 用途 |
FFFF0 | FFFFF | 16B | BIOS入口地址 |
F0000 | FFFFF | 64KB |
BIOS代码,包括上面介绍的BIOS入口地址 |
C8000 | EFFFF | 160KB | 映射硬件适配器的ROM或者内存映射式I/O |
C0000 | C7FFFF | 32KB | 显示适配器BIOS |
B8000 | BFFFF | 32KB | 用于文本模式显示适配器 |
B0000 | B7FFF | 32KB | 用于黑白显示适配器 |
A0000 | AFFFF | 64KB | 用于彩色显示适配器 |
9FC00 | 9FFFF | 1KB | EBDA(Extended BIOS Data Area)拓展BIOS数据区 |
07E00 | 9FBFF | 622080B | 可用区域 |
07C00 | 07DFF | 512B | MBR被BIOS加载到此处 |
00500 | 07BFF | 30464B | 可用区域 |
00400 | 004FF | 256B | BIOS Data Area(BIOS数据区) |
00000 | 003FF | 1KB | Interrupt Vector Table(中断向量表) |
实际上,根据上述的表格,我们也很容易知道BIOS是如何被进行加载的——直接映射入内存的对应地址即可(这里的映射是通过硬件实现的,因此和借助于操作系统等程序进行加载是不同的),并且在上述内存地址范围内的固定地址处当作入口地址进行执行,这个固定地址即为0xFFFF0。但是根据前面的介绍,实模式下我们最多也就可以访问1MB的内存空间,也就是在这个入口地址下,我们最多只能访问16B大小的指令,这必然不可能是全部的BIOS程序的指令(否则其无法实现基础功能),因此我们可以猜想到这应该是一个跳转指令。事实上也确实如此,这是一个长跳转指令,命令如下所示
jmp 0xf000:0xe05b
这里大家可以自行查找一下实模式线性地址的资料,这里简单说一下——由于寄存器是16位,而地址空间是20位,因此这里查找地址通过段基址寄存器:段偏移寄存器进行表示地址,其地址结果为16 * 段基址寄存器 + 段偏移寄存器。容易看出来,这里实际上跳转到的地址为0xFE05B处。
这里之后,就基本完成了BIOS的载入内存过程,即可以正式执行BIOS程序。实际上BIOS程序也主要就是简单的检测内存、显卡等外设信息,并初始化硬件并完成中断向量表IVT等并完成向量表的填写等。当执行完这些操作后,BIOS最后会校验启动盘位于0盘0道1扇区的内容——如果该扇区末尾的两个字节分别是0x55、0xaa,则将该扇区内容加载到0x07C00处,然后跳转至该地址并执行;否则会出错。
这里需要说明几点——首先是0盘0道1扇区,实际上就是指的是磁盘最开始的扇区。
其次是0x55,0xaa。这里可以认为是一个标识,用来标记该磁盘中存在可执行程序(实际上就是MBR,Master Boot Record程序),如果存在则表明该扇区中确实存在可执行程序;否则不存在。
下面是将其加载到0x07C00,根据前面的表格我们知道,实际上0x07C00到0x07DFF处512B空间是MBR程序,而一个扇区恰好是512B,因此恰好可以复制完全。
最后是跳转到0x07C00,这里实际上BIOS中执行的指令是
jmp 0x0:0x7C00
实际上这里将CS段基址寄存器设置为了0,IP段偏移寄存器设置为了0x7c00,之后在编写MBR程序时需要注意一下。
最后总结,即入口地址位于0xFFFF0的BIOS可执行程序最后会调用最大大小为512B的位于0x7C00处的MBR可执行程序。
下面我们简单的分析一下MBR可执行程序。
实际上MBR,又名Master Boot Record,即主引导记录,也被称为主引导扇区,主要记录着硬盘本身的相关信息以及硬盘各个分区的大小及位置信息。当然不会一开始就直接这么复杂,我们将简单分析一下,之后进行代码实验测试。
首先根据上面分析的,其通过jmp跳转后,即完成了MBR的加载,会直接执行MBR程序——这也就意味着,我们并不能通过gcc生成ELF文件作为MBR,因为ELF文件中还包含诸如魔数,段表等非代码段,其是用于操作系统进行加载的可执行文件格式,而这里要求的MBR程序是CPU可以直接运行的二进制文件,因此我们必须直接生成该二进制文件。
除此之外,二进制文件的大小不能超过512B,并且该可执行文件的511B、512B字节必须分别为0x55、0xaa。这样子BIOS程序才能正确识别并加载该MBR程序。
为了之后实现复杂的MBR程序,这里实现一个简单的MBR程序,其作用是输出“This is Hawk‘s MBR",之后会进行悬停——即无限循环,从而确保可以看到输出结果,并且不崩溃。
nasm、qemu、Linux
Linux是我们实验的宿主机平台——因为其上有各种方便的开源环境,这里我的环境是ubuntu20.04
由于我们需要生成纯二进制文件——即CPU可以直接执行的程序。因此这里我们通过nasm程序,通过编写简单的汇编程序,直接输出纯二进制文件即可。
qemu是我们的模拟器——考虑到实际情况,我们不可能找一台真正的机器去跑我们的MBR程序在观察,这样子不仅不现实,并且调试也十分麻烦。因此直接通过qemu进行模拟运行即可。qemu的安装方式在我的其他博客中也有写,可以点击查看。
; 简单的主引导程序,但并没有实现引导的功能,仅仅实现输出字符串 ; 但是其并不需要借助任何库等,仅仅依靠BIOS中断实现部分功能即可 ;------------------------------------------------------------------------ SECTION MBR vstart=0x7c00 ;这个地址表示将起始地址设置为0x7c00——因为BIOS会将MBR程序加载到0x7c00处 mov sp, 0x7c00 ;根据已知,至少0x500-0x7DFF为可用区域,则将其当用作栈即可 ; 下面首先清空屏幕,这里使用BIOS提供的中断即可, ;------------------------------------------------------------------------ ; INT 0x10; 功能号:0x06 功能描述:上卷窗口 ;------------------------------------------------------------------------ ; 输入: ; AH--功能号: 0x06 ; AL--上卷的行数(如果为0,表示全部) ; BH--上卷行属性 ; (CL, CH)--窗口左上角(X, Y)位置 ; (DL, DH)--窗口右下角(X, Y)位置 ; 输出: ; 空 mov ax, 0x600 ;AH = 0x06; AL = 0x0; mov bx, 0x700 ;BH = 0x07; BL = 0x0; mov cx, 0x0 ;CH = 0x0; CL = 0x0; mov dx, 0x184f ;DH = 0x18; DL = 0x4f; int 0x10 ;------------------------------------------------------------------------ ; 我们将上面的系统调用分析一下 ; 输入: AH--0x06; AL--0x0; BH--0x7; CL--0x0;CH--0x0; DL--0x4f;DH--0x18; ; 也就是我们调用了功能号为0x6的BIOS中断,窗口左上角为(0x0/0, 0x0/0),窗口右上角坐标为(0x4f/80, 0x18/25),上卷所有的窗口 ; 在VGA文本模式中,一般一行容纳80个字符,共25行,也就相当于清空了整个屏幕 ; 下面获取当前的光标位置 ;------------------------------------------------------------------------ ; INT 0x10; 功能号:0x03 功能描述:获取当前光标位置 ;------------------------------------------------------------------------ ; 输入: ; AH--功能号: 0x03 ; BH--带获取光标的页码号 ; 输出: ; CH--光标开始行 ; CL--光标结束行 ; DH--光标所在行号 ; DL--光标所在列号 mov ah, 0x03 ;AH = 0x3; mov bx, 0 ;BH = 0x0; BL = 0x0; int 0x10 ;------------------------------------------------------------------------ ; 我们将上面的系统调用分析一下 ; 输入: AH--0x3; BH--0x0;BL--0x0; ; 也就是我们调用了功能号为0x3的BIOS中断,获取了当前光标位置的相关信息,并将相关信息保存在对应的寄存器中 ; 下面进行打印字符串 ;------------------------------------------------------------------------ ; INT 0x10; 功能号:0x13 功能描述:打印出字符串 ;------------------------------------------------------------------------ ; 输入: ; ES:BP--字符串地址 ; AH--功能号 0x13 ; AL--设置写字符串方式 1表示光标跟随移动 ; CX--字符串长度(不包括最后的0) ; BH--设置要显示的页号 ; BL--设置字符属性 0x2表示黑底绿字 mov ax, cs mov es, ax ;es = 0x0 这里解释一下,实际上BIOS跳转到MBR时,执行jmp 0:0x7c00,将cs寄存器设置为0,因此这里最后将es段寄存器设置成了0 mov ax, String mov bp, ax ;bp = String 这里也解释一下,String表示对应的字符串的地址,这里即将String对应的字符串虚拟地址赋给了bp mov ax, 0x1301 ;AH = 0x13; AL = 0x1; mov bx, 0x2 ;BH = 0x0; BL = 0x2; mov cx, 0x12 ; int 0x10 ;------------------------------------------------------------------------ ; 我们将上面的系统调用分析一下 ; 输入: AH--0x13;AL--0x1; BH--0x0;BL--0x2 CX--0x12; ES--0;BP--String ; 也就是我们调用了功能号为0x13的BIOS中断,将0:String地址处,长度为0x12的字符串进行了输出,并且光标跟随移动 ; 下面进行循环,确保程序悬停在该处,从而观察输出 ;------------------------------------------------------------------------ jmp $ ;------------------------------------------------------------------------ ; 我们将上面的指令分析一下 ; $表示当前行的地址,这样子相当于始终执行这一行指令,从而使程序悬停 ; 下面进行字符串设置 ;------------------------------------------------------------------------ String db "This is Hawk‘s MBR"; 即伪操作指令,表示每一个元素大小为1字节 ; 下面进行空白填充,确保最后程序为512字节 ;------------------------------------------------------------------------ times 510 - ($ - $$) db 0 ;------------------------------------------------------------------------ ; 我们将上面的汇编语句分析一下 ; $表示当前行的地址,$$表示当前SECTION的起始地址,times也是伪操作指令,相当于将后面的数据重复指定次数 ; 这个指令确保了将程序填充至512字节,中间部分以0填充 ; 下面我们最后填充该512字节删除的最后两个字节,为0x55,0xaa,从而使BIOS成功识别MBR ;------------------------------------------------------------------------ db 0x55, 0xaa
实际上上面的注释已经很详细了,这里在具体说明一下
1. 这个程序编译出来的二进制文件应该放置于0盘0道1扇区位置处,应为BIOS会在改位置处判断是否存在MBR程序
2. 因为MBR程序被BIOS加载的地址为0x7c00,因此我们在编译的时候需要设置程序起始地址为0x7c00(因为后面有跳转和字符串引用,因此会有地址的引用,所以我们需要设置程序起始地址)
这样子,我们相当于完成了上述要求的简单的MBR程序的编写,下面我们需要在机器上运行,观察对应的效果。首先我们需要编译出对应的CPU可以直接执行的二进制文件,这里我们使用nasm将上述的汇编语言直接转换为CPU可直接执行的二进制文件,命令如下所示
nasm -o mbr.bin mbr.S
nasm默认生成的文件格式为CPU可直接执行的二进制文件,因此我们不需要再指定格式,我们再观察一下对应的文件大小,如图所示
可以看到,确实是512字节的大小,即一个扇区的大小。下面我们需要构造一个qemu虚拟机可以使用的磁盘,并且磁盘中0盘0道1扇区的内容就是上面的文件内容,这里我们使用dd命令来构造。
这里根据书上的内容,简单介绍一下dd命令的用法
if=FILE #从FILE中读取数据 of=FILE #将数据输出到FILE bs=BYTES #指定每一个块的大小,可以通过ibs、obs分别指定输入块、输出块的大小 count=BLOCKS #指定拷贝的块数 seek=BLOCKS #指定输出时想要跳过的块数 conv=CONVS #指定如何转换文件
这里实际上我们需要将该二进制文件放置到虚拟的磁盘的0盘0道1扇区,也就是第一个块,因此我们的命令也很简单,如下所示
dd if=/path/mbr.bin of=/path/mbr.img bs=512 count=1 conv=notrunc
操作如下所示
可以看到,我们成功完成了包含MBR程序的虚拟磁盘的构建,下面我们使用qemu启动该磁盘即可,命令如下所示
qemu-system-x86_64 mbr.img
命令执行结果如图所示,可以看到,我们确实实现了该程序的执行,并且也完成了对应的要求。
标签:实现 默认 编写 基础 二进制 好的 bio cti 固定
原文地址:https://www.cnblogs.com/hawkJW/p/13621887.html