FOURCC

2010-1-31 22:34 | by 2ndboy

  在 DirectShow 里面,各种 audio/video 的编码格式都可以用一个 GUID 来表示,比如 RGB24 的 GUID 是 MEDIASUBTYPE_RGB24(e436eb7d-524f-11ce-9f53-0020af0ba770)。但是在多媒体播放软件里(比如暴风影音),我们经常看到的是类似 AVC1、AAC2 这样的表示方法,这种用 4 个字符来表示的多媒体数据编码格式叫做 FourCC(Four-Character Codes)。

  FourCC 是一个 32 bits 的数值,基本上你可以把这个 32 bits 的值看作是 FourCC 里 4 个字符的 ASCII 码的组合,比如 YUY2 的 FourCC 是 0×32595559(Y=0×59, U=0×55, 2=0×32)。但并不是所有 FourCC 都符合这个规律,比如我上面举的 RGB24 就不是这样地:)

  FourCC 跟它对应的 GUID 之间是可以相互转换的,其实在 FourCC 后面加上 0000-0010-8000-00AA00389B71 就得到了它所对应的 GUID。比如 FourCC YUY2 所对应的 GUID MEDIASUBTYPE_YUY2 的值就是 {32595559-0000-0010-8000-00AA00389B71}。DirectShow 里面提供了一个工具类就是用来干这件事的:GUID g = (GUID)FOURCCMap( MAKEFOURCC(‘Y’,'U’,'Y’,'2′) );。

  FourCC 的分配是由 Microsoft 管理的,在这里可以找到一个已经注册的 FourCC 的列表,当然,不太全。

《True Colors》

2010-1-12 21:52 | by 2ndboy

  昨晚晚饭后在 VeryCD 上例行的逛逛,看到两张“新” iTunes Plus AAC 专辑,一张是 Dido 的《Life for Rent》,另外一张的封面造型比较前卫(是我不喜欢的那一型):

True Colors

  Erlend Bratland 的《True Colors》,歌者的名字没听说过。抱着聊胜于无的心态点了一首试听,马上惊为天人!看了看介绍,原来 2008 年 Erlend Bratland 在他 16 岁的时候就赢得了“Norway’s got talent”的冠军,果然实力不俗!

  今天专辑下载完以后马上转了一首专辑同名曲上来分享一下(原始 AAC 格式效果更好:)):
[audio:http://blog.2ndboy.net/wp-content/uploads/true_colors.mp3]

通过 IWMPGraphCreation 接口来定制 WMP 的 Filter Graph

2010-1-10 16:46 | by 2ndboy

  Windows Media Player 内部也是利用 DirectShow 来播放音视频内容的,这一点可以通过注册了 DirectShow Filter Graph SpyGraphEdit 来证实。那么有没有可能把 WMP 控件内嵌到我们自己的程序里,然后控制它的 Filter Graph 生成过程呢?答案就是 IWMPGraphCreation 接口。去年(其实是上个月;))研究了一下这个接口,最后终因版本要求过高,不适合用在我们的项目里而放弃使用,这里简单的做点笔记。

  根据 MSDN,这个接口只有在 WMP 10 及以上版本才支持。实现这个接口,跟 WMP 交互然后达到控制 WMP 的 Filter Graph 生成的步骤如下:

1) 首先要实现一个从 IOleClientSite, IWMPGraphCreation 和 IServiceProvider 这 3 个接口派生的类
2) 从 WMP 控件实例中取得 IOleObject 接口指针,然后调用 IOleObject::SetClientSite() 把我们新实现的类作为 WMP 的 site
3) WMP 会从它的 site 那里通过 QueryInterface() 得到 IServiceProvider 接口指针
4) 得到 IServiceProvider 接口以后 WMP 会调用 IServiceProvider::QueryService() 来尝试得到 IWMPGraphCreation 的接口指针
5) 在从我们这里得到 IWMPGraphCreation 接口指针以后,WMP 会调用我们的 IWMPGraphCreation::GetGraphCreationFlags(),这时我们需要给 flag 设置一个 WMPGC_FLAGS_USE_CUSTOM_GRAPH,表示我们想要定制 WMP 的 Filter Graph
6) WMP 控件在播放音视频文件之前会调用我们的 IWMPGraphCreation::GraphCreationPreRender() 实现,同时把它内部的 Filter Graph 接口指针传给我们,拿到 Filter Graph 指针以后我们就可以做我们想做的事情了

  最大的问题出在 step 5,WMPGC_FLAGS_USE_CUSTOM_GRAPH 这个 flag 在 WMP 12 及以上版本才支持,所以这个版本需求实在是有点太高了!另外一个意外是我的实测观察到,在 WMP 12 上面,step 5 及其后的动作,WMP 会做两遍,而不是我们直觉中的一遍,原因不明(Google 到一些资料,也有人遇到了这样的问题)。

犯错啦

2009-12-28 22:04 | by 2ndboy

  工作上的事,不管是多小的事,还是要再细心些、再仔细些!

  建立信誉是个长期艰巨的事儿,破坏信誉却太容易!

  不要怪运气不好,怪运气不好说明还有侥幸心理,做错了事就要承担后果/总结教训。

  犯低级错误对于完美主义者来说是种耻辱。

  鄙视一下落井下石的低素质“ren”,时不时要跳出来昭示一下你龌龊的手段。

  好了,也不要太在意一城一池的得失,站在圈子外面看的话,你也太不洒脱了!

年终总结 for 2009

2009-12-27 14:41 | by 2ndboy

  又到年底啦,来个年终总结吧,总结过去,展望未来:D

  今年生活上最大的变化就是装修和搬家,终于搬到自己的房子里住了,结束了长达 6 年收别人房租,自己又不用交房租的生活;) 虽然目前房子的位置不错(买菜乘车都很方便),但还是希望今后可以换个再大套一些的,小区环境好一些的。呵呵,不过目前杭州的这个房价实在是变态,这个愿望的实现还得再等等。

  工作方面,今年的状态那就是一个字——忙!个人感觉是从业以来最忙的一个年头,而且明年应该还会继续这样忙下去。工作之外应朋友之邀帮忙做了一些东西,所以忙上面还要再加一个累字,还好没有持续太长时间,否则可真要受不了了。虽然工作忙,但是今年在做的一个项目经历了一次比较大的调整,前期做的工作基本上等于是白忙了一场:( 最近正在从另外的方向做努力,累是累了点,但是由于接触的是以前不曾接触过的领域,所以感觉收获良多,学到的新东西也不少,从这方面讲,多少还有些安慰。

  身体方面,搬家之后周末经常会跟 LP 一起去爬山,运动量上应该说比以前是多了,不过从 US 回来以后积累的脂肪却一直不见少,看来还是要多运动,增强体力和精力。

  今年看了一些有关移民的资料和过来人经验,对移民有了更清醒的认识。虽然近几年没有移民的打算,但是这也许是几年以后的一个选项,所以英语方面,平时还要加强。

  新的一年,希望家人健康、平安、幸福。俺自己事业顺利,每天开心:D

聊聊 API hooking

2009-12-14 22:30 | by 2ndboy

  今天接着说说 API hook。API hook 大致上有两种手段:修改 IAT 和修改 API 指令。那种用一个自己写的同名 DLL 来替换目标 DLL 的手段就不提啦,可操作性太差。今天聊一下直接修改 API 指令这种方法。

  直接修改 API 指令,简单的说就是在目标 API 代码的开始处修改一些指令,让执行绪跳转到自己的代码里面。这里需要面对的问题有 3 个:如何找到 API 的代码地址,如何修改代码段,如何构造执行绪转移指令。

  前两个问题比较简单,无非是相关 API 的调用,用 GetProcAddress() 可以取到目标 API 的代码所在地址;用 VirtualProtect() 可以修改代码所在地址的访问权限,有了写权限之后就可以对目标 API 的代码做手脚了。

  至于如何构造执行绪转移指令,其实说白了就是在目标 API 代码的开始处放置一条无条件跳转指令 jmp。对于 32bits 系统,jmp 的机器码是 0xE9(这里不讨论短跳转 0xEB 的原因是通常我们自己代码跟目标 API 代码之间的偏移都大于 128),加上 32bits 的相对地址,一条无条件跳转指令的长度为 5 个字节。也就是说我们要至少覆盖目标 API 开始处的 5 个字节,但是直接覆盖别人的代码是肯定会出问题地,所以我们在覆盖目标 API 开始处指令之前要把这些指令搬到其它地方去,后面还有用。

  对于 jmp 指令后面那个相对地址,不是简单的拿目标地址减去当前 jmp 指令的地址就行,由于 CPU 要执行过 jmp 指令,拿到跳转地址再进行跳转,所以计算相对地址的时候要用“目的跳转地址 – ( jmp 所在指令地址 + 5 )”。目前为止,把对目标 API 的调用劫持到我们自己的函数这个目的已经达到了。但是我们还是得在需要的时候调用原始 API 来完成 caller 想要完成的功能,不过别忘了原始 API 已经被我们破坏掉了,所以调用原始 API 还要靠我们之前保存的 API 代码起始处的那些指令。

  假设我们刚好从目标 API 处复制了 5 个字节的完整指令(注意一定要是完整指令)放在内存 A 处,那么在 A + 5 处再构造一条跳转指令跳回到 API + 5 处就等于是把断成两截的 API 代码又连接了起来。这样一来,在需要调用原本的 API 功能时我们要调用的其实是 A。

以下是一些示例代码:

  1. typedef HRESULT (WINAPI *_DirectDrawCreate)( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter );
  2.  _DirectDrawCreate pOldDirectDrawCreate = NULL;
  3.  
  4.  HRESULT WINAPI
  5.  MyDirectDrawCreate( GUID FAR *lpGUID, LPDIRECTDRAW FAR *lplpDD, IUnknown FAR *pUnkOuter )
  6.  {
  7.      MessageBox( NULL, "MyDirectDrawCreate", "MyDirectDrawCreate", 0 );
  8.      return( pOldDirectDrawCreate( lpGUID, lplpDD, pUnkOuter) );
  9.  }
  10.  
  11.  int main( int argc, char *argv[] )
  12.  {
  13.      HMODULE hDll = LoadLibrary( "DDraw.dll" );
  14.      if( hDll )
  15.      {
  16.          pOldDirectDrawCreate = (_DirectDrawCreate)GetProcAddress( hDll, "DirectDrawCreate" );
  17.          HookAPI( (PVOID *)(&pOldDirectDrawCreate), MyDirectDrawCreate );
  18.      }
  19.  
  20.      LPDIRECTDRAW pDDraw = NULL;
  21.      HRESULT hr = DirectDrawCreate( NULL, &pDDraw, NULL );
  22.      if( DD_OK == hr )
  23.          hr = pDDraw->SetCooperativeLevel( NULL, DDSCL_NORMAL );
  24.  
  25.      return( 0 );
  26.  }

以上代码成功执行后,当我们调用 DirectDrawCreate() 的时候会被之前设置的 hook 函数截获,显示一个对话框。注意 HookAPI() 的第一个参数是指向指针的指针,所以我们开始时拿到的“原始” API 地址其实被换掉了(见上面描述,原始 API 代码已经被我们断成两截啦)。

下面是代码里用到的两个关键数据结构:

  1. #pragma pack (1)
  2.  struct HookTarget
  3.  {
  4.      char jmp;
  5.      int  address;
  6.  
  7.      HookTarget()
  8.      {
  9.          jmp = char( 0xE9 );
  10.      }
  11.  };
  12.  
  13.  struct HookThunk
  14.  {
  15.      char instructions[16];
  16.      HookTarget HookProc;
  17.  
  18.      HookThunk()
  19.      {
  20.          memset( instructions, 0x90, sizeof( instructions ) );
  21.      }
  22.  };
  23.  #pragma pack ()

HookTarget 是用来写覆盖在目标 API 起始处的无条件跳转指令的,HookThunk 是用来把断成两截的 API 代码连起来的。0×90 是 nop 的机器码,就算我们只从目标 API 那里复制了 5 个字节,后面的 nop 也会安全的让执行绪运行到 jmp 指令处。

下面是 HookAPI() 的代码:

  1. bool
  2.  HookAPI( PVOID *ppProc, LPCVOID pMyProc )
  3.  {
  4.      do
  5.      {
  6.          DWORD dwOldProtect = 0;
  7.          if( !RemoveReadOnly( *ppProc, 16, &dwOldProtect ) )
  8.              break;
  9.  
  10.          HookThunk *pThunk = new HookThunk;
  11.          int cbInstructions = CheckInstructions( (LPBYTE)*ppProc );
  12.          memcpy( pThunk, *ppProc, cbInstructions );
  13.          pThunk->HookProc.address = ( (int)*ppProc + cbInstructions )
  14.              - ( (int)pThunk + sizeof( HookThunk ) );
  15.  
  16.          DWORD dwTemp = 0;
  17.          if( !VirtualProtect( pThunk, sizeof( HookThunk ), dwOldProtect, &dwTemp ) )
  18.              break;
  19.  
  20.          HookTarget *pOriginalProc = (HookTarget *)*ppProc;
  21.          pOriginalProc->jmp     = char( 0xE9 );
  22.          pOriginalProc->address = (int)MyDirectDrawCreate - ( (int)pOriginalProc + 5 );
  23.  
  24.          if( !VirtualProtect( *ppProc, 16, dwOldProtect, &dwTemp ) )
  25.              break;
  26.  
  27.          *ppProc = pThunk;
  28.          return( true );
  29.      }
  30.      while( false );
  31.  
  32.      return( false );
  33.  }

就不详细解释了,看完了上面那一大坨原理分析,HookAPI() 这个函数很容易看懂。上面代码里面的 RemoveReadOnly() 就是对 VirtualProtect() 调用的包装而已,对于我们这个示例的目标 API——DirectDrawCreate(),CheckInstructions() 简单的返回 5 就可以了(如果程序执行异常,最好自己用调试器看一下指令边界在哪里)。

  注意以上的示例代码只是用来做测试之用,没有做完备的错误处理。而且在实际环境中目标 API 代码的起始指令边界不一定正好是 5 个字节,如果边界是 4 个字节和 6 个字节处,由于我们至少要覆盖 5 个字节,所以要多复制一些(复制 6 个字节)。另外代码中使用的指令集和地址都只适用于 32bits x86 系统,在其它 CPU 和 OS 上要做相应的修改才能正常运行。最后,在实际的环境中,还要写卸载 hook 的代码;)

Apache mod_rewrite 经验一则

2009-12-14 13:12 | by 2ndboy

  周末呼吸道发炎,今早起床发现还没好,所以在家休养。照理说这么个小毛病是不影响上班地,但是之前有过好几次类似的经验——呼吸道发炎,还去上班,结果在空气污浊的办公司待了一天,随后就严重起来啦!顺便在这儿鄙视一下公司办公室的空气,由于层高太低,电脑众多,通风不畅,基本上下午 2 点一过就头昏脑胀~~再说下去要坏了心情啦:D

  周末研究了一下 Apache 的 rewrite,这个东西有 RE 基础的人上手还是比较快的。不过期间遇到的小问题折腾了好久,我一开始试验的时候规则是放在 httpd.conf 里面的,后来移到 .htaccess 里面就没了效果,开始以为是没有开启对 .htaccess 的支持,后来经过大量折腾,才明白是规则写在 httpd.conf 和写在 .htaccess 里面,收到的 URL 不一样,比如访客用 http://www.foo.com/ 来访问,在 httpd.conf 里面拿到的 URL 串是“/”,而 .htaccess 里拿到的居然是“”,即便 .htaccess 所在目录的内容已经被配置成虚拟主机也一样,所以在 httpd.conf 里面的如下规则:

  1. RewriteRule /$ index.php?rewrite=1 [L]

放到 .htaccess 里面以后要改成:

  1. RewriteRule ^$ index.php?rewrite=1 [L]

  这就是有经验跟没经验的区别,浪费了时间,走了弯路换回来的经验,值得小记一下:)

DirectShow filter 体系初探

2009-12-6 13:11 | by 2ndboy

  再次工作上项目需要,要研究下 DirectShow 的 filter 体系,以及通过将自己实现的 filter 插入到播放 audio/video 时的 filter graph,最后达到获取进入 render filter 前的已经解码的 A/V 数据的目的。

  上周五利用下午时间已经找到了实现的方向,记录如下。其实网上公开的资料多如牛毛,现在的感受,不管是做哪方面的 research,只要不是高精尖到什么隐形飞行器涂料技术,你基本都可以在 Internet 上找到很多线索,有时候甚至是标准答案(所以说熟练运用搜索引擎很重要,这是快速学习的基础所在)。

  DirectShow 基于 COM 来构建,设计思想非常值得借鉴。在 DS 中发挥作用的组件都被称作 filter,每个 filter 有各自不同的作用,当它们被正确的连接在一起时就可以完成 audio/video 的回放。

  filter 这个名字起的不错,当多媒体数据流经一个个“过滤器”,经过 A/V 分离、decode 和 render 之后被最终呈现的用户面前。总体上说,有这么 3 类 filter:
1) source filter,负责读取源数据
2) transform filter,负责做各种数据变换
3) render filter,负责渲染结果

  每个 filter 都暴露出一到多个 pin,他们各自之间通过 pin 进行连接。source filter 只有 output pin,而 render filter 只有 input pin,transform filter 则既有 input 又有 output pin。当这些 filter 正确的通过各自的 pin 连接在一起时,就形成一个 filter graph。

  就拿用 DS 来播放一个 MP3 来说,filter graph 里会出现如下几个 filer:
一个 source filter,后面接一个 MPEG-I Stream Splitter(当然这只是一个 audio,所以不会分离出 video 数据来),然后是一个 MPEG Layer-3 Decoder,最末端是一个 Default DirectSound Device 的 render filter。
filter graph

  所以看来我们如果想在这中间插一杠子,把已经解码过的 A/V 数据拿出来的话,就需要开发一个 transform filter。经过一番 Google,发现我们要做的就是实现一个 Sample Grabber Filter,它本质上还是一个 transform filter,DS 架构里有这么一号的本意是为了在开发 transform filter 的过程中方便测试 transform filter 工作是否正常的,但 sample grabber filter 也同样非常适合抓取流经 filter graph 的数据!其实系统中是存在一个现成的 Sample Grabber Filter 的,我们要做的工作就是把这个 filter 加到 filter graph 里来,然后去实现一个 Sample Grabber Filter Callback 就可以拿到数据了。

C++ 类的内存布局

2009-12-6 00:28 | by 2ndboy

  最近工作上的项目需要 hook COM 组件,顺便把 C++ 类实例的内存布局重温和巩固了一下,特别是有 virtual function 的 class。近年来记忆力衰退:D,所以顺便记一笔吧。

  用下面两个有 virtual function 和继承关系的 class 来做例子:

  1. class base
  2.  {
  3.  public:
  4.      virtual int   func1( int   foo ) { return( foo % 3 ); }
  5.      virtual float func2( float foo ) { return( foo / 1.2f ); }
  6.  };
  7.  
  8.  class derived : public base
  9.  {
  10.  public:
  11.      derived() { m = 0xf0f0f0f0; }
  12.  
  13.      virtual int func1( int  foo ) { return( foo + m ); }
  14.      bool func3( bool bar ) { return( !bar ); }
  15.  
  16.  private:
  17.      int m;
  18.  };

C++ 标准里并没有对虚函数的实现做说明,但是几乎所有的编译器都使用虚表来实现 virtual function。下面的这段小程序围绕上面两个类做了一些事情:

  1. typedef struct
  2.  {
  3.      int *pVtbl;
  4.  } class_with_vtbl;
  5.  
  6.  int main( int argc, char *argv[] )
  7.  {
  8.      base    *pBase    = new base;
  9.      derived *pDerived = new derived;
  10.  
  11.      printf( "pBase    = %x\n", pBase );
  12.      printf( "pDerived = %x\n", pDerived );
  13.  
  14.      printf( "sizeof( base )    = %x\n", sizeof( base ) );
  15.      printf( "sizeof( derived ) = %x\n", sizeof( derived ) );
  16.  
  17.      printf( "[pBase]    = %x\n", ((int *)pBase)[0] );
  18.      printf( "[pDerived] = %x\n", ((int *)pDerived)[0] );
  19.  
  20.      printf( "[pBase    + 4] = %x\n", ((int *)pBase)[1] );
  21.      printf( "[pDerived + 4] = %x\n", ((int *)pDerived)[1] );
  22.  
  23.      printf( "pBase->pVtbl[0] = %x\n", ((class_with_vtbl *)pBase)->pVtbl[0] );
  24.      printf( "pBase->pVtbl[1] = %x\n", ((class_with_vtbl *)pBase)->pVtbl[1] );
  25.  
  26.      printf( "pDerived->pVtbl[0] = %x\n", ((class_with_vtbl *)pDerived)->pVtbl[0] );
  27.      printf( "pDerived->pVtbl[1] = %x\n", ((class_with_vtbl *)pDerived)->pVtbl[1] );
  28.  
  29.      printf( "pBase->func1 = %x\n", pBase->func1 );
  30.      printf( "pBase->func2 = %x\n", pBase->func2 );
  31.  
  32.      printf( "pDerived->func1 = %x\n", pDerived->func1 );
  33.      printf( "pDerived->func2 = %x\n", pDerived->func2 );
  34.      printf( "pDerived->func3 = %x\n", pDerived->func3 );
  35.  
  36.      pDerived->func1( 0 );
  37.  
  38.      typedef int (base::* func1_type)( int );
  39.      func1_type pFunc1 = pBase->func1;
  40.      (pBase->*pFunc1)( 0 );
  41.  
  42.      return 0;
  43.  }

程序在 VC6 下编译,某次的执行结果如下:

  1. pBase    = 431a20
  2. pDerived = 4319e0
  3. sizeof( base )    = 4
  4. sizeof( derived ) = 8
  5. [pBase]    = 42720c
  6. [pDerived] = 4271b4
  7. [pBase    + 4] = fdfdfdfd
  8. [pDerived + 4] = f0f0f0f0
  9. pBase->pVtbl[0] = 401023
  10. pBase->pVtbl[1] = 401005
  11. pDerived->pVtbl[0] = 40100a
  12. pDerived->pVtbl[1] = 401005
  13. pBase->func1 = 401028
  14. pBase->func2 = 40102d
  15. pDerived->func1 = 401028
  16. pDerived->func2 = 40102d
  17. pDerived->func3 = 401037

接下来通过对程序运行结果的解读来分析一下带有虚函数的类实例在内存中的布局(程序用 VC 6 编译)。结果中的前两行是两个类实例的内存地址。

接下来的第 3、4 行显示的是两个类实例占用的内存大小,base 没有成员变量,但是占用了 4 个 byte,而它是有 2 个虚函数的,所以这 4 个字节是一个指向虚表的指针;derived 有一个 int 型的成员变量,它同样也有虚函数,所以占用 8 个字节。

第 5、6 行是两个类实例所在内存的第一个 dword 的内容,没什么特别;但是看看第 7、8 行(显示两个类实例所在内存的第二个 dword 内容),第 8 行表明了虚表总是位于任何成员变量之前的,也就是说虚表指针总是在类实例内存布局中的第一项。

在上面程序里,我们用一个名叫 class_with_vtbl 的结构来强转类实例指针,从而可以方便的展示虚表的内容。9~12 行就是 base 和 derived 的虚表内容,可以看到,它们虚表的第二项内容相同,这是因为 derived 没有改写基类的 func2。

接下来 13~16 行的结果看上去比较令人费解:
1) pBase->func1 跟 pBase->pVtbl[0] 的内容不符,这么说来,虚表里的内容不是指向虚函数的吗?其实如果说这是一个 surprise 的话,你在调试时在 watch 窗口里输入 base::func1 看到的东西会更让你惊讶,这样看到的值居然是 0×401350!
2) 如果说 base::func2() 和 derived::func2() 的地址相同还可以理解的话,则么居然 base::func1() 和 derived::func1() 的值竟然也一样!

下面先来解决第 2 个疑问,代码和实际数据说明一切,在调试器里看一下 401028 及其周边的内容:

  1. @ILT+0(?func2@base@@UAEMM@Z):
  2. 00401005   jmp         base::func2 (00401250)
  3. @ILT+5(?func1@derived@@UAEHH@Z):
  4. 0040100A   jmp         derived::func1 (00401290)
  5. @ILT+10(??0base@@QAE@XZ):
  6. 0040100F   jmp         base::base (00401310)
  7. @ILT+15(??0derived@@QAE@XZ):
  8. 00401014   jmp         derived::derived (004011f0)
  9. @ILT+20(_main):
  10. 00401019   jmp         main (00410130)
  11. @ILT+25(?func3@derived@@UAE_N_N@Z):
  12. 0040101E   jmp         derived::func3 (004012d0)
  13. @ILT+30(?func1@base@@UAEHH@Z):
  14. 00401023   jmp         base::func1 (00401350)
  15. 00401028   jmp         `vcall' (00401240)
  16. 0040102D   jmp         `vcall' (00401280)
  17. 00401032   jmp         `vcall' (00401340)
  18. 00401037   jmp         derived::func3 (004012d0)

可以看到 401028 地址处有一条 jmp 指令,它其实是个 vcall,可以理解为专门用来调用虚函数(vfunction)的 call。401028 处的 vcall 要跳转到 401240 处,所以我们来看一下该地址处的代码:

  1. `vcall':
  2. 00401240   mov         eax,dword ptr [ecx]
  3. 00401242   jmp         dword ptr [eax]

在做 vcall 之前,caller 要把类实例指针放在 ecx 里面,这个 vcall 事实上是调用所有对象第一个虚函数的通用代码,它把虚表的第一项取出来放在 eax 里,接着就跳转到 eax 所含的那个地址去继续执行了。看看 401280 处(也是 pBase->func2 和 pBase->func2 共同指向的内容 )另外一个 vcall 的实现会更清楚一些:

  1. `vcall':
  2. 00401280   mov         eax,dword ptr [ecx]
  3. 00401282   jmp         dword ptr [eax+4]

由于是第二个虚函数,所以在把虚表入口地址放入 eax 之后,程序是取出了虚表的第二项([eax+4]),接着才跳转过去的。

这就解释了刚才的第 2 个疑问,其实在程序里用 pBase->func1 拿到的并不是 func1 的真实入口地址,而只是一个通用的 vcall thunk 地址。

下面来解决第 1 个疑问,通过上面的描述,现在可以知道 pBase->func1 指向的其实是 vcall thunk,那么 pBase->pVtbl[0] 指向的又是什么?还是来看代码,其实上面已经列出来了:

  1. @ILT+30(?func1@base@@UAEHH@Z):
  2. 00401023   jmp         base::func1 (00401350)

居然也是一条 jmp 指令,不过注意一下它 jump 到的地址——401350,这个值跟我们在 VC watch 窗口里看到 base::func1 的值是一样的,其实这个才是真正 base::func1 的地址!问题解决。

这里稍总结一下:
a. 用代码取 pBase->func1 取到的地址其实指向的是 vcall thunk,vcall thunk 最终也是借助虚表里的内容找到真正虚函数地址的
b. 虚表表项也并非直接指向相应的虚函数地址,而是指向一个跳转指令(这貌似有点兜圈子),这个跳转指令才真正的把执行绪引到真实函数体
c. 用 VC 调试器 watch 到的地址就直接是真正的函数入口地址

接下来的篇幅,我们来实际看看用虚表和 vcall 两种方法调用虚函数的过程。看上面实例代码的最后两坨:

  1. pDerived->func1( 0 );

这句代码编译后变成:

  1. 0041031D   mov         esi,esp
  2. 0041031F   push        0
  3. 00410321   mov         edx,dword ptr [ebp-14h]
  4. 00410324   mov         eax,dword ptr [edx]
  5. 00410326   mov         ecx,dword ptr [ebp-14h]
  6. 00410329   call        dword ptr [eax]

[ebp-14h] 处的 pDerived 对象指针被放入 edx,接着虚表里的第一项被放入 eax,call [eax] 导致代码流程跑到虚表项所指的那个地址继续执行(我们已经知道了,那个地方又是一个 jmp)。可以看到在调用虚函数之前,代码把对象指针(this 指针)存入 ecx,这是 __thiscall 调用习惯(其它调用习惯中,this 指针通过堆栈传递)。

接下来看看 vcall:

  1. typedef int (base::* func1_type)( int );
  2.  func1_type pFunc1 = pBase->func1;
  3.  (pBase->*pFunc1)( 0 );

成员函数类型无法强转成其它普通指针类型,所以要定义一个“::*”类型的指针来存放它,调用的时候也要用 ->*(或 .*,如果左边是非指针类型的对象)。编译后的汇编代码如下:

  1. 00410332   mov         dword ptr [ebp-18h],offset @ILT+35(`vcall') (00401028)
  2. 00410339   mov         ecx,dword ptr [ebp-18h]
  3. 0041033C   mov         dword ptr [ebp-18h],ecx
  4. 0041033F   mov         esi,esp
  5. 00410341   push        0
  6. 00410343   mov         ecx,dword ptr [ebp-10h]
  7. 00410346   call        dword ptr [ebp-18h]

调用类实例第一个虚函数的 vcall thunk 地址被放入 [ebp-18h],接着 pBase 的指针(this 指针)被放入 ecx,call [ebp-18h] 导致程序跳转到 vcall thunk 处执行,最终导致 pBase 的第一个虚函数被调用。

iPod Touch 使用体验

2009-11-26 21:16 | by 2ndboy

  iPod Touch 入手大半个月了,感觉不错!要说缺点,最大的缺点就是 Apple 为了追求完美,把 Touch 的机身做的太过纤细,从而牺牲了电池续航能力,以我目前的使用频率来说,一般两天冲一次电。做 iPhone 开发和测试的同事说他们在 Touch 上做测试,一般只能撑 4 个小时 OoO!!!

  现在虽然没有破解,不过感觉 free 的 app 也足够用啦!看电子书,当然是用 Stanza,体验相当不错,不过网上 ePub 格式的书还是嫌少。游戏方面,现在最喜欢的是 Tap Tap Revolution,节奏感相当不错,现在已经觉得 hard 级不够刺激啦。Discover 绝对不能没有,是不用数据线传数据的首选!它支持的格式也挺多,拿来看 PDF 很不错。复杂的 GTD 工具不大喜欢,所以我现在在用 To Do’s。对了,iShoot 这个游戏也挺有意思,time killer!

  其实撇开 free app,Touch 自带的程序也相当实用!以前懒得查邮箱,经常会漏收 mail,现在随时拿起来 Touch 就可以看邮件,太方便啦!当然,这也得益于公司和家里随时都有 WIFI 用。天气预报和备忘录也是使用最频繁的两个程序,照片和通讯录用的最少,真想不通为什么 iTunes 不允许手工同步照片,这不是逼我用第三方管理软件嘛。嗯,股票程序也不错,简单的看看指数的话,是条捷径,复杂的需求俺用同花顺。

  在 Touch 上看视频的体验也不错,虽然比起很多国产所谓 MP4 来,视频要先转换才能看确实麻烦了一点,但不可否认转码过后的视频在 Touch 上看起来效果和操作反应都不错,毕竟是经过了量身订做的嘛。视频转换软件,我推荐 free 的 WinMEnc!虽然拜操蛋的 GFW 所赐,WinMEnc 的官方 blog 被 block 了,但是随便 search 一下还是能方便的下载到的,目前官方最新版是 0.81 beta。转码的时候在 profile 里选“[iPhone] MP4-AVC 480×272 CRF22, AAC 96kbps.ini”即可。

  最后还不得不提一下入手 Touch 当天晚上的遭遇,买的时候在商家那里用他们的 App Store 帐号下载了个 QQ,其实我早就不玩这东西啦,只是为了让他们现身说法一下如何从 App Store 上下东西。谁知晚上回去自己申请了一个 App Store 帐号,下了几个软件以后连上 iTunes 同步了几首 MP3,发现除了自带程序之外的所有程序都没法运行了,都是界面闪一下就退出。最后折腾到很晚,用 iTunes 尝试了 6、7 次系统还原,才把 Touch 成功刷回来(前几次都失败)。后来想了想原因应该是用 iTunes 同步惹的祸,机器了的程序是用两个不同的 App Store 帐号下载的,iTunes 可能觉得有 crack 之嫌,所以把程序都锁住啦。现在想想也许当时把那唯一一个用卖家帐号下载的 QQ 删掉兴许就能恢复正常,any way,还好有惊无险,下次再也不在自己的 Touch 上用别人的帐号下东西啦。

  好啦就写到这儿,最后提个问题,有时候看邮件的时候点开个链接,Safari 自动启动,看完链接以后怎么直接回到邮件程序?我现在都是先回到 home 界面再打开邮件程序,麻烦!当然这个问题也许无解,毕竟未经破解的 iPod Touch 是“单进程”的嘛。