天天看点

局部变量入栈顺序与输出关系

C语言中入栈顺序与变量输出

1).内存区域划分:

局部变量入栈顺序与输出关系

图1 程序运行时的内存区域

       如图所示:C程序中,栈区主要存储函数的参数,局部变量等,并且栈底为高地址,栈顶为低地址(如图:由高地址向低地址扩展)。

2).入栈顺序:

A:函数参数的入栈顺序:自右向左

原因:

       函数参数的入栈顺序和具体编译器的实现有关。有些参数是从左向右入栈,如:Pascal语言从左到右入栈(不支持变参),被调用者清栈;有些语言还可以通过修饰符进行指定,如:Visual C++;但是C语言(cdecl)采用自右向左的方式入栈,调用者清栈。

这是因为自右向左入栈顺序的好处就是可以动态的变化参数个数。通过堆栈分析可知,自左向右入栈方式中,最前面的参数会被压入栈底。除非知道参数个数,否则无法通过栈指针的相对位移求得最左边的参数。这样就无法实现可变参数。因此,C语言采用自右向左入栈顺序,主要是因为实现可变长参数形式(如:printf函数)。可变长参数主要通过第一个定参数来确定参数列表,所以自右向左入栈后,函数调用时栈顶指针指向的就是参数列表的第一个确定参数,这样就可以了。

例子1:

#include <stdio.h>

void print(int x, int y, int z)
{
    printf("x = %d addr %p\n", x, &x);
    printf("y = %d addr %p\n", y, &y);
    printf("z = %d addr %p\n", z, &z);
}

int main()
{
    print(,,);//自右向入压栈
    return ;
}
           

运行结果:

x =  addr  //栈顶,后压栈
y =  addr 
z =  addr  //栈底,先入栈
           

B:局部变量的入栈顺序:

       在没有栈溢出保护机制下编译时,所有局部变量按系统为局部变量申请内存中栈空间的顺序,即:先申请哪个变量,哪个先入栈,正向的。也就是说,编译器给变量空间的申请是直接按照变量申请顺序执行的。(见例子2)

在有栈溢出保护机制下编译时,入栈顺序有所改变,先按照类型划分,再按照定义变量的先后顺序划分,即:char型先申请,int类型后申请(与编译器溢出保护时的规定相关);然后栈空间的申请顺序与代码中变量定义顺序相反(后定义的先入栈)。(见例子2)

例子2:stack.c

#include <stdio.h>

int main()
{
    int a[] = {,,,,};
    int b[] = {,,,,};
    char buf1[] = "abcde";
    char buf2[] = "fghij";
    int m = -;
    int n = -;
    printf("a[0]    = %3d, addr: %p\n", a[], &a[]);
    printf("a[4]    = %3d, addr: %p\n", a[], &a[]);
    printf("b[0]    = %3d, addr: %p\n", b[], &b[]);
    printf("b[4]    = %3d, addr: %p\n", b[], &b[]);
    printf("buf1[0] = %3d, addr: %p\n", buf1[], &buf1[]);
    printf("buf1[5] = %3d, addr: %p\n", buf1[], &buf1[]);
    printf("buf2[0] = %3d, addr: %p\n", buf2[], &buf2[]);
    printf("buf2[5] = %3d, addr: %p\n", buf2[], &buf2[]);
    printf("m       = %3d, addr: %p\n", m, &m);
    printf("n       = %3d, addr: %p\n", n, &n);
}
           

没有栈溢出保护机制下的编译:

$ gcc stack.c -g -o stack -fno-stack-protector
$ ./stack
a[]    =   , addr:  //数组内部,地址由低到高不变
a[]    =   , addr:  //栈底,高地址
b[]    =   , addr: 
b[]    =  , addr: 
buf1[] =  , addr: 
buf1[] =   , addr: 
buf2[] = , addr: 
buf2[] =   , addr: 
m       =  -, addr: 
n       =  -, addr:   //栈顶,低地址
           

可以看出入栈顺序:a -> b -> buf1 -> buf2 -> m -> n(先定义,先压栈)

栈溢出保护机制下的编译:

$ gcc stack.c -g -o stack
$ ./stack
a[]    =   , addr:  //栈顶
a[]    =   , addr: 
b[]    =   , addr: 
b[]    =  , addr:  
buf1[] =  , addr:  //char类型,优先入栈
buf1[] =   , addr: 
buf2[] = , addr: 
buf2[] =   , addr:  //栈底
m       =  -, addr: 
n       =  -, addr:  //int类型,后压栈
           

可以看出入栈顺序:buf2 -> buf1 -> n -> m -> b -> a(char类型先入栈,int类型后入栈;先定义,后压栈)

3).指针越界输出:

例子3:stack1.c

#include <stdio.h>

int main()
{
    char buf1[] = "abcef";
    char buf2[] = "fghij";
    int a[] = {,,,,};
    int b[] = {,,,,};
    int m = -;
    int n = -;
    char *p = &buf2[];
    printf("a[0]    = %3d, addr: %p\n", a[], &a[]);
    printf("a[4]    = %3d, addr: %p\n", a[], &a[]);
    printf("b[0]    = %3d, addr: %p\n", b[], &b[]);
    printf("b[4]    = %3d, addr: %p\n", b[], &b[]);
    printf("buf1[0] = %3d, addr: %p\n", buf1[], &buf1[]);
    printf("buf1[5] = %3d, addr: %p\n", buf1[], &buf1[]);
    printf("buf2[0] = %3d, addr: %p\n", buf2[], &buf2[]);
    printf("buf2[5] = %3d, addr: %p\n", buf2[], &buf2[]);
    printf("m       = %3d, addr: %p\n", m, &m);
    printf("n       = %3d, addr: %p\n", n, &n);
    printf("p[0]    = %3d, addr: %p\n", p[], &p[]);
    printf("p[6]    = %3d, addr: %p\n", p[], &p[]);
    printf("p[-6]   = %3d, addr: %p\n", p[-], &p[-]);
    printf("p[-42]  = %3d, addr: %p\n", p[-], &p[-]);
    printf("p[-43]  = %3d, addr: %p\n", p[-], &p[-]);
    printf("p[-53]  = %3d, addr: %p\n", p[-], &p[-]);
    printf("p[-54]  = %3d, addr: %p\n", p[-], &p[-]);
    printf("p[-55]  = %3d, addr: %p\n", p[-], &p[-]);
    printf("p[-56]  = %3d, addr: %p\n", p[-], &p[-]);
    printf("p[-57]  = %3d, addr: %p\n", p[-], &p[-]);
    printf("p[-58]  = %3d, addr: %p\n", p[-], &p[-]);
    printf("p[-59]  = %3d, addr: %p\n", p[-], &p[-]);
}
           

栈溢出保护机制下的编译:

$ gcc stack1.c -g -o stack1
$ ./stack1
a[]    =   , addr:  //栈顶,0xbff5ab6c,低地址
a[]    =   , addr: 
b[]    =   , addr: 
b[]    =  , addr: 
buf1[] =  , addr:  //&p[-6]
buf1[] =   , addr: 
buf2[] = , addr:  //&p[0]
buf2[] =   , addr:  //栈底,0xbff5abab,高地址--->&p[6]:越界,值随机
m       =  -, addr: 
n       =  -, addr: 
p[]    = , addr:  //&buf2[0]
p[]    =   , addr:  //&buf2[6],越界,无初始值,值随机
p[-]   =  , addr:  //&buf1[0],越界,已有初始值,buf1[0],p[-6]为97
p[-]  =   , addr:  //&a[4]
p[-]  =   , addr:  //&a[4] - 1字节,大小0x00 = 0
p[-]  =   , addr:  //&a[1] + 1字节,大小0x00 = 0
p[-]  =   , addr:  //&a[1]
p[-]  =   , addr:  //p[-55]到p[-58]能看出Linux是小端存储。
p[-]  =   , addr:  //小端存储:低地址存低位,高地址存高位
p[-]  =   , addr:  //a[0]=1,即:0x01 0x00 0x00 0x00(低位到高位)
p[-]  =   , addr:  //&a[0]
p[-]  =   -, addr:  //&a[0] - 1字节,越界,无初始值,值随机
           

入栈顺序:

(栈底:高地址)buf2 -> buf1 -> n -> m -> b -> a[4] -> a[0](栈顶:低地址)

         &p[6]—&p[0]—&p[-6]——————&p[-42]—&p[-58]—&p[-59]

提醒:指针p越界会出现问题,如果在p[-6] = ‘k’;那么会导致因越界覆盖内存里面buf1[0]的值。