在C語言中如何訪問堆棧

堆棧一般是用來保存變量之類的東西(靜態變量在內存中,雖然堆棧就是內存的一部分,但爲了防止歧義,還是分成兩部分來說),一般情況下沒必要去故意讀取堆棧的值,變量用變量名就可以直接訪問,但我曾經想要讀取函數返回後代碼繼續執行的地址,因此想到了來讀取堆棧(函數調用時,會向堆棧中壓入參數和下一個代碼執行的地址,這樣就可以在函數返回後繼續執行)。

先來測試一下我們能否讀取堆棧:

#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

所以這裏驗證了我們可以通過操作數組來讀取堆棧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章