对 Windows 内核模式驱动程序中的调用约定感到困惑

逆向工程 视窗 司机 调用约定
2021-06-21 17:12:24

对内核模式驱动程序进行逆向工程(在其 32 位 x86 版本中)我偶然发现了一个似乎很奇怪的调用约定对于我希望看到的驱动程序__cdecl__fastcall并且__stdcall具有 Microsoft 风格。由于这个驱动程序显然使用它自己的 C++ 运行时,我也希望看到__thiscall

但是,在这个驱动程序中,我看到在eax. 这是完全出乎意料的,所以我想知道这里是否有人知道会发生什么?

sub_40XXXX      proc near
    push    ebx
    push    esi
    mov     esi, eax
    push    edi
    xor     edi, edi

帧指针遗漏似乎不是我所看到的可信原因。这是 LTCG 的坏事吗?


有问题的驱动程序是一个文件系统微型过滤器驱动程序,从它的外观来看,我猜它是由 Windows 7 WDK 中的链接器链接的(尽管我不能真正说出哪个确切版本,例如7600.16385.1或另一个)。链接器版本状态9.00在 PE 可选标头中。子系统版本是 5.00,这表明它是为 Windows 2000 构建的。它还表明子系统是明确通过的,因为 Vista WDK 是最后一个支持以 Windows 2000 为目标的。当然这也可以使用 VS 构建2008 年,很难说(独立的 WDK 曾经包含与 VS 相同的优化编译器)。据我所知,它也可能是来自不同工具链的编译器和链接器的混合。但是考虑到链接器需要了解调用约定,我仍然希望看到一些标准的调用约定。

以下是相关驱动程序的线索(dumpbin /headers ...输出的精简版本):

        9.00 linker version
        1000 section alignment
         200 file alignment
        5.00 operating system version
        0.00 image version
        5.00 subsystem version

驱动程序的标准调用约定是__stdcall. 但是为了验证它__fastcall确实eax不像我在另一个驱动程序中看到的那样使用,我决定创建一个小驱动程序。为了防止编译器或链接器优化我的函数调用,我弄乱了一些随后传递给IoCreateSymbolicLink.

两个函数各自的C++代码是:

UINT_PTR __fastcall fastcall_test(UINT_PTR arg1, UINT_PTR arg2)
{
    UINT_PTR ret = arg1 + arg2;
    DbgPrint("%u, %u -> %u", arg1, arg2, ret);
    return ret;
}

UINT_PTR __stdcall stdcall_test(UINT_PTR arg1, UINT_PTR arg2)
{
    UINT_PTR ret = arg1 + arg2;
    DbgPrint("%u, %u -> %u", arg1, arg2, ret);
    return ret;
}

他们被称为:

UINT_PTR fct = fastcall_test((UINT_PTR)status, RegistryPath->MaximumLength);
UINT_PTR sct = stdcall_test((UINT_PTR)status, RegistryPath->MaximumLength);

usSymlinkName.Buffer += fct;
usSymlinkName.Length += (USHORT)fct;
usSymlinkName.Buffer += sct;
usSymlinkName.MaximumLength += (USHORT)(sct + fct);

来自使用 DDKWizard 生成的默认项目DriverEntry之间IoCreateDeviceIoCreateSymbolicLink中。

编译此目标 Windows XP 时,结果如下:

.text:00010512 unsigned int __fastcall fastcall_test(unsigned int, unsigned int) proc near
.text:00010512                                         ; CODE XREF: DriverEntry(x,x)+41p
.text:00010512 arg1 = ecx
.text:00010512 arg2 = edx
.text:00010512                 mov     edi, edi
.text:00010514                 push    esi
.text:00010515                 lea     esi, [arg1+arg2]
.text:00010518                 push    esi
.text:00010519                 push    arg2
.text:0001051A                 push    arg1
.text:0001051B                 push    offset Format   ; "%u, %u -> %u"
.text:00010520                 call    _DbgPrint
.text:00010525                 add     esp, 10h
.text:00010528                 mov     eax, esi
.text:0001052A                 pop     esi
.text:0001052B                 retn
.text:0001052B unsigned int __fastcall fastcall_test(unsigned int, unsigned int) endp
.text:00010532 unsigned int __stdcall stdcall_test(unsigned int, unsigned int) proc near
.text:00010532                                         ; CODE XREF: DriverEntry(x,x)+50p
.text:00010532
.text:00010532 arg1            = dword ptr  8
.text:00010532 arg2            = dword ptr  0Ch
.text:00010532
.text:00010532                 mov     edi, edi
.text:00010534                 push    ebp
.text:00010535                 mov     ebp, esp
.text:00010537                 mov     eax, [ebp+arg1]
.text:0001053A                 mov     ecx, [ebp+arg2]
.text:0001053D                 push    esi
.text:0001053E                 lea     esi, [eax+ecx]
.text:00010541                 push    esi
.text:00010542                 push    ecx
.text:00010543                 push    eax
.text:00010544                 push    offset Format   ; "%u, %u -> %u"
.text:00010549                 call    _DbgPrint
.text:0001054E                 add     esp, 10h
.text:00010551                 mov     eax, esi
.text:00010553                 pop     esi
.text:00010554                 pop     ebp
.text:00010555                 retn    8
.text:00010555 unsigned int __stdcall stdcall_test(unsigned int, unsigned int) endp

正如预期的那样,__fastcall最终通过ecx传递参数edx那么我的另一个司机怎么了?


同时,我发现了如何使用 vanilla Windows 7 WDK 实现 PE 标头值。这将产生一个与 Windows 2000 兼容的二进制文件,假设您为 x86 构建,并包含这些确切的值(当然,假设您不做愚蠢的事情,例如静态导入仅Windows 2000之后可用的 DDI )。

sources指定...

USE_MAKEFILE_INC=1
SUBSYSTEM_VERSION=$(SUBSYSTEM_500)
# Alternatively:
#SUBSYSTEM_VERSION=5.00

这将迫使nmake包括makefile.inc从相同的位置sources文件,并设置SUBSYSTEM_VERSION正确(对于AMD64 5.00 x86和5.02)。

然后在makefile.inc覆盖

LINKER_APP_VERSION=0.00
LINKER_OS_VERSION=$(SUBSYSTEM_VERSION)

之所以有效,是因为它makefile.inc很晚才被包含在内makefile.new,因此我们可以使用它来覆盖默认构建环境指定的默认值。

2个回答

最有可能的只是 LTCG/LTO,尤其是如果所讨论的函数只是从不从外部调用。使用它可能会导致以下结果

  • 跨模块内联

  • 过程间寄存器分配(仅限 64 位操作系统)

  • 自定义调用约定(仅限 x86)

  • 小 TLS 位移(仅限 x86)

  • 堆栈双对齐(仅限 x86)

  • 改进的内存消歧(更好的全局变量和输入参数的干扰信息)

许多非 Microsoft 编译器在EAX.

来自https://en.wikipedia.org/wiki/X86_calling_conventions

选择链接

三个词法第一(最左边)参数在EAX、 EDX 和 ECX中传递...

...

Borland 寄存器

从左到右评估参数,它通过EAX, EDX , ECX传递三个参数

...

Watcom注册

最多 4 个寄存器按eax、 edx 、 ebx 、 ecx的顺序分配给参数

...

TopSpeed / 歌乐 / JPI

前四个整数参数在寄存器eax、 ebx 、 ecx 和 edx中传递