堆棧的使用

堆棧是內存中用於存放數據的專門保留的區域,該區域的數據存放和刪除方式比較特殊。一般內存區域數據元素按照連續的方式存放到數據段,在數據段中最低內存開始存放,然後向更高的內存位置依次存放。而堆棧保留在內存區域的末尾位置,並且在當數據存放在堆棧中時,它向下增長。程序運行時使用的任何命令行參數都被送入堆棧中,並且堆棧指針被設置為指向數據元素的底部。

當每個數據被添加到堆棧數據區域中時,使用一個指針跟蹤堆棧的開始位置。寄存器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寄存器遞增,顯示堆棧長度在內存中向上擴展了。