学裕's profile土拨鼠PhotosBlogLists Tools Help

土拨鼠

老是当测试的小白鼠

学裕 卢

Photo 1 of 4

Feed

The owner hasn't specified a feed for this module yet.
October 21

一组用于hash table 的素数表

在写hash表的时候,要选择一个素数作为桶的大小,当桶太小时则要对其扩展。
合适扩展呢?设置一个桶和节点的比例因子factor,当节点个数nNode/桶的大小nBucket > factor时扩展。
如何扩展呢?偶的方法是下一个桶的大小就选择 nBucket/factor得到,于是建立了一个表,查表得到下一个桶的大小
表的数据如下:
static const size_t  g_hash_prime_list[] =
{
       61,       83,      113,      151,      211,      281,      379,      509,
      683,      911,     1217,     1627,     2179,     2909,     3881,     5179,
     6907,     9209,    12281,    16381,    21841,    29123,    38833,    51787,
    69061,    92083,   122777,   163729,   218357,   291143,   388211,   517619,
   690163,   920219,  1226959,  1635947,  2181271,  2908361,  3877817,  5170427,
  6893911,  9191891, 12255871, 16341163, 21788233, 29050993, 38734667, 51646229,
 68861641, 91815541,122420729,163227661,217636919,290182597,386910137,515880193,
 687840301,917120411,1222827239,1610612741, 3221225473ul, 4294967291ul
};
static const int g_hash_primes_num = ARRAY_SIZE(g_hash_prime_list);生成这个表的程序如下:
#include<stdio.h>
#include<math.h>
bool IsPrime(unsigned int iN)
{
    int n;
    for(n=2;n<=(int)sqrt(iN);n++)
        if(iN%n==0)
            break;
    if(n==(int)sqrt(iN)+1)
        return true;
    return false;
}
#define  HASH_FACTOR 4/3
void main()  
{
    unsigned int   i, iNext;  
    for(i=62;i<=0xffffffff && i>1;++i)  
    {
      if (IsPrime(i))
      {
           printf("%9d,",i);
           iNext = i*HASH_FACTOR - 1;
           if (iNext < i)
               i = 0;
           else
               i = iNext;
           //printf("(%d)",i+1);
      }
    }  
    putchar('\n');  
}建立表可以适当减少计算素数所需时间
September 14

请小心在你的构造和析构函数调用其他函数

今天读了《Effective C++ 》,第9条原则,绝不要在构造和析构函数中调用Virtual函数,我们都知道当调用一个虚函数的时候,调用的将是被直接实例化的类或者其基类的实现,但是,如果在构造或者析构函数调用虚函数,还会遵循这种规则么?
答案是:NO
请看下面的这段代码
#include <stdio.h>

class CQBase
{
public:
    CQBase()
    {
        printLog();
    };

    virtual ~CQBase()
    {
        printLog();
    };

    virtual void printLog()
    {
        printf("CQBase\n");
    }
};

class CQDerived : public CQBase
{
public:
    CQDerived()
    {
        printLog();
    };

    virtual ~CQDerived()
    {
        printLog();
    };

    virtual void printLog()
    {
        printf("CQDerived\n");
    }
};

int main(int argc, char* argv[])
{
    CQDerived derived;
    return 0;
}

那么这段代码输出是什么呢?
答案是:
CQBase
CQDerived
CQDerived
CQBase
什么原因呢?看看那本书就知道答案,或者你在看看下面这个例子
///把CQBase类对虚函数的调用,改成间接的调用,也是错误的,所以在构造和析构函数中调用每个函数都要小心,所以最好不调用任何函数
class CQBase
{
public:
    CQBase()
    {
        init();
    };

    virtual ~CQBase()
    {
        init();
    };

    void init()
    {
        printLog();
    }
    virtual void printLog()
    {
        printf("CQBase\n");
    }
};
 再该一下,将虚函数声明为虚函数,会怎么样呢,Crash
class CQBase
{
public:
    CQBase()
    {
        init();
    };

    virtual ~CQBase()
    {
        init();
    };

    void init()
    {
        printLog();
    }
    virtual void printLog() = 0;
};
 
June 05

反调试系列二:ZwSetInformationThread

ZwSetInformationThread 可以用来将线程隐藏,从而使调试器接收不到信息
其原型如下:
NTSTATUS
NTAPI
ZwSetInformationThread (
    __in HANDLE ThreadHandle,
    __in THREADINFOCLASS ThreadInformationClass,
    __in_bcount(ThreadInformationLength) PVOID ThreadInformation,
    __in ULONG ThreadInformationLength
    );
//
// Thread Information Classes定义如下
//ntpsapi.h
typedef enum _THREADINFOCLASS {
    ThreadBasicInformation,
    ThreadTimes,
    ThreadPriority,
    ThreadBasePriority,
    ThreadAffinityMask,
    ThreadImpersonationToken,
    ThreadDescriptorTableEntry,
    ThreadEnableAlignmentFaultFixup,
    ThreadEventPair_Reusable,
    ThreadQuerySetWin32StartAddress,
    ThreadZeroTlsCell,
    ThreadPerformanceCount,
    ThreadAmILastThread,
    ThreadIdealProcessor,
    ThreadPriorityBoost,
    ThreadSetTlsArrayAddress,
    ThreadIsIoPending,
    ThreadHideFromDebugger,//这个就是用来将线程对调试器隐藏
    ThreadBreakOnTermination,
    ThreadSwitchLegacyState,
    ThreadIsTerminated,
    MaxThreadInfoClass
    } THREADINFOCLASS;
// end_ntddk end_ntifs
 
如下:ZwSetInformationThread(GetCurrentThread( ), ThreadHideFromDebugger, NULL, 0); 这样就可以将当前线程对调试器隐藏了!
June 02

花生快快长

闲来无事,五一期间在阳台上种了两棵花生,现在已经开花了
应两棵花生的各位粉丝们(花生酱)的热切要求,尤其是小飞,
现独家爆几张内部写真,需要签名照的,请电邮我!
 
May 30

偶写的成语词典

今天在Google上搜索slimyu,不小心翻出了偶很久以前写的一个成语字典小东西
热切的欢迎各位访问
 
 截张图放在这里
May 22

反调试系列一:Anti debug vmware

Verr 指令在VMware下执行可能有异常,据此可以检测运行环境为VMWare
 
一、通过dx判断。
#include <windows.h>
#pragma comment(linker, "/entry:main")

int main()
{
 
  __asm
  {
    mov    dx, 1
_l1:
    verr  dx        //其中的 dx, 可以为其他。只要 dx & 4 == TRUE, 那么就会在 vmware 中发生异常,而外部没有发现这种现象
    shl    dx, 1
    jmp    _l1
  }

  return 0;
}
以上的这个函数自在vm5.5及之前的版本有效!

bool VMWareTest()
{
  BYTE PortValue1,PortValue2;
  __try
  {
    __asm
    {
      pushad
      pushfd
      xor ebx,ebx
      mov ecx,0xa
      mov eax, 'VMXh'      ; EAX=magic
      mov dx, 'VX'      ; DX=magic
      in  eax, dx        ; specially processed io cmd
      cmp ebx, 'VMXh'      ; also eax/ecx modified (maybe vmw/os ver?)
      jne local_001
      mov gInVMWARE,1
local_001:
      popfd
      popad
    }
  }
  __except(EXCEPTION_EXECUTE_HANDLER)
  {
    gInVMWARE=false;
  }
  return gInVMWARE;
}

bool VirtualPCTest()
{
  __try
  {
    __asm
    {
      pushad     
      mov  ebx, 0 // Flag
      mov  eax, 1 // VPC function number
      __emit 0Fh
      __emit 3Fh
      __emit 07h
      __emit 0Bh
      test ebx, ebx
      jnz local_001
      mov gInVirtualPC,1
local_001:
      popad
    }

  }
  __except(EXCEPTION_EXECUTE_HANDLER)
  {
    gInVirtualPC=false;
  }
  if(gInVirtualPC)
    DbgPrint("Syser : Host machine is VirtualPC !\n");
  return gInVirtualPC;
}
March 13

郭德纲

今天从赛思的blog上看到郭德纲的blog链接http://blog.sina.com.cn/guodegang
挺喜欢他的相声的,随便逛了下,看到一个演出布告

『德云社』演出地点之一【天桥乐茶园】

(下午场,每周五、六、日下午1400开始演出)

 

地点:宣武区北纬路乙1  (北纬路东口路北,天桥剧场斜对面)

售票:周一开始每日中午12001700

票价:前四排30/位;后四排20/位;正包厢60/位;侧包厢50/位;

 

挺想亲临现场听听他说相声,只要张后四排的票,我喜欢隔远了看。

最近是怎么了

该好好思过下了

bedell的blog改风格了

今天赛思到处为Bedell同志的blog打广告。
发现bedell的版面风格改了,全黑色调。说要增加深度!
 
March 07

SSDT hook后,恢复出现BSOD

#define SYSCALL(_idx)  (KeServiceDescriptorTable->ServiceTableBase[ _idx ])
 
对ZwXXXXXXX函数进行SSDT hook
当卸载后出现BSOD
 
原因:A驱动先修改了KeServiceDescriptorTable->ServiceTableBase中ZwXXXXXXX函数的地址;
然后,我又保存了A驱动修改后的这个地址;
再然后,A驱动恢复了KeServiceDescriptorTable->ServiceTableBase;
再再然后,A撤了;
再再再然后,我恢复了我保存的那个地址,结果这个地址是A驱动中的;
再再再再再然后,我还没来得及逃,BSOD
 
解决:我在恢复SSDT时,先检查我保存的那个地址是否还有效,如果无效,就不要恢复了,虽然不是完美的解决方案,但是也凑合,
希望以后所有进行SSDT HOOK的在恢复的时候都检查下自己保存的地址是否还有效,减少BSOD~~
恢复SSDT部分代码如下:
if (TRUE == g_HookFuns[iLoop].bHooked)
        {
            if (SYSCALL(g_HookFuns[iLoop].ulIndex)
                != (unsigned int)g_HookFuns[iLoop].realFun)   //ssdt was changed by others?
            {
                if (TRUE == MmIsAddressValid(g_HookFuns[iLoop].realFun)) //检查保存的函数的地址有效性
                {
                    SYSCALL(g_HookFuns[iLoop].ulIndex) = (unsigned int)g_HookFuns[iLoop].realFun;
                }
            }else{
                SYSCALL(g_HookFuns[iLoop].ulIndex) = (unsigned int)g_HookFuns[iLoop].realFun;
            }
            g_HookFuns[iLoop].bHooked = FALSE;
        }
 

转: u88财富快车流氓软件RK驱动分析

WQXNETQIQI 在驱动开发网上帖了u88这个流氓用的驱动分析,转过来
RK部分一共三个文件
VideoAti0.sys
VideoAti0.dll
VideoAti0.exe
驱动部分是BOOT0的,主要干了这么些事:
1.建立CreateProcessNotifyRoutine,检测到userinit.exe加载后就修改注册Run项目,以启动VideoAti0.exe,VideoAtio0.exe启动后会删除自己的RUN项目,并注入VideoAti0.dll,导致启动后无法发现其启动项目

2.Hook CmEnumerateKey,隐藏VedioAti0.sys的服务项,Is,gmer,rku等无法检测到
通过CreatePrcoessNotifyRoutine检测到是如下进程调用CmEnumerateKey时,会恢复自己的HOOK,企图蒙混过关:D
fhs.exe,knlsc13.exe

3.Hook FSD Dispatch Routine,Hook了\FileSystem\Ntfs,和\FileSystem\FastFat的IRP_MJ_CREATE和IRP_MJ_DIRECTORY_FILE,根据默认规则库会首先过滤
VideoAti0.sys
VideoAti0.dll
VideoAti0.exe
无法列出他们
规则库还可以通过R3向R0添加

4.从PsLoadMoudleList移除了自身,使得IS,gmer等工具无法检测到它,gmer可检测到 FSD HOOK,也检测不到是哪个module作了HOOK


后来R3的规则好象很BT,系统登陆后加载任何位置的driver都会失败:D
分析后的idb文件见压缩包,和读源代码没什么区别了


[ 此贴被WQXNETQIQI在2007-01-07 15:30重新编辑 ]


附件:  drv.rar (77 K) 下载次数:119
December 12

一个给计算机系学生面试的算法题目,很少学生能在 2min里解答

一个很大的文件,存放10G个整数的数列,该数列是乱序排列。
问题是要找出其中的中位数。

因为文件很大,10G*4Byte = 40GB的大小。无法一次读入内存一次处理完。需要学生有一些分步骤处理的算法思考。
----------------------------------------------------------------------------
Tony 出的这个题目对于面试可能不合适,或许放笔试会好些。
这里给大家练习一下,2min 的时间。能解决的举手。

=======================================================================================

2006-12-12

偶认为可以用来判断计数,从最高位到最低位,为了方便表述我们假设为无符号整数,即0x00000000~0xFFFFFFFF依次递增,那么可以遍历所有数据,并记录最高位为01个数(最高位为0的肯定是小于最高位为1的)记为N0、N1

那么根据N0N1的大小就可以知道中位数的最高位是0还是1

假设N0>N1,那么再计算N00N01

如果N00>(N01+N1),则说明中位数的最高两位00

再计算N000N001.。。。依次计算就能找到中位数

优化

为了减少遍历次数,可以通过提高比较的粒度来实现,以16位为计算粒度,例如第一次遍历计算

{N0000000000000000,
 N0000000000000001,
 N0000000000000010,
.。。。。。
 N1111111111111110,
 N1111111111111111},计算多少位可以根据内存的承受量来确定

内存使用2^16*64(应该是这么算吧,计数器大小为64位)应该不大吧

然后根据这些计数就可以知道中位数的前16位了,后面的我就不用说了,再确定后16位就行了。

不知道这样做是否正确,欢迎讨论!

一下是代码VC6可以编译通过,没有任何测试(包括逻辑的正确性),没有读文件部分

 
 
#define COUNTER_SIZE 0x10000    //16位为粒度
#define HALF_SIZE    0x500000000 //没写错的话应该是5G=10G/2

unsigned int
readANum()
{

    //这里可以用缓存提高速度
    return 0;
}


void
calHi16BitCounter(long * counter){
    unsigned int
num = 0;
    int
countLooper = COUNTER_SIZE + 1;
    memset(counter, 0, COUNTER_SIZE*sizeof(long));
    while
(--countLooper)
    {

        num = readANum();

        num = (num & 0xFFFF0000) >> 16;
        ++(
counter[num]);
    }

}


void
calLo16BitCounter(long * counter, unsigned int hi16Mask)
{

    unsigned int
num = 0;
    int
countLooper = COUNTER_SIZE + 1;
    memset(counter, 0, COUNTER_SIZE*sizeof(long));
    while
(--countLooper)
    {

        num = readANum();
        if
(!( (num & 0xFFFF0000) ^ hi16Mask ))
        {

            num = (num & 0x0000FFFF);
            ++(
counter[num]);
        }
    }
}


unsigned int
findMidNum(){

    long
*counter = new long[COUNTER_SIZE];
    long
sum = 0;
    unsigned int
midNum = 0;
   
    calHi16BitCounter(counter);

    //计算前16位应该是落在哪个计数器上
    sum = 0;
    long
*loopStart = counter;
    long
*loopEnd   = counter + COUNTER_SIZE;
   
    while
(loopStart < loopEnd)
    {

        sum += *loopStart;
        ++
loopStart;
        if
(sum > HALF_SIZE)
        {

            break
;
        }
    }
    --
loopStart;

    //所以高16位就是loopStart指向的单元的下标
    unsigned int hi16bit = (loopStart - counter)<<16;
    //中位数的位置在以hi16bit开头的数中的位置位
    int loMidPos = (*loopStart) - (sum - HALF_SIZE);

    //下面开始计算以hi16bit为高16位的各个低16位出现的次数
    calLo16BitCounter(counter, hi16bit);

    sum = 0;
    loopStart = counter;
    while
(loopStart < loopEnd)
    {

        sum += *loopStart;
        ++
loopStart;
        if
(sum > loMidPos)
        {

            break
;
        }
    }
    --
loopStart;

    unsigned int
lo16bit = loopStart - counter;
    //就是它了
    midNum = hi16bit | lo16bit;

    delete
[] counter;

    return
midNum;
}
November 05

[藏]垃圾收集趣史

这篇文章转自http://www.contextfree.net/wangyg/b/tech_his/gc_history.html

作者网站上还有以下技术文章,有兴趣可以看看

Fortran 2003: 完美还是虚幻

Scheme语言标准(R5RS)试译稿

BASIC四十年

Web开发技术史话

垃圾收集趣史

C++语言风格流变史

数据压缩技术简史

编写自己的IDE

Terrarium 简介


垃圾收集趣史

本文发表于2004年2月《CSDN开发高手》

写作本文的初衷是想和大家分享垃圾收集( Garbage Collection )技术简单而有趣的发展史。动笔之前,我站在窗边,望了望正在小区里装运垃圾的清洁车。和生活中环卫工人们清运垃圾的工作相似,软件开发里的垃圾收集其实就是一种自动打扫和清除内存垃圾的技术,它可以有效防范动态内存分配中可能发生的两个危险:因内存垃圾过多而引发的内存耗尽(这和生活垃圾堵塞排污管道的危险并没有什么本质的不同),以及不恰当的内存释放所造成的内存非法引用(这类似于我们在生活中买到了一瓶已经过期三年的牛奶)。

据历史学家们介绍,四千多年前的古埃及人已经在城市里建设了完善的排污和垃圾清运设施,一千多年前的中国人更是修筑了当时世界上保洁能力最强的都市——长安。今天,当我们在软件开发中体验自动垃圾收集的便捷与舒适时,我们至少应当知道,这种拒绝杂乱、追求整洁的“垃圾收集”精神其实是人类自古以来就已经具备了的。

拓荒时代

国内的程序员大多是在 Java 语言中第一次感受到垃圾收集技术的巨大魅力的,许多人也因此把 Java 和垃圾收集看成了密不可分的整体。但事实上,垃圾收集技术早在 Java 语言问世前 30 多年就已经发展和成熟起来了, Java 语言所做的不过是把这项神奇的技术带到了广大程序员身边而已。

如果一定要为垃圾收集技术找一个孪生兄弟,那么, Lisp 语言才是当之无愧的人选。 1960 年前后诞生于 MIT 的 Lisp 语言是第一种高度依赖于动态内存分配技术的语言: Lisp 中几乎所有数据都以“表”的形式出现,而“表”所占用的空间则是在堆中动态分配得到的。 Lisp 语言先天就具有的动态内存管理特性要求 Lisp 语言的设计者必须解决堆中每一个内存块的自动释放问题(否则, Lisp 程序员就必然被程序中不计其数的 free 或 delete 语句淹没),这直接导致了垃圾收集技术的诞生和发展——说句题外话,上大学时,一位老师曾告诉我们, Lisp 是对现代软件开发技术贡献最大的语言。我当时对这一说法不以为然:布满了圆括号,看上去像迷宫一样的 Lisp 语言怎么能比 C 语言或 Pascal 语言更伟大呢?不过现在,当我知道垃圾收集技术、数据结构技术、人工智能技术、并行处理技术、虚拟机技术、元数据技术以及程序员们耳熟能详的许多技术都起源于 Lisp 语言时,我特别想向那位老师当面道歉,并收回我当时的幼稚想法。

知道了 Lisp 语言与垃圾收集的密切关系,我们就不难理解,为什么垃圾收集技术的两位先驱者 J. McCarthy 和 M. L. Minsky 同时也是 Lisp 语言发展史上的重要人物了。 J. McCarthy 是 Lisp 之父,他在发明 Lisp 语言的同时也第一次完整地描述了垃圾收集的算法和实现方式; M. L. Minsky 则在发展 Lisp 语言的过程中成为了今天好几种主流垃圾收集算法的奠基人——和当时不少技术大师的经历相似, J. McCarthy 和 M. L. Minsky 在许多不同的技术领域里都取得了令人艳羡的成就。也许,在 1960 年代那个软件开发史上的拓荒时代里,思维敏捷、意志坚定的研究者更容易成为无所不能的西部硬汉吧。

在了解垃圾收集算法的起源之前,有必要先回顾一下内存分配的主要方式。我们知道,大多数主流的语言或运行环境都支持三种最基本的内存分配方式,它们分别是:

一、静态分配( Static Allocation ):静态变量和全局变量的分配形式。我们可以把静态分配的内存看成是家里的耐用家具。通常,它们无需释放和回收,因为没人会天天把大衣柜当作垃圾扔到窗外。

二、自动分配( Automatic Allocation ):在栈中为局部变量分配内存的方法。栈中的内存可以随着代码块退出时的出栈操作被自动释放。这类似于到家中串门的访客,天色一晚就要各回各家,除了个别不识时务者以外,我们一般没必要把客人捆在垃圾袋里扫地出门。

三、动态分配( Dynamic Allocation ):在堆中动态分配内存空间以存储数据的方式。堆中的内存块好像我们日常使用的餐巾纸,用过了就得扔到垃圾箱里,否则屋内就会满地狼藉。像我这样的懒人做梦都想有一台家用机器人跟在身边打扫卫生。在软件开发中,如果你懒得释放内存,那么你也需要一台类似的机器人——这其实就是一个由特定算法实现的垃圾收集器。

也就是说,下面提到的所有垃圾收集算法都是在程序运行过程中收集并清理废旧“餐巾纸”的算法,它们的操作对象既不是静态变量,也不是局部变量,而是堆中所有已分配内存块。

引用计数( Reference Counting )算法

1960 年以前,人们为胚胎中的 Lisp 语言设计垃圾收集机制时,第一个想到的算法是引用计数算法。拿餐巾纸的例子来说,这种算法的原理大致可以描述为:

午餐时,为了把脑子里突然跳出来的设计灵感记下来,我从餐巾纸袋中抽出一张餐巾纸,打算在上面画出系统架构的蓝图。按照“餐巾纸使用规约之引用计数版”的要求,画图之前,我必须先在餐巾纸的一角写上计数值 1 ,以表示我在使用这张餐巾纸。这时,如果你也想看看我画的蓝图,那你就要把餐巾纸上的计数值加 1 ,将它改为 2 ,这表明目前有 2 个人在同时使用这张餐巾纸(当然,我是不会允许你用这张餐巾纸来擦鼻涕的)。你看完后,必须把计数值减 1 ,表明你对该餐巾纸的使用已经结束。同样,当我将餐巾纸上的内容全部誊写到笔记本上之后,我也会自觉地把餐巾纸上的计数值减 1 。此时,不出意外的话,这张餐巾纸上的计数值应当是 0 ,它会被垃圾收集器——假设那是一个专门负责打扫卫生的机器人——捡起来扔到垃圾箱里,因为垃圾收集器的惟一使命就是找到所有计数值为 0 的餐巾纸并清理它们。

引用计数算法的优点和缺陷同样明显。这一算法在执行垃圾收集任务时速度较快,但算法对程序中每一次内存分配和指针操作提出了额外的要求(增加或减少内存块的引用计数)。更重要的是,引用计数算法无法正确释放循环引用的内存块,对此, D. Hillis 有一段风趣而精辟的论述:

一天,一个学生走到 Moon 面前说:“我知道如何设计一个更好的垃圾收集器了。我们必须记录指向每个结点的指针数目。” Moon 耐心地给这位学生讲了下面这个故事:“一天,一个学生走到 Moon 面前说:‘我知道如何设计一个更好的垃圾收集器了……’”

D. Hillis 的故事和我们小时候常说的“从前有座山,山上有个庙,庙里有个老和尚”的故事有异曲同工之妙。这说明,单是使用引用计数算法还不足以解决垃圾收集中的所有问题。正因为如此,引用计数算法也常常被研究者们排除在狭义的垃圾收集算法之外。当然,作为一种最简单、最直观的解决方案,引用计数算法本身具有其不可替代的优越性。 1980 年代前后, D. P. Friedman , D. S. Wise , H. G. Baker 等人对引用计数算法进行了数次改进,这些改进使得引用计数算法及其变种(如延迟计数算法等)在简单的环境下,或是在一些综合了多种算法的现代垃圾收集系统中仍然可以一展身手。

标记-清除( Mark-Sweep )算法

第一种实用和完善的垃圾收集算法是 J. McCarthy 等人在 1960 年提出并成功地应用于 Lisp 语言的标记-清除算法。仍以餐巾纸为例,标记-清除算法的执行过程是这样的:

午餐过程中,餐厅里的所有人都根据自己的需要取用餐巾纸。当垃圾收集机器人想收集废旧餐巾纸的时候,它会让所有用餐的人先停下来,然后,依次询问餐厅里的每一个人:“你正在用餐巾纸吗?你用的是哪一张餐巾纸?”机器人根据每个人的回答将人们正在使用的餐巾纸画上记号。询问过程结束后,机器人在餐厅里寻找所有散落在餐桌上且没有记号的餐巾纸(这些显然都是用过的废旧餐巾纸),把它们统统扔到垃圾箱里。

正如其名称所暗示的那样,标记-清除算法的执行过程分为“标记”和“清除”两大阶段。这种分步执行的思路奠定了现代垃圾收集算法的思想基础。与引用计数算法不同的是,标记-清除算法不需要运行环境监测每一次内存分配和指针操作,而只要在“标记”阶段中跟踪每一个指针变量的指向——用类似思路实现的垃圾收集器也常被后人统称为跟踪收集器( Tracing Collector )

伴随着 Lisp 语言的成功,标记-清除算法也在大多数早期的 Lisp 运行环境中大放异彩。尽管最初版本的标记-清除算法在今天看来还存在效率不高(标记和清除是两个相当耗时的过程)等诸多缺陷,但在后面的讨论中,我们可以看到,几乎所有现代垃圾收集算法都是标记-清除思想的延续,仅此一点, J. McCarthy 等人在垃圾收集技术方面的贡献就丝毫不亚于他们在 Lisp 语言上的成就了。

复制( Copying )算法

为了解决标记-清除算法在垃圾收集效率方面的缺陷, M. L. Minsky 于 1963 年发表了著名的论文“一种使用双存储区的 Lisp 语言垃圾收集器( A LISP Garbage Collector Algorithm Using Serial Secondary Storage )”。 M. L. Minsky 在该论文中描述的算法被人们称为复制算法,它也被 M. L. Minsky 本人成功地引入到了 Lisp 语言的一个实现版本中。

复制算法别出心裁地将堆空间一分为二,并使用简单的复制操作来完成垃圾收集工作,这个思路相当有趣。借用餐巾纸的比喻,我们可以这样理解 M. L. Minsky 的复制算法:

餐厅被垃圾收集机器人分成南区和北区两个大小完全相同的部分。午餐时,所有人都先在南区用餐(因为空间有限,用餐人数自然也将减少一半),用餐时可以随意使用餐巾纸。当垃圾收集机器人认为有必要回收废旧餐巾纸时,它会要求所有用餐者以最快的速度从南区转移到北区,同时随身携带自己正在使用的餐巾纸。等所有人都转移到北区之后,垃圾收集机器人只要简单地把南区中所有散落的餐巾纸扔进垃圾箱就算完成任务了。下一次垃圾收集的工作过程也大致类似,惟一的不同只是人们的转移方向变成了从北区到南区。如此循环往复,每次垃圾收集都只需简单地转移(也就是复制)一次,垃圾收集速度无与伦比——当然,对于用餐者往返奔波于南北两区之间的辛劳,垃圾收集机器人是决不会流露出丝毫怜悯的。

M. L. Minsky 的发明绝对算得上一种奇思妙想。分区、复制的思路不仅大幅提高了垃圾收集的效率,而且也将原本繁纷复杂的内存分配算法变得前所未有地简明和扼要(既然每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了),这简直是个奇迹!不过,任何奇迹的出现都有一定的代价,在垃圾收集技术中,复制算法提高效率的代价是人为地将可用内存缩小了一半。实话实说,这个代价未免也太高了一些。

无论优缺点如何,复制算法在实践中都获得了可以与标记-清除算法相比拟的成功。除了 M. L. Minsky 本人在 Lisp 语言中的工作以外,从 1960 年代末到 1970 年代初, R. R. Fenichel 和 J. C. Yochelson 等人也相继在 Lisp 语言的不同实现中对复制算法进行了改进, S. Arnborg 更是成功地将复制算法应用到了 Simula 语言中。

至此,垃圾收集技术的三大传统算法——引用计数算法、标记-清除算法和复制算法——都已在 1960 年前后相继问世,三种算法各有所长,也都存在致命的缺陷。从 1960 年代后期开始,研究者的主要精力逐渐转向对这三种传统算法进行改进或整合,以扬长避短,适应程序设计语言和运行环境对垃圾收集的效率和实时性所提出的更高要求。

走向成熟

从 1970 年代开始,随着科学研究和应用实践的不断深入,人们逐渐意识到,一个理想的垃圾收集器不应在运行时导致应用程序的暂停,不应额外占用大量的内存空间和 CPU 资源,而三种传统的垃圾收集算法都无法满足这些要求。人们必须提出更新的算法或思路,以解决实践中碰到的诸多难题。当时,研究者的努力目标包括:

第一,提高垃圾收集的效率。使用标记-清除算法的垃圾收集器在工作时要消耗相当多的 CPU 资源。早期的 Lisp 运行环境收集内存垃圾的时间竟占到了系统总运行时间的 40% !——垃圾收集效率的低下直接造就了 Lisp 语言在执行速度方面的坏名声;直到今天,许多人还条件反射似地误以为所有 Lisp 程序都奇慢无比。

第二,减少垃圾收集时的内存占用。这一问题主要出现在复制算法中。尽管复制算法在效率上获得了质的突破,但牺牲一半内存空间的代价仍然是巨大的。在计算机发展的早期,在内存价格以 KB 计算的日子里,浪费客户的一半内存空间简直就是在变相敲诈或拦路打劫。

第三,寻找实时的垃圾收集算法。无论执行效率如何,三种传统的垃圾收集算法在执行垃圾收集任务时都必须打断程序的当前工作。这种因垃圾收集而造成的延时是许多程序,特别是执行关键任务的程序没有办法容忍的。如何对传统算法进行改进,以便实现一种在后台悄悄执行,不影响——或至少看上去不影响——当前进程的实时垃圾收集器,这显然是一件更具挑战性的工作。

研究者们探寻未知领域的决心和研究工作的进展速度同样令人惊奇:在 1970 年代到 1980 年代的短短十几年中,一大批在实用系统中表现优异的新算法和新思路脱颖而出。正是因为有了这些日趋成熟的垃圾收集算法,今天的我们才能在 Java 或 .NET 提供的运行环境中随心所欲地分配内存块,而不必担心空间释放时的风险。

标记-整理( Mark-Compact )算法

标记-整理算法是标记-清除算法和复制算法的有机结合。把标记-清除算法在内存占用上的优点和复制算法在执行效率上的特长综合起来,这是所有人都希望看到的结果。不过,两种垃圾收集算法的整合并不像 1 加 1 等于 2 那样简单,我们必须引入一些全新的思路。 1970 年前后, G. L. Steele , C. J. Cheney 和 D. S. Wise 等研究者陆续找到了正确的方向,标记-整理算法的轮廓也逐渐清晰了起来:

在我们熟悉的餐厅里,这一次,垃圾收集机器人不再把餐厅分成两个南北区域了。需要执行垃圾收集任务时,机器人先执行标记-清除算法的第一个步骤,为所有使用中的餐巾纸画好标记,然后,机器人命令所有就餐者带上有标记的餐巾纸向餐厅的南面集中,同时把没有标记的废旧餐巾纸扔向餐厅北面。这样一来,机器人只消站在餐厅北面,怀抱垃圾箱,迎接扑面而来的废旧餐巾纸就行了。

实验表明,标记-整理算法的总体执行效率高于标记-清除算法,又不像复制算法那样需要牺牲一半的存储空间,这显然是一种非常理想的结果。在许多现代的垃圾收集器中,人们都使用了标记-整理算法或其改进版本。

增量收集( Incremental Collecting )算法

对实时垃圾收集算法的研究直接导致了增量收集算法的诞生。

最初,人们关于实时垃圾收集的想法是这样的:为了进行实时的垃圾收集,可以设计一个多进程的运行环境,比如用一个进程执行垃圾收集工作,另一个进程执行程序代码。这样一来,垃圾收集工作看上去就仿佛是在后台悄悄完成的,不会打断程序代码的运行。

在收集餐巾纸的例子中,这一思路可以被理解为:垃圾收集机器人在人们用餐的同时寻找废弃的餐巾纸并将它们扔到垃圾箱里。这个看似简单的思路会在设计和实现时碰上进程间冲突的难题。比如说,如果垃圾收集进程包括标记和清除两个工作阶段,那么,垃圾收集器在第一阶段中辛辛苦苦标记出的结果很可能被另一个进程中的内存操作代码修改得面目全非,以至于第二阶段的工作没有办法开展。

M. L. Minsky 和 D. E. Knuth 对实时垃圾收集过程中的技术难点进行了早期的研究, G. L. Steele 于 1975 年发表了题为“多进程整理的垃圾收集( Multiprocessing compactifying garbage collection )”的论文,描述了一种被后人称为“ Minsky-Knuth-Steele 算法”的实时垃圾收集算法。 E. W. Dijkstra , L. Lamport , R. R. Fenichel 和 J. C. Yochelson 等人也相继在此领域做出了各自的贡献。 1978 年, H. G. Baker 发表了“串行计算机上的实时表处理技术( List Processing in Real Time on a Serial Computer )”一文,系统阐述了多进程环境下用于垃圾收集的增量收集算法。

增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对进程间冲突的妥善处理,允许垃圾收集进程以分阶段的方式完成标记、清理或复制工作。详细分析各种增量收集算法的内部机理是一件相当繁琐的事情,在这里,读者们需要了解的仅仅是: H. G. Baker 等人的努力已经将实时垃圾收集的梦想变成了现实,我们再也不用为垃圾收集打断程序的运行而烦恼了。

分代收集( Generational Collecting )算法

和大多数软件开发技术一样,统计学原理总能在技术发展的过程中起到强力催化剂的作用。 1980 年前后,善于在研究中使用统计分析知识的技术人员发现,大多数内存块的生存周期都比较短,垃圾收集器应当把更多的精力放在检查和清理新分配的内存块上。这个发现对于垃圾收集技术的价值可以用餐巾纸的例子概括如下:

如果垃圾收集机器人足够聪明,事先摸清了餐厅里每个人在用餐时使用餐巾纸的习惯——比如有些人喜欢在用餐前后各用掉一张餐巾纸,有的人喜欢自始至终攥着一张餐巾纸不放,有的人则每打一个喷嚏就用去一张餐巾纸——机器人就可以制定出更完善的餐巾纸回收计划,并总是在人们刚扔掉餐巾纸没多久就把垃圾捡走。这种基于统计学原理的做法当然可以让餐厅的整洁度成倍提高。

D. E. Knuth , T. Knight , G. Sussman 和 R. Stallman 等人对内存垃圾的分类处理做了最早的研究。 1983 年, H. Lieberman 和 C. Hewitt 发表了题为“基于对象寿命的一种实时垃圾收集器( A real-time garbage collector based on the lifetimes of objects )”的论文。这篇著名的论文标志着分代收集算法的正式诞生。此后,在 H. G. Baker , R. L. Hudson , J. E. B. Moss 等人的共同努力下,分代收集算法逐渐成为了垃圾收集领域里的主流技术。

分代收集算法通常将堆中的内存块按寿命分为两类,年老的和年轻的。垃圾收集器使用不同的收集算法或收集策略,分别处理这两类内存块,并特别地把主要工作时间花在处理年轻的内存块上。分代收集算法使垃圾收集器在有限的资源条件下,可以更为有效地工作——这种效率上的提高在今天的 Java 虚拟机中得到了最好的证明。

应用浪潮

Lisp 是垃圾收集技术的第一个受益者,但显然不是最后一个。在 Lisp 语言之后,许许多多传统的、现代的、后现代的语言已经把垃圾收集技术拉入了自己的怀抱。随便举几个例子吧:诞生于 1964 年的 Simula 语言, 1969 年的 Smalltalk 语言, 1970 年的 Prolog 语言, 1973 年的 ML 语言, 1975 年的 Scheme 语言, 1983 年的 Modula-3 语言, 1986 年的 Eiffel 语言, 1987 年的 Haskell 语言……它们都先后使用了自动垃圾收集技术。当然,每一种语言使用的垃圾收集算法可能不尽相同,大多数语言和运行环境甚至同时使用了多种垃圾收集算法。但无论怎样,这些实例都说明,垃圾收集技术从诞生的那一天起就不是一种曲高和寡的“学院派”技术。

对于我们熟悉的 C 和 C++ 语言,垃圾收集技术一样可以发挥巨大的功效。正如我们在学校中就已经知道的那样, C 和 C++ 语言本身并没有提供垃圾收集机制,但这并不妨碍我们在程序中使用具有垃圾收集功能的函数库或类库。例如,早在 1988 年, H. J. Boehm 和 A. J. Demers 就成功地实现了一种使用保守垃圾收集算法( Conservative GC Algorithmic )的函数库(参见 http://www.hpl.hp.com/personal/Hans_Boehm/gc )。我们可以在 C 语言或 C++ 语言中使用该函数库完成自动垃圾收集功能,必要时,甚至还可以让传统的 C/C++ 代码与使用自动垃圾收集功能的 C/C++ 代码在一个程序里协同工作。

1995 年诞生的 Java 语言在一夜之间将垃圾收集技术变成了软件开发领域里最为流行的技术之一。从某种角度说,我们很难分清究竟是 Java 从垃圾收集中受益,还是垃圾收集技术本身借 Java 的普及而扬名。值得注意的是,不同版本的 Java 虚拟机使用的垃圾收集机制并不完全相同, Java 虚拟机其实也经过了一个从简单到复杂的发展过程。在 Java 虚拟机的 1.4.1 版中,人们可以体验到的垃圾收集算法就包括分代收集、复制收集、增量收集、标记-整理、并行复制( Parallel Copying )、并行清除( Parallel Scavenging )、并发( Concurrent )收集等许多种, Java 程序运行速度的不断提升在很大程度上应该归功于垃圾收集技术的发展与完善。

尽管历史上已经有许多包含垃圾收集技术的应用平台和操作系统出现,但 Microsoft .NET 却是第一种真正实用化的、包含了垃圾收集机制的通用语言运行环境。事实上, .NET 平台上的所有语言,包括 C# 、 Visual Basic .NET 、 Visual C++ .NET 、 J# 等等,都可以通过几乎完全相同的方式使用 .NET 平台提供的垃圾收集机制。我们似乎可以断言, .NET 是垃圾收集技术在应用领域里的一次重大变革,它使垃圾收集技术从一种单纯的技术变成了应用环境乃至操作系统中的一种内在文化。这种变革对未来软件开发技术的影响力也许要远远超过 .NET 平台本身的商业价值。

大势所趋

今天,致力于垃圾收集技术研究的人们仍在不懈努力,他们的研究方向包括分布式系统的垃圾收集、复杂事务环境下的垃圾收集、数据库等特定系统的垃圾收集等等。

但在程序员中间,仍有不少人对垃圾收集技术不屑一顾,他们宁愿相信自己逐行编写的 free 或 delete 命令,也不愿把垃圾收集的重任交给那些在他们看来既蠢又笨的垃圾收集器。

我个人认为,垃圾收集技术的普及是大势所趋,这就像生活会越来越好一样毋庸置疑。今天的程序员也许会因为垃圾收集器要占用一定的 CPU 资源而对其望而却步,但二十多年前的程序员还曾因为高级语言速度太慢而坚持用机器语言写程序呢!在硬件速度日新月异的今天,我们是要吝惜那一点儿时间损耗而踟躇不前,还是该坚定不移地站在代码和运行环境的净化剂——垃圾收集的一边呢?

[王咏刚,2003年12月]

September 17

OllyDBG找到按钮的处理函数

最近系统有点慢,就想优化一下,于是下了个XX大师。结果要注册才行,看来可以用来练练手了。OD一下,靠还加了壳,偶就是用一下,就不脱你了。
开始在弹出窗口MessageBoxA下断,伊,结果不是用的这个函数,看来还有点麻烦。于是在折腾了半天后偶明白了(因为是新手,所以要折腾半天),要在“全部删除”按钮的处理函数下断,但是处理函数在哪里呢?
自己又折腾了半天,愣是没找到,还是soso一下,找到一篇文章叫做《OllyDBG 入门系列(五)-消息断点及 RUN 跟踪
或者读下面这段
 
几种典型程序Button处理代码的定位
作者:木马帝国  来源: www.mmbest.com  发布时间:2006-1-5 8:58:02  发布人:trojan

 

首先
1 od 下运行程序,F12 暂停;
2 View菜单中选击Windows项,在打开的窗口中可以从Title栏看到目标按钮,从而找到它的Handle(xxxxxxxx) ;

对不同平台生成的程序,分别处理:

一、VB, Delphi, CBuilder 程序:
3 在CallWindowProcA入口下条件断点: [esp+8]==xxxxxxxx && [esp+0c]==202;
4 F9继续程序,点击目标按钮,程序中断;
5 Alt+F4,在代码段(.text)上下访问断点;
6 F9执行程序,程序中断,
   1). VB程序中断在下面代码
     PUSH DWORD PTR DS:[EAX+EBX]              ; yyyyyyy
     上面[EAX+EBX]的值(yyyyyy)就是我们要找的位置。

   2). Delphi, CBuilder 的程序
     程序直接断在我们要找的位置。

借moon一句,这姑妄称作CallWindowProcA条件断点加上code段内存断点法吧。

二、VC程序
又分MFC和Win32两种情况,二者相同之处:
3 在IsDialogMessageW入口下条件断点: [[esp+8]]==xxxxxxxx && [[esp+8]+4]==202
4 F9继续程序,点击目标按钮,程序中断;
5 Alt+F4,在代码段上下访问断点;
6 F9执行程序,程序中断, 注意这里虽然中断在code段,但却不是处理Button点击事件的代码处
  这时:
  对Win32程序,只需要按几下F7,当回到User32.dll领空后再重复一次第 5、6步就可以了;
  而对于MFC程序,我们不得不多次重复这样的操作:单步回到MFC领空,再第 5、6步。好在已经看到大陆啦!

类比,这就叫IsDialogMessageW条件断点加上code段内存断点法了。