6

我正在为我的 c# 应用程序开发一个不干涉日志机制。

这是我希望它看起来的样子:

functiona(arg1, arg2, arg 3.....)调用 function b(arg4,arg5,arg6....),后者又调用log()which 能够检测堆栈跟踪(这可以通过 来完成Environment.StackTrace)以及调用堆栈跟踪中的每个函数(例如ab)的值。

我希望它在调试和发布模式下工作(或者,至少在调试模式下)。

这可以在.net中完成吗?

4

4 回答 4

10

可证明不可能:

b调用时,a 使用的堆栈空间arg1(IL 堆栈,因此它可能从未放入堆栈,但已在调用中注册)不能保证仍由arg1.

通过扩展, ifarg1是一个引用类型,如果在调用b.

编辑

更详细一点,因为您的评论表明您没有理解这一点并且仍然认为它应该是可能的。

抖动使用的调用约定未在任何相关标准的规范中指定,这使实施者可以自由地进行改进。它们确实在 32 位和 64 位版本以及不同的版本之间存在差异。

但是,MS 人员的文章建议使用的约定类似于__fastcall约定。在您对 的调用中aarg1将被放入arg2运行代码的核心的 ECX 寄存器*和 EDX 寄存器中(我通过假设 32 位 x86 来简化,并且注册了 amd64 甚至更多的参数)。arg3将被推入堆栈并且确实存在于内存中。

请注意,在这一点上,并没有存在的内存位置arg1arg2它们仅在 CPU 寄存器中。

在执行方法本身的过程中,根据需要使用寄存器和存储器。并且b被称为。

现在,如果a需要arg1,或者arg2它必须在它调用之前推送它b。但如果它没有,那么它就不会 - 甚至可能会重新排序以减少这种需求。相反,此时这些寄存器可能已经被用于其他用途 - 抖动并不愚蠢,因此如果它需要一个寄存器或堆栈上的一个插槽,并且在方法的其余部分有一个未使用,它会继续重用那个空间。(就此而言,在此之上的级别,C# 编译器将重用 IL 生成的虚拟堆栈中使用的插槽)。

因此,当b被调用时,arg4将被放入寄存器 ECX 中,arg5放入 EDX 并arg6压入堆栈。在这一点上,arg1arg2 存在,你再也找不到它们是什么,就像你读一本书被回收并变成卫生纸之后一样。

(有趣的是,一个方法在相同位置调用另一个具有相同参数的方法是很常见的,在这种情况下,ECX 和 EDX 可以单独使用)。

然后,b根据大小,返回,将其返回值放入 EAX 寄存器或 EDX:EAX 对或 EAX 指向它的内存中,在将其返回值放入该寄存器a之前做一些更多的工作,依此类推。

现在,这是假设没有进行任何优化。实际上,可能b根本没有调用它,而是它的代码是内联的。在这种情况下,无论值在寄存器中还是在堆栈上 - 在后一种情况下,它们在堆栈上的位置,不再与b's 的签名有关,也与相关值在a's期间的位置有关执行,并且在另一个“调用”到b的情况下,甚至在另一个“调用”到bfrom的情况下,它会有所不同,因为包括其调用a的整个调用可能已经内联在一种情况下,而不是内联另一个,并且在另一个中以不同的方式内联。例如,如果abarg4直接来自另一个调用返回的值,此时它可能在 EAX 寄存器中,而在arg5ECX 中,因为它与.arg1arg6a

另一种可能性是b调用是被消除的尾调用:因为调用b也将立即返回其返回值a(或其他一些可能性),而不是推入堆栈,而是使用正在使用的值bya被就地替换,并且返回地址发生了变化,因此 return fromb跳转回调用的方法a,跳过了一些工作(并减少了内存使用,以至于一些会溢出堆栈的函数式方法改为工作并且确实运作良好)。在这种情况下,在调用 的过程中b,参数 toa很可能完全消失了,即使是那些已经在堆栈上的参数。

最后一种情况是否应该被认为是一种优化,这是非常值得商榷的。有些语言严重依赖于它的完成,因为它们提供了良好的性能,而如果它们甚至可以工作(而不是溢出堆栈),它们的性能却很糟糕。

可以有各种各样的其他优化。应该有各种各样的其他优化——如果 .NET 团队或 Mono 团队做了一些事情,使我的代码更快或使用更少的内存,但在其他方面表现相同,而无需我做任何事情,我不会抱怨!

这是假设首先编写 C# 的人从未更改过参数的值,这肯定不会是真的。考虑这段代码:

IEnumerable<T> RepeatedlyInvoke(Func<T> factory, int count)
{
  if(count < 0)
    throw new ArgumentOutOfRangeException();
  while(count-- != 0)
    yield return factory();
}

即使 C# 编译器和抖动的设计非常浪费,您可以保证不会以上述方式更改参数,但您怎么能知道count在 的调用中已经发生了factory什么?即使在第一次调用时它也不同,而且上面的代码也不是奇怪的。

所以,总结一下:

  1. 抖动:参数经常被注册。您可以期望 x86 将 2 个指针、引用或整数参数放入寄存器,而 amd64 将 4 个指针、引用或整数参数和 4 个浮点参数放入寄存器。他们没有位置可以读取它们。
  2. 抖动:堆栈上的参数经常被覆盖。
  3. Jitter:可能根本没有真正的调用,所以没有地方可以寻找参数,因为它们可能在任何地方。
  4. 抖动:“呼叫”可能会重新使用与上一个相同的帧。
  5. 编译器:IL 可以为本地人重用插槽。
  6. 人:程序员可以改变参数值。

从所有这些来看,到底怎么可能知道是什么arg1

现在,添加垃圾收集的存在。想象一下,如果我们能神奇地知道arg1无论如何,尽管如此。如果它是对堆上对象的引用,它可能仍然对我们没有好处,因为如果以上所有都意味着堆栈上没有更多活动引用 - 应该很清楚,这肯定会发生 -并且 GC 启动,然后对象可能已被收集。所以我们能神奇地得到的只是对不再存在的东西的引用——实际上很可能是堆中的一个区域现在被用于其他东西,砰地一声,整个框架的整个类型安全!

它与获得 IL 的反射没有丝毫可比性,因为:

  1. IL 是静态的,而不仅仅是给定时间点的状态。同样,我们可以从图书馆获得我们最喜欢的书籍的副本,这比我们在第一次阅读时获得我们的反应要容易得多。
  2. IL 无论如何都没有反映内联等的影响。如果每次实际使用调用时都内联,然后我们使用反射来获取MethodBody该方法的一个,那么它通常内联的事实是无关紧要的。

其他答案中关于分析、AOP 和拦截的建议与您将获得的一样接近。

*实际上,this是实例成员的真正第一个参数。让我们假装一切都是静态的,所以我们不必一直指出这一点。

于 2012-08-24T21:04:25.727 回答
3

这在.net 中是不可能的。在运行时,JITter 可能决定使用 CPU 寄存器而不是堆栈来存储方法参数,甚至重写堆栈中的初始(传递)值。因此,.net 允许在源代码中的任何位置记录参数将非常耗费性能。

据我所知,您通常可以做到的唯一方法是使用 .net CLR 分析 API。(例如 Typemock 框架能够做这样的事情,它使用 CLR 分析 API)

如果您只需要拦截虚拟函数/属性(包括接口方法/属性)调用,您可以使用任何拦截框架(例如 Unity 或 Castle)。

有一些关于 .net 分析 API 的信息:

MSDN 杂志

MSDN 博客

布赖恩·朗的博客

于 2012-08-24T21:08:41.633 回答
1

如果没有类型模拟或一些 ICorDebug 魔法,可能不会发生。甚至StackFrame类也只列出了允许您获取有关源信息的成员,而不是参数。

但是,您所追求的功能作为带有方法日志记录的 IntelliTrace 存在。您可以过滤您需要查看的内容。

于 2012-08-24T21:17:54.997 回答
1

这在 C# 中是不可能的,您应该使用 AOP 方法并在调用每个方法时执行方法参数记录。通过这种方式,您可以集中您的日志记录代码,使其可重用,然后您只需要标记哪些方法需要参数日志记录。

我相信这可以使用PostSharp之类的 AOP 框架轻松实现。

于 2012-08-24T21:09:57.437 回答