标签 堆栈 下的文章

昨天我和一些人在闲聊的时候,他们说他们并不真正了解栈是如何工作的,而且也不知道如何去查看栈空间。

这是一个快速教程,介绍如何使用 GDB 查看 C 程序的栈空间。我认为这对于 Rust 程序来说也是相似的。但我这里仍然使用 C 语言,因为我发现用它更简单,而且用 C 语言也更容易写出错误的程序。

我们的测试程序

这里是一个简单的 C 程序,声明了一些变量,从标准输入读取两个字符串。一个字符串在堆上,另一个字符串在栈上。

#include <stdio.h>
#include <stdlib.h>

int main() {
    char stack_string[10] = "stack";
    int x = 10;
    char *heap_string;

    heap_string = malloc(50);

    printf("Enter a string for the stack: ");
    gets(stack_string);
    printf("Enter a string for the heap: ");
    gets(heap_string);
    printf("Stack string is: %s\n", stack_string);
    printf("Heap string is: %s\n", heap_string);
    printf("x is: %d\n", x);
}

这个程序使用了一个你可能从来不会使用的极为不安全的函数 gets 。但我是故意这样写的。当出现错误的时候,你就知道是为什么了。

第 0 步:编译这个程序

我们使用 gcc -g -O0 test.c -o test 命令来编译这个程序。

-g 选项会在编译程序中将调式信息也编译进去。这将会使我们查看我们的变量更加容易。

-O0 选项告诉 gcc 不要进行优化,我要确保我们的 x 变量不会被优化掉。

第一步:启动 GDB

像这样启动 GDB:

$ gdb ./test

它打印出一些 GPL 信息,然后给出一个提示符。让我们在 main 函数这里设置一个断点:

(gdb) b main

然后我们就可以运行程序:

(gdb) b main
Starting program: /home/bork/work/homepage/test
Breakpoint 1, 0x000055555555516d in main ()

(gdb) run
Starting program: /home/bork/work/homepage/test

Breakpoint 1, main () at test.c:4
4   int main() {

好了,现在程序已经运行起来了。我们就可以开始查看栈空间了。

第二步:查看我们变量的地址

让我们从了解我们的变量开始。它们每个都在内存中有一个地址,我们可以像这样打印出来:

(gdb) p &x
$3 = (int *) 0x7fffffffe27c
(gdb) p &heap_string
$2 = (char **) 0x7fffffffe280
(gdb) p &stack_string
$4 = (char (*)[10]) 0x7fffffffe28e

因此,如果我们查看那些地址的堆栈,那我们应该能够看到所有的这些变量!

概念:栈指针

我们将需要使用栈指针,因此我将尽力对其进行快速解释。

有一个名为 ESP 的 x86 寄存器,称为“ 栈指针 stack pointer ”。 基本上,它是当前函数的栈起始地址。 在 GDB 中,你可以使用 $sp 来访问它。 当你调用新函数或从函数返回时,栈指针的值会更改。

第三步:在 main 函数开始的时候,我们查看一下在栈上的变量

首先,让我们看一下 main 函数开始时的栈。 现在是我们的堆栈指针的值:

(gdb) p $sp
$7 = (void *) 0x7fffffffe270

因此,我们当前函数的栈起始地址是 0x7fffffffe270,酷极了。

现在,让我们使用 GDB 打印出当前函数堆栈开始后的前 40 个字(即 160 个字节)。 某些内存可能不是栈的一部分,因为我不太确定这里的堆栈有多大。 但是至少开始的地方是栈的一部分。

我已粗体显示了 stack_stringheap_stringx 变量的位置,并改变了颜色:

  • x 是红色字体,并且起始地址是 0x7fffffffe27c
  • heap_string 是蓝色字体,起始地址是 0x7fffffffe280
  • stack_string 是紫色字体,起始地址是 0x7fffffffe28e

你可能会在这里注意到的一件奇怪的事情是 x 的值是 0x5555,但是我们将 x 设置为 10! 那是因为直到我们的 main 函数运行之后才真正设置 x ,而我们现在才到了 main 最开始的地方。

第三步:运行到第十行代码后,再次查看一下我们的堆栈

让我们跳过几行,等待变量实际设置为其初始化值。 到第 10 行时,x 应该设置为 10

首先我们需要设置另一个断点:

(gdb) b test.c:10
Breakpoint 2 at 0x5555555551a9: file test.c, line 11.

然后继续执行程序:

(gdb) continue
Continuing.

Breakpoint 2, main () at test.c:11
11      printf("Enter a string for the stack: ");

好的! 让我们再来看看堆栈里的内容! gdb 在这里格式化字节的方式略有不同,实际上我也不太关心这些(LCTT 译注:可以查看 GDB 手册中 x 命令,可以指定 c 来控制输出的格式)。 这里提醒一下你,我们的变量在栈上的位置:

  • x 是红色字体,并且起始地址是 0x7fffffffe27c
  • heap_string 是蓝色字体,起始地址是 0x7fffffffe280
  • stack_string 是紫色字体,起始地址是 0x7fffffffe28e

在继续往下看之前,这里有一些有趣的事情要讨论。

stack_string 在内存中是如何表示的

现在(第 10 行),stack_string 被设置为字符串stack。 让我们看看它在内存中的表示方式。

我们可以像这样打印出字符串中的字节(LCTT 译注:可以通过 c 选项直接显示为字符):

(gdb) x/10x stack_string
0x7fffffffe28e: 0x73    0x74    0x61    0x63    0x6b    0x00    0x00    0x00
0x7fffffffe296: 0x00    0x00

stack 是一个长度为 5 的字符串,相对应 5 个 ASCII 码- 0x730x740x610x630x6b0x73 是字符 s 的 ASCII 码。 0x74t 的 ASCII 码。等等...

同时我们也使用 x/1s 可以让 GDB 以字符串的方式显示:

(gdb) x/1s stack_string
0x7fffffffe28e: "stack"

heap_stringstack_string 有何不同

你已经注意到了 stack_stringheap_string 在栈上的表示非常不同:

  • stack_string 是一段字符串内容(stack
  • heap_string 是一个指针,指向内存中的某个位置

这里是 heap_string 变量在内存中的内容:

0xa0  0x92  0x55  0x55  0x55  0x55  0x00  0x00

这些字节实际上应该是从右向左读:因为 x86 是小端模式,因此,heap_string 中所存放的内存地址 0x5555555592a0

另一种方式查看 heap_string 中存放的内存地址就是使用 p 命令直接打印 :

(gdb) p heap_string
$6 = 0x5555555592a0 ""

整数 x 的字节表示

x 是一个 32 位的整数,可由 0x0a 0x00 0x00 0x00 来表示。

我们还是需要反向来读取这些字节(和我们读取 heap_string 需要反过来读是一样的),因此这个数表示的是 0x000000000a 或者是 0x0a,它是一个数字 10;

这就让我把把 x 设置成了 10

第四步:从标准输入读取

好了,现在我们已经初始化我们的变量,我们来看一下当这段程序运行的时候,栈空间会如何变化:

printf("Enter a string for the stack: ");
gets(stack_string);
printf("Enter a string for the heap: ");
gets(heap_string);

我们需要设置另外一个断点:

(gdb) b test.c:16
Breakpoint 3 at 0x555555555205: file test.c, line 16.

然后继续执行程序:

(gdb) continue
Continuing.

我们输入两个字符串,为栈上存储的变量输入 123456789012 并且为在堆上存储的变量输入 bananas;

让我们先来看一下 stack_string(这里有一个缓存区溢出)

(gdb) x/1s stack_string
0x7fffffffe28e: "123456789012"

这看起来相当正常,对吗?我们输入了 12345679012,然后现在它也被设置成了 12345679012(LCTT 译注:实测 gcc 8.3 环境下,会直接段错误)。

但是现在有一些很奇怪的事。这是我们程序的栈空间的内容。有一些紫色高亮的内容。

令人奇怪的是 stack_string 只支持 10 个字节。但是现在当我们输入了 13 个字符以后,发生了什么?

这是一个典型的缓冲区溢出,stack_string 将自己的数据写在了程序中的其他地方。在我们的案例中,这还没有造成问题,但它会使你的程序崩溃,或者更糟糕的是,使你面临非常糟糕的安全问题。

例如,假设 stack_string 在内存里的位置刚好在 heap_string 之前。那我们就可能覆盖 heap_string 所指向的地址。我并不确定 stack_string 之后的内存里有一些什么。但我们也许可以用它来做一些诡异的事情。

确实检测到了有缓存区溢出

当我故意写很多字符的时候:

 ./test
Enter a string for the stack: 01234567891324143
Enter a string for the heap: adsf
Stack string is: 01234567891324143
Heap string is: adsf
x is: 10
*** stack smashing detected ***: terminated
fish: Job 1, './test' terminated by signal SIGABRT (Abort)

这里我猜是 stack_string 已经到达了这个函数栈的底部,因此额外的字符将会被写在另一块内存中。

当你故意去使用这个安全漏洞时,它被称为“堆栈粉碎”,而且不知何故有东西在检测这种情况的发生。

我也觉得这很有趣,虽然程序被杀死了,但是当缓冲区溢出发生时它不会立即被杀死——在缓冲区溢出之后再运行几行代码,程序才会被杀死。 好奇怪!

这些就是关于缓存区溢出的所有内容。

现在我们来看一下 heap_string

我们仍然将 bananas 输入到 heap_string 变量中。让我们来看一下内存中的样子。

这是在我们读取了字符串以后,heap_string 在栈空间上的样子:

需要注意的是,这里的值是一个地址。并且这个地址并没有改变,但是我们来看一下指向的内存上的内容。

(gdb) x/10x 0x5555555592a0
0x5555555592a0: 0x62    0x61    0x6e    0x61    0x6e    0x61    0x73    0x00
0x5555555592a8: 0x00    0x00

看到了吗,这就是字符串 bananas 的字节表示。这些字节并不在栈空间上。他们存在于内存中的堆上。

堆和栈到底在哪里?

我们已经讨论过栈和堆是不同的内存区域,但是你怎么知道它们在内存中的位置呢?

每个进程都有一个名为 /proc/$PID/maps 的文件,它显示了每个进程的内存映射。 在这里你可以看到其中的栈和堆。

$ cat /proc/24963/maps
... lots of stuff omitted ...
555555559000-55555557a000 rw-p 00000000 00:00 0                          [heap]
... lots of stuff omitted ...
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]

需要注意的一件事是,这里堆地址以 0x5555 开头,栈地址以 0x7fffff 开头。 所以很容易区分栈上的地址和堆上的地址之间的区别。

像这样使用 gdb 真的很有帮助

这有点像旋风之旅,虽然我没有解释所有内容,但希望看到数据在内存中的实际情况可以使你更清楚地了解堆栈的实际情况。

我真的建议像这样来把玩一下 gdb —— 即使你不理解你在内存中看到的每一件事,我发现实际上像这样看到我程序内存中的数据会使抽象的概念,比如“栈”和“堆”和“指针”更容易理解。

更多练习

一些关于思考栈的后续练习的想法(没有特定的顺序):

  • 尝试将另一个函数添加到 test.c 并在该函数的开头创建一个断点,看看是否可以从 main 中找到堆栈! 他们说当你调用一个函数时“堆栈会变小”,你能在 gdb 中看到这种情况吗?
  • 从函数返回一个指向栈上字符串的指针,看看哪里出了问题。 为什么返回指向栈上字符串的指针是不好的?
  • 尝试在 C 中引起堆栈溢出,并尝试通过在 gdb 中查看堆栈溢出来准确理解会发生什么!
  • 查看 Rust 程序中的堆栈并尝试找到变量!
  • 噩梦课程 中尝试一些缓冲区溢出挑战。每个问题的答案写在 README 文件中,因此如果你不想被宠坏,请避免先去看答案。 所有这些挑战的想法是给你一个二进制文件,你需要弄清楚如何导致缓冲区溢出以使其打印出 flag 字符串。

via: https://jvns.ca/blog/2021/05/17/how-to-look-at-the-stack-in-gdb/

作者:Julia Evans 选题:lujun9972 译者:amwps290 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在探秘“栈”的倒数第二篇文章中,我们提到了 尾调用 tail call 、编译优化、以及新发布的 JavaScript 上 合理尾调用 proper tail call

当一个函数 F 调用另一个函数作为它的结束动作时,就发生了一个尾调用。在那个时间点,函数 F 绝对不会有多余的工作:函数 F 将“球”传给被它调用的任意函数之后,它自己就“消失”了。这就是关键点,因为它打开了尾调用优化的“可能之门”:我们可以简单地重用函数 F 的栈帧,而不是为函数调用 创建一个新的栈帧,因此节省了栈空间并且避免了新建一个栈帧所需要的工作量。下面是一个用 C 写的简单示例,然后使用 mild 优化 来编译它的结果:

int add5(int a)
{
    return a + 5;
}

int add10(int a)
{
    int b = add5(a); // not tail
    return add5(b); // tail
}

int add5AndTriple(int a){
    int b = add5(a); // not tail
    return 3 * add5(a); // not tail, doing work after the call
}

int finicky(int a){
    if (a > 10){
        return add5AndTriple(a); // tail
    }

    if (a > 5){
        int b = add5(a); // not tail
        return finicky(b); // tail
    }

    return add10(a); // tail
}

简单的尾调用 下载

在编译器的输出中,在预期会有一个 调用 的地方,你可以看到一个 跳转 指令,一般情况下你可以发现尾调用优化(以下简称 TCO)。在运行时中,TCO 将会引起调用栈的减少。

一个通常认为的错误观念是,尾调用必须要 递归。实际上并不是这样的:一个尾调用可以被递归,比如在上面的 finicky() 中,但是,并不是必须要使用递归的。在调用点只要函数 F 完成它的调用,我们将得到一个单独的尾调用。是否能够进行优化这是一个另外的问题,它取决于你的编程环境。

“是的,它总是可以!”,这是我们所希望的最佳答案,它是著名的 Scheme 中的方式,就像是在 SICP上所讨论的那样(顺便说一声,如果你的程序不像“一个魔法师使用你的咒语召唤你的电脑精灵”那般有效,建议你读一下这本书)。它也是 Lua 的方式。而更重要的是,它是下一个版本的 JavaScript —— ES6 的方式,这个规范清晰地定义了尾的位置,并且明确了优化所需要的几个条件,比如,严格模式。当一个编程语言保证可用 TCO 时,它将支持 合理尾调用 proper tail call

现在,我们中的一些人不能抛开那些 C 的习惯,心脏出血,等等,而答案是一个更复杂的“有时候”,它将我们带进了编译优化的领域。我们看一下上面的那个 简单示例;把我们 上篇文章 的阶乘程序重新拿出来:

#include  <stdio.h>

int factorial(int n)
{
    int previous = 0xdeadbeef;

    if (n == 0 || n == 1) {
        return 1;
    }

    previous = factorial(n-1);
    return n * previous;
}

int main(int argc)
{
    int answer = factorial(5);
    printf("%d\n", answer);
}

递归阶乘 下载

像第 11 行那样的,是尾调用吗?答案是:“不是”,因为它被后面的 n 相乘了。但是,如果你不去优化它,GCC 使用 O2 优化结果 会让你震惊:它不仅将阶乘转换为一个 无递归循环,而且 factorial(5) 调用被整个消除了,而以一个 120 (5! == 120) 的 编译时常数来替换。这就是调试优化代码有时会很难的原因。好的方面是,如果你调用这个函数,它将使用一个单个的栈帧,而不会去考虑 n 的初始值。编译算法是非常有趣的,如果你对它感兴趣,我建议你去阅读 构建一个优化编译器ACDI

但是,这里没有做尾调用优化时到底发生了什么?通过分析函数的功能和无需优化的递归发现,GCC 比我们更聪明,因为一开始就没有使用尾调用。由于过于简单以及很确定的操作,这个任务变得很简单。我们给它增加一些可以引起混乱的东西(比如,getpid()),我们给 GCC 增加难度:

#include <stdio.h> 
#include <sys/types.h>
#include <unistd.h>

int pidFactorial(int n)
{
    if (1 == n) {
        return getpid(); // tail
    }

    return n * pidFactorial(n-1) * getpid(); // not tail
}

int main(int argc)
{
    int answer = pidFactorial(5);
    printf("%d\n", answer);
}

递归 PID 阶乘 下载

优化它,unix 精灵!现在,我们有了一个常规的 递归调用 并且这个函数分配 O(n) 栈帧来完成工作。GCC 在递归的基础上仍然 为 getpid 使用了 TCO。如果我们现在希望让这个函数尾调用递归,我需要稍微变一下:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int tailPidFactorial(int n, int acc)
{
    if (1 == n) {
        return acc * getpid(); // not tail
    }

    acc = (acc * getpid() * n);
    return tailPidFactorial(n-1, acc); // tail
}

int main(int argc)
{
    int answer = tailPidFactorial(5, 1);
    printf("%d\n", answer);
}

tailPidFactorial.c 下载

现在,结果的累加是 一个循环,并且我们获得了真实的 TCO。但是,在你庆祝之前,我们能说一下关于在 C 中的一般情形吗?不幸的是,虽然优秀的 C 编译器在大多数情况下都可以实现 TCO,但是,在一些情况下它们仍然做不到。例如,正如我们在 函数序言 中所看到的那样,函数调用者在使用一个标准的 C 调用规则调用一个函数之后,它要负责去清理栈。因此,如果函数 F 带了两个参数,它只能使 TCO 调用的函数使用两个或者更少的参数。这是 TCO 的众多限制之一。Mark Probst 写了一篇非常好的论文,他们讨论了 在 C 中的合理尾递归,在这篇论文中他们讨论了这些属于 C 栈行为的问题。他也演示一些 疯狂的、很酷的欺骗方法

“有时候” 对于任何一种关系来说都是不坚定的,因此,在 C 中你不能依赖 TCO。它是一个在某些地方可以或者某些地方不可以的离散型优化,而不是像合理尾调用一样的编程语言的特性,虽然在实践中可以使用编译器来优化绝大部分的情形。但是,如果你想必须要实现 TCO,比如将 Scheme 转译 transpilation 成 C,你将会 很痛苦

因为 JavaScript 现在是非常流行的转译对象,合理尾调用比以往更重要。因此,对 ES6 及其提供的许多其它的重大改进的赞誉并不为过。它就像 JS 程序员的圣诞节一样。

这就是尾调用和编译优化的简短结论。感谢你的阅读,下次再见!


via:https://manybutfinite.com/post/tail-calls-optimization-es6/

作者:Gustavo Duarte 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

早些时候,我们探索了 “内存中的程序之秘”,我们欣赏了在一台电脑中是如何运行我们的程序的。今天,我们去探索栈的调用,它在大多数编程语言和虚拟机中都默默地存在。在此过程中,我们将接触到一些平时很难见到的东西,像 闭包 closure 、递归、以及缓冲溢出等等。但是,我们首先要作的事情是,描绘出栈是如何运作的。

栈非常重要,因为它追踪着一个程序中运行的函数,而函数又是一个软件的重要组成部分。事实上,程序的内部操作都是非常简单的。它大部分是由函数向栈中推入数据或者从栈中弹出数据的相互调用组成的,而在堆上为数据分配内存才能在跨函数的调用中保持数据。不论是低级的 C 软件还是像 JavaScript 和 C# 这样的基于虚拟机的语言,它们都是这样的。而对这些行为的深刻理解,对排错、性能调优以及大概了解究竟发生了什么是非常重要的。

当一个函数被调用时,将会创建一个 栈帧 stack frame 去支持函数的运行。这个栈帧包含函数的局部变量和调用者传递给它的参数。这个栈帧也包含了允许被调用的函数(callee)安全返回给其调用者的内部事务信息。栈帧的精确内容和结构因处理器架构和函数调用规则而不同。在本文中我们以 Intel x86 架构和使用 C 风格的函数调用(cdecl)的栈为例。下图是一个处于栈顶部的一个单个栈帧:

在图上的场景中,有三个 CPU 寄存器进入栈。 栈指针 stack pointer esp(LCTT 译注:扩展栈指针寄存器) 指向到栈的顶部。栈的顶部总是被最后一个推入到栈且还没有弹出的东西所占据,就像现实世界中堆在一起的一叠盘子或者 100 美元大钞一样。

保存在 esp 中的地址始终在变化着,因为栈中的东西不停被推入和弹出,而它总是指向栈中的最后一个推入的东西。许多 CPU 指令的一个副作用就是自动更新 esp,离开寄存器而使用栈是行不通的。

在 Intel 的架构中,绝大多数情况下,栈的增长是向着低位内存地址的方向。因此,这个“顶部” 在包含数据的栈中是处于低位的内存地址(在这种情况下,包含的数据是 local_buffer)。注意,关于从 esplocal_buffer 的箭头不是随意连接的。这个箭头代表着事务:它专门指向到由 local_buffer 所拥有的第一个字节,因为,那是一个保存在 esp 中的精确地址。

第二个寄存器跟踪的栈是 ebp(LCTT 译注:扩展基址指针寄存器),它包含一个 基指针 base pointer 或者称为 帧指针 frame pointer 。它指向到一个当前运行的函数的栈帧内的固定位置,并且它为参数和局部变量的访问提供一个稳定的参考点(基址)。仅当开始或者结束调用一个函数时,ebp 的内容才会发生变化。因此,我们可以很容易地处理在栈中的从 ebp 开始偏移后的每个东西。如图所示。

不像 espebp 大多数情况下是在程序代码中通过花费很少的 CPU 来进行维护的。有时候,完成抛弃 ebp 有一些性能优势,可以通过 编译标志 来做到这一点。Linux 内核就是一个这样做的示例。

最后,eax(LCTT 译注:扩展的 32 位通用数据寄存器)寄存器惯例被用来转换大多数 C 数据类型返回值给调用者。

现在,我们来看一下在我们的栈帧中的数据。下图清晰地按字节展示了字节的内容,就像你在一个调试器中所看到的内容一样,内存是从左到右、从顶部至底部增长的,如下图所示:

局部变量 local_buffer 是一个字节数组,包含一个由 null 终止的 ASCII 字符串,这是 C 程序中的一个基本元素。这个字符串可以读取自任意地方,例如,从键盘输入或者来自一个文件,它只有 7 个字节的长度。因为,local_buffer 只能保存 8 字节,所以还剩下 1 个未使用的字节。这个字节的内容是未知的,因为栈不断地推入和弹出,除了你写入的之外,你根本不会知道内存中保存了什么。这是因为 C 编译器并不为栈帧初始化内存,所以它的内容是未知的并且是随机的 —— 除非是你自己写入。这使得一些人对此很困惑。

再往上走,local1 是一个 4 字节的整数,并且你可以看到每个字节的内容。它似乎是一个很大的数字,在8 后面跟着的都是零,在这里可能会误导你。

Intel 处理器是 小端 little endian 机器,这表示在内存中的数字也是首先从小的一端开始的。因此,在一个多字节数字中,较小的部分在内存中处于最低端的地址。因为一般情况下是从左边开始显示的,这背离了我们通常的数字表示方式。我们讨论的这种从小到大的机制,使我想起《格里佛游记》:就像小人国的人们吃鸡蛋是从小头开始的一样,Intel 处理器处理它们的数字也是从字节的小端开始的。

因此,local1 事实上只保存了一个数字 8,和章鱼的腿数量一样。然而,param1 在第二个字节的位置有一个值 2,因此,它的数学上的值是 2 * 256 = 512(我们与 256 相乘是因为,每个位置值的范围都是从 0 到 255)。同时,param2 承载的数量是 1 * 256 * 256 = 65536

这个栈帧的内部数据是由两个重要的部分组成:前一个栈帧的地址(保存的 ebp 值)和函数退出才会运行的指令的地址(返回地址)。它们一起确保了函数能够正常返回,从而使程序可以继续正常运行。

现在,我们来看一下栈帧是如何产生的,以及去建立一个它们如何共同工作的内部蓝图。首先,栈的增长是非常令人困惑的,因为它与你你预期的方式相反。例如,在栈上分配一个 8 字节,就要从 esp 减去 8,去,而减法是与增长不同的奇怪方式。

我们来看一个简单的 C 程序:

Simple Add Program - add.c

int add(int a, int b)
{
    int result = a + b;
    return result;
}

int main(int argc)
{
    int answer;
    answer = add(40, 2);
}

简单的加法程序 - add.c

假设我们在 Linux 中不使用命令行参数去运行它。当你运行一个 C 程序时,实际运行的第一行代码是在 C 运行时库里,由它来调用我们的 main 函数。下图展示了程序运行时每一步都发生了什么。每个图链接的 GDB 输出展示了内存和寄存器的状态。你也可以看到所使用的 GDB 命令,以及整个 GDB 输出。如下:

第 2 步和第 3 步,以及下面的第 4 步,都只是函数的 序言 prologue ,几乎所有的函数都是这样的:ebp 的当前值被保存到了栈的顶部,然后,将 esp 的内容拷贝到 ebp,以建立一个新的栈帧。main 的序言和其它函数一样,但是,不同之处在于,当程序启动时 ebp 被清零。

如果你去检查栈下方(右边)的整形变量(argc),你将找到更多的数据,包括指向到程序名和命令行参数(传统的 C 的 argv)、以及指向 Unix 环境变量以及它们真实的内容的指针。但是,在这里这些并不是重点,因此,继续向前调用 add()

mainesp 减去 12 之后得到它所需的栈空间,它为 ab 设置值。在内存中的值展示为十六进制,并且是小端格式,与你从调试器中看到的一样。一旦设置了参数值,main 将调用 add,并且开始运行:

现在,有一点小激动!我们进入了另一个函数序言,但这次你可以明确看到栈帧是如何从 ebp 到栈建立一个链表。这就是调试器和高级语言中的 Exception 对象如何对它们的栈进行跟踪的。当一个新帧产生时,你也可以看到更多这种典型的从 ebpesp 的捕获。我们再次从 esp 中做减法得到更多的栈空间。

ebp 寄存器的值拷贝到内存时,这里也有一个稍微有些怪异的字节逆转。在这里发生的奇怪事情是,寄存器其实并没有字节顺序:因为对于内存,没有像寄存器那样的“增长的地址”。因此,惯例上调试器以对人类来说最自然的格式展示了寄存器的值:数位从最重要的到最不重要。因此,这个在小端机器中的副本的结果,与内存中常用的从左到右的标记法正好相反。我想用图去展示你将会看到的东西,因此有了下面的图。

在比较难懂的部分,我们增加了注释:

这是一个临时寄存器,用于帮你做加法,因此没有什么警报或者惊喜。对于加法这样的作业,栈的动作正好相反,我们留到下次再讲。

对于任何读到这里的人都应该有一个小礼物,因此,我做了一个大的图表展示了 组合到一起的所有步骤

一旦把它们全部布置好了,看上起似乎很乏味。这些小方框给我们提供了很多帮助。事实上,在计算机科学中,这些小方框是主要的展示工具。我希望这些图片和寄存器的移动能够提供一种更直观的构想图,将栈的增长和内存的内容整合到一起。从软件的底层运作来看,我们的软件与一个简单的图灵机器差不多。

这就是我们栈探秘的第一部分,再讲一些内容之后,我们将看到构建在这个基础上的高级编程的概念。下周见!


via:https://manybutfinite.com/post/journey-to-the-stack/

作者:Gustavo Duarte 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 Linux 内核中发现了一个名为 “Stack Clash” 的严重安全问题,攻击者能够利用它来破坏内存数据并执行任意代码。攻击者可以利用这个及另一个漏洞来执行任意代码并获得管理帐户(root)权限。

在 Linux 中该如何解决这个问题?

the-stack-clash-on-linux-openbsd-netbsd-freebsd-solaris

Qualys 研究实验室在 GNU C Library(CVE-2017-1000366)的动态链接器中发现了许多问题,它们通过与 Linux 内核内的堆栈冲突来允许本地特权升级。这个 bug 影响到了 i386 和 amd64 上的 Linux、OpenBSD、NetBSD、FreeBSD 和 Solaris。攻击者可以利用它来破坏内存数据并执行任意代码。

什么是 CVE-2017-1000364 bug?

来自 RHN

在用户空间二进制文件的堆栈中分配内存的方式发现了一个缺陷。如果堆(或不同的内存区域)和堆栈内存区域彼此相邻,则攻击者可以使用此缺陷跳过堆栈保护区域,从而导致进程堆栈或相邻内存区域的受控内存损坏,从而增加其系统权限。有一个在内核中减轻这个漏洞的方法,将堆栈保护区域大小从一页增加到 1 MiB,从而使成功利用这个功能变得困难。

据原研究文章

计算机上运行的每个程序都使用一个称为堆栈的特殊内存区域。这个内存区域是特别的,因为当程序需要更多的堆栈内存时,它会自动增长。但是,如果它增长太多,并且与另一个内存区域太接近,程序可能会将堆栈与其他内存区域混淆。攻击者可以利用这种混乱来覆盖其他内存区域的堆栈,或者反过来。

受到影响的 Linux 发行版

  1. Red Hat Enterprise Linux Server 5.x
  2. Red Hat Enterprise Linux Server 6.x
  3. Red Hat Enterprise Linux Server 7.x
  4. CentOS Linux Server 5.x
  5. CentOS Linux Server 6.x
  6. CentOS Linux Server 7.x
  7. Oracle Enterprise Linux Server 5.x
  8. Oracle Enterprise Linux Server 6.x
  9. Oracle Enterprise Linux Server 7.x
  10. Ubuntu 17.10
  11. Ubuntu 17.04
  12. Ubuntu 16.10
  13. Ubuntu 16.04 LTS
  14. Ubuntu 12.04 ESM (Precise Pangolin)
  15. Debian 9 stretch
  16. Debian 8 jessie
  17. Debian 7 wheezy
  18. Debian unstable
  19. SUSE Linux Enterprise Desktop 12 SP2
  20. SUSE Linux Enterprise High Availability 12 SP2
  21. SUSE Linux Enterprise Live Patching 12
  22. SUSE Linux Enterprise Module for Public Cloud 12
  23. SUSE Linux Enterprise Build System Kit 12 SP2
  24. SUSE Openstack Cloud Magnum Orchestration 7
  25. SUSE Linux Enterprise Server 11 SP3-LTSS
  26. SUSE Linux Enterprise Server 11 SP4
  27. SUSE Linux Enterprise Server 12 SP1-LTSS
  28. SUSE Linux Enterprise Server 12 SP2
  29. SUSE Linux Enterprise Server for Raspberry Pi 12 SP2

我需要重启我的电脑么?

是的,由于大多数服务依赖于 GNU C Library 的动态连接器,并且内核自身需要在内存中重新加载。

我该如何在 Linux 中修复 CVE-2017-1000364?

根据你的 Linux 发行版来输入命令。你需要重启电脑。在应用补丁之前,记下你当前内核的版本:

$ uname -a
$ uname -mrs

示例输出:

Linux 4.4.0-78-generic x86_64

Debian 或者 Ubuntu Linux

输入下面的 apt 命令 / apt-get 命令来应用更新:

$ sudo apt-get update && sudo apt-get upgrade && sudo apt-get dist-upgrade

示例输出:

Reading package lists... Done
Building dependency tree       
Reading state information... Done
Calculating upgrade... Done
The following packages will be upgraded:
  libc-bin libc-dev-bin libc-l10n libc6 libc6-dev libc6-i386 linux-compiler-gcc-6-x86 linux-headers-4.9.0-3-amd64 linux-headers-4.9.0-3-common linux-image-4.9.0-3-amd64
  linux-kbuild-4.9 linux-libc-dev locales multiarch-support
14 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Need to get 0 B/62.0 MB of archives.
After this operation, 4,096 B of additional disk space will be used.
Do you want to continue? [Y/n] y
Reading changelogs... Done
Preconfiguring packages ...
(Reading database ... 115123 files and directories currently installed.)
Preparing to unpack .../libc6-i386_2.24-11+deb9u1_amd64.deb ...
Unpacking libc6-i386 (2.24-11+deb9u1) over (2.24-11) ...
Preparing to unpack .../libc6-dev_2.24-11+deb9u1_amd64.deb ...
Unpacking libc6-dev:amd64 (2.24-11+deb9u1) over (2.24-11) ...
Preparing to unpack .../libc-dev-bin_2.24-11+deb9u1_amd64.deb ...
Unpacking libc-dev-bin (2.24-11+deb9u1) over (2.24-11) ...
Preparing to unpack .../linux-libc-dev_4.9.30-2+deb9u1_amd64.deb ...
Unpacking linux-libc-dev:amd64 (4.9.30-2+deb9u1) over (4.9.30-2) ...
Preparing to unpack .../libc6_2.24-11+deb9u1_amd64.deb ...
Unpacking libc6:amd64 (2.24-11+deb9u1) over (2.24-11) ...
Setting up libc6:amd64 (2.24-11+deb9u1) ...
(Reading database ... 115123 files and directories currently installed.)
Preparing to unpack .../libc-bin_2.24-11+deb9u1_amd64.deb ...
Unpacking libc-bin (2.24-11+deb9u1) over (2.24-11) ...
Setting up libc-bin (2.24-11+deb9u1) ...
(Reading database ... 115123 files and directories currently installed.)
Preparing to unpack .../multiarch-support_2.24-11+deb9u1_amd64.deb ...
Unpacking multiarch-support (2.24-11+deb9u1) over (2.24-11) ...
Setting up multiarch-support (2.24-11+deb9u1) ...
(Reading database ... 115123 files and directories currently installed.)
Preparing to unpack .../0-libc-l10n_2.24-11+deb9u1_all.deb ...
Unpacking libc-l10n (2.24-11+deb9u1) over (2.24-11) ...
Preparing to unpack .../1-locales_2.24-11+deb9u1_all.deb ...
Unpacking locales (2.24-11+deb9u1) over (2.24-11) ...
Preparing to unpack .../2-linux-compiler-gcc-6-x86_4.9.30-2+deb9u1_amd64.deb ...
Unpacking linux-compiler-gcc-6-x86 (4.9.30-2+deb9u1) over (4.9.30-2) ...
Preparing to unpack .../3-linux-headers-4.9.0-3-amd64_4.9.30-2+deb9u1_amd64.deb ...
Unpacking linux-headers-4.9.0-3-amd64 (4.9.30-2+deb9u1) over (4.9.30-2) ...
Preparing to unpack .../4-linux-headers-4.9.0-3-common_4.9.30-2+deb9u1_all.deb ...
Unpacking linux-headers-4.9.0-3-common (4.9.30-2+deb9u1) over (4.9.30-2) ...
Preparing to unpack .../5-linux-kbuild-4.9_4.9.30-2+deb9u1_amd64.deb ...
Unpacking linux-kbuild-4.9 (4.9.30-2+deb9u1) over (4.9.30-2) ...
Preparing to unpack .../6-linux-image-4.9.0-3-amd64_4.9.30-2+deb9u1_amd64.deb ...
Unpacking linux-image-4.9.0-3-amd64 (4.9.30-2+deb9u1) over (4.9.30-2) ...
Setting up linux-libc-dev:amd64 (4.9.30-2+deb9u1) ...
Setting up linux-headers-4.9.0-3-common (4.9.30-2+deb9u1) ...
Setting up libc6-i386 (2.24-11+deb9u1) ...
Setting up linux-compiler-gcc-6-x86 (4.9.30-2+deb9u1) ...
Setting up linux-kbuild-4.9 (4.9.30-2+deb9u1) ...
Setting up libc-l10n (2.24-11+deb9u1) ...
Processing triggers for man-db (2.7.6.1-2) ...
Setting up libc-dev-bin (2.24-11+deb9u1) ...
Setting up linux-image-4.9.0-3-amd64 (4.9.30-2+deb9u1) ...
/etc/kernel/postinst.d/initramfs-tools:
update-initramfs: Generating /boot/initrd.img-4.9.0-3-amd64
cryptsetup: WARNING: failed to detect canonical device of /dev/md0
cryptsetup: WARNING: could not determine root device from /etc/fstab
W: initramfs-tools configuration sets RESUME=UUID=054b217a-306b-4c18-b0bf-0ed85af6c6e1
W: but no matching swap device is available.
I: The initramfs will attempt to resume from /dev/md1p1
I: (UUID=bf72f3d4-3be4-4f68-8aae-4edfe5431670)
I: Set the RESUME variable to override this.
/etc/kernel/postinst.d/zz-update-grub:
Searching for GRUB installation directory ... found: /boot/grub
Searching for default file ... found: /boot/grub/default
Testing for an existing GRUB menu.lst file ... found: /boot/grub/menu.lst
Searching for splash image ... none found, skipping ...
Found kernel: /boot/vmlinuz-4.9.0-3-amd64
Found kernel: /boot/vmlinuz-3.16.0-4-amd64
Updating /boot/grub/menu.lst ... done

Setting up libc6-dev:amd64 (2.24-11+deb9u1) ...
Setting up locales (2.24-11+deb9u1) ...
Generating locales (this might take a while)...
  en_IN.UTF-8... done
Generation complete.
Setting up linux-headers-4.9.0-3-amd64 (4.9.30-2+deb9u1) ...
Processing triggers for libc-bin (2.24-11+deb9u1) ...

使用 reboot 命令重启桌面/服务器:

$ sudo reboot

Oracle/RHEL/CentOS/Scientific Linux

输入下面的 yum 命令

$ sudo yum update
$ sudo reboot

Fedora Linux

输入下面的 dnf 命令:

$ sudo dnf update
$ sudo reboot

Suse Enterprise Linux 或者 Opensuse Linux

输入下面的 zypper 命令:

$ sudo zypper patch
$ sudo reboot

SUSE OpenStack Cloud 6

$ sudo zypper in -t patch SUSE-OpenStack-Cloud-6-2017-996=1
$ sudo reboot

SUSE Linux Enterprise Server for SAP 12-SP1

$ sudo zypper in -t patch SUSE-SLE-SAP-12-SP1-2017-996=1
$ sudo reboot

SUSE Linux Enterprise Server 12-SP1-LTSS

$ sudo zypper in -t patch SUSE-SLE-SERVER-12-SP1-2017-996=1
$ sudo reboot

SUSE Linux Enterprise Module for Public Cloud 12

$ sudo zypper in -t patch SUSE-SLE-Module-Public-Cloud-12-2017-996=1
$ sudo reboot

验证

你需要确认你的版本号在 reboot 命令之后改变了。

$ uname -a
$ uname -r
$ uname -mrs

示例输出:

Linux 4.4.0-81-generic x86_64

给 OpenBSD 用户的注意事项

此页获取更多信息。

给 Oracle Solaris 的注意事项

见此页获取更多信息。

参考


作者简介:

Vivek Gite

作者是 nixCraft 的创始人,对于 Linux 操作系统/Unix shell脚本有经验丰富的系统管理员和培训师。他曾与全球客户及各行各业,包括 IT、教育、国防和空间研究以及非营利部门合作。在 TwitterFacebookGoogle + 上关注他。


via: https://www.cyberciti.biz/faq/howto-patch-linux-kernel-stack-clash-vulnerability-cve-2017-1000364/

作者:Vivek Gite 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出