堆棧指針sp的內容是什么
? ? 堆棧是一塊保存數據的連續內存。一個名為堆棧指針(SP)的寄存器指向堆棧的頂部。 堆棧的底部在一個固定的地址。
堆棧的大小在運行時由內核動態地調整。 CPU實現指令 PUSH和POP,向堆棧中添加元素和從中移去元素。 堆棧由邏輯堆棧幀組成。 當調用函數時邏輯堆棧幀被壓入棧中,當函數返回時邏輯 堆棧幀被從棧中彈出。
堆棧幀包括函數的參數,函數地局部變量,以及恢復前一個堆棧 幀所需要的數據,其中包括在函數調用時指令指針(IP)的值。 堆棧既可以向下增長(向內存低地址)也可以向上增長, 這依賴于具體的實現。在我 們的例子中,堆棧是向下增長的。
這是很多計算機的實現方式,包括Intel,Motorola,SPARC和MIPS處理器。 堆棧指針(SP)也是依賴于具體實現的。 它可以指向堆棧的最后地址,或者指向堆棧之后的下一個空閑可用地址。 在我們的討論當中,SP指向堆棧的最后地址。除了堆棧指針(SP指向堆棧頂部的的低地址)之外,為了使用方便還有指向幀內固定 地址的指針叫做幀指針(FP)。
有些文章把它叫做局部基指針(LB-local base pointer)。從理論上來說,局部變量可以用SP加偏移量來引用。然而,當有字被壓棧和出棧后,這些偏移量就變了。盡管在某些情況下編譯器能夠跟蹤棧中的字操作,由此可以修正偏移 量,但是在某些情況下不能。而且在所有情況下,要引入可觀的管理開銷。而且在有些機器上,比如Intel處理器, 由SP加偏移量訪問一個變量需要多條指令才能實現。因此,許多編譯器使用第二個寄存器, FP, 對于局部變量和函數參數都可以引用,因為它們到FP的距離不會受到PUSH和POP操作的影響。
在Intel CPU中,BP(EBP)用于這 個目的。在Motorola CPU中, 除了A7(堆棧指針SP)之外的任何地址寄存器都可以做FP。 考慮到我們堆棧的增長方向,從FP的位置開始計算,函數參數的偏移量是正值, 而局部 變量的偏移量是負值。 當一個例程被調用時所必須做的第一件事是保存前一個FP(這樣當例程退出時就可以 恢復)。 然后它把SP復制到FP,創建新的FP,把SP向前移動為局部變量保留空間。這稱為 例程的序幕(prolog)工作。 當例程退出時,堆棧必須被清除干凈,這稱為例程的收尾 (epilog)工作。Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令,都可以用于 有效地序幕和收尾工作。
一個簡單的堆棧例子
example1.c:
------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
------------------------------------------------------------------
使用gcc的-S選項編譯, 以產生匯編代碼輸出:
$ gcc -S -o example1.s example1.c
通過查看匯編語言輸出, 我們看到對function()的調用被翻譯成:
pushl $3
pushl $2
pushl $1
call function
以從后往前的順序將function的三個參數壓入棧中, 然后調用function()。 指令call會把指令指針(IP)也壓入棧中。 我們把這被保存的IP稱為返回地址(RET)。 在函數中所做的第一件事情是例程的序幕工作:
pushl ?p
movl %esp,?p
subl $20,%esp
將幀指針EBP壓入棧中。 然后把當前的SP復制到EBP, 使其成為新的幀指針。 我們把這個被保存的FP叫做SFP。 接下來將SP的值減小, 為局部變量保留空間。
內存只能以字為單位尋址。 一個字是4個字節, 32位。 因此5字節的緩沖區會占用8個字節(2個字)的內存空間, 而10個字節的緩沖區會占用12個字節(3個字)的內存空間。 這就是為什么SP要減掉20的原因。 這樣我們就可以想象function()被調用時堆棧的模樣(每個空格代表一個字節):
內存低地址 內存高地址
buffer2 buffer1 sfp ret a b c
《------ [ ][ ][ ][ ][ ][ ][ ]
堆棧頂部 堆棧底部
制造緩沖區溢出
現在試著修改我們第一個例子, 讓它可以覆蓋返回地址, 而且使它可以執行任意代碼。堆棧中在buffer1[]之前的是SFP, SFP之前是返回地址。 ret從buffer1[]的結尾算起是4個字節。應該記住的是buffer1[]實際上是2個字即8個字節長。 因此返回地址從buffer1[]的開頭算起是12個字節。 我們會使用這種方法修改返回地址, 跳過函數調用后面的賦值語句‘x=1;’, 為了做到這一點我們把返回地址加上8個字節。 代碼看起來是這樣的:
example3。c:
--------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 + 12;
(*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf(“%d\n”,x);
}
-------------------------------------------------------------------
我們把buffer1[]的地址加上12, 所得的新地址是返回地址儲存的地方。 我們想跳過賦值語句而直接執行printf調用。
如何知道應該給返回地址加8個字節呢? 我們先前使用過一個試驗值(比如1), 編譯該程序, 祭出工具gdb:
-----------------------------------------------------------------
[aleph1]$ gdb example3
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type “show copying” to see the conditions。
There is absolutely no warranty for GDB; type “show warranty” for details。
GDB 4。15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc.。。
(no debugging symbols found)。。.
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 : pushl ?p
0x8000491 : movl %esp,?p
0x8000493 : subl $0x4,%esp
0x8000496 : movl $0x0,0xfffffffc(?p)
0x800049d : pushl $0x3
0x800049f : pushl $0x2
0x80004a1 : pushl $0x1
0x80004a3 : call 0x8000470
0x80004a8 : addl $0xc,%esp
0x80004ab : movl $0x1,0xfffffffc(?p)
0x80004b2 : movl 0xfffffffc(?p),êx
0x80004b5 : pushl êx
0x80004b6 : pushl $0x80004f8
0x80004bb : call 0x8000378
0x80004c0 : addl $0x8,%esp
0x80004c3 : movl ?p,%esp
0x80004c5 : popl ?p
0x80004c6 : ret
0x80004c7 : nop
------------------------------------------------------------------
我們看到當調用function()時, RET會是0x8004a8, 我們希望跳過在0x80004ab的賦值指令。 下一個想要執行的指令在0x8004b2。 簡單的計算告訴我們兩個指令的距離為8字節。
評論
查看更多