通过 汇编了解C语言 指针 悬垂指针概念

栈是 高地址向低地址 递增的结构,拥有后入先出的特点。
在程序运行时,程序段代码是不可变的,所以 需要借助栈来动态的 取出存放计算结果.
每个程序 在运行时 都需要一个栈来协助存储运行时的动态数据,C语言 对每次 调用(Call) 一个方法时,都会用 ebp 保存调用者的栈 位置 esp ,这样每次 方法调用完毕了 将 esp 设置为上一个栈所在位置 就可以 恢复栈了 通俗的说 就像存档点一样 每一次 打怪前 存一下档 只不过 不一样的是 打完怪了 计算机里面 我们每次打完怪了 要恢复成存档点 前的状态,但是 打怪的成果 怎么办呢? 通常C语言 通过eax 传递 返回参数 也就是 调用完子程序后,其他东西都没有变化只有 eax 寄存器 可能保存返回的值 或返回值的指针。
了解了栈 我们 还得 了解 局部变量的
假设我们在一个方法里面 定义了 一个变量

int main(){
int a = 0; 
int b = *a;
}

定义了 总要有个地方存放这个值吧 ,得知道他 放在哪 才能知道 去哪找到它 并 使用它。
在这里插入图片描述
在 C语言里面 局部变量 是由作用域的,作用域 就是 被{} 锁包裹的范围内,
当我们 在main 函数内 声明一个变量,那么它的作用域 就 限制于 main 生命周期 当然main 是整个程序的运行生命周期 所以 main 函数内部的 也就是全局变量 ,
就像上图 所展示的 一样 局部变量会被分配在 栈上。

栈方法调用

void swap(int * a, int *b) {
int c;
c = *a; *a = *b; *b = c;
}

int main(){
    int a, b;
    a=16;b=32;
    swap(&a, &b);
    return (a - b);
}

上面 是一个 普通的C语言 程序,但是 通过汇编 来分析上面的程序 可以加深我们对计算机内部原理的认识,同时 也可以更好的 了解栈 指针在程序里面到底是怎样的存在.

在这里插入图片描述
我们都知道CPU的计算器 就那么几个,然而在运算时 那么多复杂的计算 寄存器是不够的用的 所以 我们要借助内存可以是栈 把 寄存器的值 暂时 保存一下,然后再去干其它计算。然后用到 这个值的时候再 恢复出来使用。这样 即使靠 几个寄存器 就能完成各种计算任务, 所以 栈就是用来 保存运行时 数据的一个结构。

上面的程序 我们定义了 a b 所以 我们需要在栈里申请空间去存放它,esp 指向的是栈顶 32 位下 栈每次 压入4个字节 往下 压栈 就是 - 4 往上 出栈 就是 + 4。 当我们视试图调用 一个方法 时 ,代码是没有状态的 它并不知道 谁调用了它,但是 调用一个方法一定是要执行 一定的运算 然后给调用者 答案 啊,那怎么办呢 我们说过栈是用来存储运行时数据的地方,那么 我调用后 把 调用者 记录下 在栈内 计算完成后 我到栈里面找到调用者 返回给它不就行了嘛,所以当Call 被执行后 CPU会把 调用者的栈ip(near调用)记录在栈内。 当我们 要掉用 一个栈里面的数据时 我们希望栈方法调用完成后 要把 被调用方法产生的 运算时 栈数据 清除 我们只要结果,要不然 每调用一个方法 就要消耗一点栈内存 这多大内存都挺不住啊,。
那 其实只要记录下,方法调用前 栈的位置 调用完成后 咱们就不要管后面的数据了 当做垃圾数据覆盖掉就好了,所以每次调用方法 记录下 栈的位置 方法调用完成后 把栈的位置 恢复下就好了。
所以 就有了常见的

保存 栈信息

push ebp ;保存上一个栈的位置
mov ebp,esp ;保存当前栈的位置

恢复栈信息:

mov ebp,esp ;保存 esp的值
pop ebp ;恢复上一个栈的位置

当我们 执行完后 只需要 恢复上一个栈的位置,这样 我们 就可以在子程序中使用 栈 当程序运行完毕后只要恢复成 上一个调用栈的地址,有点像链表的意思,每个链表挂一个方法,执行完成后 返回上一个链表所在位置。
在这里插入图片描述

悬垂指针
int * getstackval(int * args) {
    (*args) = 1;
    int c = 0;
    return &c;
}

int main(){
   int m = 0;
    //当 getstackval() 运行完成后 栈就会被回收 之后函数运行 运行时数据 就可能覆盖 指针指向的值
   int * val = getstackval(&m);
   //每次打印出不一样的值
   printf("\n%d\n",val);
   //被子函数 修改
   printf("\n%d\n",m);
}

由于 局部变量是 存放在 栈上的,假设A方法 内部声明了一个局部变量 然后 返回指向这个 变量的指针 ,但是 当这个方法 运行完毕后 这个被分配的栈内存空间 就会被回收 再次使用 那么 我们返回的这个指针 指向地址 如果被其他方法 给 覆盖掉的话,那么 这个值 就毫无异议了,但在像GO 语言 会使用 变量逃逸分析,如果这个值 在作用域 范围外 被使用 那么 它会捕获 这个变量 就会 丢到堆里面去 堆比 栈 要慢很多。
而在Rust 中 就显示的 引入了 声明周期 这个概念 如果你 返回的指针 的生命周期 要大于等于 传入参数的生命周期 否则编译器 就会报错。

在这里插入图片描述
在这里插入图片描述

上图 演示了当call
一个函数栈发生的变化 ,可以看到 当栈return 后 栈 的esp
,ebp 变成了 第一幅图的 一开始的值,并且 返回的指针 被赋予val 其实是 指向运行时栈的地址,当方法返回后 这块栈 就可能会被后面的方法重新利用所以,就会产生 悬垂指针 得到不可预料的值。

从以上分析可知,C 语言在调用函数时是在堆栈上临时存放被调函数参数的值,即 C 语言是传值类语 言,没有直接的方法可用来在被调用函数中修改调用者变量的值。因此为了达到修改的目的就需要向函数 传递变量的指针(即变量的地址)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值