赞
踩
- /*!
- * \file main.cpp
- * \date 2018/12/26 20:42
- *
- * \author wangjinju
- * Contact: user@company.com
- *
- * \brief 仅用递归函数和栈操作逆序一个栈
- *
- * TODO: long description
- *
- * \note
- */
- #include <stack>
- #include <vector>
- #include <iostream>
- using namespace std;
-
-
- //将栈stack的栈底元素返回并移除
- template <typename T>
- T getAndRemoveLastEle(stack<T>& s)
- {
- T result = s.top();
- s.pop();
- if (s.empty())
- return result;
- else
- {
- T last = getAndRemoveLastEle(s);
- s.push(result);
- return last;
- }
- }
-
- //逆序一个栈
- template <typename T>
- void reverse(stack<T>& s)
- {
- if (s.empty())
- return;
- else
- {
- auto last = getAndRemoveLastEle(s);
- reverse(s);
- s.push(last);
- }
- }
-
- int main()
- {
- vector<int> v = { 0,1,2,3 };
- stack<int> s;
- for (auto item : v)
- s.push(item);
-
- reverse(s);
-
- for (; !s.empty();)
- {
- cout << s.top() << endl;
- s.pop();
- }
-
- cin.get();
- return 0;
- }
在Windows 等大部分操作系统中, 每个运行中的二进制程序都配有一个调用栈( call stack ) 或执行栈( execution stack ) 。
调用栈的作用首先在于跟踪属于同一程序的所有函数, 记录它们之间的相互调用, 以保证被调用的函数执行完毕后可以准确地返回调用函数。
调用栈以帧( frame )作为基本单位。每次函数调用时, 都会相应地创建一帧, 记录其在二进制程序中的返回地址( return address ) , 井将该帧(地址)压人调用栈,若在该函数返回之前又发生新的调用, 则同样地要将与新函数对应的一帧压人栈中, 成为新的栈顶,函数一旦运行完毕, 对应的帧随即弹出, 操作系统将把运行控制权交还给该函数的上层调用函数,并按照该帧中记录的返回地址确定在二进制程序中继续执行的位置。
因此, 在任一时刻调用栈中的各帧,分别对应于那些己被调用但尚未返回的函数实例,称作这时刻的活跃函数实例( active function instance ) ,只有一个。特别地,位于栈底的那帧必然对应于程序运行的入口主函数main() , 它从调用栈中弹出意味着整个程序的运行结束,此后控制权将交还给操作系统。
在 x86-64 中,所谓的栈,实际上一块内存区域,这个区域的数据进出满足先进后出的原则。越新入栈的数据,地址越低,所以栈顶的地址是最小的。下图中箭头所指的就是寄存器 %rsp 的值,这个寄存器是栈指针,用来记录栈顶的位置
我们假设一开始 %rsp 为红色,对于 push
操作,对应的是 pushq Src
指令,具体会完成下面三个步骤:
Src
中取出操作数这个时候 %rsp 就对应蓝色。
重来一次,假设一开始 %rsp 为红色,对于 pop
操作,对应的是 popq Dest
指令,具体会完成下面三个步骤:
Dest
中(这里必须是一个寄存器)这时候 %rsp 就对应黄色。
递归既可等效地理解为函数的自我调用
可以将递归实例视作一般的函数调用,井在调用栈中为其创建对应的一帧。 如此,同一个函数可能同时拥有多个实例, 并在调用栈中分别存有一帧。当然,即便是同一函数的不同实例,在各自的帧中对同名的参数或变量都有独立的副本, 故其数值不尽相同。
防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出
用好递归,要时刻明白函数是干什么用的!!!并且注意每一帧函数值和传入参数的改变
线性递归,二分递归,多分支递归等
思路:
- def hannotaMove(n,a,b,c):
- if n == 1:
- print("move",a,'-->',c) #递归基(非递归处理)base case of recursion,,可能不止一个
- else:
- hannotaMove(n-1,a,c,b)
- hannotaMove(1,a,b,c)
- hannotaMove(n-1,b,a,c)
- hannotaMove(4,'A','B','C')
1.二分递归版
- int fib(int n)//O(2^n):指数复杂度,实际中无意义
- {
- //若到达递归基,直接取值
- return (2>n)?(int)n:fib(n-1)+fib(n-2);
- }
2.线性递归版:改进
为什么会改进?因为将每次计算的结果存了起来,不用重复计算了
- int fib(int n, int &prev)//prev辅助变量记录前一项,复杂度O(n)
- {
- if(n == 0)
- {
- prev = 1;
- return 0;
- }
- else
- {
- int prevPrev;
- prev = fib(n-1,prevPrev);//递归计算前两项
- return prevPrev+prev;
- }
- }
3.动态规划版(循环问题用while往往起到意想不到的效果):借助少量的辅助空间,记录子问题的解答
动态规划的思想:
- int fibI(int n) //O(n)
- {
- int f=1,g=0; //初始化fib(1)=1,fib(0)=0
- while(0 < n--)
- {
- int temp=g;
- g+=f;
- f=temp;
- }
- return g;
- }
来源:https://blog.csdn.net/zcyzsy/article/details/77151709
普通递归需要压栈和出栈,时间空间消耗大;尾递归则没有(可以通过编译器优化);
解释:
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
原理:
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
以尾递归方式实现阶乘函数的实现:
- int facttail(int n, int res)
- {
- if (n < 0)
- return 0;
- else if(n == 0)
- return 1;
- else if(n == 1)
- return res;
- else
- return facttail(n - 1, n *res);
- }
res(初始化为1)维护递归层次的深度。这就让我们避免了每次还需要将返回值再乘以n。然而,在每次递归调用中,令res=n*res并且n=n-1。继续递归调用,直到n=1,这满足结束条件,此时直接返回res即可。
其实最精髓就是 通过参数传递结果,达到不压栈的目的
堆:地址增长(有 8MB 的大小限制,一般用来保存局部变量)
栈:地址减小(动态的内存分配会在这里处理,例如 malloc()
, calloc()
, new()
等)
然后是数据,指的是静态分配的数据,比如说全局变量,静态变量,常量字符串。最后是共享库等可执行的机器指令,这一部分是只读的。
可以见到,栈在最上面,也就是说,栈再往上就是另一个程序的内存范围了,这种时候我们就可以通过这种方式修改内存的其他部分了。
比如:
- typedef struct
- {
- int a[2];
- double d;
- } struct_t;
- double fun(int i)
- {
- volatile struct_t s;
- s.d = 3.14;
- s.a[i] = 1073741824; // 可能会越界
- return s.d;
- }
不同的 i 可能的执行结果是:
fun(0)
-> 3.14fun(1)
-> 3.14fun(2)
-> 3.1399998664856fun(3)
-> 2.00000061035156fun(4)
-> 3.14fun(6)
-> Segmentation fault之所以会产生这种错误,是因为访问内存的时候跨过了数组本身的界限修改了 d 的值。你没看错,这是个大问题!如果不检查输入字符串的长度,就很容易出现这种问题,尤其是针对在栈上有界限的字符数组。
在 Unix 中,gets()
函数的实现是这样的:
- // 从 stdin 中获取输入
- char *gets(char *dest)
- {
- int c = getchar();
- char *p = dest;
- while (c != EOF && c != '\n')
- {
- *p++ = c;
- c = getchar();
- }
- *p = '\0';
- return dest;
- }
可以看到并没有去检测最多能读入多少字符(于是很容易出问题),类似的情况还在 strcpy
, strcat
, scanf
, fscanf
, sscanf
中出现。比如说
- void echo() {
- char buf[4]; // 太小
- gets(buf);
- puts(buf);
- }
- void call_echo() {
- echo();
- }
我们来测试一下这个函数,可能的结果是:
- unix> ./echodemo
- Input: 012345678901234567890123
- Output: 012345678901234567890123
- unix> ./echodemo
- Input: 0123456789012345678901234
- Segmentation Fault
为什么明明在 echo()
中声明 buf
为 4 个 char,居然一开始输入这么多都没问题?我们到汇编代码里去看看:
- 00000000004006cf <echo>:
- 4006cf: 48 83 ec 18 sub $0x18, %rsp
- 4006d3: 48 89 e7 mov %rsp, %rdi
- 4006d6: e8 a5 ff ff ff callq 400680 <gets>
- 4006db: 48 89 e7 mov %rsp, %rdi
- 4006de: e8 3d fe ff ff callq 400520 <puts@plt>
- 4006e3: 48 83 c4 18 add $0x18, %rsp
- 4006e7: c3 retq
- # call_echo 部分
- 4006e8: 48 83 ec 08 sub $0x8, %rsp
- 4006ec: b8 00 00 00 00 mov $0x0, %eax
- 4006f1: e8 d9 ff ff ff callq 4006cf <echo>
- 4006f6: 48 83 c4 08 add $0x8, %rsp
- 4006fa: c3 retq
我们看 4006cf
这一行,可以发现实际上给 %rsp 分配了 0x18 的空间,所以可以容纳不止 4 个 char。
在调用 gets
函数之前(第 4006d6
行),内存中栈帧示意图为:
结合上面代码可以看到,call_echo
栈帧中保存着调用之前执行指令的地址 4006f6
,用于返回之后继续执行。我们输入字符串 01234567890123456789012
之后,栈帧中缓冲区被填充,如下:
虽然缓冲区溢出了,但是并没有损害当前的状态,程序还是可以继续运行(也就是没有出现段错误),但是如果再多一点的话,也就是输入 0123456789012345678901234
,内存中的情况是这样的:
就把返回地址给覆盖掉了,当 echo
执行完成要回到 call_echo
函数时,就跳转到 0x400034
这个内容未知的地址中了。也就是说,通过缓冲区溢出,我们可以在程序返回时跳转到任何我们想要跳转到的地方!攻击者可以利用这种方式来执行恶意代码!
那么我们现在来看看,怎么处理缓冲区溢出攻击,有几种方式:
第一种,避免缓冲区溢出,我们用更安全的方法,如:fgets
, strncpy
等等。
第二种,栈的位置不确定,让缓冲区溢出没办法影响到,并且每次位置都不一样,就不怕被暴力破解。并且也可以把一段内存标记为只读,那么就避免因为缓冲区溢出而导致的重写。
第三种,使用认证机制(Stack Canaries)。简单来说,就是在超出缓冲区的位置加一个特殊的值,如果发现这个值变化了,那么就知道出问题了。
但是,除了缓冲区溢出,还有另一种攻击的方式,称为返回导向编程[4]。可以利用修改已有的代码,来绕过系统和编译器的保护机制,攻击者控制堆栈调用以劫持程序控制流并执行针对性的机器语言指令序列(称为Gadgets)。每一段 gadget 通常结束于 return 指令,并位于共享库代码中的子程序。系列调用这些代码,攻击者可以在拥有更简单攻击防范的程序内执行任意操作。
具体利用缓冲区进行攻击的例子,会在【读厚 CSAPP】III Attack Lab 中进行讲解,这里不再赘述。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。