分析堆栈及_INTSIZEOF/va_list/va_start/va_arg/va_end

时间:2021-04-09 03:40:05

参考:
C语言中可变参数的用法
va_list/va_start/va_arg/va_end分析
判断栈和堆的生长方向
printf背后的故事
printf源码
在面向对象中,我们使用继承、泛型、等内容其实在C中也可以实现。
方法就是指针。

在使用printf这类函数时,用到了可变参数。想一想其实只要将地址和个数传入就可以实现,今天来看看C底层是怎么做的。


  1. va_list用于声明一个变量,我们知道函数的可变参数列表其实就是一个字符串,所以va_list才被声明为字符型指针,这个类型用于声明一个指向参数列表的字符型指针变量,例如:va_list ap;//ap:arguement pointer
  2. va_start(ap,v),它的第一个参数是指向可变参数字符串的变量,第二个参数是可变参数函数的第一个参数,通常用于指定可变参数列表中参数的个数。
  3. va_arg(ap,t),它的第一个参数指向可变参数字符串的变量,第二个参数是可变参数的类型。
  4. va_end(ap) 用于将存放可变参数字符串的变量清空(赋值为NULL).
#include<stdio.h>
#include<STDARG.H>
void arg_test(int a,float b,int num, ...);
int main(int argc,char *argv[])
{
int int_size = _INTSIZEOF(int);
printf("int_size=%d\n", int_size);
arg_test(0,1.0, 4,5,6,7,8);

return 0;
}
void arg_test(int a,float b,int num, ...)
{
int j=0;
va_list arg_ptr;
printf("a=%d &a=%x b=%f &b=%x num=%d &num=%x\n",a,&a,b,&b,num,&num);//打印参数在堆栈中的地址
va_start(arg_ptr, num);
printf("arg_ptr = %p\n", arg_ptr);
//打印va_start之后arg_ptr地址,
//应该比参数i的地址高sizeof(int)个字节
//这时arg_ptr指向下一个参数的地址
for(int k = 0;k < num;k++){
int m=va_arg(arg_ptr, int);
printf("m= %d arg_ptr=%x\n",m,arg_ptr);
}
va_end(arg_ptr);
printf("return\n");
}


----------


显示结果:

int_size=4
a=0 &a=18ff28 b=1.000000 &b=18ff2c num=4 &num=18ff30
arg_ptr = 0018FF34
m= 5 arg_ptr=18ff38
m= 6 arg_ptr=18ff3c
m= 7 arg_ptr=18ff40
m= 8 arg_ptr=18ff44
return
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
typedef char * va_list;
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )

_INTSIZEOF(n) 实现的是将n按照int对齐,如果n=sz(int)*q则返回q,否则返回q+1,各个平台的int可能会不同。
原理:
后面一部分~(sizeof(int) - 1) 是将int长度的低位全部置零,高位全为1,前面部分则这样理解:若n=sz(int)*q那么加上sz(int)-1只是后面变为1与运算一下也没有,不影响。若n=sz(int)*q+r ,加上sz(int)-1则定然进位,然后与运算后面置零。

va_list 定义了一个字符指针
va_start(ap,v) 这个实现了转型的操作,将指针指向了变参的第一个。
函数参数的入栈顺序自右至左,下面是内存情况:
高地址|—————————–|
|函数返回地址 |
|—————————–|
|……. |
|—————————–|
|第n个参数(第一个可变参数) |
|—————————–| <–va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|—————————–| <– &v

代码解析:先将最后一个固定参数v强转为va_list类型再按类型对齐移到后一位(内存中都是要按照类型对齐的,即便传入的char,也会填充)
va_arg(ap,t) 作用就是将ap指向下一个可变参数,返回ap指向的内容(移动之前的那个)。
代码解析:先是ap += _INTSIZEOF(t),表明ap移动到了后面,ap内容已经改变,但是返回时又减掉再转型,就指向之前的了(加的时候有赋值,减的时候没有!),(t ) 这样强转就类似泛型

PS:

  1. 为什么栈向下增长?
    “这个问题与虚拟地址空间的分配规则有关,每一个可执行C程序,从低地址到高地址依次是:text,data,bss,堆,栈,环境参数变量;其中堆和栈之间有很大的地址空间空闲着,在需要分配空间的时候,堆向上涨,栈往下涨。”

这样设计可以使得堆和栈能够充分利用空闲的地址空间。如果栈向上涨的话,我们就必须得指定栈和堆的一个严格分界线,但这个分界线怎么确定呢?平均分?但是有的程序使用的堆空间比较多,而有的程序使用的栈空间比较多。所以就可能出现这种情况:一个程序因为栈溢出而崩溃的时候,其实它还有大量闲置的堆空间呢,但是我们却无法使用这些闲置的堆空间。所以呢,最好的办法就是让堆和栈一个向上涨,一个向下涨,这样它们就可以最大程度地共用这块剩余的地址空间,达到利用率的最大化!!

2.判断栈增长方向

#include<stdio.h>
int main(){
int a,b;
char c[10];
printf("&a=%p &b=%p c[0]=%p c[9]=%p",&a,&b,c,c+9);
return 0;
}

显示:
&a=0018FF44 &b=0018FF40 c[0]=0018FF34 c[9]=0018FF3D


----------


int a = 0; //全局初始化区

char *p1; //全局未初始化区

int main()

{

int b;//栈

char s[] = "abc"; //栈

char *p2; //栈

char *p3 = "123456"; //123456在常量区,p3在栈上。

static int c =0//全局(静态)初始化区

p1 = (char *)malloc(10);

p2 = (char *)malloc(20);

//分配得来得10和20字节的区域就在堆区。

strcpy(p1, "123456"); //123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。

}