注册调用约定:写在石头上,还是泥里?

逆向工程 调用约定 德尔福 登记
2021-06-09 07:06:16

在反汇编旧的 Delphi 3 可执行文件时,我发现一些例程在寄存器 EAX、EDX 和堆栈中传递参数——但在 ECX 中没有!

对于这些例程,ECX 永远不会设置为“合理”值。这可以的小功能,这里面的代码可以看出使用EAX,EDX,和堆栈,并且当这样的程序被称为“紧”内部块内,这应该是含自尽可能函数参数去. (这个版本的 Delphi 明显早于调用堆栈优化。)

这非常令人惊讶,因为根据Delphi 目前的所有者(以及到目前为止,根据我自己的经验),Delphi 一直使用register

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

最初,我vcl30.dpl在标准库中的一些例程中发现了这一点,因此我认为这是该特定构建的一个特性(也许该库是使用不使用 ECX 的更旧版本的 Delphi 创建的)。但现在我也发现缺少 ECX 的用户例程!(在被调用函数和调用它时,该函数都有许多堆栈参数。)在被调用函数中,一个参数可能未被使用,但编译器不会知道这一点,它仍然会提供该参数。

这搞砸了我的拆卸;不仅我必须在原始函数的原型中提供一个虚拟参数,而且回溯也会失败,因为我的代码找不到对 ECX 的赋值,因此它假定被调用的函数只使用前 2 个参数。

这似乎违反了严格的register调用约定。是否有使用其他 2 个寄存器但不使用ECX的调用约定


示例 – 在调用库函数之前使用和破坏 ECX 的片段:

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 并破坏它,但不保存它:

                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:

System.@LStrCatN:
    push   ebx
    push   esi
    push   edx
    push   eax         <-- not in the Save List
    mov    ebx, edx
    xor    eax, eax
    mov    ecx, [esp+4*edx+10h]  <-- overwrite ECX!
    test   ecx, ecx
    jz     41304AA7h

41304AA4h:
    add    eax, [ecx-4]

41304AA7h:
    dec    edx
    jnz    41304A9Ch

41304AAAh:
    call   System.@NewAnsiString
    ...

其他覆盖 ECX(写而不读)的例程确实将 ECX 保存在序言中。


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

2个回答

如果编译器可以证明它控制了给定函数的所有调用点,那么它可以丢弃约定并根据自己的喜好安排事情。几十年来,Microsoft 的 C/C++ 编译器在链接时代码生成和配置文件引导优化方面一直在这样做,尤其是编译器的内部副本,例如用于编译 Visual FoxPro 可执行文件的编译器。在使用 IDA 分析此类可执行文件时,这会带来无穷无尽的乐趣,因为所有预编程的约定基本上都在窗口之外。

不过,这仅适用于 32 位模式。在 64 位模式下,Windows 要求所有非叶函数(包括在元数据中注册调用框架布局)都遵守其 ABI,以确保完整的堆栈框架可追溯性。这意味着编译器在这里没有很大的余地......

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

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

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

从您的链接:

符合条件前三个参数按顺序在 EAX、EDX 和 ECX 寄存器中传递

(强调我的)。

如果函数有两个参数,则没有第三个参数可以传入ECX,因此只有在调用之前设置了EAXEDX因此,单参数函数只使用EAXand not EDXor ECX