在C语言中如何访问堆栈

11 篇文章 1 订阅

堆栈一般是用来保存变量之类的东西(静态变量在内存中,虽然堆栈就是内存的一部分,但为了防止歧义,还是分成两部分来说),一般情况下没必要去故意读取堆栈的值,变量用变量名就可以直接访问,但我曾经想要读取函数返回后代码继续执行的地址,因此想到了来读取堆栈(函数调用时,会向堆栈中压入参数和下一个代码执行的地址,这样就可以在函数返回后继续执行)。

先来测试一下我们能否读取堆栈(或者说数组越界访问会怎么样):

#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

通过汇编的内容,我们可以看出可以使用一个数组来访问整个有效堆栈(当前程序的整个堆栈)内的全部内容(超出堆栈界限会引发错误)。输出结果如下,很明显程序访问了并不属于b数组的内容。

当调用一个函数时(使用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

所以,这里验证了我们可以通过操作一个数组来读取当前程序整个堆栈的内容。当然C语言理论上也应该是这样。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,下面是一个简单的C语言结构体实现堆栈模拟的示例代码: ```c #include <stdio.h> #include <stdlib.h> #define MAX_SIZE 10 // 定义结构体Stack typedef struct { int data[MAX_SIZE]; // 存放元素 int top; // 顶指针 } Stack; // 初始化 void init_stack(Stack *s) { s->top = -1; } // 判断是否为空 int is_empty(Stack *s) { return s->top == -1; } // 判断是否已满 int is_full(Stack *s) { return s->top == MAX_SIZE - 1; } // 入操作 void push(Stack *s, int ele) { if (is_full(s)) { // 判断是否已满 printf("Stack is full.\n"); return; } s->top++; // 顶指针加1 s->data[s->top] = ele; // 将元素入 } // 出操作 int pop(Stack *s) { if (is_empty(s)) { // 判断是否为空 printf("Stack is empty.\n"); return -1; } int ele = s->data[s->top]; // 取出顶元素 s->top--; // 顶指针减1 return ele; } // 获取顶元素 int get_top(Stack *s) { if (is_empty(s)) { // 判断是否为空 printf("Stack is empty.\n"); return -1; } return s->data[s->top]; // 返回顶元素 } // 输出所有元素 void print_stack(Stack *s) { if (is_empty(s)) { // 判断是否为空 printf("Stack is empty.\n"); return; } printf("Stack: "); for (int i = s->top; i >= 0; i--) { printf("%d ", s->data[i]); } printf("\n"); } // 主函数 int main() { Stack s; init_stack(&s); push(&s, 1); push(&s, 2); push(&s, 3); print_stack(&s); printf("Top element: %d\n", get_top(&s)); int ele = pop(&s); printf("Pop element: %d\n", ele); print_stack(&s); return 0; } ``` 在这个示例代码,我们定义了一个结构体Stack,其包含一个整型数组data来存储的元素,以及一个整型top来表示顶指针。接下来,我们实现了的常见操作,如初始化、判断是否为空、判断是否已满、入操作、出操作、获取顶元素和输出所有元素等。最后,在主函数,我们演示了如何使用这些操作,包括入、获取顶元素、出以及输出所有元素等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值