拆卸旧的Delphi 3可执行文件时,我发现一些例程可以在寄存器EAX,EDX和堆栈中传递参数,而不能在ECX中传递参数。合理的价值。可以在使用EAX,EDX和堆栈的小型函数的代码中看到,也可以在“紧密”内部块中调用这样的例程时看到,这些内部函数应尽可能包含函数自变量。 (此版本的Delphi明显早于调用堆栈优化。)

这是相当令人惊讶的,因为根据Delphi的当前所有者(到目前为止,以我个人的经验),Delphi一直使用register />

寄存器约定
在寄存器约定下,最多三个参数在CPU寄存器中传递,其余参数(如果有)在堆栈上传递。参数按声明顺序传递(与pascal约定一样),合格的前三个参数按该顺序传递给EAX,EDX和ECX寄存器。


最初,我在驻留在标准库vcl30.dpl中的一些例程中发现了这一点,因此我认为这是该特定构建的特殊性(也许该库是使用甚至不使用ECX的更旧版本的Delphi创建的)。但是现在我也找到了缺少ECX的用户例程! (在被调用的函数和调用它的函数中,该函数都有许多堆栈参数。)在被调用的函数内部,可能未使用参数,但是编译器不会知道,并且仍会提供该参数。 br />
这弄乱了我的拆卸;不仅我必须在原始函数的原型中提供虚拟参数,而且回溯失败,因为我的代码找不到对ECX的赋值,因此它假定被调用函数仅使用前两个参数。
似乎违反了严格的register调用约定。有没有使用其他2个寄存器但不使用ECX的调用约定?


示例–在调用库函数之前,使用和破坏ECX的片段: br />
8D4DFC          lea    ecx, [ebp+local_4]
33D2            xor    edx, edx
8BC6            mov    eax, esi
8B18            mov    ebx, [eax]
FF5350          call   [ebx+50h]  <- GetSaveFileName; this uses ECX as a proper argument
A144831041      mov    eax, [lpEnginePtr]
FF702C          push   [eax+2Ch]   <- probably a local path
6870277355      push   (address)"/Saved Games/"
FF75FC          push   [ebp+local_4]
8D45F8          lea    eax, [ebp+local_8]
BA03000000      mov    edx, 3
E869EAFCFF      call   System.@LStrCatN   <- wot no ECX?
8B55F8          mov    edx, [ebp+local_8]
A144831041      mov    eax, [lpEnginePtr]
E860630600      call   Engine.SaveFile
...

我将其反编译为

call GetSaveFileName (esi, 0, addressof (local_4))
eax = lpEnginePtr
push (eax.field_2C)
push ("/Saved Games/")
push (local_4)
call System.@LStrCatN (addressof (local_8), 3)
call Engine.SaveFile (lpEnginePtr, local_8)


例程GetSaveFileName使用ECX而不使用clobbers:

                GetSaveFileName:
53              | push   ebx
8BD9            mov    ebx, ecx     
A140A08F55      mov    eax, lpGameSettings
8B90E4000000    mov    edx, [eax+0E4h]
8BC3            mov    eax, ebx     
B944267355      mov    ecx, (address)".sav"
E856EBFCFF      call   System.@LStrCat3 

                5573263Ah:
5B              | pop    ebx
C3              | retn


库函数System.@LStrCatN确实根本无法读取ECX:



前面已经提到过,在IDA中使用EAX / EDX的调用约定,但根据评论,这是一种误解毕竟使用了ECX。

#1 楼

如果编译器可以证明在其控制下具有给定功能的所有调用站点,则它可以放弃约定并按自己的喜好安排事情。微软的C / C ++编译器已经在链接时代码生成和配置文件引导的优化方面进行了数十年的研究,特别是编译器的内部副本,例如用于编译Visual FoxPro可执行文件的内部副本。当使用IDA分析此类可执行文件时,这不会引起额外的乐趣,因为所有预编程的约定基本上都无法使用。在64位模式下,Windows要求所有非叶子功能(包括在元数据中注册调用帧布局)都必须遵循其ABI,以确保完全堆栈帧可追溯性。这意味着编译器在这里没有太多余地...

鉴于Delphi的工作方式,可以想象的是,编译器可能会对局部函数的参数传递进行类似的调整前提是永远不要将函数的地址带到外部并传递给单元或嵌套函数的实现部分。

与Rad Lexus的注释对话引发了另一个重要方面:系统功能不一定发挥通过与“普通”函数相同的规则,尤其是那些打算由编译器生成的代码隐式调用而不是由用户代码显式调用的函数。编译器可能具有有关这些系统功能的扩展信息,例如损坏的寄存器,异常的参数位置,“ nothrow”,“ noreturn”等。此扩展信息可以在系统单位元数据中,也可以直接硬编码到编译器中。

@LStrCatN是一个特殊的功能,因为它是带有被调用方清除功能的vararg函数(非常不寻常)。在任何情况下,编译器都需要对它进行特殊处理,因为编译器必须将堆栈上的实际指针数量作为函数的参数传递。

评论


合理,但是我发现了一个很好的反例,它是您的“ ...在单元的实现部分本地...”。用户代码调用系统函数,并且知道ECX未被用作参数。

–杂件
16-12-29在20:58

@Rad:@LStrCatN是特殊的,因为它是带有被调用方清除(!)的vararg函数,在任何情况下都需要编译器进行特殊处理(请参阅编译器必须将指针计数作为隐藏参数发出)。很容易看出为什么他们希望所有指针都放在堆栈上,而不是从ECX中提取第一个指针,尽管这并不困难,只需对循环逻辑进行微调。在任何情况下,Turbo Pascal和Delphi中的系统函数都不一定要遵循与普通函数相同的规则,并且编译器可能已扩展了某些信息(硬编码?)。

– DarthGizka
16 Dec 30'8:33



您对那些varargs的看法是正确的,@ LStrCatN结尾的堆栈操作破坏了我的反编译器😜(而且我已经认为不值得花时间尝试解决该问题)。我应该尝试找到一个纯用户代码示例吗?如果找不到,您将获得批准的绿色标记。

–杂件
16 Dec 30'8:40

@Rad:dcc32是什么,我的猜测是您只会发现系统功能/内部异常-这对您的项目很有用,因为System单位是有限的。 :-)但是,扫描数GB的Delphi生成的可执行文件不会有任何伤害...或更准确地说,是要完善自动化的“假设验证程序”测试以包括对参数用法的完整检查(寄存器值的定义)与呼叫者中的“未初始化”用法相比)。如果您自己的Disasm引擎为此需要太多工作,请查看令人惊叹的Capstone

– DarthGizka
16 Dec 30'9:20

我发现了一些情况,但是所有这些情况要么在基类中(稍后将被覆盖),要么在相反的情况下在派生类中(并且至少有一个父母确实使用了寄存器)。我想我必须将异常硬编码到我的反编译器中。谢谢!

–杂件
16-12-31 at 17:08



#2 楼

在您的链接中:


合格的前三个参数按以下顺序在EAX,EDX,
和ECX寄存器中传递


(强调我的)。如果函数有两个参数,则没有第三个参数要传递给ECX,因此在调用之前仅设置了EAXEDX。因此,单参数函数仅使用EAX而不使用EDXECX

评论


但是对于这些神秘的函数,在结尾处加上retn XXX,并且代码本身清楚地表明堆栈上提供了更多参数。因此,我感到困惑。

–杂件
16-12-28 19:26



在这种情况下,需要更多信息;也许尝试在Delphi RTL源代码中找到此功能。

–伊戈尔·斯科钦斯基♦
16 Dec 28'在20:04

我的示例来自用户代码,但是的,我敢肯定我也可以在标准库中找到一些示例。我不认为所有的公开信息都是公开的。到目前为止,我只发现了一些零散的碎片。

–杂件
16年12月28日在20:56