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

Posted

技术标签:

【中文标题】C ++挂钩我自己的程序功能【英文标题】:C++ hooking my own programs functions 【发布时间】:2013-01-27 18:19:11 【问题描述】:

所以我想分析我的应用程序,我特别想记录从程序开始到进入和退出程序内部调用的每个函数(忽略 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 指令可能会有所帮助?

感谢您的帮助

【问题讨论】:

如果您使用的是 linux,除了 valgrind 之外,解决方案是使用 LD_PRELOAD 来加载挂钩您的函数的共享对象。我不知道这是否可以在 Windows 中实现,您可能想检查一下。这将比替换 ASM 指令容易得多。 这样做可能会扭曲结果,因为浏览挂钩代码会扰乱指令缓存,使某些函数运行得比不这样做时更慢。 @Bo 这是一个很好的观点,你是对的,但这与试图控制整体代码执行以及准确的时间一样重要。它是一个运行数百万行 C++ 的大型程序,每一帧都有不同的尖峰。 规则是你的大部分代码都是无可指责的。因此,与其对所有东西进行检测,不如创建一个 RAII 检测器并手动二进制搜索问题所在。一百万行代码中的lg(n) 是 30 次迭代。 30 次迭代比编写跳转表技巧慢的几率是多少? 如果您的总体目标恰好是找出您需要修复什么以使代码运行得更快,那么much quicker and more effective way to do it. 【参考方案1】:

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

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

那么 ILT 代表什么,我假设某个查找表,我将如何获取它?

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

【讨论】:

【参考方案2】:

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/ 是一篇很好的文章,其中包含如何使用它的示例。

【讨论】:

【参考方案3】:

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

【讨论】:

如果您向您的学生推荐 gprof,这里有一些 notes about it,并查看 this Bentley lecture 的第 35 页。 我拥有这本书并在十多年前阅读它。它适合它的时代,但有点过时了。介绍散列或二分搜索的奇迹并不能帮助所有人,但最初级的程序员除外。它没有考虑现代 CPU 和 GPU 架构,即乱序执行、硬件预取、高速缓存行、多个高速缓存、SIMD、多个处理器内核等。并行性在附录中作为单行提到过一次:“程序应该被结构化以在底层硬件中尽可能多地利用并行性。我不会继续。 它一如既往的热门。当然,技术先进(多核、NUMA、GPU 作为处理主力,高级语言隐藏了复杂数据结构的血淋淋的细节、巨大的内存),但基础保持不变:在深入研究之前进行测量,尝试更高级的方法(新的数据结构,不同的软件组织)在低级黑客攻击(使用位旋转而不是算术运算)之前,检查您的编译器是否已经完成了改进,仔细考虑您的更改对可维护性的影响。不要让我开始谈论“高级程序员”。【参考方案4】:
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() dotimestamp _(get_current_time(), __FILE__, __LINE__);while(false)

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

在您认为有问题的函数开头插入TIMESTAMP_SCOPE(_)

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

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

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

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

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

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

【讨论】:

以上是关于C ++挂钩我自己的程序功能的主要内容,如果未能解决你的问题,请参考以下文章

在运行时将一个窗口的控件替换为另一个

如何从内存中读取 3rd 方应用程序的变量?

如何在第三方进程中隔离我自己的 OpenGL 调用?

如何从使用状态挂钩的功能性父组件传递道具?

C++ 键盘钩子 CTRL 键卡住

如何将 setState 值插入 useEffect 挂钩?