C ++挂钩我自己的程序功能

时间:2013-01-27 18:19:12

标签: c++ x86 profiling hook

所以我想要分析我的应用程序,我特别想记录从程序开始的时间,当程序内部调用的每个函数(在DLL中的内容函数)被输入和退出时,即我想要一个简单的表格看起来像这样:

THREAD_ID FUNCTION_ADDRESS TIME EVENT_TYPE
5520      0xFF435360       0    ENTERED
5520      0xFF435ED3       25   ENTERED
5520      0xFF433550       40   ENTERED
5520      0xFF433550       50   EXITED
5520      0xFF433550       60   ENTERED
5520      0xFF433550       70   EXITED
5520      0xFF435ED3       82   EXITED
5520      0xFF435360       90   EXITED

对于看起来像这样忽略编译器优化的程序:

void test1(void)
{
   int a = 0;
   ++a;
}

void test(void)
{
    test1();
    test1();
}

void main(void)
{
    test();
}

我找不到任何现成的解决方案,我可以找到的最近的是微软VSPerfReport,但它只输出在进入和退出时每个功能花了多长时间。

所以我开始研究用一个简单的函数挂钩我的所有函数,这个函数生成一个缓冲区,我可以从中生成上面的表。为了做到这一点,我只想创建一个在main的开头调用的函数,它可以通过整个exe修改CALL指令来调用我的钩子函数。

像MinHook等那样的库对我来说似乎都有点OTT,可能不会工作,因为它是一个x64应用程序而且我不是试图挂钩DLL函数。

所以我只想修改每条CALL指令中的JMP指令,即这个程序:

void main(void)
{
...asm prologue 
    test();
002375C9  call        test (235037h) 
}
...asm epilogue

此处的调用将转到JMP表:

@ILT+40(__set_errno):
0023502D  jmp         _set_errno (243D80h)  
@ILT+45(___crtGetEnvironmentStringsA):
00235032  jmp         __crtGetEnvironmentStringsA (239B10h)  
test:
00235037  jmp         test (237170h)  
@ILT+55(_wcstoul):
0023503C  jmp         wcstoul (27C5D0h)  
@ILT+60(__vsnprintf_s_l):

我想通过这个表并将与应用程序的.exe中的函数相关的所有JMP重新路由到包含定时代码的钩子函数,然后返回到调用函数。

那么ILT代表什么呢?我假设有一些Lookup Table,我将如何抓住它?

这是否有可能我听说过IAT挂钩但是我认为只有在挂钩DLL时才会这样。在这里我也忽略了退出,虽然另一个JMP代替RET指令可能有帮助吗?

感谢您的帮助

4 个答案:

答案 0 :(得分:2)

你有没有看过Googles profiling tools?您可能会发现修改它更容易,而不是自己制作。它确实执行代码插入以执行其分析,因此它最小化,它们的注入框架对您有益。

然而,对于这样的事情,你大多想避免计时开销,所以我建议按地址跟踪,然后在完成分析时,将地址转换为符号名称。挂钩本身也是一项艰巨的任务,我建议制作一个多功能的包装器,它不会改变功能入口或出口,而是重定向呼叫站点。

  

那么ILT代表什么呢?我假设有一些Lookup Table,我将如何抓住它?

导入查找表,如果您计划分析内部函数,它也不会有多大用处。掌握它需要了解平台模块格式(PE,ELF,MACH-O)的内部结构。

答案 1 :(得分:0)

在Linux上,您可以使用gprof(1)来获取该数据。但是请在他的"Programming Pearls"中谈到Bentley的表现。第二部分是他的“写作高效程序”(很遗憾地绝版)的精炼,非常详细地讨论了如何(以及更重要的是,什么时候)优化代码。

答案 2 :(得分:0)

struct my_time_t;
my_time_t get_current_time(); // may be asm


struct timestamp;
struct timer_buffer {
  std::unique_ptr<timestamp[]> big_buffer;
  size_t buffer_size;
  size_t current_index;
  size_t written;
  buffer( size_t size ): big_buffer( new timestamp[size] ), buffer_size(size), current_index(0), written(0) {}
  void append( timestamp const& t ) {
    big_buffer[current_index] = t;
    ++current_index;
    ++written;
    current_index = current_index % buffer_size;
  }
};
struct timestamp {
  static timer_buffer* buff;
  timestamp const* loc;
  my_time_t time;
  const char* filename;
  size_t linenum;
  timestamp( my_time_t t, const char* f=nullptr, size_t l = 0 ):
    loc(this), time(t), f(filename), l(linenum)
  {
    go();
  }
  void go() {
    buff->append(*this);
  }
};
struct scoped_timestamp:timestamp {
  scoped_timestamp( my_time_t t, const char* f=nullptr, size_t l = 0 ):
    timestamp(t, f, l)
  {}
  ~scoped_timestamp() {
    go();
  }
};
#define TIMESTAMP_SCOPE( NAME ) scoped_timestamp NAME(get_current_time(), __FILE__, __LINE__);
#define TIMESTAMP_SPOT() do{timestamp _(get_current_time(), __FILE__, __LINE__);}while(false)

在某处创建timestamp::buff。使buff足够大。写一个快速有效的get_current_time()

在您认为遇到问题的功能开头插入TIMESTAMP_SCOPE(_)

在您认为需要时间的位置之间插入TIMESTAMP_SPOT();

在关闭之前添加timer_buffer的一些后处理 - 将其写入磁盘或其他任何内容。请关注written&gt; current_index,在这种情况下,您包装了缓冲区。请注意,上述代码都不包含任何分支,因此它应该是相对性能友好的(除了不断地将buff拥有的数组移动到缓存中)。

loc存在,因此您可以相对轻松地找到创建/破坏对(因为它的二进制值跟踪堆栈的值!),因此您可以在事件调用花费太长时间后分析缓冲区。把一个可视化器放在一起并不是那么难,而且我已经看到了类似上面用于检测视频流驱动程序代码中的ms级别定时故障和打嗝的事情。

current_index开始分析并向后工作,寻找配对,直到您点击0(或者如果written!= current_index,直到您回到current_index+1)。恢复呼叫图表(如果需要)应该不难。

删除上述大部分内容,并且只为每个timestamp使用唯一标记,如果缓冲区可以减小大小,但会使重建调用图更加困难。

是的,这不是自动检测。但是代码中行为缓慢的部分将是其中相对较小的一部分。所以开始使用上面的内容进行检测,我猜你得到的答案要快于解组编译器的二进制输出并乱用跳转表。

答案 3 :(得分:0)

gcc可以选择为函数入口和出口生成钩子调用。 使用-finstrument-functions进行编译,编译器会生成对__cyg_profile_func_enter__cyg_profile_func_exit的调用。您可以在gcc文档http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html中阅读更多内容。这里有一篇关于如何使用它的例子的好文章http://www.ibm.com/developerworks/library/l-graphvis/