一、总体构想:
1)分布式跨平台的开发工具
2)在同一界面下能进行混合语言编程
3)拥有一个的无语言差别的基本类库,并包含一个无系统差异的基本系统类库
4)在源码调试基础上实现各种
5)实现基本的自动测试
6)一定的变形繁衍能力
跨平台是指为一般用户提供一个无系统差别统一界面的开发平台,同时又能为高级用户提供具体系统的接口
分布式特征是指允许同一个开发小组的开发人员能同时针对同一个项目编码和调试,使得对分布式系统的调试更加有效
混合编程的含义是,在同一个项目中使用多种语言编码,语言的差别由编译器来处理,这样能让开发者使用自己最拿手的语言开发,而不必为
统一开发语言而费时费力
现今随着编程语言的不断进化,主流开发语言正趋向一致的特征,尤其在面向对象的类结构出现后,多数语言除了关键字的名称不同外,基本
是一致的,而结构分支语句在长期发展中基本统一了名称,就IF/ELSE、WHILE、与FOR的多种变形,而现在所存在的不同语言开发工具中的接口
不统一问题是一种商业行为导致的,和语言本身无关,现今计算机语言总共有三个特点
1)多数语言设计时的差别是一种历史差别,本质是并无差别(即使象LISP等非结构化语言,差别是问题描述不同)
2)主流语言发展的趋势是相互吸收统一,看看C#和JAVA、DELPHI等等就知道,从语言角度是一致的
3)不同语言产生的结果是一样的,都是一种目标文件(即使是临时的和可执行文件的目标文件也是一样的性质),目标文件描述上有差别与系
统有关,和语言本身无关
而就是第三条是导致语言最终无差别的基础
(具体分析将在编译器模块分析中展开)
这为构造无语言差别的类库奠定了基础,而且类库本身的特征是一种工具库,使用者本质上只需要知道类名、属性名、功能名和它们的用法就
能编程了,无需知道其内部细节,当然在调试中是允许进入类库源码
系统无差异的基本类库,是根据现有操作系统理论中中相对成熟的那些部分,在较高的层次上(如多数通用的系统API名称)建立一个基本的形
式类库,然后针对不同系统,予以接口映射
在通用类库基础上,将编译器框架化(比如引入COM形式),变可构造无语言差别的编译器了
为此我们从语言角度讨论一下现有开发工具
如Borland的产品WINDOWS下的DELPHI与LINUX下的Kylix基本一致,体现了跨平台的趋势(我想老外的目标也应该是这个),Delphi与C++
Builder的比较可以看出它们的类库是等价的,体现了一种无差别性,只是,它还是以不同语言来表达了几遍
而微软的C#就是以一种统一平台的姿势露脸的,只是一种新的语言形式(虽然和JAVA很象),成为一种好的商业模式的同时,却增加了开发人
员的负担,花精力去学习新的形式,会使其推广要经历长时间,不过它的应用结构和我想的很类似,值得学习
LINUX下GCC以开放源码的形式,发展挺快,但本身没有一个良好的集成开发环境,限制了普通用户的使用欲望,而一个开发工具发展到现在,
操作界面已经成为发展的重点,毕竟内部的操作对用户来说无错就行了,而操作是直接面对的问题,对效率的影响很大
总的来讲,现有开发工具的调试功能和以前DOS和UNIX下的字符界面时代并没有多大变化,没有更多的去挖掘,象UNIX及其类似系统下有很多的
辅助调试工具,可惜都停留在字符状态,而且综合程度不够,很是遗憾,就象我遇到的不少UNIX开发者,他们就是喜欢用VI编程,只是一种长
时间形成的习惯,从另一种角度也说明类UNIX系统上相关产品对用户的需求调查的不够,最终导致用它们的都是高手,一般人望而生畏,好在
LINUX正在改变这种状态
本文的一个重点就是分析调试功能,以调试操作系统为基础,力求分析出多数情况下的调试需求
以调试操作系统为基础,是因为一般的系统是建立在操作系统上的,调试中有了操作系统的帮助,难度上大大降低,而调试操作系统时,困难
就很明显了,现在唯一能依靠的只有硬件本身了,而且在调试一般软件时,由操作系统屏蔽了很多的硬件特征,如单进程和多进程、单处理器
和多处理器、单机系统和分布式系统等等,这些差异在调试操作系统中是不能避免的
后面将会针对这些情况详细讨论
自动测试功能是一个难度不低的课题,相关的文章很多,只是好的实用工具还不多,在此仅涉及与编码相关的白盒测试,讨论以源代码本身的
结构为基础的一种自动测试方案,有一定的局限性,需要使用者进行一定量的先期描述,如消息接受的正确顺序、操作过程描述、状态变化的
描述,而对非对象编程而言还要描述消息定义,就是描述在正常状态情况下程序的走向,然后由测试器结合一定的随机操作对代码(源码或者
可执行代码都可)进行自动测试,用户也可以指定测试过程
变形和繁衍
变形是指用户定制,比如EMACS就是很不错的方式,它后面有一个LISP支持,那是复杂了点,关键是如何在设计中留出各种操作接口,供二次开
发使用
繁衍是指一种自动移植功能,当有一种新的操作系统生成了,最好是在此开发环境下生成的,要么就是提供了足够的接口描述脚本,然后此开
发工具按一定的描述规则为新系统生成一个配套的开发工具,最基本的是生成命令行方式的如GCC,图形界面是属于操作系统的一种应用软件,
可以为第三方供应,由此自动生成图形界面的开发工具的难度比较大
二、开发工具的基本结构分析(写的简单了点)
开发工具的基本功能就是编译功能,连调试功能都不能算是最基本的模块
发展至今,开发工具从原始的汇编工具到非集成方式的开发环境,到字符状态的集成环境,最后是现在的可视化集成环境
主要模块划分成
编译模块、调试模块、库(包括类库和基本函数库)、编辑器
分析中大部分以编译和调试为主,将库分析作为一个独立部分,而编辑器将在最后分析,因为必须等前面所有模块的需求分析完后,才能得到
一个基本的接口表,这个表是编辑器和各模块交互的接口,如此对编辑器分析将能更全面
而所有接口细节将在按视图角度分析各模块后得到
第一部分 基本功能分析
一、编译器分析
编译器 ——将某种指定语言的源代码翻译成某种指定的目标代码,同时检查源码中的静态逻辑是否符合语言定义
基本包含三大块内容:语言选择、错误检查、翻译
编译器从流程看是一个单纯的数据流处理过程,分析源码,查找语法错误,判断正确后,将源码转化成中间代码,进行优化,最后按硬件的要
求转化成目标代码,这就是编译器的基本运转图
基本结构如下:
源码 —— 词法分析 (词定义规则)
|
|—— 语法分析 (语法定义规则)
|
|—— 语义分析 (语义描述规则)
|
|—— 代码优化 (优化规则)
|
|—— 中间代码生成 (转换规则)
|
|—— 目标代码生成 (转换规则)
|
|—— 可执行文件 (代码连接规则)
1、计算机语言分析
每种语言产生时,都说知己是最好的,至少在某方面,实际情况呢,个人认为,在计算机语言发展到现在,还是强调哪个语言适合哪个方面,
本质上是一种商业操作,而非语言自身的问题
主流开发语言,如:
Ada —— 美国政府强推的号语言,但不能被广泛使用
C —— 被广泛使用,但定义不严格
Pascal —— 定义很严格,长期被用于教学,但几乎被淘汰,幸好有了DELPHI才重新恢复了生机
C++ —— 源自C被广泛使用,但和C一样有许多不严格之处
VB —— 门槛较低的语言,在视窗初期,由于开发工具的种类单调,使其获得了广泛的使用
Java —— 产生于商业竞争,做为网络开发工具前景不错,还不适合底层开发
C# —— 新的品种,存在和JAVA等语言严重等同之处,其应用结构不错,发展如何尚待时间来证明一切
1)从发展趋势看语言的特征
语言的发展趋势是差异逐步缩小,各种语言之间的交流接口是人为制造的商业障碍,正在被标准化渐渐消除,语言使用的一些早期约定,如内
存分配方式、参数排列顺序、对象垃圾回收等等,都是于语言本质上无关的内容,是操作系统和编译器之间的约定
语言 —— 是一种表达规则,让使用者方便的按照一定格式描述自己需要表达的逻辑
而现在经过长期的商业宣传后,使多数人产生了如下的误解,如C++就是VC或C++Builder,Pascal就是DELPHI,把语言和一个完整的开发工具等
同了,使得用某种语言必然是用某种开发工具,是一种商品僵化开发人员思想的体现,属于一种比较成功的商业行为,从一定程度上迫使开发
人员跟着商业产品打转,总是忙于各种各样的语言学习中,而主要是学习各种开发工具的使用
语言发展在面向对象分析推广后,除了原本的关键字差异外,在类上使用的符号基本一致,由于开发中类库的大量使用,使得语言侧重点从基
本语句的表达,转向类库设计中,而类库本身是面向对象的产物,无传统语言的痕迹,所以各开发语言工具的类库相似就不足为奇了
而最近出现的C#,第一眼看上去就是JAVA或DELPHI,和C/C++的形象相差很大了,以明显的方式告诉世人语言的未来是一致的
2)对语言的一些分工考察
早期的语言诞生是为了一些特定的目的,如BASIC是一种解释语言,但语言发展至今,还要说语言分工的话,就是一种商业行为了
BASIC至今仍旧是一种解释型语言,而它一度被广泛使用,原因就是VC较难用或者是使用方式不友好,而当时能竞争一下的BC没能跟上时代,而
被淘汰了,使得VC那种格式保留至今而未有改变,个人认为那是一种严重限制自由的风格
就象当年说的,VB做界面和数据库、VC做底层,说白了是微软的一种商业策略,而和语言自身无关
其实对一般用户来说,是解释还是编译的本质差别根本就不用考虑,他们考虑的是问题的结果,就是是否能得到一个速度满意的可执行文件,
而解释型语言现在也形成了可执行文件了,还不断提高效率,从结果上说和编译型没差别,中间的实现过程和语言无关的,因为并没有法律规
定BASIC就必须是解释型的,同样的代码是以解释方式还是编译方式对语言本身是没有区别的,纯粹是一种商业运作
毕竟市场是垄断的,开发者这样规定了,使用者就只能只能这样认为了
正因为如此,和VB的特点很象的DELPHI和C++builder侧重于可视化编程的工具一出现,就获得很好的口碑,不过奇怪的是C++Builder在市场上
若隐若现,一直来推广不利,这与DELPHI的流行形成强对比,因为两者本质上是一个界面和一个类库及函数库,差别仅在于关键字的叫法不同
,或许是不愿意和微软VC过于正面冲突,采取了低姿态
3)近来语言的发展
从DELPHI、C++Builder和Kylix可以看出Borland正是充分地利用了语言无差别性,轻松的扩展其产品线,而能保证他们地统一界面,基本一致
的类库,和一样的调试方式,可以看出它的良苦用心,以传统语言方式和微软抗衡
而微软的以C#核心的.NET计划,同样是试图统一开发平台的计划,只是它建立在新语言上,这本身是一种风险,幸好微软的实力大,有强制推
广新语言的能力,而且它是以一种语言为根基,从中间代码上下功夫,以的中间代码转化方式来适应各种硬件平台,实质和GCC能适应各种平台
的本质是一样的(是否算抄袭呢)
Java经历了跨世纪的发展,到现在已经成为一种标准,面对微软C#的挑战,加上编译形式限制了速度,可以说今后的路难走,毕竟模仿它的新
语言还会出现,而Java由于兼容性,必然会越来越显老化,看其自身变化是否能跟上时代了
这也是传统语言共同面对的问题,不进则退
总结:
1)语言趋向一致
2)开发新语言成本与风险高,周期太长
结论:
1)不开发新的语言种类
2)改变编译器的结构,使其不再受限制于特定语言
采用统一界面,以界面不变微基础,吸收各种语言形式,做到以不变应万变,再好的语言都能直接使用,省去推广新语言的风险,比较适合国
内的形式,而单纯的算法突破不足以改变现有编译结构,除非编译理论有重大突破,即使真的这样了,也只需要改变编译器的前半部分的功能
,毕竟所产生的目标代码还是机器代码,而后半部分的大改变是要计算机结构有本质变化时才可能,而那也只是改变编译器的后一半,与语言
本身是无关的
所以重点放在编译器的结构上
2、目标代码和中间代码(写的简单了点)
目标代码 —— 针对特定系统执行所需的格式,是源代码最终被编译器翻译成的结果
不一定和硬件有关,比如一个解释型操作系统,它用解释方式来屏蔽硬件的细节,则目标代码就可以和硬件无关了,实质是一种中间代码
一般来讲目标代码使用硬件提供的功能越多,效率也越好,关键是看所在软件平台对硬件细节做多大程度的屏蔽
中间代码 —— 一种处于源码语言和目标代码之间的表达形式,可以为任何形式,与任何硬件都无关,是一种纯粹的语言逻辑表达方式
中间代码具有很高的灵活性,是唯一可以有编译器开发者完全支配的语言形式,象编程语言一般都有相应的国际标准,而目标代码更是受制于
应用平台,因此它的表达方式很多,可以是某种汇编语言,还有教材中常见的三元式、四元式、DAG等,在各种编译理论分析中,都属于很成熟
的部分
而在类UNIX系统中,以PDP-11汇编语言来统一平台,代码中涉及硬件的也可由
目标代码的趋同性:(针对编译器的范畴)
虽然不同的硬件,使用的机器指令也不同,但作为基本结构是冯氏的,基本指令本质上是雷同的,如寄存器操作、内存读取、逻辑操作和跳转
指令,而这些操作是目标代码的主体,剩余的特殊指令、I/O指令、中断和非中断方式等操作,可以通过库函数一级来统一
由此可以看出,目标代码也存在无差别性,为编译器组件化提供了依据
在GCC中已经能兼容多种机型,为我们提供了现实基础,当然难度还是很不小的
总的来讲,问题是一个:还没有业界认可的统一表达形式,体现了现今形式表达的不完善
幸好这点对于编译器的组件化构造不是致命的,而组件化本身就是一种体现多表达式相结合的形式
3、编译器的组件化
从编译器处理过程看:
前面讨论的语言无差别问题仅涉及了编译器模块的前半功能,中间代码转化是编译器的中间部件,目标代码转化和连接是最终部件
三者之间基本是独立的,有相应的转化规则来定义处理过程,正是这种良好的分工方式,为编译器的组件化提供了自由的设计空间
1)基本组件接口分析
而现今成熟的编译理论使得结构基本很稳定了,而语言本身还是不断变化的,因为存在市场争夺的需要,国内暂时还不具备开发新语言并推广
的实力和权威性,而语言的正则描述也很成熟了,一时不存在语言突破正则表达的范围,这是语言现在的不变性
这为构造无差别的编译器提供了理论基础,即使有变化,也时对编译器的前半部分产生影响
下面的各种接口是一定理想化的描述,实际情况中,很多是一体的,如语法分析、语义分析和中间代码转化经常是同一个步骤
接口一:
源码处理接口 —— 词法描述
功能 : 检查词法错误
输出 : 有效部分的词流
接口二:
语法接口 —— 语言形式文法描述
功能 :供编译器进行词法分析、语法分析
输出 :语法树
接口三:
中间代码转化接口 —— 中间代码描述
功能 : 将语法树转化为中间代码形式
输出 : 中间代码流(以树型结构比较好)
接口四:
优化接口 —— 优化行为描述
功能 : 对中间代码进行优化
输出 : 优化后的中间代码流
接口五:
目标代码转化接口 —— 目标代码描述
功能 : 生成具有相对地址的目标代码
输出 : 相对独立的目标代码模块
接口六:
连接接口 —— 连接规则描述
功能 : 将各独立的目标代码连接成一个完整的可执行文件
输出 : 可执行文件
如此划分的出发点是尽可能的划分各模块的界限,而且理论上是可行的,只是现在都趋向于一次编译解决问题,使得认为的模糊了界限
现在划分界限的结果肯定会使得编译器的执行速度比以前慢些,但从组件化的优势考虑是值得的
2)关于组件化本身的探讨
组件化是一个公认的好趋势,但存在如下缺点(个人意见)
A)必须有操作系统支持才能实现组件化
B)一旦系统的注册表崩溃,则所有的组件都将不识别,即使它还存在于硬盘,从庞大的硬盘中手工找出它们然后注册似乎不现实
C)组件是一种趋势,但并非是操作系统必须的形式,作为编译器应该考虑不支持组件的系统
另外有一个重要的想法是:
如果能实现编译器内部自身的组件形式会更方便,即不依赖操作系统的组件方式,从现有的组件化的发展历史就能看出内部组件方式很早就
有了,而不是新的内容
汇编语言设计中最体现内部组件的方式是它的指令指针的自由性,可以将指令指针指向任何的区域,所以,很多模块可以按数据的方式读入
内存,然后指定好代码段、数据段、和栈位置,就可以直接将指令指针跳转至该数据区,使它们以代码方式执行
在DOS阶段,就存在一种高级语言的实现这种汇编级功能的格式,给程序段添加一个额外的代码管理模块,允许程序执行中自由载入和卸载模
块,这就是最早的DLL形式,只是不由操作系统管理
到了WINDOWS阶段,这些形式就开始向DLL变化,但DLL是由系统管理的,因为在进入图形化操作阶段,一般的开发群体对操作系统的核心了解
没有DOS那么深入了,而且系统过于庞大,已经很难使用以前的技巧
然后就是组件概念的出现,使得模块开发成为一种可统一重用的方式,本质上依旧是将以前用户自己管理改变成系统管理
这种趋势总体是好的,但并不应该是全部,以前使用的内部组件方式有它的优势,就是稳定,而且无须平台要求,这个优势是现在的组件无
法替代的
而作为通用结构开发的编译器结构,如直接使用组件方式,必然使得很多操作系统不能支持,但组件化是主流,不支持是结构上的落伍
有如下方案供参考:
基础:编译器的特殊性,它自身存在一个连接器,使得编译器本身的代码结构可以更好的根据开发者的需求自由组合
编译器自身以内部组件的方式构成,对外提供一个外部组件定义接口,在有组件功能的操作系统中,允许外部组件一定义的接口来替换原有
的内部组件
内部组件,本身和一般组件的定义可以是一样的,不同的是由编译器自己管理,而不是由操作系统管理,这样在提供组件功能的操作系统中
,可以将内部组件直接以外部组件的形式出现
这种折中方式的实现可以有很多种,可建立在不同级别上,如:源码级、中间代码级、目标代码级
由于编译器本身就是生成最终代码的工具,因此可以完成对自身代码的动态修改,只要将足够的内部信息独立保留即可
3)编译器组件化结构
组件结构的基础是拥有一个整体框架,以这个框架为中心将各种组件连接在一起完成工作目标
对于编译器来讲,其数据流是最好的框架分析线索,前面叙述的那种理想描述可以作为一种结构
框架本身与应用是无关的,甚至连各模块的接口细节都不一定要知道,只要知道知道一个连接过程即可,因为在编译器结构中是一种一条线到底的
流水作业,出口和入口都是唯一的,中间最复杂的是所有模块共享的数据结构的处理,如符号表、错误输出、关联表等等很多数据
在此引入数据库存储方式,要求程序内部以数据库形式存储,为和传统形式兼容,可以按文件方式将工程由数据库中导出,但最好以数据库中
内容为主,然后提供一个更新工具,为那些习惯于文件编辑的人将在外编辑的代码更新到相应数据库位置
这里有一点很重要,开发者不必知道实际存储方式,在编辑界面中看到的还是一个个文件,而外部同样存在一个工程目录,把所有代码以文件
方式展现(是从兼容角度考虑),所有的更新工作有开发工具自动完成
内部以数据库方式存储有如下优点:
A)为编译器各模块提供一个统一的输入输出接口
B)实现编程期间的同步前期编译,如词法和语法分析,直接在数据库中保存分析结果(代码独立保存),提高实际编译速度
C)由同步分析得到的数据,可以使得代码视图能同步实现(这点在视图分析中详细讨论)
D)为同步文档设计留下了自由发挥的空间
E)对于分布式整体设计,提供统一的接口
F)能够系统地保存开发期间各种版本
G)可添加级别保护,便于进行开发人员的管理
关于数据库的设计:
A)按标准的方式设计当然最好,或者使用现成的数据库(局限性太大),只是工程量比较大,标准数据库设计可是另一个大项目
B)简化方式,先以最简单的方式实现,即使是文件方式也可,重点是分析出一个和开发工具的标准接口,即按组件的方式定义出标准接口
,这样可以逐步升级数据库的设计
C)接口分为内部和外部两种,外部接口用于直接和商业数据库连接,将存储外包,内部接口是一种编码上的自定义,用于和自己设计的数据
库连接,这样能保证没有数据库的操作系统也能使用,有好的数据库的操作系统可以使用高性能产品
数据库的析和开发工具的设计是两个完全独立的范畴,只是一种使用关系,不是重点,准备放在全文末尾部分
而数据库和开发工具的接口要等到所有开发工具模块的细节分析结束,总结出一个相对完善接口定义,每个模块相对独立,均有一个相对独立
的接口定义,准备在视图分析中总结各模块接口
4、预处理(简单了点)
在面向对象编程中,由于类结构的引入和消息机制的使用,使得编程中很多操作细节被屏蔽了,而在编译前,必须恢复其真实面目,由此出
现了程序的预处理,将面向对象代码转化为过程方式的代码,硬件体制还是只认识过程方式
这一步是在进行正式编译前完成的(也可和词法分析一同进行,个人认为是的),相对独立,且属于面向对象范畴,将在类库设计中讨论
二、调试器分析
调试器 ——属于白盒测试的范畴,是一种有目的的测试,即:知道了错误现象后,基于源代码级别的动态测试过程
调试器的目的是解决已知的错误,而不是测试那样为了找错误,因此与白盒测试略有不同
和编译器不同,调试器没有强大的理论背景,只有一些单纯的技巧组合,本身功能与硬件结合密切
在此仅从个人思考的角度分析各种调试情况,在后面视图分析中,将引进图论中的概念进行深入分析,也算是一个小的理论基础吧
调试器是按一定的规则向源代码中添加一定调试代码(调试框架代码),并结合使用者描述的调试内容(单步、断点或条件)生成实际调试代
码并嵌入源码中,经编译器生成可执行代码,在最终执行中由内嵌的调试代码向调试器汇报各种数据,由调试器展现给调试者判断
这就是现有开发工具中使用的调试方式,基本上本文也是按此法进行分析
1、调试器实现方案
基本有如下五种方案:仿真器、纯软件式调试器、纯硬件调试器、结合硬件的软调试器、结合软件的硬件调试器
1)仿真器
仿真就是用一个系统的功能模仿另一个系统功能
调试功能仅是仿真器的一个简单的功能,属于举手之劳,一般我们常用的开发工具不属于这种范畴
仿真器可分为两种:软件式仿真器和硬件式仿真器
软件仿真器 —— 以软件方式描述硬件设备,包含机器代码的解释器
硬件仿真器 ——以功能强大的硬件设备来模拟功能较弱的硬件设备,如单片机仿真器
大型机中虚拟主机的概念是一种结合特定硬件的软仿真器
2)纯软件式调试器
不使用硬件功能,通过纯软件方式调试可分为两种:解释器方式和单纯代码嵌入式调试技术
解释器 —— 用于开发语言编写的代码,采用解释方式执行代码,速度会低很多,但有很多控制上的优点
单纯代码嵌入技术 —— 直接以源码的编写语言将调试指令内嵌,但不使用硬件系统提供的调试功能,采取通讯方式和调试器连接
3)纯硬件式调试器
依靠独立的附加硬件设备从总线中提取运行数据进行分析
缺点是:当系统使用高速缓存时,无法从总线获取真实信息,因为CPU直接从缓存获取数据,外部设备无法正确判断
4)结合硬件的软调试器
常见的开发工具如VC、VB、DELPHI等等,都属于这种调试类型即结合主机芯片中提供的调试功能,实现高效率的调试
如单步方式、断点设置等都是硬件级功能,由CPU的来实现
5)结合软件的硬件调试器
如CODETEST,从《2001嵌入式系统及单片机国际学术交流会论文集》中发现的,是一篇深圳华唐公司的员工写的论文,让我了解到很多,差
点想去仔细分析这种工具了,不过对于硬件还是入门水平,以后有机会再说了
这种调试方式,是吸取了纯硬件调试受到高速缓存的影响的缺点,要求在源代码中内嵌很简单的指令,而这些指令的执行和调试硬件直接相
关,使得硬件调试绕过缓存障碍,真实的从总线中获得数据,同时具有硬件调试的高速性能
本文的主题是开发工具的设计,因此只讨论软仿真器、纯软件式调试器和结合硬件的软调试器
2、简述软仿真器、纯软件式调试器和结合硬件的软调试器
软仿真器和纯软件式调试器有一个交集,即源码级的仿真器和纯软件式中解释器是同一概念
1)
软仿真器可以按对硬件细节描述的深浅程度来分出多种层次
如高级语言解释级、汇编语言解释级——只需要简单模拟操作系统的API结果即可
深入模拟级——模拟中断机制、I/O调用模仿、BIOS模仿(直接对机器代码进行解释)
更深入可以是芯片级模拟——芯片接口、总线仿真等等
可以通过逐步求精的方式一步步仿真出一个完整的硬件系统
2) 纯软件式调试中的解释器方式同上描述
其单纯代码嵌入技术——其实就是每个开发者惯用的方法,如多写几个打印语句或者是显示语句,就是一种
本质是由开发者自行添加调试代码
此方法中的调试代码属于源码的一部分,本身也是被调试的范畴,一般因为其使用语法比较简单(以打印语句为主),较少成为故障点
当然,我就碰到过这样的事,是在UNIX下C的调试经历,采用UNIX中的字符屏幕显示技术,将需要的调试信息直接显示在屏幕中的固定位置,但
在消息比较长时,超过4K时,系统屏幕显示的缓冲区溢出,导致COREDUMP,这个小错误开始总被忽略,因为多数信息都很短,以至出错时,一
时还没注意它的问题
所以单纯的代码嵌入调试技术的缺点就是——本身也是被调试的范围,也是故障点,完全由开发者自己控制,容易使用不当
其优点是直接性,如果加以系统控制,就能形成好的调试工具(但牺牲效率)
以软件形式的模拟硬件调试功能,如实现软件单步方式、软件断点方式,而可以由调试器从整体出发生成对开发者不可见的代码,加入源码中
,一起编译执行,细节在后面的“基本调试功能分析”中讨论
存在的缺陷是:自动生成的调试代码可能和源码一样多,1:1甚至更多,使得调试中软件运行的速度降低很多
3)结合硬件的软调试器
硬件提供的最基本的调试功能是单步方式和断点设置,通过产生软中断由软件执行态切换到调试状态,但容易被软件中一些特殊设计干扰
如基于INTEL的芯片的操作系统中的软件如果修改了相应的调试软中断INT03等,就会使这些调试手段失效,在DOS时代这是一种比较好的反跟踪
手段
更好的硬调试功能是提供硬中断,如INTEL386以上级别的芯片中的调试寄存器DR0~DR7,将需跟踪的地址存入这些寄存器,软件运行到该地址
时,由系统激活硬中断,这种方式是无法反跟踪的
4)一些特殊话题
三种软方式均有自己的特点,假如有三个调试器A、B和C分别代表软仿真器、纯软件式调试器和结合硬件的软调试器
如何实现A、B和C之间的互调?
这个问题的实质是如何实现解释器与其他调试器的的互调
本文重点讨论的是结合硬件的软调试器
3、基本调试功能分析
因为还没有分析过真正的调试器源码,如GDB,所以基本是根据个人的推理得出的结果,有待考证
调试的基本技术有四种:单步、断点、条件、断言
除断言外均为从硬件角度引出的调试技术
单步——顾名思义,就是让程序按照源代码中的语句顺序,一句一句执行,在汇编中则几乎和一条一条机器代码执行相近
断点——即当程序运行到断点设置处就停止运行,将权利转交给调试器,是最常用的方式,通过编辑器在某源码行上打上标记,由调试器来安
排运行中进入断点
条件断点——是一种比较复杂的断点方式,由调试者给出一个表达式,当程序运行至表达式成立时,激活断点,切换至调试器,难度在于事先无
法判断断点位置,只能根据条件表达式判断出断点的分布位置
断点从设置方式分两种:静态断点和动态断点
静态断点设置的代表是VC,在VC中断点是在编译前设置的,调试中不能修改和添加,所以称为静态断点
动态断点正好相对,可以在调试过程中自由添加和删除断点,代表是DELPHI等工具,象VB那样的工具甚至能动态修改运行的“代码指针”,毕
竟是在解释状态中,自由度比编译型的大了很多
两种断点方式在实现上明显有难度差异
静态断点的实现很方便,再多的断点,都能通过在断点设置位置前直接添加相应的断点中断指令,就能完成(好象有点滥用中断的嫌疑,希望
实际代码中能找到更方便的实现方法),然后经过编译便可完事,
当然通过函数入口激活也比较可行,比如调试器在有设置的断点前,添加一段照硬件断点设置方式设置断点的代码,这样在一些中断使用受限
制的系统中比较方便,如在类UNIX系统中比较可行
动态断点的实现难度很大,需要从调试结构来考虑
类似的条件断点也可分为:静态条件断点和动态条件断点
条件断点,首先得由调试器分析,分解成基本元素,一般现有的调试器中描述的条件断点都比较简单,或者是多个复合,而非一个条件中有多种元素
组成,因为过多元素会导致为判断一个条件而设置太多的断点
而断言本身就是一种纯软件的调试方式,典型代表是VC的ASSERT的使用,是一种函数库提供的调试函数
关于断言的理论在《面向对象系统的测试》(人民邮电出版社)一书中由专门的一章来讨论,在此就简单描述了,因为是纯软件方式,在实现技
术上就没有硬件的限制,不存在技术难度
对于这些调试技术分三种方式讨论:纯软件方式、单步陷阱与INT3方式和调试寄存器方式,均以INTEL芯片为讨论对象,其它类型的芯片没有资
料,准备在GDB分析中仔细考察
无论是那种方式,都是建立一种被调试程序和调试器的一种通讯方式,以此通讯形式来完成调试过程
1)纯软件方式
此方式使用在如下情况中:对硬件系统未提供调试功能或开发者对此技术细节步清楚的时候,属于纯软件方式的内嵌调试技术
分两种表现形式:显示表达和隐含表达
显示表达——编程者可以看到调试器内嵌的调试语句,但应当示不可修改的
隐含表达——不显示,直接在内部嵌入并完成编译,对开发者而言是不必了解的细节
设计的前提是增加的代码能做到不干涉源代码的的数据和整体结构
缺点是:内嵌的代码量极大,至少和源代码一样多
首先是建立一种进程通讯方式,可以为任意一种操作系统提供的进程间通讯方式,提供一个切换函数,一旦被调试程序运行了此函数,就被挂
起,将权利移交给调试器,直到调试器发出进一步指令才继续工作
由此需为源码添加一段通讯用的代码,往往量比较大,可以使用独立文件单独列出,而切换函数就是它为调试提供的主调用语句
由于添加新代码,必须在编译之前完成,运行中是无法添加的,但并不说明这种调试方式是静态方式,它可以成为动态,但是建立在软件单步
上的动态,这样内嵌的代码量是最大的
软件方式的单步:
在源代码中的每一行前嵌入一条切换语句(即调用切换函数)
软件方式的断点:
只在断点那行前嵌入一条切换语句
软件方式的条件断点:
首先由调试器分析软件断点的表达式,提取相关的基本元素(如变量名,函数名等等),在源码中搜索出所有相关元素的位置,然后在这个
位置的前后都添加入条件断点的表达式和一条切换语句,仅当条件成立时才使用切换语句
因此,须对表达式有一定的约束,必须为自包含的,不能影响表达式之外的代码语句
2)单步陷阱与INT3方式
单步陷阱是由CPU提供的一种调试手段,设置单步陷阱后,CPU执行一条指令后就产生一个单步中断,它是指每条机器指令一次单步,而非编程
中的一句源码的含义
INT3方式是指在INTEL芯片中指定由第三号中断来实现断点调试,而INT3的中断向量中具体处理方法由用户自行定义,而且INT3指令是一条仅占
一个字节的指令,使用很方便,所以INT3是使用非常普遍的调试技术
单步:
由于单步陷阱是按机器指令来计算的,不能直接使用在程序单步上,要修改的是单步激活的中断处理,使其能按指令地址判断是否到了程
序单步的位置,由这样一个判断方法,每次单步前,中断中记录的是下一步可能去的所有分支地址,然后在每次机器指令单步激活后和指令地
址比较,就能完成判断了
所以单步的实现是比较简单的,而且在汇编调试中就更方便了,基本是一对一的,对中断的修改很少
INT3断点:
使用INT3方式的断点,是一种动态断点方式,当然任何静态断点的方式都能以动态方式实现
对静态方式的断点:首先在编译中记录断点在源码中的位置和在目标代码中的起始代码地址,将起始代码直接换成INT3指令,将被替换位
置的数据和位置地址独立保存,由调试器将这些数据通知给INT3中断处理程序,在运行中,执行到断点位置则激活了INT3中断,比较地址即可
判断出中断位置,并将原指令写回,并切换到调试器,等后下以步指令,当下以步指令为继续执行时,得先设置单步标志,这样执行完第一个
后继指令后,就进入了单步中断,这里由一步很重要,就是将刚才修改的INT3位置恢复,保证下一次中断正常完成
这种是静态方式,也可由动态方式完成
动态断点方式:记录下编译前设置的断点位置,但不做任何代码改动,只是将这些设置和后来的动态断点地址看成同样的断点,在被调试
程序载入内存,准备执行时,才进行修改,同样将地址中的原内容保留,替换成INT3指令,后面的操作和静态方式下的操作一样(同样要单步中
断的帮助),对于运行中设置的断点,由调试器通知INT3处理程序,因为设置时被调试程序处于等待状态的
可能出现的问题:
A)由于目标代码一般是相对地址,要在执行前重新定位的,这大概就是,现有调试软件,都由一个载入命令,以控制方式执行被调试
程序,获得高的控制级别,便于读取实际地址,以和源代码位置相对应
B)在进入汇编级调试中,INT3方式是通过修改指令完成的,因此对进入BIOS代码的调试是无法完成的,这点在调试寄存器出现后得以解
决
C)INT3是指令方式的断点,所以无法实现对数据区的断点设置,调试寄存器可以实现
D)容易产生冲突,比如那些反跟踪的软件就可利用INT3中断的可重定义特点,在代码中加入了修改INT3中断处理程序的功能,这样使用
INT3跟踪会导致失败
条件断点:
以INT3方式可以实现一些简单的条件断点,比如简单的变量比较,但实现过程很繁杂
首先是条件分析,在编译中为调试器保留语法树和与目标代码的对照表,运行中,用户描述了一个条件断点,根据对条件的分析,得出条件的基
本量,根据编译时留下的图表,搜索出所有和条件相关的位置
然后在这些位置设置INT3断点,根据前面的方法即可,只是在中断处理中判断条件是否成立,以识别是否进行切换
显然是一个非常复杂的实现方式
3)调试寄存器方式
在INTEL386之后的芯片中都增加了调试寄存器,使得调试能力大大的增强,因为这是硬件调试手段,不存在那样被反跟踪破坏的可能,而且能直
接将断点定位于数据区或不可写的ROM区
最近查遍了书店,发现相关书比较少,有一本清华大学出版的《80X86汇编语言设计》中有很详细的介绍,还有相关汇编代码的简单例子,而
在INUX下的书是《LINUX2.4版源码分析大全》(机械工业出版社)中有较详细的讨论
先简要的介绍一个这些调试器:
名称是DR0~DR7,其中DR0~DR3可用于保存断点地址,DR4、DR5为INTEL保留,DR6是调试状态寄存器,DR7是调试控制寄存器(具体资料以后
会在文中补上)
可实现的功能:
A)DR0~DR3中能保存的地址是任何有效地址,可以是全局地址或局部地址(由TSS任务模式产生的差异)
B)激活条件是相应地址被读、读写或执行,产生INT1,由此可以将断点设置在数据区和ROM区
C)调试器修改受两种方式的保护,访问设置位GD,GD=0时只能在实方式或特权级0时由访问调试寄存器,GD=1时任何访问都产生异常,激活INT1
中断
应用:
由于可设置地址的只有四个寄存器,大规模使用存在数量冲突,因此在SOFTICE中都限制为四次
不过对于非汇编级的高级语言调试中,可以较好的使用它,在一定程度上突破数量限制,原因在于,直接的机器代码中难以精确判断出函数的
界限,而这点在高级语言编程中是明了的事
通过编程中函数设计而产生的天然界限,可以灵活的在全局变量和局部变量之间分配四个寄存器
设计数据区的断点(包括条件断点)最好的方法是使用调试寄存器,而数据区直接和代码中的变量相对应
当要设置断点的全局变量为四个时,显然没有任何回旋的余地了,同样,想在同一个函数中设置同范畴内的四个局部变量为断点,也时没有回
旋余地的
能有发挥空间的是数据断点(也可以是代码断点)分别属于不同的范畴,而且每个范畴中的断点在同一段时间内出现不超过四个,所谓同一段
时间是指一个函数的范围(或者函数中的模块范畴,针对函数中小范围中定义的局部变量而言)
在这种情况下,可以通过和单步与INT3的合作来灵活分配四个调试寄存器,显然操作比较繁杂,但理论上并不复杂
例如:
在一个函数内有四个局部变量,想考虑其条件断点,使用四个调试寄存器跟踪这些变量的读写,可以很方便的考察条件是否成立,而在进
入函数前是没有使用调试寄存器的必要,因此使用INT3断点方式在函数入口处设置一个断点,当该函数被调用时,就直接进入INT3中断处理,此时
将条件断点进行设置,使用调试寄存器,就比较合理了,如此可以在进入之前将调试寄存器用于其它需要的地方了,
有一点就是,在函数出口是否放置断点?有必要,但存在一定困难的,函数出口是不唯一的尤其在有消息机制的系统中,如果不修改会导致
全局变量上的断点设置出现问题,这里存在一个变量生存范围的概念,如何判断是否出界,有一个笨的方法是,在编译中,为每个函数的出入口都添
加INT3断点,不是对现存指令进行修改,是添加,得包括对函数的调用前后的设置,因为可以调用库函数和系统函数,那里去加入INT3断点不是很好
,之在必要的时候才这样做,而函数运行范围判断中还不需要这样,而消息机制在编译中也是转化成过程方式的,所以是同理
4)断言
(基本内容摘自《面向对象系统的测试》)
断言是一种内嵌的测试机制,供开发者将测试语句设置在需要的地方,
一个可执行的断言包含三部分: 谓词表达式、动作和允许(ENABLE/DISABLE)
格式一般为: ASSERT(~)
断言的用途:
A)检查特定于实现的假设
B)检查在方法的入口必须为真的条件(前置条件)
C)检查在方法的出口必须为真的条件(后置条件)
D)检查在任何时候对于一个对象必须为真的条件(不变式)
(以下是个人的分析)
一般认为断言应该是显示表达的,为开发者主动使用,这样就将断言使用限制在测试范围了,而它完全可以成为一种特殊的条件断点,用于调试
中,由调试器统一安排
现在由开发者使用的断言,均为人为维护,关键是在编译中使用编译开关,将断言开启或关闭,而且很多情况下断言随着正式产品一起发布了,
因为很多时候,关闭了断言会导致一些错误在运行中被发现而无法定位了, 这样做的情况使得断言维护变得困难,编写者常常会将一些自己写的
断言的用途都忘记了(可以使用注释),但如果由系统统一管理显然就方便了
在此将断言分成显示断言和隐型断言两种
显示断言是由开发者自己嵌入的断言,但有系统自动统计,对于这些断言的注释,应当在文档中记载,而不仅仅是在程序中加注释语句(这点将
在文档部分详细讨论)
隐型断言是指由调试器提供的断言,可以是嵌入式,也可以是断点方式的断言,定义这种断点的理由是前面写的断点的四个用途
从断点的四个用途中可以明显看出,后三种方式都有固定位置(全局也是一种固定位置),如此情况下,由调试器完成就很轻松了,而用户所要做
的是描述断言(条件),由此调试器进行分析,并生成相应的调试文档,然后结合INT3和调试寄存器实现这些隐型断言
同样隐型断言也可以是内嵌方式,在硬件受限制的条件下,就能这样做,在编译的时候添加隐型断言(以ASSERT语句方式的断言),由此,也可得
出这样一个结论,隐型断言也可以显示出现,只是这些断言不能让用户修改,但可以看见(修改也是可以的,只是调试器须跟踪记录用户的相关修改
生成相应的文档)
总的讲就是将各种断言接受系统管理,而不是以简单的编译开关来完成管理,从视图角度考虑,断言整体也是一张调试图,可以从整体角度给予
新的分析,在视图中进行分析
未完代续
4、基本调试器的框架
(与编译器的约定、嵌入代码的设计规则、调试器的结构)
5、一些额外的讨论