Foxdisk08-如何调试2

有段时间一直沉迷于裸机上跑程序,学习怎么让一个操作系统跑起来。使用过VirtualPC(现在被微软收购了),VMware,VirtualBOX,Bochs,最终还是觉得Bochs比较好用。它的最大好处是集合了完整的调试环境,对指令是软件解析的,速度虽然慢,但对程序员来说,一步步看代码是如何执行的,非常契合需求。

Bochs的文档非常的完整,使用起来也比较方便。我只针对如何调试Foxdisk,以及如何下断点进行记录,其他的可以查其自带的帮助文档。我使用的是Bochs2.2.6,帮助文档在Bochs-2.2.6\docs目录下,目前已经到了版本2.6.9,估计也差不多。

第一步是制作磁盘镜像。Bochs本身提供了镜像制作工具bximage,使用起来也比较简单。当然,也可以使用其他工具制作,比如windows下的winimage或者Linux下直接用dd命令。以制作一个4G虚拟硬盘文件为例,命令如下:

..\bximage.exe -hd -size=4096 -mode=growing -q myhd.img

Bochs的菜单栏中(我使用的是windows)提供了Disk Image Creation Tool的快捷菜单,其实就是bximage。

具体还可以参考帮助文档中Bochs User Manual-8.1 How to make a simple disk image。

第二步是编辑bochsrc.bxrc文件,指定需要启动的设备。可以启动虚拟软盘、虚拟光盘或者虚拟硬盘。找个例子看一下,修改为自己想启动的设备即可。比如虚拟硬盘,boot: c即可启动。需要提醒的是,在Foxdisk中,为了尽可能的模拟真实硬件,磁盘的参数要注意修改,一般spt=63,heads=16即可。很早以前的版本中,我的代码中没有主要到这些参数是可变的,虚拟机调试就出问题。现在的版本中,硬盘驱动的代码已经修补了这个问题,如果不去调试硬盘驱动,不必理会这些参数。双击bochsrc.bxrc即可启动,如图1,启动了一个DOS环境下的软件,当年和联想开发的一款安全计算机的界面。

图1 Bochs调试DOS程序

另外,还有一个问题,如何把需要使用的文件拷贝到虚拟硬盘中呢?我的方法比较简单,我做了一个虚拟软盘,使用winimage将需要的文件拷贝到虚拟软盘中。启动虚拟硬盘的时候,虚拟软盘作为一个挂接设备存在(注意修改bochsrc.bxrc,比如 floppya: 1_44=LBDEBUG.IMG, status=inserted  ),从软盘中将文件拷贝过去即可。

一切准备就绪后,就可调试了。最后一步,启动bochsdbg.exe,命令行如下:

…\Bochs-2.2.6\bochsdbg.exe -q -f bochsrc.bxrc。断在了bios最早的位置f000:fff0上,这时就可以进入调试了。如图2所示。

图2  bochsdbg界面

如何在bochs中对Foxdisk进行调试呢?最常用的是定位在mbr区,跟踪Foxdisk的运行。即利用bochs可以对内存下断点,定位想调试的位置,进行跟踪。典型的代码运行位置,一个是mbr的0x7c00,以及Foxdisk的Code_Seg(也即0x7000处)。以mbr定位为例:

<bochs:1> lb 0x7c00     //下断点

<bochs:2>c            //继续执行直到遇到断点

参考Foxdisk的代码,对照跟踪,很容易理解系统的运作原理。

总觉得bochs的调试工具和linux下gdb很类似,为便于查询,将常用的一些命令记录下来,详细的仍可参考其官方文档。

执行控制命令
c 继续执行,遇到断点将停止
step [count] 执行count条指令, 默认为1条
s [count] step的缩写
Ctrl-C 停止执行,返回命令行
Ctrl-D 执行完所有命令后,退出
q(quit) 退出调试器

设置断点

vb(vbreak) seg:off 在指定的虚拟地址(段+偏移)设置断点,在保护模式下也可以使用
lb(lbreak) addr 在一个线性地址设置断点
pb(pbreak) [*] addr 在一个物理地址设置断点
info break 显示所有断点状态
d(delete) n 删除一个断点

查看内存

x /nuf addr 查看一个线性地址的内存
xp /nuf addr 查看一个物理地址的内存
    n 显示多少个单位的内存
    u 内存单位大小,可以是
       b: 字节;  h: 字(2个字节);  w: 双字(4个字节);  g : 4字(8字节)
f 打印格式,可以是
    x: 16进制格式打印;  d: 10进制格式打印;  u: 无符号10进制格式打印;
o: 8进制格式打印;   t: 2进制格式打印

寄存器操作

Info registers  列举CPU整型寄存器内容
dump_cpu    查看所有与CPU相关的寄存器状态
where        打印当前call stack
set $reg=val   改变寄存器的内容,如set $edx=25

最奇怪的是,在安装Foxdisk的时候,颜色会不对,到现在我也没搞清楚怎么回事。

图3 Bochs安装Foxdisk-变色了

Foxdisk07-如何调试1

在开发过程中,我主要采用两种方式进行调试,一是使用Debug.exe,通过U盘或者光盘,启动DOS,用debug观察程序是否如想象中的写入硬盘指定区域。在早期的版本中,我也曾经去除了写硬盘以及替换MBR的功能,编译了可以在DOS下运行的Foxdisk,方便自己在DOS下对其进行调试。Debug是个很好的工具,当年开发隔离卡和双网隔离计算机的所有底层代码,均是依靠它来进行的,简单而强大。

另外一种调试方式则必须借助虚拟机了,我最常用的是使用bochs,它自带的调试功能,允许程序员看到所有的执行过程,当然也包括MBR的运行,以及如何把控制权转交给Foxdisk。因为Foxdisk是图形模式,进入图形模式之后,debug就没办法发挥作用了,后期所有的开发调试都是借助了bochs才得以进行下去。

当然,所有的调试环境使用的都是X86的汇编代码,intel指令集的参考书得常备手边。
Debug的命令不多,如图1所示,大概20几条,很容易记忆。
Foxdisk07-debuglist

图1 debug常用命令

以一个例子开始debug的使用,尝试用debug将MBR区(也即硬盘线性地址LBA0)的512字节读出,并存到文件mbr.bin中。在当年流行mbr病毒的时候,这是经常使用的方法。

如图2,使用命令’a’,编写一串汇编指令。
Foxdisk07-readmbr

图2 读取mbr

Debug中所有数据缺省都是16进制的,int 13实际上表示的是int 0x13(或者说int 13h)。使用int 0x13的02功能,读取mbr所在的位置(磁头0,磁柱0,扇区1)的数据,并拷贝到内存偏移0x200处(当前段)。

使用命令’d’查看读取到的数据。这是Foxdisk中Loader.asm编译后的二进制码,安装的时候写入到了mbr区域。
Foxdisk07-mbr2file

图3 写数据到文件

如图3,命令’w’将把BX:CX寄存器中指定字节数的内容写磁盘文件。因此,首先用命令’r’修改寄存器bx和cx。示例中需要保存的字节数为512个,因此,bx=0,cx=0x200。
‘w 200’的含义是将ds:0x200处的开始的数据,保存指定的字节数到文件中。

为了便于调试,Foxdisk中有些地方插入了标志字符串,比如Loader.asm开始的“LUOB”,这其实也是为了便于debug定位问题。

特将debug常用的命令记录在此,方便之后查看。
-? 显示 Debug 命令列表
-a [address] 指定键入汇编语言指令的位置。对 address 使用十六进制值,并键入不以
“h”字符结尾的每个值。如果不指定地址,a 将在它上次停止处开始汇编。
-d [range] 指定要显示其内容的内存区域的起始和结束地址,或起始地址和长度。有如
果不指定 range,Debug 程序将从以前 d 命令中所指定的地址范围的末尾
开始显示 128 个字节的内容。
-t [=address] 从指定地址起执行一条指令后停下来,显示所有寄存器内容及标志位的
值。如未指定地址则从当前的CS:IP开始执行。
-p [=address] 从指定地址起执行指令,直到遇到断点,如未指定地址则从当前的CS:
IP开始执行。
-g [=address] 从指定内存地址运行当前在内存中的程序。
-u [range] 对指定范围内的存储单元进行反汇编。
-r [register] ‘r’显示CPU内所有寄存器内容和标志位状态,‘r register name’则修改
某个寄存器内容。比如-r ax 修改寄存器ax的内容。
-q 退出debug。

1,794 total views, no views today

Foxdisk06-启动原理3

Loader.asm控制权转交给BootEntry后,开始启动Foxdisk的主体代码。BootEntry()函数中,只调用了Foxdisk()函数。BootEntry没有其他作用,只是用来定位的,方便Loader.asm去加载。

看代码前,首先熟悉一下将要用到的结构体,定义在Common.h中。
Foxdisk06-datastruct

图1 数据结构对应图

程序中允许安装四个不同的操作系统,每个系统的引导代码Foxdisk都会保存起来,理论上可以兼容所有类型的操作系统。如图1,准备了四个导航条与之对应。数据结构navBar定义了导航条的显示资料,包括字体、字符串、颜色等等;数据结构menuitem则定义了与导航条连接的菜单项的显示资料。数据结构_WallPaper是壁纸的数据结构,包含壁纸存放在硬盘中的位置及其是否有效。壁纸从1024×768的bmp中提取,没有压缩,直接存储在硬盘上,其大小为1024×768 bit。总共可以存7张不同的壁纸,在安装的时候指定。

数据结构OperationSystem对应每个导航条指定的系统,允许用户添加密码。数据结构FoxdiskPara定义了Foxdisk所用的必要信息,包括倒计时时间、默认进入的系统、foxdisk的存储位置等。

在以上数据结构的支持下,Foxdisk的启动才得以实现。在foxdisk()中,第一步启用磁盘访问的驱动,建立硬盘参数表,允许硬盘访问。第二步,获取FoxdiskPara等参数,这些参数在安装的时候已经存储在硬盘上了,接着调用bootMenu()。出现指定的壁纸及倒计时信息。如下图。
Foxdisk06-bootmenu

图2 启动界面

这是指定了壁纸的启动界面,如果没有指定,则启动缺省的foxdisk界面,如图3。
Foxdisk06-defbootmenu

图3 缺省启动界面

在启动界面上,如果按F2键,则进入Foxdisk的设置界面,类似图1。如果不按,倒计时结束将进入显示中正在进入的系统。当时为了设计右下角的显示信息,也费了一番功夫。最大的问题是,如何在不同的图片上,能够将倒计时的数据显示出来。固定颜色是不行的,很难保证图片颜色与倒计时颜色不同。最终用了些小技巧,用反色、倒影的方式进行显示,解决了这个问题。

读到现在,我也发现之前设计上的不足,很多地方过于繁复了,有些需要显示的信息,比如当前有哪些操作系统,在界面上却没有显示。

最后一步,调用SysRun()。也即在第二步中,如果按F2,将执行此处代码,进入Foxdisk的设置界面。
Foxdisk06-configmenu

图4 设置界面

设置界面的右下角提供了快捷选项。F1键唤出帮助界面;F2键为系统菜单,也即类似图1中显示的界面;F3键唤出参数设定,如图4显示;F4键还没有实现;F5键唤出关于菜单,打印关于Foxdisk的一些说明。

至此,所有启动相关的信息全部介绍完毕。Foxdisk在运行过程中,需要用到硬盘访问、分区设置、图形显示、汉字显示、时间中断等,最早曾经把扬声器也放进去了,觉得没有太大作用,后来把它去除了。

后续的博客中,将针对上述实现一一阐述,记下我在秦淮河畔曾经挥汗调试的日子。

1,333 total views, no views today

Foxdisk05-启动原理2

观察Foxdisk3的makefile,可以看到使用的是bcc -ms的编译开关。这是要求编译器采用Samll内存模式进行编译,其特点是栈和数据都在64K以内,Code在另外一个64K内。为什么采用Small方式?主要是我当时设计的时候,没有考虑到后面的各种新的需求,以为代码可以控制在64K内完成;另外,这种模式比较便于汇编和C的混合编写,不用去考虑复杂的数据段、代码段的定位。

在整个代码编写过程中,除了引导部分(也即Loader.asm),其他的代码尽量用C编写。但是有两部分的代码仍旧无法避免使用汇编,一是时钟中断,为了让几个任务并行工作,使用了汇编;二是32位数的乘除计算。我们在平常使用C/C++及其他高级语言的时候,大数的计算很平常。那是因为编译器已经提供了库函数调用,方便程序员使用,操作系统已经提供了这样的机制进行支持。Foxdisk某种程度上相当于简单的操作系统,这些机制完全没有提供。因此,在进行大数计算时,只能自己来写这些函数了。

回到我们的启动话题。Loader.asm包含了所有启动的秘密,其承载了将Foxdisk的演出场景安排好的任务,相当于整个程序的导演,也是整个程序最难读的地方。我试着以自己的语言,将其原理解释清楚。

Loader.asm在编译后,将链接成一段512字节的二进制码。当然,它不是单独存在的,是与其他代码混杂着存在于Foxdisk.exe中。这段512字节码将由Setup.c中的main()函数拷贝到MBR区,起到引导的作用。

Bios在释放控制权的时候,将把MBR区的代码加载到0x7C00处,并跳转执行。也即从Loader.asm中的load_start处开始执行。

整个Loader.asm实现的功能可以概括为三个:1) 将存储在硬盘上的Foxdisk代码拷贝到指定的内存;2)将Foxdisk运行的数据拷贝到另外一段64K的内存中;3) 设定栈,并远程跳转(retf)至Foxdisk的BootEntry处,开始运行Foxdisk。Loader.asm中的几个变量,nCodeSect:Foxdisk代码段占用的硬盘扇区数;nDataSect:Foxdisk数据段占用的扇区数;iBegin: Foxdisk程序镜像所存储的硬盘起始位置;这些变量均由Setup.c中的函数进行初始化,也即安装的时候确定。另外一个变量iDisk,其值为0x80或者0x81,表示BIOS所认的第一个硬盘还是第二个硬盘,也是由Setup.c中函数对其进行初始化的。如果Foxdisk镜像存储在第二个硬盘上,则必须设置为0x81。现在想来,其作用不大,而且容易给调试带来麻烦。

load_start到load_chs,尝试使用扩展中断0x13(ah=0x40以上的int 0x13)访问磁盘中的数据,加载Foxdisk的代码段至0x7000:0起始的64K内存,数据段加载至0x8000:0开始的64K内存。

如果磁盘不支持磁盘的扩展中断(这种情况基本不可能),则回到原始的硬盘访问中断,尝试将Foxdisk的代码段和数据段加载到指定的内存中。从load_chs到load_ok间做的就是这件事,其功能与上一段其实是相同的,大部分情况下也不会执行。

从标志load_ok开始,进行设置栈以及跳转的工作。Retf是条远跳转指令,它将栈中的4个字节当成CS:IP,跳转过去执行。以上工作均在实模式下进行,不用考虑复杂的权限问题。注意,在retf执行前,bx中已经包含了BootEntry的地址,执行前的push bx就是将地址压栈,准备远程跳转。

至此,我们可以暂时抛去晦涩难懂的汇编,进入了C的世界。

下一篇博客是启动原理的最后一篇,介绍Foxdisk的整体逻辑框架,从逻辑层面介绍我是如何编排实现Foxdisk的功能。

1,593 total views, no views today