【问题标题】:Delphi SampleProfiler: How is this code calling into ntdll.dll?Delphi SampleProfiler:这段代码如何调用 ntdll.dll?
【发布时间】:2010-04-12 15:17:39
【问题描述】:

我使用Delphi Sampling Profiler 分析了我的应用程序的一部分。 Like most people,我看到大部分时间都花在了ntdll.dll

注意:我打开了忽略Application.Idle时间的选项,并从System.pas调用。所以 不在ntdll 内,因为 应用程序处于空闲状态:

经过多次运行,多次,大部分时间似乎都在ntdll.dll内部度过,但奇怪的是调用者是谁:

调用者来自虚拟树视图:

PrepareCell(PaintInfo, Window.Left, NodeBitmap.Width);    

注意:应用程序不在ntdll.dll 中,因为 应用程序是空闲的,因为 来电者不是Application.Idle

让我困惑的是,这行本身(即不是inside PrepareCell)是ntdll的调用者。更令人困惑的是:

  • 不仅不是内部的东西PrepareCell()
  • 它甚至不是PrepareCell设置(例如弹出堆栈变量、设置隐式异常帧等)是调用者。这些东西会在分析器中显示为 PrepareCell 内 begin 上的热点。

VirtualTrees.pas:

procedure TBaseVirtualTree.PrepareCell(var PaintInfo: TVTPaintInfo; WindowOrgX, MaxWidth: Integer);
begin
   ...
end;

所以我想弄清楚这条线是怎么回事:

PrepareCell(PaintInfo, Window.Left, NodeBitmap.Width);    

正在呼叫ntdll.dll


唯一的其他方式是三个参数:

  • PaintInfo
  • Window.Left
  • NodeBitmap.Width

其中一个可能是函数或属性获取器,它会调用ntdll。于是我在行上放了一个断点,在运行时查看CPU窗口:

里面有一行可能是罪魁祸首:

call dword ptr [edx+$2c]

但是当我跟随那个跳跃时,它并没有在ntdll.dll结束,而是TBitmap.GetWidth

正如您所见,它不会在任何地方调用;当然不会进入ntdll.dll


那么线路怎么样:

PrepareCell(PaintInfo, Window.Left, NodeBitmap.Width);    

呼叫ntdll.dll


注意:我很清楚它并没有真正调用 ntdll.dll。因此,任何有效的答案都必须包含“Sampling Profiler 具有误导性;该行没有调用 ntdll.dll”。答案还必须要么说大部分时间没有花在 ntdll.dll 中,要么突出显示的行不是调用者。最后,任何答案都必须解释为什么 Sampling Profiler 是错误的,以及如何修复它。

更新 2

什么是ntdll.dll? Ntdll 是 Windows NT 的原生 API 集。 Win32 API 是ntdll.dll 的包装器,看起来像 存在于 Windows 1/2/3/9x 中的 Windows API。为了真正进入 ntdll,你必须调用一个直接或间接使用 ntdll 的函数。

例如,当我的 Delphi 应用程序空闲时,它通过调用 user32.dll 函数等待消息:

WaitMessage;

当你真正看到它的时候:

USER32.WaitMessage
  mov eax,$00001226
  mov edx,$7ffe0300
  call dword ptr [edx]
  ret

调用$7ffe0300 指定的函数是Windows 转换到Ring0 的方式,调用EAX 中指定的FunctionID。在这种情况下,被调用的系统函数是 0x1226。在我的操作系统Windows Vista上,0x1226对应系统函数NtUserWaitMessage

这是进入 ntdll.dll 的方法:你可以调用它。

当我说出原始问题时,我拼命试图避免挥手不回答。通过非常具体,仔细指出我所看到的现实,我试图防止人们忽视事实,并试图使用挥手的论据。


更新三

我转换了两个参数:

PrepareCell(PaintInfo, Window.Left, NodeBitmap.Width);

入栈变量:

_profiler_WindowLeft := Window.Left;
_profiler_NodeBitmapWidth := NodeBitmap.Width;
PrepareCell(PaintInfo, _profiler_WindowLeft, _profiler_NodeBitmapWidth);

确认瓶颈不是调用

  • Windows.Left,或
  • Nodebitmap.Width

Profiler 仍然指示该行

PrepareCell(PaintInfo, _profiler_WindowLeft, _profiler_NodeBitmapWidth);

本身就是瓶颈; 内部 PrepareCell 没有任何东西。这必须意味着它是在调用准备单元格的设置中,或者在 PrepareCell 的开头:

VirtualTrees.pas.15746: PrepareCell(PaintInfo, _profiler_WindowLeft, _profiler_NodeBitmapWidth);
   mov eax,[ebp-$54]
   push eax
   mov edx,esi
   mov ecx,[ebp-$50]
   mov eax,[ebp-$04]
   call TBasevirtualTree.PrepareCell

没有任何东西调用ntdll。现在是 PrepareCell 本身的序言:

VirtualTrees.pas.15746: begin
   push ebp
   mov ebp,esp
   add esp,-$44
   push ebx
   push esi
   push edi
   mov [ebp-$14],ecx
   mov [ebp-$18],edx
   mov [ebp-$1c],eax
   lea esi,[ebp-$1c]
   mov edi,[ebp-$18]

里面没有任何东西调用ntdll.dll


问题仍然存在:

  • 为什么将一个变量压入堆栈而将另外两个变量压入寄存器成为瓶颈?
  • 为什么 PrepareCell 本身不是瓶颈?

【问题讨论】:

  • 并非每个函数调用都以 ntdll.dll 结尾。 CPU 不能只是神奇地跳入 ntdll。 ntdll 是本机 Windows NT API。为了进入 ntdll,你必须实际调用一些调用 ntdll 的东西。 mov ebp,esp 不会调用 ntdll。它只是不是。

标签: delphi delphi-5


【解决方案1】:

嗯,这个问题实际上是我制作自己的采样分析器的主要原因:
http://code.google.com/p/asmprofiler/wiki/AsmProfilerSamplingMode

也许并不完美,但你可以试一试。让我知道你的想法。

顺便说一句,我认为这与几乎所有调用都以调用内核(内存请求、绘制事件等)这一事实有关。只有计算不需要调用内核。 大多数调用以等待内核结果结束:

ntdll.dll!KiFastSystemCallRet

您可以在带有线程堆栈视图的 Process Explorer 中看到这一点,或者在 Delphi 中,或者在我的 AsmProfiler 的“实时视图”中使用 StackWalk64 API:
http://code.google.com/p/asmprofiler/wiki/ProcessStackViewer

【讨论】:

    【解决方案2】:

    那里可能发生了两件事。

    第一个是 SamplingProfiler 通过遍历堆栈来识别调用者,直到它遇到看起来像是从 Delphi 代码到 Delphi 的有效调用点。

    问题是,某些程序可能会一次保留大量堆栈,而无需重新初始化它。这可能会导致误报。那么唯一的线索就是你的误报是最近被调用的。

    第二个ntdll 本地化,这是众所周知的,但是,ntdll 是您在用户空间中的等待点,而作为 user197220,ntdll 是您最终的目标大部分时间都在等待你调用系统的东西并等待结果。

    在您的情况下,除非您降低采样率,否则您会看到 247 毫秒的 CPU 工作时间,如果这 247 个样本是在数秒内实时收集的,这可能会被视为空闲。由于误报指向 VirtualTree 绘制准备,我敢打赌 ntdll 时间实际上是绘制时间(驱动程序或操作系统软件)。 您可以尝试注释掉实际绘制的代码以确保。

    【讨论】:

    • 我真的很想你扩展并澄清这个答案。因为,老实说,我大部分都不懂。 “第二件事是 ntdll 本地化,这是众所周知的,但是,ntdll 是您在 user-sapce 中的等待点,并且作为 user197220,ntdll 是您在调用系统内容的大部分时间最终会等待的地方并等待结果。” “ntdll 本地化”是什么意思?当然,与 Windows 的不同语言环境没有任何关系。 "that is known for certain" 什么是众所周知的,ntdll 本地化,或者那个 ntdll 是我的...
    • 整个“第二件事”段落是一个长句。我尝试对其进行编辑以使其更清楚 - 但我不知道哪个短语与哪个主题有关。你从哪里得到user197220,那个地址是$197220?我看到了 ntdll 中的 247 个样本,但是你是如何达到 247 毫秒的 CPU 时间的呢?这意味着采样需要 1 毫秒的 CPU 时间——这是不对的。
    • 从 Delphi 代码到 Delphi 代码的有效“调用点”是什么样的?您的意思是该过程分配了大量的堆栈空间而没有初始化它(换句话说,留下任何垃圾)?我不知道要重新初始化堆栈变量的过程。
    • 抱歉,这里有几个错别字:在 user197220 之后缺少一个“说”,即上面回答您的人。而不是“本地化”,您应该阅读“位置”。一个有效的调用点是一个堆栈返回地址,它指向一个正确的调用和 Delphi 中的堆栈设置,是的,这是由一个分配大量堆栈并使其保持不变的过程引起的。这可能是一个 Delphi 程序,但也可能在 Windows DLL 中。
    • 采样频率默认为1kHz,每秒1000个样本,统计上,如果你卡在一个电话或一系列电话中,247个样本大致转换为247毫秒(只要有没有共振)。
    猜你喜欢
    • 1970-01-01
    • 2017-01-14
    • 2023-03-27
    • 1970-01-01
    • 2021-09-30
    • 2021-12-02
    • 1970-01-01
    • 2020-09-17
    • 2013-09-16
    相关资源
    最近更新 更多