close

4:38 PM
变速齿轮的工作原理与研究手记

我看“变速齿轮”的工作原理 

  最近,我在“大众软件”上看到一则关于软件“变速齿轮”的报道。我很少上网,所以对这方面了解比较少,不知道它在网上已经流行好几个月了。当时的感觉就是太惊奇了,很佩服王荣先生是怎么做到如此神奇的事,尤其是他如何保证各种游戏的兼容,他如何知道不同游戏对时间的处理。我立刻上网DOWN了个0.22b版,在试用的过程中,我发现“变速齿轮”不但可以加速游戏,实际上,它可以加速任何windows程序(从某种程度上),我逐渐认识到它的工作原理,不一定对,仅是猜测而已。先声明一点,我是使用UNIX的,并不是很了解windows编程,所以只能给出概念上大体的认识,而无法说出具体的实现办法。
  首先,先看看计算机是如何有时间概念的。在主板上有一个时钟晶振,依靠电池供电,本质上就是一块电子表。计算机软件中所有的时间概念,归根结底都来自着个“硬件时间”,换句话说,如果这个时间不对,则任何运行在该主板上的程序对时间的处理都不可能正确。(包扩各种操作系统)
  那么,操作系统是如何知道这个时间的呢?这是因为这个时钟每过一定时间都会产生一个硬件中断(INT)操作系统可以截取这个中断并做相应的处理,从而获得时间的概念。好象是20ms产生一次中断,一秒钟50次。具体的中断号我忘了,就称为 INT A 吧。对Dos而言,它在启动时就准备好了对INT A的中断处理程序(Dos核心的一部分),当我们使用DOs的时候,在提示符状态下,即使不做任何操作,Dos内部在一秒钟时间内,也会接受50次INT A,执行50次中断处理程序。只是这一过程在幕后完成,我们无法感受到。Dos的中断处理程序所做的,就是让Dos能够了解当前的时间。(如保留当前日期,时间在内部变量中等操作)。但很重要的一点,在INT A中断处理程序的末尾,又调用INT B。
INT B是Dos为用户保留的软中断,在缺省情况下,Dos的INT B中断处理程序立即返回,不做任何事。而
用户可以编写自己的INT B中断处理程序定时处理自己的操作,然后把它替换Dos原来的空INT B中断程序。比如我有一个程序需要在12:00运行,一种方法是写如下代码:
for(;;){
if(时间是12:00) break;
sleep(5); /*休息5秒钟,这句在Dos中没有,意为让程序不做任何事,只是等待一段时间*/
}
{具体的处理程序}
  然后在提示符状态下运行。因为Dos没有多用户的概念,在程序12:00退出以前,无法在使用这台机器。另一种方法是,把要运行的程序写成TSR(长驻内存程序),运行后执行代码长驻在内存中,程序本身返回提示符,供用户使用。那么该执行码如何保证在12:00被执行呢,就要靠INT B,在长驻该程序时,也要编写新的INT B中断处理程序,内容大概是:
{屏蔽INT B中断} /*这一步是因为Dos的大部分中断是不可重入的,
即在中断还未处理完时,再次*/
/*被中断,这一般会让Dos死掉*/
{执行旧的INT B中断处理程序} /*这一步的目的是防止自己的TSR影响其他TSR程序*/
if(时间是12:00){
把处理转向TSR程序的入口;
} else {
{恢复被屏蔽的中断}
退出;
}
综合上面讲到的,Dos下的时间处理大概是:
机器时间------->INT A(操作系统用)-------->INT B(用户用)-------->TSR程序
当然,由于Dos对运行级别几乎没有控制,用户也可以绕过INT A,INT B直接访问硬件。这时我们即使改变操作系统的时间,用户程序也能得到正确的时间。
在windows中,情况也很类似。但程序不会直接访问硬件,而是通过叫VxD的虚拟设备驱动程序来工作。由VxD来和硬件打交道,而应用程序只和VxD交流信息。对时钟中断的处理也是一样,windows有专门的时钟虚拟设备驱动来捕获来自晶振的硬件中断,并为windows提供时间和定时的功能。这与Dos下的INT A功能基本是一致的,但更强大,功能更广。
我们在来看应用程序需要怎样的时间处理机制。一个典型的游戏,如射击游戏,如果没有时间控制,敌人的飞机如果要连开10枪,程序应该是:
for(i=0;i<10;i++){
开枪;
}
但是有一个问题,机器的速度可能太快,以至于只需要0.1秒十枪就完成了,另外在不同的机器上,这段程会有不同的运行时间。为了解决这个问题,我们改进这段程序:
for(i=0;i<10;i++){
开枪;
sleep(1); /*休息1秒,不做任何事*/
}
这样我们至少保证一秒只开一枪。但还有一个问题,就是可能屏幕上有多个敌人,不可能在一个敌人开10枪的过程中其他敌人不动(而且自己也不动)。所以我认为,一般的游戏程序都是用定时器来实现主要功能的。即先为每一种动作编写相应的处理程序,如开枪,移动等,在为每个对象申请一个定时器,一旦定时器的时间到,就激活该对象相应动作的程序代码。如屏幕上的十个敌人对应十个定时器,定时器互不干涉,哪个时间到转向哪个处理程序,控制他是否该移动或射击。至于定时器的创建,由应用程序向系统申请。
|--->用户定时器1------->用户程序1
|--->用户定时器2------->用户程序2
机器时间------>windows的时钟VxD---|--->用户定时器3------->用户程序3
|--->系统接口------->系统应用
  这种机制可以保证星际争霸中的小狗在P100上和在PIII上奔跑的速度一样快,前提是这两台机器的硬件时间是一样准的,但是,如果有一台机器的时钟快或慢了,那问题就有变化了。(注意,这里的快或慢,并不是指两台机器的时间不一样,而是指在相同的现实时间下,他们产生硬件中断的次数不一样)
  那么,说了这么多,我认为“变速齿轮”的工作原理,就是修改用户申请的windows定时器中的等待时间。我不太了解windows编程,不好说这种修改是如何实现的。他没有修改VxD因为windows系统的时间并没有因为启动“变速齿轮”而跑快或跑慢,某些应用,如双击桌面图标时的间隔时间上的设定(即两次击鼠标的间隔时间多长以内才算是“双击”)也没有变。通俗的描述是:在启动“变速齿轮”后,当应用程序(特别是游戏)向windows申请定时器时,“变速齿轮”修改了申请的等待时间参数,因次改变了程序正常的定时,才使程序有了不正长的
速度(这个结论只是猜的)。只所以这样猜,是因为对已经启动的程序,他并不能改变速度,而只能先启动“变速齿轮”,再运行程序。另外,某些系统接口,也是无法修改的。
  所以,一旦启动“变速齿轮”,所有应用程序(申请了定时器,并要依靠定时器来做一些操作的程序)都会受到他的影响。在Word中,等待输入的光标会因为调成了加速而更快的闪动,各种提示信息的出现时间会变快(或变慢)很多,最夸张的是,当把速度调成最慢时,在同一位置,间隔十秒钟击一次鼠标会被系统认为是双击(发生在应用程序内,而不是桌面上)
  我很佩服王荣先生的想象力和编程能力。“变速齿轮”的出现,证明在虚拟的世界里:没有做不到的,只有想不到的。

也许是我孤陋寡闻吧,说出来不怕您笑话,对于“变速齿轮”这样著名的软件,我一直到五天前,也就是2001年2月28号才第一次听说。我有几个同学很喜欢玩图形MUD,整天见了面就在一起切磋“泥”技。我对MUD本身并没有多大兴趣,但是那天早上偶尔听他们说某个MUD站点明文规定严禁使用“齿轮”,这才好奇地问他们什么是“齿轮”。别人告诉我,“齿轮”是一个软件,能对Windows下的游戏加速,他们在玩MUD时就依靠这个软件作弊。这不禁令我一头雾水,能让Windows游戏改变速度,太神奇了!
我一贯对技术很有兴趣,听说有这么一个神奇的软件,当然要想想它是怎么实现的。这个软件看起来并不复杂,我原以为一个早自习好好琢磨琢磨就行,可是我想了好几节课,始终不得其要领。说来也巧,我们这学期有一面必修课是Linux内核原理分析,这几天正好学到了进程调度,老师说,当一个时钟中断发生的时候,操作系统要做很多事情,比如必要时要重新调度进程从而实现抢先式多任务,还要更新系统时钟......慢着,我突发奇想,如果让时钟中断产生的更快,会发生什么事情呢?
我们已经学过“微机原理”这门课程,我知道让时钟中断产生的更快不是难事,以前我就用DOS下的汇编语言写过这样的程序,这是我们当时的作业。可是我以前的程序在Windows下虽然可以运行,但并不能对Windows系统加速,道理很显然:Windows9x是使用x86虚拟机的机制来兼容DOS程序的,我的程序只能改变虚拟机,就是那个黑窗口的时钟中断。
于是我试图把以前的DOS程序搬到32位环境中。用VC内嵌汇编做这件事再合适不过了,在一个VC程序框架中加上一个__asm,然后只管把以前的汇编程序往里贴就行。我满怀希望地运行这样一个拼凑出来的怪物,结果,出现了一个大家都很熟悉的“该程序执行了非法操作”,我的试验以失败告终。
后来冷静下来仔细想想,这次失败的原因是显然的。Windows作为一个复杂的32位操作系统,如果能让你随便对硬件进行操作,那也许运行不了几个程序就崩溃了。但是如何绕过操作系统去操作硬件呢?我首先想到了vxd,编写一个驱动程序肯定可以操作硬件,但是,很可惜,我不会设计驱动程序。于是我想到了以前看到的CIH的源码,CIH没有写vxd,却能操作硬件去烧毁BIOS,陈盈豪真是太伟大了,他的程序精巧之处我至今记忆犹新。于是我模仿他的技术,修改IDT表,创建一个中断门,然后发生中断,进入ring0,现在我可以做任何事情了,按照以前的DOS程序那样,往8253定时器里写一个控制字,再分两次写入新的时钟中断发生频率,一切顺利!(详细技术请您参考我的“兄弟变速器”源码)我看到VC编辑区的光标疯狂的闪烁;双击已经失效了,因为Windows认为我双击的时间间隔太长;Windows任务栏右方的时间飞快跳动,应该说,我已经成功了。
当时我想当然的以为“变速齿轮”的原理也是如此,可是当我从同学那里把“齿轮”拷来并研究时,发现Windows的时钟并不变快,而游戏速度照样可以加上去,也就是说,“齿轮”采用了与我的程序不同的技术,是什么技术呢?我决定继续研究。
我访问了“变速齿轮”的主页,这个主页上有一个“你问我答”的栏目,由“齿轮”的作者王荣先生进行技术支持。我试图在这里找到一些关于“齿轮”的技术细节,但是很可惜,没有找到,王荣先生只是告诉大家这个程序不能用VB编写等等根本连皮毛也不涉及的问题,好不容易见到一个外国人问能不能公布源代码,其实这也是我想问的,但是王荣先生明确表示不行,这不禁令我感到非常失望。
我也想过写信去索取原码,也许他不向外国人公布,中国人可不一定。但是咱们“臭老九”最爱一个面子,我实在拉不下脸去问。这时已经是晚上10点了,我决定祭出SoftIce,用一夜时间去研究他的程序。
当时使用的工具是SoftIce,WD32ASM和VC,手边两本参考书是《微型计算机系统原理及应用》和《Linux操作系统内核分析》(都是我们的课本,呵呵)。
起初,“变速齿轮”0.2版的一个叫hook.dll的文件很大程度上吸引了我的注意力,我怀疑他使用Windows消息钩子实现变速,消息钩子我很熟悉,但我把MSDN上面关于钩子的介绍看了好久,也没有想出它和变速有什么联系,这时偶然看了一下在王荣先生的主页上得到的“变速齿轮”0.1版,才发现老版本中并没有这个文件,也就是说,我只需要反汇编他的主程序就够了,于是,二话不说,用WD32ASM先把0.1版的“齿轮”给拆了,汇编代码5000多行,并不算多。
我是从这个程序的导入函数着手的,以前编程时用于定时的SetTimer,timeGetTime,timeSetEvent等等这里都导入了,看看它们被引用的地方,我发现这些函数都是集中出现的,而且大都以这样的形式出现:
* Reference To: WINMM.timeGetTime, Ord:0098h
:00401F3E 8B0D64424000 mov ecx, dword ptr [00404264]
:00401F44 8B11 mov edx, dword ptr [ecx]
也就是说,他并没有调用这些函数,只是取得了函数的入口地址,保存在ecx中,然后又根据这个入口地址得到了函数的前面几个字节,保存在edx中。
这让我想到了前些日子在CSDN上面和别人讨论的Hook API的原理,当时我还索取了一份Hook API的例程,如果我要Hook这里的函数timeGetTime,修改ecx中的地址或者修改edx处的头几条指令就行,用汇编语言写,与上面看到的这段代码类似。
为了测试“齿轮”是不是要Hook这里的timeGetTime,我自己编写了一个很简单的小程序,调用timeGetTime,每秒钟显示一个数字。用“齿轮”进行加速后,果然显示的速度快多了。再用SoftIce跟进这个timeGetTime函数,第一条指令变成一个跳转,这充分说明“齿轮”确实Hook了这几个API,不难猜测,他要改变函数的返回值,也就是说在timeGetTime结束时还要再跳入“齿轮”自身的代码,耐心跟下去,我发现回到timeGetTime时栈里多压了一个地址,这样,当timeGetTime用ret指令返回时,先返回“齿轮”的代码(这个思想确实很巧),返回值经过处理后,才跳回我的应用程序。至于怎么处理这个返回值就简单了,改到原先的2倍,应用程序速度也就提高了2倍。
回头再看WD32ASM反汇编的代码,我又发现在Hook API前面的不远处使用了一次SGDT指令和两次SLDT指令,这是x86保护方式的特有指令,用于获得全局描述符表,进一步得到局部描述符表,这段代码引起了我的兴趣,用SoftIce跟进去,往下走几步,一边跟一边猜,大致整理出了这样的思路:
1.创建一个内存映射,把自己的代码映射到0x80000000以上的地方,在Win9x下,这块虚存是所有进程共享的。
2.先得到局部描述符表的地址,然后利用这张表修改代码段的特权级。
3.用局部描述符表创建一个调用门,在x86的保护模式下要进入ring0必须通过门来进行,CIH是用中断门完成的,这里用调用门完成,异曲同工。
4.保存几个关键函数前六个字节,改为一条跳转指令,跳到自己已经映射到高端的代码。
5.发生函数调用时进入自己的代码,通过调用门进入ring0,恢复函数开头的几个字节,修改返回值。
这时已经是凌晨5点了,既然主要思想已经掌握,我也就没有细看这段代码,8点钟还要上课,睡觉去也。
回头想想,我认为王荣先生的代码还有几点值得推敲之处:
1.如果要Hook API,一定要改变函数的第一条指令吗?如果仅仅改变函数的入口地址,不是既容易编也容易调试吗?
2.即使要改变函数第一条指令,一定要进入ring0吗?
3.即使要进入ring0,使用中断门不是比用调用门更方便吗?
当然,按照王荣先生在他的主页上的说法,“变速齿轮”0.1版是他在三年前即1997年写的,那时Windows95刚刚出来两年,能有这样的技术已经难能可贵了,这里对王荣先生的钻研精神表示由衷的敬佩。
在我研究出“变速齿轮”的原理后三天,我以自己原先的研究结果为核心,编写出了“兄弟变速器”的最初版本,不用“变速齿轮”的技术是因为我认为我的技术更优越,何况也没有拾人牙慧之嫌了 ^_^
最后再次对王荣先生表示感谢,这样精彩的创意值得我们敬佩。
Views: 1055 | Added by: ystyle | Rating: 0.0/0
Total comments: 0
Only registered users can add comments.
[ Sign Up | Log In ]