【问题标题】:Windows: avoid pushing full x86 context on stackWindows:避免在堆栈上推送完整的 x86 上下文
【发布时间】:2010-11-02 22:23:58
【问题描述】:

我已经实现了PARLANSE,这是一种在 MS Windows 下使用仙人掌堆栈来实现并行程序的语言。堆栈块在每个函数上分配 基础并且是只是处理局部变量的正确大小, 表达式临时推送/弹出,以及对库的调用(包括 库例程工作的堆栈空间)。这样的堆栈 实际上,帧可以小至 32 字节,而且通常如此。

这一切都很好,除非代码做了一些愚蠢的事情并且 导致硬件陷阱...此时 Windows 似乎 坚持将整个 x86 机器上下文“推入堆栈”。 如果包含 FP/MMX/等,这大​​约是 500+ 字节。寄存器, 它确实如此。自然地,在 32 字节堆栈上推送 500 字节 粉碎不应该的东西。 (硬件推几句 在一个陷阱上,但不是整个上下文)。

[编辑 2012 年 11 月 27 日:见this for measured details on the rediculous amount of stack Windows actually pushes]

我可以让 Windows 存储异常上下文块吗 其他地方(例如,到特定于线程的位置)? 然后软件可以接受异常 命中线程并处理它而不会溢出我的 小的堆栈帧。

我不认为这是可能的,但我想我会问一个更大的问题 观众。是否有操作系统标准调用/接口 会导致这种情况发生吗?

如果我可以让 MS 让我的 进程可选地定义一个上下文存储位置,“contextp”,它 默认情况下初始化为启用当前的旧行为。 然后替换中断/陷阱向量代码:

  hardwareint:   push  context
                mov   contextp, esp

...与...

  hardwareint:  mov <somereg> contextp
                test <somereg>
                jnz  $2
                push  context
                mov   contextp, esp
                jmp $1 
         $2:    store context @ somereg
         $1:    equ   *

具有保存 somereg 等所需的明显更改。

[我现在要做的是:检查每个函数的生成代码。 如果它有机会产生陷阱(例如,除以零), 或者我们正在调试(可能是错误的指针 deref 等),添加 为 FP 上下文提供足够的堆栈帧空间。堆栈帧 现在最终大小为 ~~ 500-1000 字节,程序不能 recurse as far,这有时是一个真正的问题 我们正在写的应用程序。所以我们有一个可行的解决方案, 但它使调试复杂化]

编辑 8 月 25 日:我已设法将这个故事传达给 Microsoft 内部工程师 谁显然有权找出 MS 中的谁实际上可能 关心。解决方案可能希望渺茫。

编辑 9 月 14 日:MS Kernal Group Architect 听说了这个故事并表示同情。他说 MS 将考虑一种解决方案(如提议的解决方案),但不太可能包含在服务包中。可能需要等待下一个版本的 Windows。 (唉……我可能会变老……)

编辑:2010 年 9 月 13 日(1 年后)。微软没有采取任何行动。我最近的噩梦:在 Windows X64 上运行 32 位进程的陷阱是否会在中断处理程序假装推送 32 位上下文之前将整个 X64 上下文推送到堆栈上?那会更大(整数寄存器的两倍宽,SSE 寄存器的两倍(?))?

编辑:2012 年 2 月 25 日:(1.5 年过去了......)微软方面没有任何反应。我想他们只是不关心我的并行性。我认为这是对社区的伤害; MS在正常情况下使用的“大堆栈模型”限制了通过吃大量VM可以在任何时刻活着的并行计算量。 PARLANSE 模型将让一个应用程序拥有一百万个处于运行/等待状态的实时“颗粒”;这确实发生在我们的一些应用程序中,其中“并行”处理了 1 亿个节点图。 PARLANSE 方案可以使用大约 1Gb 的 RAM 来做到这一点,这非常易于管理。如果您尝试使用 MS 1Mb“大堆栈”,您将需要 10^12 字节的 VM 仅用于堆栈空间,我很确定 Windows 不会让您管理一百万个线程。

编辑:2014 年 4 月 29 日:(4 年过去了)。 我猜 MS 只是没有读懂 SO。 我在 PARLANSE 上做了足够多的工程,所以我们只在调试期间或正在进行 FP 操作时支付大堆栈帧的代价,所以我们已经设法找到非常实用的方法来解决这个问题。 MS继续令人失望;不同版本的 Windows 推送到堆栈上的东西的数量似乎有很大差异,而且超出了对硬件环境的需求。有一些迹象表明,这种可变性的一部分是由于非 MS 产品(例如防病毒)在异常处理链中卡住了它们的鼻子造成的;为什么他们不能从我的地址空间之外做到这一点?无论如何,我们通过简单地为 FP/调试陷阱添加一个大的 slop 因子来处理所有这些,并等待现场中不可避免的 MS 系统超过该数量。

【问题讨论】:

  • 如果你在内存中打了 ntdll.dll,修改只会在当前进程中可见(copy-on-write)。我假设使用的是直接地址,而不是 IAT,但您可以使用 JMP 将处理程序的前几个字节覆盖到您自己的代码并返回到 ring 3。Windows 可能有一些安全措施来防止这种东西,但值得一试。
  • 现在,这是一个想法。您是在建议 IDT 的目标在 ntdll.dll 中,我可以踩到它吗?如何确定 IDT 指向的位置,或者是 ntdll.dll 中已发布的入口点?在哪里可以找到有关 ntdll.dll 结构的更多信息?呼应我刚刚听到的一句话,“这会让我忙一阵子。谢谢”!
  • oops.. 我用过 IDT,我的意思是中断向量或者这些天 x86 架构所称的任何东西。 (我有 x86 手册,所以这是一个修辞声明 :-)
  • 这个怎么样......在可能导致异常的指令之前,您将 xSP 设置为指向一个位置,该位置有足够的空间容纳所有包含 CPU/FPU 状态的堆栈上异常数据,以及什么不并在该指令后恢复 xSP?如果没有例外,开销很小。如果有,您甚至不会注意到开销。
  • @Alex:不错的主意,如果所有中断对于某些代码事件都是纯粹同步的。对于这种语言,我还异步启动和停止线程以确保某种程度的计算公平性。所以有时这种推送可能是由外部引起的。我可能会放弃它以获得更易于管理的堆栈帧。

标签: exception assembly stack-overflow cpu-registers threadcontext


【解决方案1】:

Windows 异常处理称为 SEH。您可以禁用 IIRC,但您使用的语言的运行时可能不喜欢它。

【讨论】:

  • 我知道 SEH,我们将其设置为指向我们的异常陷阱处理程序。如何禁用它,然后硬件陷阱会去哪里?我正在使用的语言的运行时间完全在我的控制之下。大部分并行语言运行时是用 C 实现的,但是当运行此类代码时,软件会巧妙地将堆栈从仙人掌样式堆栈切换到标准的 MS“大”堆栈;如果它解决了我的堆栈溢出问题,我也可以切换异常处理程序。
  • 如果您禁用 SEH,您的应用会在被零除时崩溃。如果你能以某种方式禁用异常,你会期望 CPU 在被零除.....三重故障时做什么?
  • 我没有禁用 SEH,我只是将它设置为指向我的处理程序。当我的处理程序获得控制权时,Windows 已经将完整的堆栈帧推入堆栈。
【解决方案2】:

如果 Windows 使用 x86 硬件来实现其陷阱代码,则需要环 0 访问(通过驱动程序或 API)来更改用于陷阱的门。

x86 的门点概念之一:

  • 在整个寄存器上下文(包括返回地址)被压入当前堆栈(=当前 esp)时调用的中断地址(代码段 + 偏移指针),或
  • 一个任务描述符,它切换到另一个任务(可以看作是硬件支持的线程)。而是将所有相关数据推送到该任务的堆栈 (esp)。

你当然想要后者。我会看看 Wine 是如何实现它的,这可能比问谷歌更有效。

我的猜测是,不幸的是,您需要实现一个驱动程序才能使其在 x86 上运行,并且根据Wikipedia,驱动程序不可能在 IA64 平台上更改它。第二个最佳选择可能是在堆栈中交错空间,以便从陷阱中推送的上下文始终适合?

【讨论】:

  • 我可以看看 Wine,但我不确定我会从 Windows 中学到什么。首先,Wine 在 Linux 下运行;没有具体理由相信它的操作系统调用可以用于 Windows。其次,没有理由相信 Windows 会让我控制硬件中断门或任务描述符。 (但是,奇迹可能会发生,我去看看......你是在告诉我我可以通过标准的 MS API 访问?哪个?还是你建议我构建一个驱动程序并作弊?)
  • 您将完整上下文推送到 int 处理程序的假设是错误的。唯一保证位于堆栈上的是:errorCode(可选)、eip、codesegment 选择器、eflags、esp 和堆栈段选择器(按此顺序)。您无法更改此行为,因为它在 CPU 中是硬连线的
  • 对,硬件必须推送一些上下文。这个适度的数量很好,我总是可以将它包含在我的堆栈帧所需的填充中。有用于存储 FP 上下文的机器指令;仔细完成后,它可以存储在任何足够大的缓冲区中,包括堆栈上。但是硬件并没有将 FP 上下文推送到我的堆栈上。 Windows 似乎正在这样做。从我的角度来看,无论是硬件还是 Windows 都没有关系,如果它被推送并且我的堆栈框架很小。重要的是我能否让 Windows 不推送 FP 上下文。
  • 正如我所说,您可以通过重新实现相应的中断处理程序来更改额外推送的内容,其余的无法更改。当然,windows 需要自己保存完整的上下文,否则用户模式异常处理程序将无法检索线程上下文(并可能对其进行修改并将其应用于下一个线程调度)。
  • 快速评论 -- 虽然 Wine 可以为 Windows 编译(据说),但 IIRC 它完全在用户模式下运行,所以我认为查看它的代码不会有帮助。
【解决方案3】:

基本上,您需要重新实现许多中断处理程序,即将自己挂接到 中断描述符表 (IDT)。 问题是,您还需要重新实现内核模式 -> 用户模式回调(对于 SEH,此回调位于 ntdll.dll 并命名为 KiuserExceptionDispatcher,这会触发所有 SEH 逻辑)。关键是,系统的其余部分依赖于 SEH 以现在的方式工作,并且您的解决方案会破坏事情,因为您在系统范围内执行它。也许你可以检查你在中断时所处的进程。 但是,整体概念容易出错,并且非常严重地影响系统稳定性恕我直言。
这些实际上是类似于 rootkit 的技术。

编辑:
更多细节:您需要重新实现中断处理程序的原因是,异常(例如除以零)本质上是软件中断,并且总是通过 IDT。当异常被抛出时,内核收集上下文并将异常信号返回给用户模式(通过前面提到的 ntdll 中的 KiUserExceptionDispatcher)。此时您需要进行干预,因此您还需要提供一种机制来返回用户模式。 (ntdll 中有一个函数用作内核模式的入口点 - 我不记得名字了,但它是 KiUserACP 的东西.....)

【讨论】:

  • 是的,这很激进。我不确定是否要修补操作系统。
  • 可以,但是没有其他方法可以达到你想要的效果,因为整个异常处理过程都是从内核态触发的。
  • 我希望 MS 足够聪明,能够理解我遇到的问题(毕竟,它们不是为 Windows 的未来提供了基础吗 :-),所以我所拥有的使用正确的 API。听起来没有这样的运气。
  • 那么 IDT 是否仅由用户进程可见/可更改?怎么样?
【解决方案4】:

我的评论框空间用完了...

无论如何,我不确定向量指向哪里,我的评论基于 SDD 的回答并提到“KiUserExceptionDispatcher”...除非进一步搜索 (http://www.nynaeve.net/?p=201),此时看起来可能是太晚了。

SIDT 可以在环 3 中执行...这将显示中断表的内容,您可能能够加载该段并至少读取表的内容。运气好的话,您可以读取(例如)向量 0/除以零的条目,并读取处理程序的内容。

此时我会尝试匹配十六进制字节以将代码与系统文件匹配,但可能有更好的方法来确定代码属于哪个文件(不一定是 DLL,可能是 win32k. sys,或者它可以动态生成,谁知道)。我不知道是否有办法从用户模式转储物理内存布局。

如果一切都失败了,您可以设置内核模式调试器或模拟 Windows (Bochs),您可以在其中直接查看中断表和内存布局。然后您可以一直追踪到 CONTEXT 被推送的那一点,并在此之前寻找机会获得控制权。

【讨论】:

  • 真的 真的不想修补内核代码。我只是想让 MS 让我要求将上下文放入我提供的缓冲区中,而不是将其塞进我当前堆栈的喉咙。
【解决方案5】:

考虑将参数/本地堆栈与实际堆栈分离。使用另一个寄存器(例如 EBP)作为有效的堆栈指针,让基于 ESP 的堆栈按照 Windows 想要的方式保留。

您不能再使用 PUSH/POP。您必须使用 SUB/MOV/MOV/MOV 组合而不是 PUSH。但是,嘿,比修补操作系统要好。

【讨论】:

  • 是的,这在技术上可行。它确实放弃了很多代码密度。我的方案可行,但代价是当周围有浮点操作时堆栈帧太大,和/或当程序可能陷入非法内存引用并且我想提供良好的回溯时。我们目前在两种模式下编译:a) 生产模式,具有最少的堆栈帧(有时小至 32 字节),但除了“program dead @xxx”之外无法从机器陷阱中恢复,以及 b) 调试模式,其中向每个堆栈帧添加了惊人的数量(1500 字节),为 MS 提供了足够的 slop。
  • 我以为你是为了优化速度而牺牲内存。
  • 限制您使用的指令集(尤其是基本的、高度优化的指令,如 push 和 pop)通过模拟多个指令来替换它们的效果,不会让您加快速度。你是对的,我实际上并不介意代码密度,因为我认为处理器非常擅长获取指令。但是我们做出的妥协意味着我们不会牺牲使用指令集任何部分的能力;这只是意味着我们与 MS 粗心的堆栈管理交叉方式。 (我在我的问题中提供了一个真正简单的解决方案,但我怀疑 MS 会不会这样做。)
  • 甚至像 Parallels 这样的知名软件供应商也公开抱怨 MS 不允许它们进入内核。也就是说,您的模型是否允许可恢复的 CPU 级异常?换句话说,内核破坏堆栈空间的代价是什么——只是无法获得良好的故障转储?此外,在 x86_64 上还有一堆额外的寄存器;只是在说'。 :) 另外,实现一个基于寄存器的调用约定——这将大大减少对 PUSH 的需求。
  • 另外,想想这个。对基于 ESP 的有效堆栈的需求源于 x86 处理中断的方式,包括硬件中断。任何高于 ESP 的东西都是公平的游戏,因为任何时候都可能出现中断。当您移动参数并将寄存器保存在人工堆栈上时 - 您不需要堆栈指针始终保持一致。并且可以在编译时计算帧指针的静态偏移量。换句话说,PUSH/POP 的情况并不像真正的堆栈那样紧急,即中断到来的堆栈。
猜你喜欢
  • 2020-11-22
  • 2014-08-04
  • 2011-08-29
  • 2014-03-11
  • 1970-01-01
  • 2012-09-23
  • 2010-11-05
  • 1970-01-01
  • 2014-04-17
相关资源
最近更新 更多