C编译器学习笔记(六)函数调用及栈分析

调用函数时的参数传递无非就是将参数放到固定位置,跳转到函数中再到固定位置取值,常用两个位置:

  1. 放到寄存器中
  2. 放到栈中
  3. 前几个参数放到寄存器,后面的参数放到栈中

固定参数的函数调用

在《Arm v7-M Architecture Reference Manual》中有这么一句话:

The Armv7-M architecture uses a full-descending stack

意思是Armv7-M架构的栈是从高地址向低地址生长的。

image

变参函数

变参函数是必须要有第一个参数的,且这个参数必须是字符串,因为我们需要根据字符串中的“特殊字符”确定其余参数的类型。最常见的变参函数:

int printf(const char *format, ...);

按照C标准,参数按从右到左的次序依次入栈。我们知道第一个参数的内存地址,根据格式化字符串还可以知道其他参数的类型及个数,从而可以推算出其他参数在栈中的位置,进而访问他们:

float a = 1.0f;
double b = 2.0;
int c = 3;
char d = 'q';

// 不同的平台栈的内存布局不同,以下代码仅适用于i386
void OurPrintfV1(const char *format, ...){
	unsigned int addr = (unsigned int) &format;
	addr += sizeof(char *);
	printf("%f ", *((double *)addr));
	addr += sizeof(double);
	printf("%f ", *((double *)addr));
	addr += sizeof(double);
	printf("%d ", *((int *)addr));
	addr += sizeof(int);
	printf("%c ", (char)(*((int *)addr)));
}

int main(){
	OurPrintfV1("a=%f,b=%f,c=%d,d=%c \n",a,b,c,d);
	return 0;
}

把定位其他无名参数位置的代码可以写成宏,这样可以让代码看起来更简洁:

int printf(const char *format, ...);

float a = 1.0f;
double b = 2.0;
int c = 3;
char d = 'q';

// 不同的平台栈的内存布局不同,以下代码仅适用于i386
typedef char * va_list;
#define ALIGN_INT(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define va_start(list, start) (list = (va_list)&start + ALIGN_INT(start))
#define va_arg(list, t) (*(t *)((list += ALIGN_INT(t)) - ALIGN_INT(t)))
#define va_end(list) (list = (va_list)0)

void OurPrintfV1(const char *format, ...){
	va_list ap;
	va_start(ap,format);
	printf("%f ",va_arg(ap,double));
	printf("%f ",va_arg(ap,double));
	printf("%d ",va_arg(ap,int));
	printf("%c \n",(char)va_arg(ap,int));
	va_end(ap);
}

int main(){
	OurPrintfV1("a=%f,b=%f,c=%d,d=%c \n",a,b,c,d);
	return 0;
}

上述的宏其实已经有人帮我们写好了,在标准C头文件stdarg.h中。头文件stdarg.h中的宏有对各种平台都有不同的处理,所以下面的代码可以在所有平台上运行:

#include <stdarg.h>

int printf(const char *format, ...);

float a = 1.0f;
double b = 2.0;
int c = 3;
char d = 'q';


void OurPrintfV1(const char *format, ...){
	va_list ap;
	va_start(ap,format);
	printf("%f ",va_arg(ap,double));
	printf("%f ",va_arg(ap,double));
	printf("%d ",va_arg(ap,int));
	printf("%c \n",(char)va_arg(ap,int));
	va_end(ap);
}

int main(){
	OurPrintfV1("a=%f,b=%f,c=%d,d=%c \n",a,b,c,d);
	return 0;
}

参数提升问题

c标准规定,对于变参函数,要对相应的实参做Integer Promotion,此外,相应的实参如果是float型的也要被提升为double类型,这条规则称为Default Argument Promotion。

参数中含有...,属于变参函数,最常见的变参函数莫过于:

int printf(const char *format, ...);

参数提升的意思是,给printf传递char、short等类型时,在printf函数里面会得到一个int类型;给printf传递float类型时,在printf函数里面会得到一个double类型。

这也是为什么我们在上个例子中取float要用va_arg(ap,double)的原因。

我查了半天也没弄明白为什么C标准会定下这么奇怪的规定,没发现这个规定有啥用呀,反而写变参函数的时候一不小心就掉坑里了。

猜测可能是因为历史原因才有这条规定吧,我们自己写编译器完全没必要理会这条规定。

数组、结构体的传参与返回

数组类型传参会自动转换为指针类型,这在语义检查时就完成了转换。

按 C 的语法要求,返回值不可以是数组类型。

结构体传参会跟int、float等类型一样,保存到栈中。

当结构体作为函数返回值时,情况比较特殊,例如:

typedef struct Data{
      ......
} dt;

dt = GetData();

如果结构体大小是{1,2,4,8},返回值同int、float一样,放入约定的寄存器中即可。

如果结构体大小不是{1,2,4,8},则C编译器会为函数添加结构体指针作为第1个参数,经 C 编译器处理后,真正执行的函数调用为:

dt = GetData(&dt);

为了简单,mcc不支持结构体作为返回值。

posted @ 2023-01-02 22:07:57
评论加载中...
发表评论