堆棧一般是用來保存變量之類的東西(靜態變量在內存中,雖然堆棧就是內存的一部分,但爲了防止歧義,還是分成兩部分來說),一般情況下沒必要去故意讀取堆棧的值,變量用變量名就可以直接訪問,但我曾經想要讀取函數返回後代碼繼續執行的地址,因此想到了來讀取堆棧(函數調用時,會向堆棧中壓入參數和下一個代碼執行的地址,這樣就可以在函數返回後繼續執行)。
先來測試一下我們能否讀取堆棧:
#include<stdio.h>
int main()
{
volatile int a=24;/*設置一個我們要讀取的變量,volatile 可以告訴gcc不要優化這行代碼,僅對變量有效*/
volatile int b[2]={1,2};/*建立一個數組,這個數組是關鍵,這時b作爲數組指針,指向第一個元素,即
1在堆棧中的儲存位置,因此我們就可以利用b來讀取堆棧的任意位置(該程序所擁有的堆棧)*/
volatile int c=b[2];
printf("%d\n",c);//打印出指定位置堆棧的值
return 0;
}
當然,如果不設定編譯器的參數,這樣的代碼可能是不會編譯通過的(注意:可能),命令如下:
gcc -Wno-unused -m32 -S -O0 -o test.s test.c
源文件名爲test.c,參數說明:
-Wno-unused:不警告未使用的變量(上面的程序不需要,但爲了方便自己分析,放在這裏)
-m32:編譯爲32位程序
-S:編譯爲彙編文件
-O0:優化等級爲0
-o:重命名輸出文件
現在讓我們看看彙編文件是什麼樣的:
.file "test.c"
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "%d\12\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB10:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
pushl %esi
pushl %ebx
andl $-16, %esp
subl $32, %esp
.cfi_offset 6, -12
.cfi_offset 3, -16
call ___main
movl $24, 28(%esp) //將24存入堆棧,位置是28+esp的值
movl $1, %ebx //1存入ebx
movl $2, %esi //2存入esi
movl %ebx, 20(%esp) //1存入20(%esp)
movl %esi, 24(%esp) //2存入24(%esp)
movl 28(%esp), %eax //將28(%esp)的值存入eax,這裏對應的代碼就是c=b[2],即將24存入了eax
movl %eax, 16(%esp) //剩下的就是將參數壓入堆棧,然後調用printf,這裏不再解釋
movl 16(%esp), %eax
movl %eax, 4(%esp)
movl $LC0, (%esp)
call _printf
movl $0, %eax
leal -8(%ebp), %esp
popl %ebx
.cfi_restore 3
popl %esi
.cfi_restore 6
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE10:
.ident "GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
.def _printf; .scl 2; .type 32; .endef
通過彙編的內容,我們可以看出可以使用一個數組來訪問有效堆棧內的全部內容(超出堆棧界限會引發錯誤)。輸出結果如下:
當調用一個函數時(使用call指令),壓入參數的同時會壓下一個代碼的地址,使函數返回後可以繼續執行。現在來嘗試獲取這個地址,代碼如下:
#include<stdio.h>
void fun()
{
volatile int a[1];/*設置一個數組,使用這個數組來訪問堆棧*/
a[0]=14;
printf("a[4]=%d\n",a[4]);/*打印出call指令壓入的地址,這裏很有意思,我之前以爲這個地址在
a[2],a[0]=14,a[1]是esp的值(C語言中所有函數的開頭都會有push ebp的代碼,將ebp的值保存進堆棧,然後
將esp保存進ebp),但實際上發現總會有兩個不知名的值佔據着a[2],a[3]的位置,具體可以參見彙編代碼*/
goto *(a[4]);/*使用goto語句可以讓程序跳向任何合法地址,goto不僅可以用標號或者行號,還可以是任
何void*型的變量(前提是程序可以訪問該地址),goto會被程序翻譯爲jmp指令,而(*(void(*)
(void))0x100000)();這樣的跳轉方式將會被翻譯爲call指令,會使堆棧中多出一個地址,具體要使用哪個需要
參考實際。*/
}
int main()
{
fun();
printf("hello");/*理論上如果上面的goto生效,那麼hello將會被執行兩次(調用fun函數時,堆棧被壓入該
地址,然後使用了goto後,跳轉到這裏執行一次,打印出一個hello,在下面的return 0;語句中,程序會認爲當
前還在fun函數,畢竟堆棧中的地址還沒有釋放,因此重新返回到這裏,再執行一次),否則由於地址錯誤,程
序將被迫退出,不會在控制檯看到hello*/
return 0;
}
下面是實際執行的情況,可見我們確實得到了之前壓入的那個地址:
以下是上面的那個程序的彙編程序:
.file "test.c"
.section .rdata,"dr"
LC0:
.ascii "a[4]=%d\12\0"
.text
.globl _fun
.def _fun; .scl 2; .type 32; .endef
_fun:
LFB10:
.cfi_startproc
pushl %ebp //將ebp的值送入堆棧
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $40, %esp //空出40字節的位置用來儲存變量
movl $14, -12(%ebp) //14存入a[0],所以a的值即是ebp偏移12個字節,
//可以推斷出a[1]在-8(%ebp),a[2]在-4(%ebp),a[3]在0(%esp),所以a[3]是之前保存的ebp的值
//那麼a[4]就是call指令保存的值,這裏比較令人好奇爲什麼a[0]在-12(%ebp)
movl 4(%ebp), %eax
movl %eax, 4(%esp)
movl $LC0, (%esp)
call _printf
movl 4(%ebp), %eax
jmp *%eax //goto被翻譯爲jmp指令,然後跳向了我們指定的地址
.cfi_endproc
LFE10:
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC1:
.ascii "hello\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB11:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
call ___main
call _fun
movl $LC1, (%esp)
call _printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE11:
.ident "GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
.def _printf; .scl 2; .type 32; .endef
所以這裏驗證了我們可以通過操作數組來讀取堆棧。