在 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
,而不是整个声明。
我自己确实有点困惑。由于包含<stdio.h>
,因此已经有::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 寄存器,编译器会生成序言和尾声代码以保存和恢复它们。
最后一段对您的代码很重要。 ESI、EDI、EBX 和 EBP 寄存器是非易失性和如果它们被修改,则必须由被调用函数保存和恢复。你的代码破坏了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 中调用标准库函数的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin标准库函数 ① ( apply 标准库函数 | let 标准库函数 )