1. 程式人生 > 實用技巧 >棧空間分配和棧對齊訪問

棧空間分配和棧對齊訪問

棧空間分配

之前對於函式棧空間的理解就是棧空間由系統自動分配自動釋放,但是棧空間何時分配,棧空間大小等細節還是沒有過多瞭解。

每個函式都有屬於自己的一個函式棧幀,假設函式呼叫關係為:main->func1->func2,那麼在執行到func2的時候,該程序的堆疊空間如下所示:

main棧幀
func1棧幀
func2棧幀

棧幀一般包含如下資訊:

  • 函式的實參和區域性變數
  • 函式呼叫連結資訊-呼叫函式時要儲存某些CPU暫存器的值,如PC,以便返回時能繼續執行下一條指令

下面我們通過彙編函式來簡單的分析一下棧幀的內容以及棧幀是如何分配和回收的。

首先我們寫一段簡單的函式呼叫C程式碼,並將其編譯成彙編檔案,亦可通過objdump -dS 命令將可執行檔案反彙編得到彙編指令

#include <stdio.h>

int foo(int a, int b)
{
    char x =1;
    int c = 0;
    c = a + b + x;
    return c;
}

int main()
{
    int ret = 0;
    ret = foo(2, 3);
    return 0;
}
foo:
.LFB0:
    .file 1 "call_no_stack.c"
    .loc 1 4
0 .cfi_startproc pushq %rbp //rbp入棧 (rsp-8) .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp //rsp 賦值給 rbp,這裡rsp並沒有移動,可能是因為這裡是最後一個函式呼叫,所以不需要移動rsp .cfi_def_cfa_register 6 movl %edi, -20(%rbp) //這裡通過rbp來訪問棧,將main函式中的實參2放入rbp-20記憶體 movl %esi, -
24(%rbp) //這裡表示棧空間分配了24位元組,猜測:函式中的引數值從棧頂開始儲存 .loc 1 5 0 movb $1, -5(%rbp) //區域性變數x入棧,x佔用1個位元組,相當於x後入棧:棧的地址是向下減少的 .loc 1 6 0 movl $0, -4(%rbp) //區域性變數c入棧,放在rbp-4處 .loc 1 7 0 movl -20(%rbp), %edx movl -24(%rbp), %eax addl %eax, %edx //相加操作 movsbl -5(%rbp), %eax addl %edx, %eax movl %eax, -4(%rbp) .loc 1 8 0 movl -4(%rbp), %eax //將c變數的結果儲存到eax暫存器,以便函式返回 .loc 1 9 0 popq %rbp //將堆疊pop,此時棧頂儲存著呼叫函式的rbp值,將棧頂元素賦予rbp暫存器(恢復rbp暫存器) .cfi_def_cfa 7, 8 ret //跳轉回上一層處繼續執行 .cfi_endproc .LFE0: .size foo, .-foo .globl main .type main, @function main: .LFB1: .loc 1 12 0 .cfi_startproc pushq %rbp //rbp:64位暫存器——指向棧底,將rbp暫存器內的值入棧-pushq操作會改變rsp的值 .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp //rsp:64位堆疊指標暫存器——指向棧頂,將rsp值存入rbp暫存器內 .cfi_def_cfa_register 6 subq $16, %rsp //rsp-16,這裡講棧頂指標向下移動16位元組,相當於為main函式預留了16位元組的棧空間-儲存區域性變數包括實參 .loc 1 13 0 movl $0, -4(%rbp) //對應區域性變數ret = 0 .loc 1 14 0 movl $3, %esi //這裡直接將實參存入esi暫存器而不是放入堆疊,可加快訪問速度 movl $2, %edi call foo //呼叫foo函式:call指令有另個作用:1,將call指令的下一條指令入棧-並改變rsp 2,修改程式計數器eip,跳轉到foo函式的開頭執行 movl %eax, -4(%rbp) //eax暫存器儲存著返回值,這裡將eax賦值給rbp-4的位置,也就是ret .loc 1 15 0 movl $0, %eax .loc 1 16 0 leave //leave指令是函式開頭的pushq %rbpmovq %rsp,%rbp的逆操作,
                  //有兩個作用:1,把rbp賦值給rsp 2,然後把該函式棧棧頂儲存的rbp值恢復到rbp暫存器中,同時rsp+4(第二部的操作相當於pop棧頂元素)
.cfi_def_cfa 7, 8 ret //現在棧頂元素儲存的是下一條執行的指令,ret的作用就是pop棧頂元素,並將棧頂元素賦值給程式計數器bip,然後程式跳轉回bip所在地址繼續執行 .cfi_endproc .LFE1: .size main, .-main

上述彙編程式碼可以用下圖較為直觀的展示:

可以看出:編譯器生成彙編程式碼時,在當前函式開頭,新增對應的對sp/esp/rsp(對應16/32/64位堆疊指標暫存器)的值減去所需堆疊記憶體大小,即對該函式分配(其實是預留)了堆疊記憶體。另外需要注意的是,在呼叫鏈的最後一層,即後續沒有呼叫其他函式,那麼堆疊指標是不會移動(估計和編譯器實現有關)。上述main函式棧幀中,有這麼一句:subq $16, %rsp,這個操作直接將棧指標往下移了16個位元組,這就是在為堆疊分配空間以儲存區域性變數和實參。大家可以試一下在函式內分配一個數組,看看生成的彙編有什麼變化。

棧空間對齊

這一部分引用相關文章:https://www.cnblogs.com/reload/p/3159053.html https://www.cnblogs.com/tcctw/p/11333743.html

棧的位元組對齊,實際是指棧頂指標必須須是16位元組的整數倍。棧對齊幫助在儘可能少的記憶體訪問週期內讀取資料,不對齊堆疊指標可能導致嚴重的效能下降。

上文我們說,即使資料沒有對齊,我們的程式也是可以執行的,只是效率有點低而已,但是某些型號的Intel和AMD處理器對於有些實現多媒體操作的SSE指令,如果資料沒有對齊的話,就無法正確執行。這些指令對16位元組記憶體進行操作,在SSE單元和記憶體之間傳送資料的指令要求記憶體地址必須是16的倍數。

因此,任何針對x86_64處理器的編譯器和執行時系統都必須保證分配用來儲存可能會被SSE暫存器讀或寫的資料結構的記憶體,都必須是16位元組對齊的,這就形成了一種標準:

  • 任何記憶體分配函式(alloca, malloc, calloc或realloc)生成的塊起始地址都必須是16的倍數。
  • 大多數函式的棧幀的邊界都必須是16直接的倍數。

如上,在執行時棧中,不僅傳遞的引數和區域性變數要滿足位元組對齊,我們的棧指標(%rsp)也必須是16的倍數