@amwps290
This commit is contained in:
Xingyu Wang 2021-07-05 10:38:09 +08:00
parent 024dd65cdd
commit 1734a7cc07

View File

@ -2,15 +2,17 @@
[#]: via: "https://jvns.ca/blog/2021/05/17/how-to-look-at-the-stack-in-gdb/" [#]: via: "https://jvns.ca/blog/2021/05/17/how-to-look-at-the-stack-in-gdb/"
[#]: author: "Julia Evans https://jvns.ca/" [#]: author: "Julia Evans https://jvns.ca/"
[#]: collector: "lujun9972" [#]: collector: "lujun9972"
[#]: translator: " " [#]: translator: "amwps290"
[#]: reviewer: " " [#]: reviewer: "wxy"
[#]: publisher: " " [#]: publisher: " "
[#]: url: " " [#]: url: " "
使用 GDB 查看程序栈空间 使用 GDB 查看程序栈空间
====== ======
昨天我和一些人在闲聊的时候,他们说他们并不了解栈是如何工作的,而且也不知道如何去查看栈空间。 ![](https://img.linux.net.cn/data/attachment/album/202107/05/103738f00boylephggpeyh.jpg)
昨天我和一些人在闲聊的时候,他们说他们并不真正了解栈是如何工作的,而且也不知道如何去查看栈空间。
这是一个快速教程,介绍如何使用 GDB 查看 C 程序的栈空间。我认为这对于 Rust 程序来说也是相似的。但我这里仍然使用 C 语言,因为我发现用它更简单,而且用 C 语言也更容易写出错误的程序。 这是一个快速教程,介绍如何使用 GDB 查看 C 程序的栈空间。我认为这对于 Rust 程序来说也是相似的。但我这里仍然使用 C 语言,因为我发现用它更简单,而且用 C 语言也更容易写出错误的程序。
@ -51,13 +53,13 @@ int main() {
### 第一步:启动 GDB ### 第一步:启动 GDB
像这样启动 GDB 像这样启动 GDB
``` ```
$ gdb ./test $ gdb ./test
``` ```
它打印出一些 GPL 东西,然后给出一个提示符。让我们在 `main` 函数这里设置一个断点 它打印出一些 GPL 信息,然后给出一个提示符。让我们在 `main` 函数这里设置一个断点
``` ```
(gdb) b main (gdb) b main
@ -81,8 +83,7 @@ Breakpoint 1, main () at test.c:4
### 第二步:查看我们变量的地址 ### 第二步:查看我们变量的地址
让我们从了解我们的变量开始。 它们每个都在内存中有一个地址,我们可以像这样打印出来: 让我们从了解我们的变量开始。它们每个都在内存中有一个地址,我们可以像这样打印出来:
``` ```
(gdb) p &x (gdb) p &x
@ -93,13 +94,13 @@ $2 = (char **) 0x7fffffffe280
$4 = (char (*)[10]) 0x7fffffffe28e $4 = (char (*)[10]) 0x7fffffffe28e
``` ```
因此,如果我们查看栈的地址,那我们应该能够看到所有的这些变量! 因此,如果我们查看那些地址的堆栈,那我们应该能够看到所有的这些变量!
### 概念:栈指针 ### 概念:栈指针
我们将需要使用栈指针,因此我将尽力对其进行快速解释。 我们将需要使用栈指针,因此我将尽力对其进行快速解释。
有一个名为 ESP 的 x86 寄存器,称为“栈指针”。 基本上,它是当前函数的栈起始地址。 在 GDB 中,您可以使用 `$sp` 来访问它。 当您调用新函数或从函数返回时,栈指针的值会更改。 有一个名为 ESP 的 x86 寄存器,称为“<ruby>栈指针<rt>stack pointer</rt></ruby>”。 基本上,它是当前函数的栈起始地址。 在 GDB 中,你可以使用 `$sp` 来访问它。 当你调用新函数或从函数返回时,栈指针的值会更改。
### 第三步:在 `main` 函数开始的时候,我们查看一下在栈上的变量 ### 第三步:在 `main` 函数开始的时候,我们查看一下在栈上的变量
@ -112,7 +113,7 @@ $7 = (void *) 0x7fffffffe270
因此,我们当前函数的栈起始地址是 `0x7fffffffe270`,酷极了。 因此,我们当前函数的栈起始地址是 `0x7fffffffe270`,酷极了。
现在,让我们使用 GDB 打印出当前函数堆栈开始后的前 40 个字即160个字节。 某些内存可能不是栈的一部分,因为我不太确定这里的堆栈有多大。 但是至少开始的地方是栈的一部分。 现在,让我们使用 GDB 打印出当前函数堆栈开始后的前 40 个字(即 160 个字节)。 某些内存可能不是栈的一部分,因为我不太确定这里的堆栈有多大。 但是至少开始的地方是栈的一部分。
``` ```
(gdb) x/40x $sp (gdb) x/40x $sp
@ -128,19 +129,17 @@ $7 = (void *) 0x7fffffffe270
0x7fffffffe300: 0xf9ce816d 0x7533d7c8 0xa91a816d 0x7533c789 0x7fffffffe300: 0xf9ce816d 0x7533d7c8 0xa91a816d 0x7533c789
``` ```
我已粗体显示了 stack_stringheap_string 和 x 变量的位置,并改变了颜色(译者注:可以在原网页查看) 我已粗体显示了 `stack_string``heap_string``x` 变量的位置,并改变了颜色:
* `x` 是红色字体,并且起始地址是 `0x7fffffffe27c` * `x` 是红色字体,并且起始地址是 `0x7fffffffe27c`
* `heap_string` 是蓝色字体,起始地址是 `0x7fffffffe280` * `heap_string` 是蓝色字体,起始地址是 `0x7fffffffe280`
* `stack_string` 是紫色字体,起始地址是 `0x7fffffffe28e` * `stack_string` 是紫色字体,起始地址是 `0x7fffffffe28e`
我想在此处将一些变量的位置加粗了一点点可能会有点偏差,但这大概是它们所在的位置。 你可能会在这里注意到的一件奇怪的事情是 `x` 的值是 0x5555但是我们将 `x` 设置为 `10` 那是因为直到我们的 `main` 函数运行之后才真正设置 `x` ,而我们现在才到了 `main` 最开始的地方。
您可能会在这里注意到的一件奇怪的事情是 x 的值是 0x5555但是我们将 x 设置为 10 那是因为直到我们的 `main` 函数运行之后才真正设置 `x` ,而我们现在才到了 `main` 最开始的地方。
### 第三步:运行到第十行代码后,再次查看一下我们的堆栈 ### 第三步:运行到第十行代码后,再次查看一下我们的堆栈
让我们跳过几行,等待变量实际设置为其初始化值。 到第 10 行时,`x` 应该设置为10。 让我们跳过几行,等待变量实际设置为其初始化值。 到第 10 行时,`x` 应该设置为 `10`
首先我们需要设置另一个断点: 首先我们需要设置另一个断点:
@ -159,15 +158,13 @@ Breakpoint 2, main () at test.c:11
11 printf("Enter a string for the stack: "); 11 printf("Enter a string for the stack: ");
``` ```
好的! 让我们再来看看堆栈里的内容! `gdb` 在这里格式化字节的方式略有不同,实际上我也不太关心这些(译者注:可以查看 GDB 手册中 x 命令,可以指定 c 来控制输出的格式)。 这里提醒一下您,我们的变量在栈上的位置: 好的! 让我们再来看看堆栈里的内容! `gdb` 在这里格式化字节的方式略有不同,实际上我也不太关心这些LCTT 译注:可以查看 GDB 手册中 `x` 命令,可以指定 `c` 来控制输出的格式)。 这里提醒一下你,我们的变量在栈上的位置:
* `x` 是红色字体,并且起始地址是 `0x7fffffffe27c` * `x` 是红色字体,并且起始地址是 `0x7fffffffe27c`
* `heap_string` 是蓝色字体,起始地址是 `0x7fffffffe280` * `heap_string` 是蓝色字体,起始地址是 `0x7fffffffe280`
* `stack_string` 是紫色字体,起始地址是 `0x7fffffffe28e` * `stack_string` 是紫色字体,起始地址是 `0x7fffffffe28e`
``` ```
(gdb) x/80x $sp (gdb) x/80x $sp
0x7fffffffe270: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fffffffe270: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
@ -186,9 +183,9 @@ Breakpoint 2, main () at test.c:11
### `stack_string` 在内存中是如何表示的 ### `stack_string` 在内存中是如何表示的
现在(第10行`stack_string` 被设置为 “stack”。 让我们看看它在内存中的表示方式。 现在(第 10 行),`stack_string` 被设置为字符串`stack`。 让我们看看它在内存中的表示方式。
我们可以像这样打印出字符串中的字节(译者注:可以通过 c 选项直接显示为字符) 我们可以像这样打印出字符串中的字节LCTT 译注:可以通过 `c` 选项直接显示为字符)
``` ```
(gdb) x/10x stack_string (gdb) x/10x stack_string
@ -196,8 +193,7 @@ Breakpoint 2, main () at test.c:11
0x7fffffffe296: 0x00 0x00 0x7fffffffe296: 0x00 0x00
``` ```
`stack` 是一个长度为 5 的字符串,相对应 5 个 ASCII 码- `0x73`、`0x74`、`0x61`、`0x63` 和 `0x6b`。`0x73` 是字符 `s` 的 ASCII 码。 `0x74``t` 的 ASCII 码。等等...
"stack" 是一个长度为 5 的字符串,相对应 5 个 ASCII 码-`0x73`, `0x74`, `0x61`, `0x63`, 和 `0x6b`。`0x73` 是字符 `s` 的 ASCII 码。 `0x74``t` 的 ASCII 码。等等...
同时我们也使用 `x/1s` 可以让 GDB 以字符串的方式显示: 同时我们也使用 `x/1s` 可以让 GDB 以字符串的方式显示:
@ -206,13 +202,11 @@ Breakpoint 2, main () at test.c:11
0x7fffffffe28e: "stack" 0x7fffffffe28e: "stack"
``` ```
### `heap_string``stack_string` 有何不同 ### `heap_string``stack_string` 有何不同
你已经注意到了 `stack_string``heap_string` 在栈上的表示非常不同: 你已经注意到了 `stack_string``heap_string` 在栈上的表示非常不同:
* `stack_string` 是一段字符串内容("stack") * `stack_string` 是一段字符串内容`stack`
* `heap_string` 是一个指针,指向内存中的某个位置 * `heap_string` 是一个指针,指向内存中的某个位置
这里是 `heap_string` 变量在内存中的内容: 这里是 `heap_string` 变量在内存中的内容:
@ -221,7 +215,7 @@ Breakpoint 2, main () at test.c:11
0xa0 0x92 0x55 0x55 0x55 0x55 0x00 0x00 0xa0 0x92 0x55 0x55 0x55 0x55 0x00 0x00
``` ```
这些字节实际上应该是从右向左读:因为 X86 是小端模式,因此,`heap_string` 中所存放的内存地址 `0x5555555592a0` 这些字节实际上应该是从右向左读:因为 x86 是小端模式,因此,`heap_string` 中所存放的内存地址 `0x5555555592a0`
另一种方式查看 `heap_string` 中存放的内存地址就是使用 `p` 命令直接打印 另一种方式查看 `heap_string` 中存放的内存地址就是使用 `p` 命令直接打印
@ -234,9 +228,9 @@ $6 = 0x5555555592a0 ""
`x` 是一个 32 位的整数,可由 `0x0a 0x00 0x00 0x00` 来表示。 `x` 是一个 32 位的整数,可由 `0x0a 0x00 0x00 0x00` 来表示。
我们还是需要反向来读取这些字节(和我们读取 `heap_string` 需要反过来读是一样的),因此这个数表示的是 `0x000000000a` 或者是 `0x0a`, 它是一个 10; 我们还是需要反向来读取这些字节(和我们读取 `heap_string` 需要反过来读是一样的),因此这个数表示的是 `0x000000000a` 或者是 `0x0a`,它是一个数字 `10`;
这就让我把把 x 设置成了 10 这就让我把把 `x` 设置成了 `10`
### 第四步:从标准输入读取 ### 第四步:从标准输入读取
@ -256,7 +250,6 @@ gets(heap_string);
Breakpoint 3 at 0x555555555205: file test.c, line 16. Breakpoint 3 at 0x555555555205: file test.c, line 16.
``` ```
然后继续执行程序: 然后继续执行程序:
``` ```
@ -266,14 +259,14 @@ Continuing.
我们输入两个字符串,为栈上存储的变量输入 `123456789012` 并且为在堆上存储的变量输入 `bananas`; 我们输入两个字符串,为栈上存储的变量输入 `123456789012` 并且为在堆上存储的变量输入 `bananas`;
### 让我们先来看一下 `stack_string` (这里有一个缓存区溢出) ### 让我们先来看一下 `stack_string`(这里有一个缓存区溢出)
``` ```
(gdb) x/1s stack_string (gdb) x/1s stack_string
0x7fffffffe28e: "123456789012" 0x7fffffffe28e: "123456789012"
``` ```
这看起来相当正常,对吗?我们输入了 `12345679012`,然后现在它也被设置成了 `12345679012`(译注:实测 gcc 8.3 环境下,会直接段错误)。 这看起来相当正常,对吗?我们输入了 `12345679012`,然后现在它也被设置成了 `12345679012`LCTT 译注:实测 gcc 8.3 环境下,会直接段错误)。
但是现在有一些很奇怪的事。这是我们程序的栈空间的内容。有一些紫色高亮的内容。 但是现在有一些很奇怪的事。这是我们程序的栈空间的内容。有一些紫色高亮的内容。
@ -290,7 +283,7 @@ Continuing.
0x7fffffffe2b8: 0x00 0xa0 0xfc 0xf7 0x01 0x00 0x00 0x00 0x7fffffffe2b8: 0x00 0xa0 0xfc 0xf7 0x01 0x00 0x00 0x00
``` ```
令人奇怪的是 **stack_string 只支持 10 个字节**。但是现在当我们输入了 13 个字符以后,发生了什么? 令人奇怪的是 **`stack_string` 只支持 10 个字节**。但是现在当我们输入了 13 个字符以后,发生了什么?
这是一个典型的缓冲区溢出,`stack_string` 将自己的数据写在了程序中的其他地方。在我们的案例中,这还没有造成问题,但它会使你的程序崩溃,或者更糟糕的是,使你面临非常糟糕的安全问题。 这是一个典型的缓冲区溢出,`stack_string` 将自己的数据写在了程序中的其他地方。在我们的案例中,这还没有造成问题,但它会使你的程序崩溃,或者更糟糕的是,使你面临非常糟糕的安全问题。
@ -313,7 +306,7 @@ fish: Job 1, './test' terminated by signal SIGABRT (Abort)
这里我猜是 `stack_string` 已经到达了这个函数栈的底部,因此额外的字符将会被写在另一块内存中。 这里我猜是 `stack_string` 已经到达了这个函数栈的底部,因此额外的字符将会被写在另一块内存中。
当你故意去使用这个安全漏洞时,它被称为“堆栈粉碎”,我也不完全了解这是如何检测到的 当你故意去使用这个安全漏洞时,它被称为“堆栈粉碎”,而且不知何故有东西在检测这种情况的发生
我也觉得这很有趣,虽然程序被杀死了,但是当缓冲区溢出发生时它不会立即被杀死——在缓冲区溢出之后再运行几行代码,程序才会被杀死。 好奇怪! 我也觉得这很有趣,虽然程序被杀死了,但是当缓冲区溢出发生时它不会立即被杀死——在缓冲区溢出之后再运行几行代码,程序才会被杀死。 好奇怪!
@ -348,7 +341,7 @@ fish: Job 1, './test' terminated by signal SIGABRT (Abort)
我们已经讨论过栈和堆是不同的内存区域,但是你怎么知道它们在内存中的位置呢? 我们已经讨论过栈和堆是不同的内存区域,但是你怎么知道它们在内存中的位置呢?
每个进程都有一个名为 `/proc/$PID/maps` 的文件,它显示了每个进程的内存映射。 在这里可以看到其中的栈和堆。 每个进程都有一个名为 `/proc/$PID/maps` 的文件,它显示了每个进程的内存映射。 在这里可以看到其中的栈和堆。
``` ```
$ cat /proc/24963/maps $ cat /proc/24963/maps
@ -364,7 +357,7 @@ $ cat /proc/24963/maps
这有点像旋风之旅,虽然我没有解释所有内容,但希望看到数据在内存中的实际情况可以使你更清楚地了解堆栈的实际情况。 这有点像旋风之旅,虽然我没有解释所有内容,但希望看到数据在内存中的实际情况可以使你更清楚地了解堆栈的实际情况。
我真的建议像这样来把玩一下 gdb —— 即使你不理解你在内存中看到的每一件事,我发现实际上像这样看到我程序内存中的数据会使抽象的概念,比如“栈”和“ 堆”和“指针”更容易理解。 我真的建议像这样来把玩一下 gdb —— 即使你不理解你在内存中看到的每一件事,我发现实际上像这样看到我程序内存中的数据会使抽象的概念,比如“栈”和“堆”和“指针”更容易理解。
### 更多练习 ### 更多练习
@ -374,9 +367,7 @@ $ cat /proc/24963/maps
* 从函数返回一个指向栈上字符串的指针,看看哪里出了问题。 为什么返回指向栈上字符串的指针是不好的? * 从函数返回一个指向栈上字符串的指针,看看哪里出了问题。 为什么返回指向栈上字符串的指针是不好的?
* 尝试在 C 中引起堆栈溢出,并尝试通过在 gdb 中查看堆栈溢出来准确理解会发生什么! * 尝试在 C 中引起堆栈溢出,并尝试通过在 gdb 中查看堆栈溢出来准确理解会发生什么!
* 查看 Rust 程序中的堆栈并尝试找到变量! * 查看 Rust 程序中的堆栈并尝试找到变量!
* 在 [噩梦课程][1] 中尝试一些缓冲区溢出挑战。每个问题的答案写在 README 文件中,因此如果您不想被宠坏,请避免先去看答案。 所有这些挑战的想法是给你一个二进制文件,你需要弄清楚如何导致缓冲区溢出以使其打印出 “flag” 字符串。 * 在 [噩梦课程][1] 中尝试一些缓冲区溢出挑战。每个问题的答案写在 README 文件中,因此如果你不想被宠坏,请避免先去看答案。 所有这些挑战的想法是给你一个二进制文件,你需要弄清楚如何导致缓冲区溢出以使其打印出 `flag` 字符串。
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -385,7 +376,7 @@ via: https://jvns.ca/blog/2021/05/17/how-to-look-at-the-stack-in-gdb/
作者:[Julia Evans][a] 作者:[Julia Evans][a]
选题:[lujun9972][b] 选题:[lujun9972][b]
译者:[amwps290](https://github.com/amwps290) 译者:[amwps290](https://github.com/amwps290)
校对:[校对者ID](https://github.com/校对者ID) 校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出