c语言 stdcall

99ANYc3cd6
预计阅读时长 17 分钟
位置: 首页 C语言 正文

stdcall 是一种函数调用约定,它定义了函数参数如何传递到栈上,以及函数执行完毕后由谁来清理栈,理解调用约定对于编写 DLL、与 Windows API 交互、跨语言编程以及调试都至关重要。


什么是 stdcall

stdcall 是 "Standard Call" 的缩写,是微软的 32 位和 64 位 Windows API 主要使用的调用约定,它的核心规则可以概括为:

  1. 参数传递:所有参数从右向左依次压入栈中。
  2. 栈清理:被调用的函数自己负责在返回前清理栈空间(即 ret 指令会带一个参数,表示要清理的字节数)。

与其他常见调用约定的对比

为了更好地理解 stdcall,我们通常会和另外两种调用约定进行比较:cdeclfastcall

特性 stdcall cdecl fastcall
参数传递顺序 从右向左 从右向左 从左向右(前两个通过寄存器)
谁清理栈 被调用函数 调用者 被调用函数
命名修饰 _functionName@<参数大小> _functionName @functionName@<参数大小>
典型用途 Windows API, Pascal C/C++ 默认 对性能要求高的内部函数

关键区别点:

  • stdcall vs cdecl:最大的区别在于谁清理栈。stdcall 由被调用函数清理,而 cdecl 由调用者清理,这意味着如果一个函数被声明为 cdecl,调用者必须知道这个约定并生成清理栈的代码,如果调用者错误地调用了 stdcall 函数,可能会导致栈不平衡(内存泄漏或程序崩溃)。
  • stdcall vs fastcallfastcall 试图通过使用 CPU 寄存器来传递前两个参数,从而减少栈操作,提高性能,而 stdcall 则统一使用栈传递所有参数。

stdcall 在 C 语言中的使用

在 C 语言中,你可以使用 __stdcall 关键字来指定函数的调用约定。

语法

// 函数定义
返回类型 __stdcall 函数名(参数列表) {
    // 函数体
}
// 函数声明
返回类型 __stdcall 函数名(参数列表);

示例代码

下面是一个简单的例子,展示了 stdcall 函数的定义和调用。

#include <stdio.h>
// 使用 __stdcall 修饰的函数
int __stdcall add(int a, int b) {
    printf("Inside add function. a = %d, b = %d\n", a, b);
    return a + b;
}
int main() {
    int result;
    // 调用 stdcall 函数
    // 在 x86 平台上,调用过程如下:
    // 1. 调用者将参数 b (4字节) 压入栈
    // 2. 调用者将参数 a (4字节) 压入栈
    // 3. 执行 call add 指令
    // 4. add 函数执行
    // 5. add 函数执行 ret 8 指令,自动弹出 8 字节 (a 和 b) 的参数,清理栈
    // 6. 返回到 main 函数
    result = add(10, 20);
    printf("Result from add: %d\n", result);
    return 0;
}

编译和运行: 在支持 __stdcall 的编译器(如 MSVC)中,这段代码可以正常编译和运行,在 GCC/Clang 中,__stdcall 是一个 Microsoft 扩展,但通常也能被识别。


为什么 stdcall 很重要?(核心应用场景)

与 Windows API 交互

这是 stdcall 最重要、最常见的用途,Windows 操作系统提供的所有 DLL(如 kernel32.dll, user32.dll)中的函数都遵循 stdcall 约定。

错误示例: 如果你在 C++ 中声明一个 Windows API 函数时没有使用 stdcall,会导致严重问题。

// 错误的声明!
// Windows API 函数实际上是 stdcall
int MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
// 在 main 函数中调用...
// 如果编译器默认是 cdecl,main 函数在调用 MessageBoxA 后会尝试清理栈。
// 但 MessageBoxA 内部已经清理了,导致栈被清理了两次,程序崩溃。

正确的做法:

// 正确的声明,使用 __stdcall
// 在 Windows 头文件中,这些宏通常已经定义好了
// #define MessageBoxA __MessageBoxA
int __stdcall MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

创建和调用 DLL (Dynamic Link Library)

当你创建一个 DLL 并导出函数供其他程序使用时,你必须明确指定调用约定,DLL 中的函数是 stdcall,那么调用它的 EXE 也必须使用 stdcall 来声明,否则会因栈清理不一致而出错。

DLL 示例 (mylib.c):

// 使用 __declspec(dllexport) 导出函数,并指定 __stdcall
__declspec(dllexport) int __stdcall add(int a, int b) {
    return a + b;
}

调用程序示例 (main.c):

// 使用 __declspec(dllimport) 导入函数,并指定 __stdcall
__declspec(dllimport) int __stdcall add(int a, int b);
int main() {
    int result = add(5, 7);
    printf("DLL result: %d\n", result);
    return 0;
}

命名修饰 (Name Mangling)

C++ 编译器为了支持函数重载,会对函数名进行“修饰”,即添加额外的信息(如参数类型)。stdcall 调用约定也会影响修饰后的名字,这在混合使用 C 和 C++ 或者手动处理 DLL 导入时非常重要。

在 MSVC 中,stdcall 函数的命名修饰规则是: _ + 函数名 + + 参数总大小(以字节为单位)

函数 int __stdcall foo(int a, double b) 的修饰名可能是: @foo@12 (int 是 4 字节,double 是 8 字节,总共 12 字节)

cdecl 版本 int foo(int a, double b) 的修饰名可能是: ?foo@@YAHHN@Z (这是 C++ 的修饰方式,C 语言下会是 _foo)

当你需要使用 LoadLibraryGetProcAddress 在运行时动态加载一个 stdcall 函数时,你必须知道它的修饰名,或者使用 extern "C" 来禁用 C++ 的修饰。


现代视角:64 位 Windows 和 C++

64 位 Windows 的调用约定

在 64 位 Windows 平台上(x64 架构),情况发生了变化,微软定义了一个新的、统一的调用约定,它取代了 stdcall, cdecl, fastcall 等。

  • 参数传递:前四个整型/指针参数通过寄存器 (RCX, RDX, R8, R9) 传递,浮点/向量参数通过 XMM0XMM3 传递,多余的参数才通过栈传递。
  • 栈清理调用者负责清理栈。
  • 命名修饰:函数名不再有 @<size> 这样的后缀。

这意味着,在 64 位代码中,你几乎不需要再使用 __stdcall,Windows API 在 64 位下已经统一使用新的约定,编译器会自动处理,如果你在 64 位代码中硬编码 __stdcall,编译器可能会给出警告,因为它在 x64 架构下是多余的。

C++ 的 extern "C"

在 C++ 中,为了使 C 风格的函数能够被 C 语言编译器正确链接,通常使用 extern "C",这会告诉 C++ 编译器使用 C 语言的链接和命名规则,即不进行函数重载修饰

extern "C" {
    // 这个函数在 C++ 中会被当作 C 函数处理
    // 它的修饰名会是 "add",而不是 "?add@..." 这样的 C++ 名
    // 默认情况下,C 函数使用 C 的默认调用约定,即 cdecl
    int add(int a, int b);
}
// 如果你想同时指定 C 语言链接和 stdcall 调用约定
extern "C" {
    int __stdcall add_std(int a, int b);
}

特性
核心规则 参数从右向左入栈,被调用函数自己清理栈。
主要用途 32位 Windows API、创建 DLL、与旧代码交互。
重要性 在 32 位环境下,调用约定不匹配是导致程序崩溃的常见原因之一。
现代趋势 64 位 Windows 上,stdcall 已被统一的调用约定取代,不再需要。
命名修饰 会改变函数的最终符号名,影响动态链接(GetProcAddress)。

stdcall 是一个与 Windows 平台(尤其是 32 位)紧密相关的历史遗留但依然重要的概念,在编写跨平台代码或现代 64 位应用时,你可能很少会直接使用它,但在维护旧项目、调用 Windows API 或进行底层系统编程时,它是必须掌握的基础知识。

-- 展开阅读全文 --
头像
C语言settimer函数如何正确使用?
« 上一篇 05-01
织梦关联文章副栏目,如何实现?
下一篇 » 05-01

相关文章

取消
微信二维码
支付宝二维码

目录[+]