4

所以我想分析我的应用程序,我特别想记录从程序开始到进入和退出程序内部调用的每个函数(忽略 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 代表什么?

这可能吗?我听说过 IAT 挂钩,但在我看来,只有在挂钩 DLL 时才如此。同样在这里,我忽略了退出,尽管另一个 JMP 代替 RET 指令可能会有所帮助?

谢谢你的帮助

4

4 回答 4

2

您是否研究过Google 的分析工具?您可能会发现修改而不是自己制作更容易。它确实进行代码插入以执行其分析,因此至少,它们的注入框架将对您有益。

但是,对于这样的事情,您主要希望避免时间开销,所以我建议按地址跟踪,然后在分析完成后,将地址转换为符号名称。挂钩本身也可能是一项艰巨的任务,我建议制作一个一体式包装器,它不会改变函数入口或出口,而是重定向调用站点。

那么 ILT 代表什么?

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

于 2013-01-28T05:34:14.677 回答
1

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

于 2013-01-29T06:26:42.080 回答
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在关闭之前添加一些后处理- 将其写入磁盘或其他任何内容。注意 if written> current_index,在这种情况下你包装了缓冲区。请注意,以上代码均不包含任何分支,因此它应该相对性能友好(除了不断将拥有的数组移动buff到缓存中)。

存在因此loc您可以相对轻松地找到创建/销毁对(因为它的二进制值跟踪堆栈的值!),因此您可以在函数调用花费太长时间之后分析缓冲区。将可视化器放在一起并不是那么难,而且我已经看到了与上述类似的方法,用于检测视频流驱动程序代码中的毫秒级时序故障和打嗝。

开始分析current_index并向后工作,寻找对,直到你命中0(或者,如果written!= current_index,直到你环绕到current_index+1)。恢复调用图应该不难(如果需要)。

去掉上面的大部分内容,并简单地为每个 使用唯一的标签timestamp,可以减少缓冲区的大小,但会使重建调用图变得更加困难。

是的,这不是自动仪表。但是你的代码中运行缓慢的部分将是其中相对较小的一部分。因此,开始使用类似上面的东西进行检测,我猜你会比乱搞反汇编编译器的二进制输出和乱搞跳转表更快地得到答案。

于 2013-01-28T23:43:04.503 回答
0

在 Linux 上,您可以使用它gprof(1)来获取该数据。但请接受 Bentley 在他的“Programming Pearls”中关于性能的说法。第二部分是对他的“编写高效程序”(遗憾的是已绝版)的提炼,非常详细地讨论了如何(更重要的是,何时)优化代码。

于 2013-01-28T00:38:58.183 回答