在 MASM 中调用标准库函数

Posted

技术标签:

【中文标题】在 MASM 中调用标准库函数【英文标题】:Calling a standard-library-function in MASM 【发布时间】:2019-08-06 13:39:32 【问题描述】:

我想以混合 C++/Assembly 的方式开始使用 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

一些更新

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

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

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

新的(但不是更好的)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("\0"); // 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

【问题讨论】:

extern 在函数声明前面可能不会像你想的那样做,因为它没有效果 @KonradRudolph:it has no effect 是一个相当强的声明,因为代码使用该声明进行编译并且没有该声明就无法编译。对于纯(标准)C++ 程序,您的评论可能是正确的,但这是 C++ 和汇编的混合。显然,此编译器、此汇编器和此链接器之间的交互以 extern 必须完成某事的方式进行。 @MSalters 删除 extern 关键字不会改变任何东西(这里是一个红鲱鱼),因为函数声明总是隐含的外部。所以,是的,它没有效果。 @MSalters Konrad 谈论的是extern,而不是整个声明。 我自己确实有点困惑。由于包含&lt;stdio.h&gt;,因此已经有::std::printf::printf 的声明,视情况而定。不过弄明白确实是件好事。 @AnonymousAnonymous,如果您删除 just extern,它仍然有效? 【参考方案1】:

我最好的猜测是,这与 Microsoft 从 VS 2015 开始重构 C 库并且现在内联某些 C 库(包括 printf)并且实际上不在默认 @987654327 中的事实有关@ 文件。

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

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

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

我相信这与 2015 年的 *** question 和 my answer 有关。如果您想从 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("\0"); // this would replace the declaration
    foo(5, 5);
    return 0;

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


MASM 代码中的调用约定错误

您可以在 Microsoft 文档以及此 Wiki article 中阅读有关 CDECL calling convention for 32-bit Windows 代码的信息。 Microsoft 将 CDECL 调用约定总结为:

在 x86 平台上,所有参数在传递时都扩展为 32 位。返回值也扩展为 32 位并在 EAX 寄存器中返回,但 8 字节结构除外,它们在 EDX:EAX 寄存器对中返回。较大的结构在 EAX 寄存器中作为指向隐藏返回结构的指针返回。参数从右到左压入堆栈。不是 POD 的结构不会在寄存器中返回。

如果函数中使用了 ESI、EDI、EBX 和 EBP 寄存器,编译器会生成序言和尾声代码以保存和恢复它们。

最后一段对您的代码很重要。 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 并在函数执行 ret 指令时恢复 EBX。生成的指令如下所示:

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

【讨论】:

非常好的答案,谢谢!我不知道必须恢复 ebx - 我只是希望这将在 cdecl 的文档中说明,而不是在其他页面上,列出所有调用约定...我的意思是,来吧 Microsoft :)【参考方案2】:

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

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

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

汇编程序也很愚蠢。您的汇编器显然没有告诉链接器它需要来自 C++ 的 printf

一般的解决方案是不要在汇编中做复杂的事情。这不是组装的好处。从 C 到汇编的调用通常运行良好,但以其他方式调用会出现问题。

【讨论】:

不是我的反对意见,而且我已经很久没有使用 MSVC 了,但是源代码中的声明真的告诉链接器任何事情吗?在 GCC/clang/... 上这是错误的,因为符号声明和库链接大多是正交的;尤其是 C++ 源文件中的 printf 声明将无效 一方面__stdcall printf 不存在于任何库中,编译器应该警告变量函数参数不能与__stdcall 一起使用。虽然在 x64 上这实际上并没有改变任何调用约定,但它确实改变了导出的名称仍然 AFAIK

以上是关于在 MASM 中调用标准库函数的主要内容,如果未能解决你的问题,请参考以下文章

标准C库函数和系统调用的关系

编程中系统允许对库函数重新定义吗?

Kotlin标准库函数 ① ( apply 标准库函数 | let 标准库函数 )

Kotlin标准库函数 ④ ( takeIf 标准库函数 | takeUnless 标准库函数 )

linux内核系统调用和标准C库函数的关系分析

minGW可以在win下调用linux库函数吗?默认情况下