C语言的变长参数
C语言的变长参数原理
引言: 在看《程序员的自我修养:第十一章运行库-11.2.2C语言标准库》时讲到C语言的变长参数以及cdecl,
所以搜集一些资料帮助理解。
https://zh.wikipedia.org › zh-hans
关于调用约定(cdecl、fastcall、stcall、thiscall) 的一点知识- 风 ...
https://www.laruence.com › 2008/.../...
c语言如何判断一个声明到底是函数还是指针还是数组 ... - 知乎
https://www.zhihu.com › question
变长参数函数:
Int printf(const char* format, …);
cdecl:
一种X86的调用约定,和语言没有关系。变长参数实现的基础是cdecl。
cdecl(C declaration,即C声明)是源起C语言的一种调用约定,也是C语言的事实上的标准。在x86架构上,其内容包括:
- 函数实参在线程栈上按照从右至左的顺序依次压栈。
- 函数结果保存在寄存器EAX/AX/AL中
- 浮点型结果存放在寄存器ST0中
- 编译后的函数名前缀以一个下划线字符
- 调用者负责从线程栈中弹出实参(即清栈)
- 8比特或者16比特长的整形实参提升为32比特长。
- 受到函数调用影响的寄存器(volatile registers):EAX, ECX, EDX, ST0 - ST7, ES, GS
- 不受函数调用影响的寄存器: EBX, EBP, ESP, EDI, ESI, CS, DS
- RET指令从函数被调用者返回到调用者(实质上是读取寄存器EBP所指的线程栈之处保存的函数返回地址并加载到IP寄存器)
事实上C++语言默认也是cdecl, 下面的汇编可以清晰的看到调用者完成了清理栈操作
补充stdcall
stdcall是由微软创建的调用约定,是Windows API的标准调用约定。非微软的编译器并不总是支持该调用协议。GCC编译器如下使用:
int __attribute__((__stdcall__ )) func()
stdcall是Pascal调用约定与cdecl调用约定的折衷:被调用者负责清理线程栈,参数从右往左入栈。其他各方面基本与cdecl相同。但是编译后的函数名后缀以符号"@",后跟传递的函数参数所占的栈空间的字节长度。寄存器EAX, ECX和EDX被指定在函数中使用,返回值放置在EAX中。stdcall对于微软Win32 API和Open Watcom C++是标准。
微软的编译工具规定:PASCAL, WINAPI, APIENTRY, FORTRAN, CALLBACK, STDCALL, __far __pascal, __fortran, __stdcall均是指此种调用约定。
也就是说调用windows api的代码的汇编,调用者不清理参数栈,由api函数的汇编代码清理栈。
变长参数部分待补充
uint8_t spdlogInit(const char *section);
void logerror(const char *__restrict fmt, ...);
#define SPDLOGC_ERROR(fmt, ...) logerror(fmt, ##__VA_ARGS__)
uint8_t spdlogInit(const char *section)
{
return util::spdlogInit(section);
}
void logerror(const char *__restrict fmt, ...)
{
va_list args;
va_start(args, fmt);
char s[1024];
vsnprintf(s, 1024, fmt, args);
SPDLOG_ERROR(s);
va_end(args);
}
有意思的技巧:
来源:https://www.zhihu.com/question/439224121
Int *foo[3]
看见[N] 读作 an array of N
看见() 读作 a function that returns
看见T* 读作 a point to T
然后从变量开始,先读右边的东西,再读左边的东西,然后被括号包裹着的话,就跳出去重复这个动作。
比如int *foo[3]
An array of 3 points to int