调用函数时的参数传递无非就是将参数放到固定位置,函数内部再到固定位置取取值,常用两个位置:
不同架构对于函数传参有不同的约定,例如x86-32架构是把参数全部放到栈中的,x86-64和ARMv7架构前4个参数放到指定寄存器中,其余参数放到栈中。
想搞清楚各架构函数如何传参的,其实只需要写一个最简单的demo,然后用下面的命令生产各架构的汇编,分析汇编就能知道如何入栈出栈的:
arm-none-eabi-gcc -S -mcpu=cortex-m4 -mthumb demo.c -o armv7.c
gcc -S -fno-asynchronous-unwind-tables demo.c -o x86-64.c
gcc -S -m32 -fno-asynchronous-unwind-tables demo.c -o x86-32.c
x86架构汇编相对比较繁琐,并且用的最多的STM32系列芯片是ARM架构的。所以我们以ARMv7-m架构为例。
The registers R0-R12, SP, LR, and PC are named the Arm core registers. These registers can be described as R0-R15。R7经常用作基址寄存器,对应于x86下的BP寄存器,取值时以R7为基准,因为SP不能随意改动。
C语言中调用函数只需要一句:
myfunction(v1,v2,v3);
但如果翻译成汇编,其实做了很多事情:
......
pushal # 寄存器压栈
pushl %eax # 参数压栈
pushl %ebx
pushl %ecx
call foo # 调用函数
popl %ecx
popl %ebx
popl %eax
popal
......
foo:
pushl %ebp
movl %esp, %ebp
......
popl %ebp
ret
如果不理解汇编,没关系,来看图:
在进入新的函数之前,需要将寄存器放入栈中保存,调用函数时的参数压入栈中,函数的返回地址压入栈中。函数调用时还额外保存了一个叫call's EBP的东西,并且用一个寄存器EBP指向这个位置。这是做什么的?
EBP主要有两个功能:
#include <stdio.h>
void f(int a, int b){ ------- a, b are parameters
int c = a + b; ------- c is local variables.
printf("%d \n",c);
}
int main(){
f(3,4);
return 0;
}
movl 20(%ebp), %eax -------- a is 20(%ebp)
addl 24(%ebp), %eax -------- b is 24(%ebp)
movl %eax, -4(%ebp) -------- c is -4(%ebp)
#include <stdio.h>
float a = 1.0f;
double b = 2.0;
int c = 3;
char d = 'q';
int main(int argc, char * argv[]){
printf("a = %f,b = %f,c = %d,d = %c\n",a,b,c,d);
return 0;
}
在定义printf的时候,是不知道我们要传多少个参数的。例如我们的参数a、b、c、d,这些参数在函数 printf 的声明中并没有与之对应的形参。
按 C 标准的要求,C 编译器会对这些无名参数进行实参提升的操作,即把小于 int 型的 char 和 short 提升为 int 类型,把 float 类型提升为 double 类型。
从编译器生成的抽象语法树中可以看出实参 a 从float类型被转换成 double 类型,实参 d 从char类型被转换成 int 类型:
function main
{
(call printf
str0,
(cast double
a),
b,
c,
(cast int
d))
(ret 0)
}
编译器产生的中间代码更直观地反映了实参提升的过程:
function main
t0 :&str0;
t1 : (double)(float)a;
t2 : (int)(char)d;
printf(t0, t1, b, c, t2);
return 0;
ret
再从汇编的角度看一下参数如何传递的:
.data
.str0: .string "a = %f,b = %f,c = %d,d = %c\012"
.align 4
.globl a
a: .long 1065353216
.align 8
.globl b
b: .long 0
.long 1073741824
.globl c
c: .long 3
.globl d
d: .byte 113
.text
.globl main
main:
pushl %ebp
pushl %ebx
pushl %esi
pushl %edi
movl %esp, %ebp
subl $16, %esp
.BB0:
leal .str0, %eax
flds a // 把 float 类型的 a 转换为 double 类型
fstpl -12(%ebp) // 转换后的结果存到临时变量-12(%ebp)中
movsbl d, %ecx // 把参数 d 由 char 到 int 的转换
pushl %ecx // 参数d入栈
pushl c // 参数c入栈
subl $8, %esp // 从全局静态数据区加载双精度浮点数 b 并入栈
fldl b //
fstpl (%esp) //
subl $8, %esp // 从临时变量-12(%ebp)中 加载双精度浮点数,并入栈
fldl -12(%ebp) //
fstpl (%esp) //
pushl %eax // 把格式化字符串的首地址入栈
call printf // 调用printf
addl $28, %esp
movl $0, %eax
movl %ebp, %esp
popl %edi
popl %esi
popl %ebx
popl %ebp
ret
由于所有的参数都是存放在栈中,printf只需按照格式化字符串的说明,从栈中取出相应的参数:
// 实际上只有 10 这一个参数,但 printf 看到有两个%d,
// 于是仍试图从栈中取两个参数,打印出形如 10,1074172310 的垃圾值
printf(“%d, %d “,10);
// 实际上有 10,20,30 这 3 个参数,但 printf 只看到一个%d,
// 于是只打印出参数 10
printf(“%d”, 10,20,30);
按照 C 调用约定,参数按从右到左的次序依次入栈:
我们通过表达式&format可以知道 format 在栈中的内存地址。根据格式化字符串还可以知道其他参数的类型及个数,从而可以推算出其他参数在栈中的位置,进而访问他们:
int printf(const char *format, ...);
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;
}
由 format 的地址定位其他无名参数位置的代码可以写成宏定义,这样可以让代码看起来更简洁:
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;
}