形参取&+那像这种printf(“%d,%d“,a)+C中可变参数函数实现原理 +_stdcall,_cdecl,_fastcall

本文深入探讨C语言中可变参数函数的实现原理,包括函数调用的栈结构,参数传递规则,以及__stdcall、__cdecl、__fastcall等调用约定的详细解释。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

形参取&

#include <stdio.h>
void main(int argc, char** argv)
{
	char* p = (char*)"abcdef";
	printf("%d,%d", (int)&argc, (int)&argv);
}
  • 15727976,15727980
  • 果然是argv的位置高啊

那像这种printf("%d,%d",a)

  • 他报错吗
  • 会的
  • 编译的时候发现你少了一个参数啊哈哈哈哈

C中可变参数函数实现原理

C函数调用的栈结构

  • 可变参数函数的实现与函数调用的栈结构相关,
  • 正常C函数参数入栈规则为__stdcall

这个他妈的是不支持变长的啊,C语言默认是__cdecl啊!

void fun(int a, int b, int c)
{
    int d;
    ...
}
0x1ffc-->d
0x2000-->a
0x2004-->b
0x2008-->c

  • 32位系统的多数编译器,栈单元大小是sizeof(int),
  • 每个参数都至少占一个栈单元大小
  • void fun1(char a, int b, double c, short d)
    • 32的系统其栈的结构就是
      0x1ffc–>a (4字节)(为了字对齐)
      0x2000–>b (4字节)
      0x2004–>c (8字节)
      0x200c–>d (4字节)
  • 所有参数是存储在线性连续的栈空间中
  • 就可从可变参数函数中必须有的第一个普通参数来寻址
    • 后续的所有可变参数的类型及其值

void fixed_args_func(int a, double b, char *c)
{
    printf("a = 0x%p\n", &a);
    printf("b = 0x%p\n", &b);
    printf("c = 0x%p\n", &c);
}
  • 对固定的
  • 每个参数名称、类型直接可见
  • 他们的地址也都是可直接得到的,
  • &a得到a地址,并通过函数原型声明了解到a是int类型

  • 对变长参数的函数
  • 按C标准,支持变长参数的函数在原型声明中,
    • 须有至少一个最左固定参数(传统C允许不带任何固定参数的纯变长参数函数)
  • 这样可得到其中固定参数的地址,
    • 但依然无法从声明中得到其他变长参数的地址
void var_args_func(const char * fmt, ...) 
{
    ... ... 
}
  • 只能得到fmt这固定参数的地址,仅从函数原型我们是无法确定"…"中有几个参数、参数都是什么类型
  • 无论"…"中有多少个参数、每个参数是什么类型
  • 它们都和固定参数的传参过程是一样
  • 都是栈操作
  • 一旦知道某函数帧的栈上的一个固定参数的位置
    • 可推导出其他变长参数的位置

int main() 
{
    fixed_args_func(17, 5.40, "hello world");
    return 0;
}

a = 0x0022FF50
b = 0x0022FF54
c = 0x0022FF5C

  • 参数从右到左,逐一压入栈中
  • 先入栈的参数,地理位置最高
 c.addr = b.addr + x_sizeof(b);  /*注意:  x_sizeof !=sizeof */
 b.addr = a.addr + x_sizeof(a);

  • 有了以上"等式",
  • 可推导出var_args_func可变参数位置
  • 起码第一个可变参数的位置应该是:
  • first_vararg.addr = fmt.addr + x_sizeof(fmt);
  • 根据这一结论试着实现一个支持可变参数的函数:
#include <stdarg.h>
#include <stdio.h>

void var_args_func(const char * fmt, ...) 
{
    char    *ap;

    ap = ((char*)&fmt) + sizeof(fmt);
    printf("%d\n", *(int*)ap);  
        
    ap =  ap + sizeof(int);
    printf("%d\n", *(int*)ap);

    ap =  ap + sizeof(int);
    printf("%s\n", *((char**)ap));
}

int main()
{
    var_args_func("%d %d %s\n", 4, 5, "hello world");
    return 0;
}

未完待续

canci

_stdcall、cdecl、fastcall理解

_stdcall

  • int function(int a,int b)
  • 调用时只用result = function(1,2)就可使用这个函数
  • 当高级语言被编译成机器码时
    • 没办法知道一个函数调用需多少个、啥样的参数,也没有硬件可保存这些参数。
    • 计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。
  • 为此,计算机提供了栈来支持参数传递

栈有一个存储区、一个栈顶指针。

  • 函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在栈中取得数据,并进行计算。
  • 函数结束后,或者调用者、或者函数本身修改栈,使堆栈恢复原装。

在参数传递中,有两个很重要的问题必须得到明确:

  • 当参数个数多于一,按什么顺序把参数压栈
  • 函数调用后,谁来把堆栈恢复原装。
  • 高级语言通过函数调用约定来说明这两个问题。
  • 常见的调用约定:
    stdcall,cdecl,fastcall,thiscall,naked call

stdcall

  • stdcall称pascal调用约定,pascal是早期的程序设计语言,使用的函数调用约定就是stdcall
  • 在Microsoft C++系列的C/C++编译器中,用PASCAL宏来声明这个调用约定,类似的宏WINAPI和CALLBACK。
  • stdcall语法
    int __stdcall function(int a,int b)
    • 意味:1)从右向左,2)函数自身修改 3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。
  • function(1,2)调用处翻译成汇编语言:
  • push 2
  • push 1
  • call function 此时自动把cs:eip入栈
  • 函数翻译为:
  • push ebp 保存ebp,该寄存器将用来保存栈顶,可在函数退出时恢复
  • mov ebp,esp 保存堆栈指针
  • mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
    add eax,[ebp + 0CH] ebp + 12处保存了b
    mov esp,ebp 恢复esp
    pop ebp
    ret 8
  • 编译时函数名为_function@8
  • 不同编译器会插入自己的汇编代码以提供编译的通用性,但大体代码如此。
    • 在函数开始处保留esp到ebp,结束时恢复
  • 2和1依次被push进栈,
    • 函数通过相对于ebp(即刚进函数时的堆栈指针)的偏移量存取参数
  • ret 8:清理8字节的堆栈,函数自己恢复堆栈。

cdecl

  • C调用约定,是C缺省的调用约定
  • int function (int a ,int b) //不加修饰就是C调用约定
  • int __cdecl function(int a,int b)//明确指出C调用约定
  • 顺序和stdcall一样,从右向左
    • 调用者负责清理。
    • 允许函数的参数的个数不固定,是C特色。
  • 前面的function,用cdecl后的汇编码变成:调用处
push 1
push 2
call function
add esp,8 调用者在恢复堆栈
  • 被调用函数_function处
push ebp //保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复
mov ebp,esp //保存堆栈指针
mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b
mov esp,ebp 恢复esp
pop ebp
ret 注意,这里没修改堆栈
  • MSDN中说,该修饰自动在函数名前加前导的下划线,
  • 因此函数名在符号表中被记录为_function,
  • 但我编译时没有看到啊。
  • 参数从右向左,因此最开始的参数在最接近栈顶的位置,因此当采用不定个数参数时,第一个参数在栈中的位置肯定知道
  • 只要不定的参数个数能够根据第一个后者后续的明确的参数确定下来,
  • 就可用不定参数,
  • CRT中的sprintf函数,定义为:
  • int sprintf(char* buffer,const char* format,…)
  • 由于所有的不定参数都可通过format确定,因此用不定个数的参数没有问题。

fastcall

  • 它意味:
  • 一和二DWORD参数(或尺寸更小的)通过ecx和edx传递,
    • 其他参数通过从右向左压
  • 被调用函数清理堆栈
    • 和谁类似
  • int fastcall function(int a,int b)

thiscall

thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成
员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理,th
iscall意味着:
参数从右向左入栈
如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针
在所有参数压栈后被压入堆栈。
对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈
为了说明这个调用约定,定义如下类和使用代码:
class A
{
public:
int function1(int a,int b);
int function2(int a,…);
};
int A::function1 (int a,int b)
{
return a+b;
}
#i nclude
int A::function2(int a,…)
{
va_list ap;
va_start(ap,a);
int i;
int result = 0;
for(i = 0 i < a i ++)
{
result += va_arg(ap,int);
}
return result;
}
void callee()
{
A a;
a.function1 (1,2);
a.function2(3,1,2,3);
}
callee函数被翻译成汇编后就变成:
//函数function1调用
0401C1D push 2
00401C1F push 1
00401C21 lea ecx,[ebp-8]
00401C24 call function1 注意,这里this没有被入栈
//函数function2调用
00401C29 push 3
00401C2B push 2
00401C2D push 1
00401C2F push 3
00401C31 lea eax,[ebp-8] 这里引入this指针
00401C34 push eax
00401C35 call function2
00401C3A add esp,14h
可见,对于参数个数固定情况下,它类似于stdcall,不定时则类似cdecl
naked call
这是一个很少见的调用约定,一般程序设计者建议不要使用。编译器不会给这种函数增加初始化和清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果。这一般用于实模式驱动程序设计,假设定义一个求和的加法程序,可以定义为:
__declspec(naked) int add(int a,int b)
{
__asm mov eax,a
__asm add eax,b
__asm ret
}
注意,这个函数没有显式的return返回值,返回通过修改eax寄存器实现,而且连退出函数的ret指令都必须显式插入。上面代码被翻译成汇编以后变成:
mov eax,[ebp+8]
add eax,[ebp+12]
ret 8
注意这个修饰是和__stdcall及cdecl结合使用的,前面是它和cdecl结合使用的代码,对于和stdcall结合的代码,则变成:
__declspec(naked) int __stdcall function(int a,int b)
{
__asm mov eax,a
__asm add eax,b
__asm ret 8 //注意后面的8
}
至于这种函数被调用,则和普通的cdecl及stdcall调用函数一致。函数调用约定导致的常见问题如果定义的约定和使用的约定不一致,则将导致堆栈被破坏,导致严重问题,下面是两种常见的问题:
函数原型声明和函数体定义不一致
DLL导入函数时声明了不同的函数约定
以后者为例,假设我们在dll种声明了一种函数为:
__declspec(dllexport) int func(int a,int b);//注意,这里没有stdcall,使用的是
cdecl
使用时代码为:
typedef int (*WINAPI DLLFUNC)func(int a,int b);
hLib = LoadLibrary(…);
DLLFUNC func = (DLLFUNC)GetProcAddress(…)//这里修改了调用约定
result = func(1,2);//导致错误
由于调用者没有理解WINAPI的含义错误的增加了这个修饰,上述代码必然导致堆栈被破坏,MFC在编译时插入的checkesp函数将告诉你,堆栈被破坏了。

参考链接

  • https://www.cnblogs.com/z-road/archive/2012/09/03/2669757.html

c++编译时函数名修饰问题_stdcall,_cdecl,_fastcall

  • windef.h中:
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define cdecl _cdecl

#ifndef CDECL
#define CDECL _cdecl
#endif

几乎每一个WINDOWS API函数都是__stdcall类型的,为什么?

  • WINDOWS函数调用时需要用到栈
    • 调用完成后,栈要清除,如何清除?
  • __cdecl,栈的清除工作由客户
  • 这样带来了一个棘手的问题,不同的编译器产生栈的方式不尽相同,那么调用者能否正常的完成清除工作呢?不能。
  • __stdcall:函数自己清除
  • 所以,在跨平台的调用中,都用__stdcall(有时以WINAPI的样子出现)。
  • 为啥还要_cdecl?
    • fprintf()参数是可变,不定长,
    • 被调用者事先无法知道参数的长度,事后的清除工作也无法正常进行,
    • 因此,这种情况只能用_cdecl

_stdcall C方式压栈,Pascal方式清理,用于win32API

__cdecl from 百度百科

  • C Declaration
  • C语言默认的函数调用方法:
  • 从右到左,调用者清除,称手动清栈。
  • 函数不要求调用者传递多少参数,
    • 调用者传递过多或者过少的参数,
    • 甚至完全不同的参数都不产生编译阶段的错误。

__stdcall from baidubaike

  • 从右向左
  • 由被调用者清理
    • 函数自己呗
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fgh431

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值