堆棧是內存中用於存放數據的專門保留的區域,該區域的數據存放和刪除方式比較特殊。一般內存區域數據元素按照連續的方式存放到數據段,在數據段中最低內存開始存放,然後向更高的內存位置依次存放。而堆棧保留在內存區域的末尾位置,並且在當數據存放在堆棧中時,它向下增長。程序運行時使用的任何命令行參數都被送入堆棧中,並且堆棧指針被設置為指向數據元素的底部。
當每個數據被添加到堆棧數據區域中時,使用一個指針跟蹤堆棧的開始位置。寄存器esp包含堆棧起始位置的內存地址,所以在編寫程序時,不允許把esp寄存器用作其它用途,如果堆棧起始位置丟失了,程序運行將出現意想不到的結果。在編寫彙編語言時,主要工作時跟蹤堆棧中的數據,不需要手動去設置堆棧指針,IA-32指令集包含指令用於訪問堆棧數據的指令。
把新的數據放到堆棧中稱為入棧,其對應指令為push,格式為:
pushx source
x是一個字符,表示數據長度,類似mov指令,但只能是l(32位)和w(16位)。source為push操作的數據元素,可以是16位或32位寄存器、16或32位內存值、16位段寄存器、8位或16位或32位立即數值。 從堆棧中獲取數據使用pop命令,其格式為:
popx dest
x表示數據長度,dest為接受數據的目標,可以是16位或32為寄存器、16或32為內存位置,16位段寄存器。 如下一個示例可以幫助理解堆棧是如何工作的:
#pushpop.s
.section .data
data:
.int 111
.section .text
.globl _start
_start:
nop
movl $91, %eax
movw $45, %bx
movb $22, %cl
pushl %eax
pushw %bx
pushl %ecx
pushl data
pushl $data
popl %edx
popl %edx
popl %edx
popw %dx
popl %edx
movl $0, %ebx
movl $1, %eax
int $0x80
使用上一節Makefile編譯鏈接程序,在調試器中運行該程序,並且監視esp寄存器的值。
(gdb) b _start
Breakpoint 1 at 0x8048074: file pushpop.s, line 9.
(gdb) r
Starting program: /home/allen/as/3_pushpop/pushpop
Breakpoint 1, _start () at pushpop.s:9
9 nop
(gdb) info registers
eax 0x0 0
ecx 0x0 0
edx 0x0 0
ebx 0x0 0
esp 0xbffff3c0 0xbffff3c0
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x8048074 0x8048074 <_start>
eflags 0x212 [ AF IF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x0 0
(gdb)
然後單步執行,查看寄存器值:
(gdb) print/x $esp
$3 = 0xbffff3c0
(gdb) s
12 movb $22, %cl
(gdb) print/x $esp
$4 = 0xbffff3c0
(gdb) s
14 pushl %eax
(gdb) print/x $esp
$5 = 0xbffff3c0
(gdb) s
15 pushw %bx
(gdb) print/x $esp
$6 = 0xbffff3bc
(gdb) s
16 pushl %ecx
(gdb) print/x $esp
$7 = 0xbffff3ba
(gdb) s
17 pushl data
(gdb) print/x $esp
$8 = 0xbffff3b6
(gdb) s
18 pushl $data
(gdb) print/x $esp
$9 = 0xbffff3b2
(gdb)
可以發現esp指針移動了,沒push一次,esp寄存器遞減了,指向新的堆棧起始位置。這說明堆棧在內存中確實向下擴展了。 執行完所有pop命令:
(gdb) s
24 popl %edx
(gdb) s
26 movl $0, %ebx
(gdb) print/x $esp
$12 = 0xbffff3c0
(gdb)
發現在所有數據出棧之後,堆棧其實位置恢復到之前數據進棧的地址了。pop命令每彈出一個數據,esp寄存器遞增,顯示堆棧長度在內存中向上擴展了。