天天看點

局部變量入棧順序與輸出關系

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]的值。