返回介绍

20.5 其他调用约定

发布于 2024-10-11 21:05:48 字数 2572 浏览 0 评论 0 收藏 0

在第 6 章中,我们讨论了 C 和 C++ 代码常用的许多调用约定。虽然在连接两个已编译模块时,我们必须遵循一个已发布的调用约定,但是在单独一个模块中,函数使用自定义调用约定并不会受到任何限制。高度优化的函数常常使用自定义调用约定,不过,这些函数不能从它们所在的模块之外调用。

下面的代码是一个使用非标准调用约定的函数的前 4 行:

    .text:000158AC sub_158AC       proc near  
    .text:000158AC  
➊  .text:000158AC arg_0           = dword ptr  4  
    .text:000158AC  
    .text:000158AC                 push    [esp+arg_0]  
    .text:000158B0              ➋ mov     edx, [eax+118h]  
    .text:000158B6                 push    eax  
    .text:000158B7              ➌ movzx   ecx, cl  
    .text:000158BA                 mov     cl, [edx+ecx+0A0h]

根据 IDA 的分析,该函数的栈帧中只有一个参数(➊)存在。但是,经过仔细分析,我们发现,这个函数使用了 EAX 寄存器(➋)和 CL 寄存器(➌),但没有进行任何初始化。据此,我们得出的唯一结论是,EAX 和 CL 寄存器应由调用方初始化。因此,我们应把这个函数看成是一个包含 3 个参数的函数,而不是仅包含一个参数的函数。在调用它时,你必须特别小心,以确保它的 3 个参数都处在正确的位置。

通过设置函数的“类型”,IDA 能够指定任何函数的自定义调用约定。通过 Edit ▶ Functions ▶Set function type (编辑▶函数▶设置函数类型)菜单项输入函数的原型并使用 IDA 的_usercall 调用约定,即可做到这一点。用于为上一个示例中的 sub_158AC 设置类型的对话框如图 20-1 所示。

图 20-1 将函数指定为 _usercall

为清楚起见,下面再次提供了函数声明:

int __usercall sub_158AC(struc_1 *, unsigned __int8 index, int)

其中,IDA 关键字 _usercall 用于替代 _cdecl_stdcall 等标准调用约定。要使用 _usercall ,我们需要将保存函数的返回值的寄存器名称附加到函数名称之前(此例中得到 sub_158AC<eax> ),告知 IDA 该寄存器的名称。如果函数不返回值,则可以省略返回寄存器。在参数列表中,还必须在将对应的寄存器名称附加到参数的数据类型之前,为每个基于寄存器的参数提供注释。设置函数的类型后,IDA 将向实施调用的函数传播参数信息,清楚说明函数调用顺序,如下面的代码所示:

.text:00014B9F        ➊  lea     eax, [ebp+var_218] ; struc_1 *  
.text:00014BA5        ➋  mov     cl, 1           ; index  
.text:00014BA7        ➌  push    edx             ; int  
.text:00014BA8            call    sub_158AC

从上述代码中可以明显看出,IDA 识别出:EAX 将保存函数的第一个参数(➊),CL 将保存第二个参数(➋),第三个参数将位于栈上(➌)。

即使在一个可执行文件中,调用约定也可能截然不同。为说明这一点,我们提供了另一个取自同一个二进制文件的、使用自定义调用约定的示例,如下所示:

    .text:0001669E sub_1669E       proc near  
    .text:0001669E  
➊  .text:0001669E arg_0           = byte ptr  4  
    .text:0001669E  
    .text:0001669E              ➋ mov     eax, [esi+18h]  
    .text:000166A1                add     eax, 684h  
    .text:000166A6                cmp     [esp+arg_0], 0

在这个例子中,IDA 同样指出,函数仅访问了栈帧中的一个参数(➊)。仔细分析代码后会发现,在调用这个函数之前,还应对 ESI 寄存器(➋)进行初始化。这个例子说明,即使对同一个二进制文件而言,保存与寄存器有关的参数的寄存器也可能因函数而异。

通过这些例子,我们得到的教训是:你必须了解一个函数如何初始化它所使用的每一个寄存器。如果一个函数在初始化一个寄存器之前使用了该寄存器,那么这个寄存器是用来传递一个参数的。请参阅第 6 章,了解各种编译器和调用约定使用哪些寄存器。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文