原理非常简单,无非就是从栈中取出数据而已,为了实现这一目的,必须通过第1个参数指定后续参数的数目和类型,这样我们才能解析出栈中的数据。这也就是为什么参数可变的函数,都至少带着一个有名形参,例如printf(const char* fmt, ...),这个有名形参由两个作用:
(1)用来指出后续形参的数目和类型,例如printf函数就是通过%X的形式来指定的,有多少个%就有多少个后续参数,参数的类型由%后面的标识符来指定,例如%c,%d,%X。实际上fmt字符串就是一套标准化的传输协议而已,你完全可以自定义一套你自己的实参解析协议。
(2)用来提供本函数的栈的首地址,只有借助这个首地址,才能根据偏移取出后续参数。换句话说,如果你拿不到本函数运行时栈的地址,根本就无法取参。
注意:不是所有的实参都压栈,有些实参被压到了寄存器中,到底有“多少个实参入寄存器、多少的实参入栈?”,这个问题跟硬件平台有关,也跟编译器有关,也跟操作系统的位数等等诸多因素有关。为了保证程序取参的兼容性,建议不要手动取参,而要采用C库函数va_start等来取。https://www.jb51.net/article/93490.htm 《浅谈C语言函数调用参数压栈的相关问题》
下面是一个测试程序:
#include <stdio.h>
#include <stdarg.h>
#include <stdint.h>
/*
功能:打印内存中的数据
形参:提示信息s,要打印的内存首地址p,要打印的字节数len
*/
void print_mem(const char *s, void *p, int len)
{
printf("%s", s);
for(int i = 0; i < len; i++)
{
printf("%02X ", ((uint8_t*)p)[i]);
if(0 == (i+1)%4)
{
printf("| ");
}
}
printf("\r\n");
}
/*
功能:用两种方法从栈中取出实参
形参: para_cnt为后置参数的数目,para1, para2, para3...为匿名参数
*/
void test1(int para_cnt, ...)
{
int para1;
char para2;
double para3;
float para4;
//打印实参内存
print_mem("", ¶_cnt, 4+4+4+8+30);
//方法1:纯手动从栈中取出实参
char* stackPtr = (char*)¶_cnt;
printf("para_cnt = %d\r", *(int*)stackPtr);
/* para1的地址 = para_cnt的地址+sizeof(para_cnt) */
stackPtr += sizeof(int);
para1 = *(int*)stackPtr;
/* para2的地址 = para1的地址+sizeof(para1) */
stackPtr += sizeof(int);
para2 = *(char*)stackPtr;
/* para3的地址 = para2的地址+sizeof(para2) */ //注意,每次压栈都是int的整数倍,所以sizeof(char)应替换为sizeof(int)
stackPtr += sizeof(int);//sizeof(char);
para3 = *(double*)stackPtr;
/* para4的地址 = para3的地址+sizeof(para3) */
stackPtr += 8;//32 sizeof(double)//这个偏移要改成32才能正确取出float(见打印的内存),原因未知。而且,库函数和我手动取出的数据同样是错的
para4 = *(float*)stackPtr;
printf("手动取出的实参 : %d, %c, %f, %f\r", para1, para2, para3, para4);
//方法2:使用C库函数从栈取出实参
va_list ptr;//等价于char * ptr;
va_start(ptr, para_cnt);
para1 = va_arg(ptr, int);
para2 = va_arg(ptr, char);
para3 = va_arg(ptr, double);
para4 = va_arg(ptr, float);
va_end(ptr);//本质上就是ptr = NULL,显然,这一句有和没有对程序没啥影响
printf("库函数取出的实参: %d, %c, %f, %f\r", para1, para2, para3, para4);
}
void test2(int para_cnt, int para1, char para2, double para3, float para4)
{
print_mem("", ¶_cnt, 4+4+4+8+4);
printf("实参的栈地址:\r");
printf("¶_cnt = %d\r", ¶_cnt);
printf("¶1 = %d\r", ¶1);
printf("¶2 = %d\r", ¶2);
printf("¶3 = %d\r", ¶3);
printf("¶4 = %d\r", ¶4);
printf("每个实参所占的栈空间/B:\r");
printf("para_cnt: %d\r", (char*)¶1 - (char*)¶_cnt);
printf("para1: %d\r", (char*)¶2 - (char*)¶1);
printf("para2: %d\r", (char*)¶3 - (char*)¶2);
printf("para3: %d\r", (char*)¶4 - (char*)¶3);
}
void main(void)
{
printf("start, sizeof(int) = %d\r\n", sizeof(int));
int para_cnt =4;
int para1 = 15;
char para2 = 'E';
double para3 = 3.141592;
float para4 = 3.1415926f;
print_mem("para3 mem= ", ¶3, 8);
print_mem("para4 mem= ", ¶4, 4);
test1(para_cnt, para1, para2, para3, para4);
test2(para_cnt, para1, para2, para3, para4);
}
程序输出如下:
start, sizeof(int) = 4
para3 mem= 7A 00 8B FC | FA 21 09 40 |
para4 mem= DA 0F 49 40 |
04 00 00 00 | 0F 00 00 00 | 45 00 00 00 | 7A 00 8B FC | FA 21 09 40 | 00 00 00 40 | FB 21 09 40 | 7A 00 8B FC | FA 21 09 40 | 04 00 00 00 | 0F 00 00 00 | DA 0F 49 40 | 1C FE
para_cnt = 4
手动取出的实参 : 15, E, 3.141592, 2.000000
库函数取出的实参: 15, E, 3.141592, 2.000000
04 00 00 00 | 0F 00 00 00 | 45 00 00 00 | 7A 00 8B FC | FA 21 09 40 | DA 0F 49 40 |
实参的栈地址:
¶_cnt = 2686440
¶1 = 2686444
¶2 = 2686448
¶3 = 2686452
¶4 = 2686460
每个实参所占的栈空间/B:
para_cnt: 4
para1: 4
para2: 4
para3: 8
经过测试发现,不管是手动取数,还是使用库函数取数,都有bug,原因未知。但是原理就是这样了,而且我们发现,手动取数和库函数取数,出的错误都是一样的,这至少说明,我写的手动取数的方法,和库函数的实现原理是一样的