在 MASM 中调用标准库函数

Calling a standard-library-function in MASM

我想以混合 C++/汇编的方式开始使用 MASM。 我目前正在尝试从汇编中的 PROC 调用标准库函数(例如 printf),然后我在 C++ 中调用它。

在我的 cpp 文件中声明 printf 的签名后,我的代码就可以工作了。但是我不明白为什么我必须这样做,以及我是否可以避免那样做。

我的 cpp 文件:

#include <stdio.h>

extern "C" {
    extern int __stdcall foo(int, int);
}

extern int __stdcall printf(const char*, ...); // When I remove this line I get Linker-Error "LNK2019: unresolved external symbol"

int main()
{
    foo(5, 5);
}

我的 asm 文件:

.model flat, stdcall

EXTERN printf :PROC ; declare printf

.data

tstStr db "Mult: %i",0Ah,"Add: %i",0 ; 0Ah is the backslash - escapes are not supported

.code

foo PROC x:DWORD, y:DWORD

mov eax, x
mov ebx, y
add eax, ebx
push eax
mov eax, x
mul ebx
push eax
push OFFSET tstStr
call printf
ret

foo ENDP

END

一些更新

为了回应评论,我尝试重新编写代码以符合 cdecl 调用约定。不幸的是,这并没有解决问题(代码在 extern 声明下运行良好,但没有抛出错误)。

但通过反复试验我发现,extern 似乎强制外部链接,即使不需要关键字,因为外部链接应该是函数声明的 default .

我可以通过在我的 cpp 代码中使用该函数来省略声明(即,如果在源文件中的某处添加一个 printf("[=16=]");,则链接器可以正常工作并且一切正常。

新的(但不是真的更好)cpp 文件:

#include <stdio.h>

extern "C" {
    extern int __cdecl foo(int, int);
}
extern int __cdecl printf(const char*, ...); // omiting the extern results in a linker error

int main()
{
    //printf("[=12=]"); // this would replace the declaration
    foo(5, 5);
    return 0;
}

asm 文件:

.model flat, c

EXTERN printf :PROC

.data

tstStr db "Mult: %i",0Ah,"Add: %i",0Ah,0 ; 0Ah is the backslash - escapes are not supported

.code

foo PROC

push ebp
mov ebp, esp
mov eax, [ebp+8]
mov ebx, [ebp+12]
add eax, ebx
push eax
mov eax, [ebp+8]
mul ebx
push eax
push OFFSET tstStr
call printf
add esp, 12
pop ebp
ret

foo ENDP

END

这确实有点没有意义,不是吗?

链接器通常是非常愚蠢的东西。他们需要被告知目标文件需要 printf。链接器无法从缺少的 printf 符号中找出这一点,这已经够愚蠢了。

当你写extern int __stdcall printf(const char*, ...);时,C++编译器会告诉链接器它需要printf。或者,这是正常的方式,编译器会在您实际调用 printf 时告诉链接器。但是你的 C++ 代码没有调用它!

汇编程序也很笨。您的汇编器显然无法告诉链接器它需要来自 C++ 的 printf

一般的解决方案是不要在汇编中做复杂的事情。这不是汇编的好处。从 C 到程序集的调用通常运行良好,从其他方式调用是有问题的。

我最好的猜测是,这与 Microsoft 从 VS 2015 开始重构 C 库和一些 C 库现在内联(包括 printf)并且实际上不在默认 .lib 个文件。

我的猜测是在这个声明中:

extern int __cdecl printf(const char*, ...);

extern 强制将旧的遗留库包含在 link 过程中。这些库包含非内联函数 printf。如果 C++ 代码不强制 MS linker 包含遗留 C 库,那么 MASM 代码对 printf 的使用将无法解决。

我相信这与 2015 年的这个 Whosebug and 有关。如果您想从 C++ 代码中删除 extern int __cdecl printf(const char*, ...);,您不妨考虑将此行添加到您的 MASM 代码中:

includelib legacy_stdio_definitions.lib

如果您使用 CDECL 调用约定并将 C/C++ 与程序集混合使用,您的 MASM 代码将如下所示:

.model flat, C      ; Default to C language
includelib legacy_stdio_definitions.lib

EXTERN printf :PROC ; declare printf

.data

tstStr db "Mult: %i",0Ah,"Add: %i",0 ; 0Ah is the backslash - escapes are not supported

.code

foo PROC x:DWORD, y:DWORD   
    mov eax, x
    mov ebx, y
    add eax, ebx
    push eax
    mov eax, x
    mul ebx
    push eax
    push OFFSET tstStr
    call printf
    ret
foo ENDP

END

您的 C++ 代码将是:

#include <stdio.h>

extern "C" {
    extern int foo(int, int); /* __cdecl removed since it is the default */
}

int main()
{
    //printf("[=13=]"); // this would replace the declaration
    foo(5, 5);
    return 0;
}

在汇编代码中传递 includelib 行的替代方法是将 legacy_stdio_definitions.lib 添加到 Visual Studio 项目的 linker 选项的依赖项列表中,或者命令行选项,如果您手动调用 linker。


在您的 MASM 代码中调用约定错误

您可以阅读 CDECL calling convention for 32-bit Windows code in the Microsoft documentation as well as this Wiki article。 Microsoft 将 CDECL 调用约定总结为:

On x86 platforms, all arguments are widened to 32 bits when they are passed. Return values are also widened to 32 bits and returned in the EAX register, except for 8-byte structures, which are returned in the EDX:EAX register pair. Larger structures are returned in the EAX register as pointers to hidden return structures. Parameters are pushed onto the stack from right to left. Structures that are not PODs will not be returned in registers.

The compiler generates prologue and epilogue code to save and restore the ESI, EDI, EBX, and EBP registers, if they are used in the function.

最后一段对您的代码很重要。 ESIEDIEBXEBP 寄存器是非易失性的,如果它们被修改,必须由被调用函数保存和恢复。您的代码损坏 EBX,您必须保存并恢复它。您可以通过在 PROC 语句中使用 USES 指令让 MASM 做到这一点:

foo PROC uses EBX x:DWORD, y:DWORD    
    mov eax, x
    mov ebx, y
    add eax, ebx
    push eax
    mov eax, x
    mul ebx
    push eax
    push OFFSET tstStr
    call printf
    add esp, 12               ; Remove the parameters pushed on the stack for
                              ;     the printf call. The stack needs to be
                              ;     properly restored. If not done, the function
                              ;     prologue can't properly restore EBX
                              ;     (and any registers listed by USES)
    ret
foo ENDP

uses EBX 告诉 MASM 生成额外的序言和结尾代码以在开始时保存 EBX 并在开始时恢复 EBX函数执行 ret 指令。生成的指令类似于:

0000                    _foo:
0000  55                        push            ebp
0001  8B EC                     mov             ebp,esp
0003  53                        push            ebx
0004  8B 45 08                  mov             eax,0x8[ebp]
0007  8B 5D 0C                  mov             ebx,0xc[ebp]
000A  03 C3                     add             eax,ebx
000C  50                        push            eax
000D  8B 45 08                  mov             eax,0x8[ebp]
0010  F7 E3                     mul             ebx
0012  50                        push            eax
0013  68 00 00 00 00            push            tstStr
0018  E8 00 00 00 00            call            _printf
001D  83 C4 0C                  add             esp,0x0000000c
0020  5B                        pop             ebx
0021  C9                        leave
0022  C3                        ret