我们如何访问堆栈变量而不弹出它们?

2024-02-11

据我所知,C 中有两种变量,栈变量和堆变量。堆栈变量速度很快,并由编译器和 CPU 自动管理。我关于堆栈变量的问题是:

  • 堆栈变量真的存储在堆栈 FILO 数据结构中吗?
  • 如果是这样,为什么我们可以使用它们而不弹出它们并丢失它们的值?
  • 为什么要用栈来存储它们呢?出队或列表有什么问题?

作为程序员,您不必担心将变量压入或弹出堆栈;生成的机器代码会为您处理所有这些事情。每次调用函数时,程序都会将项目推送到硬件堆栈上。其中一些项目是您传递给函数的数据,但大多数是当前程序状态(寄存器值、返回地址等),以便当函数返回时程序可以在正确的位置继续执行。

一个例子可能会有所帮助。采用以下简单的 C 程序:

#include <stdio.h>

int afunc( int a, int b )
{
  int c = a * b;
  return c;
}

int main( void )
{
  int x;
  int y;
  int z;

  x = 2;
  y = 3;

  z = afunc( x, y );
  printf( "z = %d\n", z );

  return 0;
}

在老化的 Red Hat 机器上使用 gcc 2.96 编译它,如下所示:

gcc -o demo -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=demo.lst demo.c

给我以下输出列表:

   1                            .file   "demo.c"
   2                            .version        "01.01"
   5                    .text
   6                    .Ltext0:
 165                            .align 4
 169                    .globl afunc
 171                    afunc:
   1:demo.c        **** #include <stdio.h>
   2:demo.c        ****
   3:demo.c        **** int afunc( int a, int b )
   4:demo.c        **** {
 173                    .LM1:
 174                    .LBB2:
 175 0000 55                    pushl   %ebp
 176 0001 89E5                  movl    %esp, %ebp
 177 0003 83EC04                subl    $4, %esp
   5:demo.c        ****   int c = a * b;
 179                    .LM2:
 180 0006 8B4508                movl    8(%ebp), %eax
 181 0009 0FAF450C              imull   12(%ebp), %eax
 182 000d 8945FC                movl    %eax, -4(%ebp)
   6:demo.c        ****   return c;
 184                    .LM3:
 185 0010 8B45FC                movl    -4(%ebp), %eax
 186 0013 89C0                  movl    %eax, %eax
   7:demo.c        **** }
 188                    .LM4:
 189                    .LBE2:
 190 0015 C9                    leave
 191 0016 C3                    ret
 192                    .Lfe1:
 197                    .Lscope0:
 199                                    .section        .rodata
 200                    .LC0:
 201 0000 7A203D20              .string "z = %d\n"
 201      25640A00
 202                    .text
 203 0017 90                    .align 4
 205                    .globl main
 207                    main:
   8:demo.c        ****
   9:demo.c        **** int main( void )
  10:demo.c        **** {
 209                    .LM5:
 210                    .LBB3:
 211 0018 55                    pushl   %ebp
 212 0019 89E5                  movl    %esp, %ebp
 213 001b 83EC18                subl    $24, %esp
  11:demo.c        ****   int x;
  12:demo.c        ****   int y;
  13:demo.c        ****   int z;
  14:demo.c        ****
  15:demo.c        ****   x = 2;
 215                    .LM6:
 216 001e C745FC02              movl    $2, -4(%ebp)
 216      000000
  16:demo.c        ****   y = 3;
 218                    .LM7:
 219 0025 C745F803              movl    $3, -8(%ebp)
 219      000000
  17:demo.c        ****
  18:demo.c        ****   z = afunc( x, y );
 221                    .LM8:
 222 002c 83EC08                subl    $8, %esp
 223 002f FF75F8                pushl   -8(%ebp)
 224 0032 FF75FC                pushl   -4(%ebp)
 225 0035 E8FCFFFF              call    afunc
 225      FF
 226 003a 83C410                addl    $16, %esp
 227 003d 89C0                  movl    %eax, %eax
 228 003f 8945F4                movl    %eax, -12(%ebp)
  19:demo.c        ****   printf( "z = %d\n", z );
 230                    .LM9:
 231 0042 83EC08                subl    $8, %esp
 232 0045 FF75F4                pushl   -12(%ebp)
 233 0048 68000000              pushl   $.LC0
 233      00
 234 004d E8FCFFFF              call    printf
 234      FF
 235 0052 83C410                addl    $16, %esp
  20:demo.c        ****
  21:demo.c        ****   return 0;
 237                    .LM10:
 238 0055 B8000000              movl    $0, %eax
 238      00
  22:demo.c        **** }
 240                    .LM11:
 241                    .LBE3:
 242 005a C9                    leave
 243 005b C3                    ret
 244                    .Lfe2:
 251                    .Lscope1:
 253                            .text
 255                    .Letext:
 256                            .ident  "GCC: (GNU) 2.96 20000731 (Red Hat Linux 7.2 2.96-112.7.2)"

所以,从main,我们有以下几行:

 211 0018 55                    pushl   %ebp
 212 0019 89E5                  movl    %esp, %ebp
 213 001b 83EC18                subl    $24, %esp

%esp points to the top of the stack; %ebp points into the stack frame, between the local variables and function arguments. These lines save the current value of %ebp by pushing it onto the stack, then write the location of the current top of the stack to %ebp, and then advance %esp by 24 bytes (the stack grows "down", or towards decreasing addresses, on x86). Stepping through the execution of this program in a debugger on my system, we see the stack is set up as follows1:

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  <-- %ebp, 0xbfffda18 is the previous value
0xbfffd9d4     0x08  0x04  0x96  0x40  <-- x
0xbfffd9d0     0x08  0x04  0x95  0x40  <-- y
0xbfffd9cc     0x08  0x04  0x84  0x41  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c  <-- %esp

然后我们就有了线条

216 001e C745FC02              movl    $2, -4(%ebp)

and

219 0025 C745F803              movl    $3, -8(%ebp)

将 2 和 3 分配给x and y, 分别。请注意,这些位置被称为offsets from %ebp。所以现在我们的堆栈看起来像这样:

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  <-- %ebp
0xbfffd9d4     0x00  0x00  0x00  0x02  <-- x
0xbfffd9d0     0x00  0x00  0x00  0x03  <-- y
0xbfffd9cc     0x08  0x04  0x84  0x41  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c <-- %esp

现在我们打电话afunc。为此,我们首先需要推动论点x and y在调用堆栈上:

 222 002c 83EC08                subl    $8, %esp
 223 002f FF75F8                pushl   -8(%ebp)
 224 0032 FF75FC                pushl   -4(%ebp)

所以现在我们的堆栈看起来像

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  <-- %ebp
0xbfffd9d4     0x00  0x00  0x00  0x02  <-- x
0xbfffd9d0     0x00  0x00  0x00  0x03  <-- y
0xbfffd9cc     0x08  0x04  0x84  0x41  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c
0xbfffd9bc     0x40  0x14  0xd7  0xf0
0xbfffd9b8     0x40  0x14  0xe8  0x38
0xbfffd9b4     0x00  0x00  0x00  0x03 <-- b
0xbfffd9b0     0x00  0x00  0x00  0x02 <-- a, %esp

现在我们打电话afunc。我们要做的第一件事是保存当前值%ebp,然后再次调整我们的寄存器:

 175 0000 55                    pushl   %ebp
 176 0001 89E5                  movl    %esp, %ebp
 177 0003 83EC04                subl    $4, %esp

留给我们

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  
0xbfffd9d4     0x00  0x00  0x00  0x02  <-- x
0xbfffd9d0     0x00  0x00  0x00  0x03  <-- y
0xbfffd9cc     0x08  0x04  0x84  0x41  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c 
0xbfffd9bc     0x40  0x14  0xd7  0xf0
0xbfffd9b8     0x40  0x14  0xe8  0x38
0xbfffd9b4     0x00  0x00  0x00  0x03 <-- b
0xbfffd9b0     0x00  0x00  0x00  0x02 <-- a
0xbfffd9ac     0x08  0x04  0x84  0x9a 
0xbfffd9a8     0xbf  0xff  0xd9  0xd8 <-- %ebp
0xbfffd9a4     0x40  0x14  0xd7  0xf0 <-- c, %esp

现在我们执行计算afunc:

 180 0006 8B4508                movl    8(%ebp), %eax
 181 0009 0FAF450C              imull   12(%ebp), %eax
 182 000d 8945FC                movl    %eax, -4(%ebp)

注意相对于的偏移量%ebp:这次它们是正数(函数参数存储在“下面”%ebp,局部变量存储在它的“上方”)。然后结果存储在c:

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  
0xbfffd9d4     0x00  0x00  0x00  0x02  <-- x
0xbfffd9d0     0x00  0x00  0x00  0x03  <-- y
0xbfffd9cc     0x08  0x04  0x84  0x41  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c 
0xbfffd9bc     0x40  0x14  0xd7  0xf0
0xbfffd9b8     0x40  0x14  0xe8  0x38
0xbfffd9b4     0x00  0x00  0x00  0x03 <-- b
0xbfffd9b0     0x00  0x00  0x00  0x02 <-- a
0xbfffd9ac     0x08  0x04  0x84  0x9a 
0xbfffd9a8     0xbf  0xff  0xd9  0xd8 <-- %ebp
0xbfffd9a4     0x00  0x00  0x00  0x06 <-- c, %esp

函数返回值保存在寄存器中%eax。现在我们从函数返回:

 185 0010 8B45FC                movl    -4(%ebp), %eax
 186 0013 89C0                  movl    %eax, %eax

 190 0015 C9                    leave
 191 0016 C3                    ret

当我们从函数返回时,我们将堆栈中的所有内容弹出回原来的位置%esp在我们进入之前就指着afunc(那里有一些魔力,但要认识到%ebp指向一个包含旧值的地址%ebp):

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  <-- %ebp
0xbfffd9d4     0x00  0x00  0x00  0x02  <-- x
0xbfffd9d0     0x00  0x00  0x00  0x03  <-- y
0xbfffd9cc     0x08  0x04  0x84  0x41  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c  <-- %esp

现在我们将结果保存到z:

 228 003f 8945F4                movl    %eax, -12(%ebp)

留给我们:

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  <-- %ebp
0xbfffd9d4     0x00  0x00  0x00  0x02  <-- x
0xbfffd9d0     0x00  0x00  0x00  0x03  <-- y
0xbfffd9cc     0x00  0x00  0x00  0x06  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c  <-- %esp

请注意,这是特定硬件/软件组合和特定编译器上的情况;编译器之间的细节会有所不同(最新版本的 gcc 使用寄存器将函数参数传递到任何可以的地方,而不是将它们推入堆栈),但一般概念是相同的。只是不要假设这是the做事的方式。


1. The values stored between 0xbfffd9c4 and 0xbfffd9c8 are (most likely) not related to our code; they're just the bit patterns that were left after those memory locations were used in another operation. I think the compiler assumes a minimum amount of space for setting up the local frame.
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

我们如何访问堆栈变量而不弹出它们? 的相关文章

  • WinForms:如何确定窗口是否不再活动(没有子窗口具有焦点)?

    我的应用程序使用多个窗口 我想隐藏一个特定窗口 以防应用程序失去焦点 当活动窗口不是应用程序窗口时 source https stackoverflow com questions 466354 how can i tell if a wi
  • 在C语言中使用“void”

    我很困惑为什么我们需要通过void转换为 C 函数 int f void return 0 versus int f return 0 什么是正确的做法以及为什么 In C int f 是一种老式的声明 它说f需要固定但未指定数量和类型的参
  • 具有子列表属性映射问题的自动映射器

    我有以下型号 Models public class Dish Required public Int64 ID get set Required public string Name get set Required public str
  • 为什么我不能用 `= delete;` 声明纯虚函数?

    Intro 纯虚函数使用通用语法声明 virtual f 0 然而 自 c 11 以来 有一种方法可以显式地传达non existence 特殊 成员函数的 Mystruct delete eg default constructor Q
  • 向 ExpandoObject 添加方法时,“关键字 'this' 在静态属性、静态方法或静态字段初始值设定项中无效”

    我尝试向 ExpandoObject 添加一个动态方法 该方法将返回属性 动态添加 给它 但它总是给我错误 我在这里做错了什么吗 using System using System Collections Generic using Sys
  • C++:重写已弃用的虚拟方法时出现弃用警告

    我有一个纯虚拟类 它有一个纯虚拟方法 应该是const 但不幸的是不是 该接口位于库中 并且该类由单独项目中的其他几个类继承 我正在尝试使用这个方法const不会破坏兼容性 至少在一段时间内 但我找不到在非常量方法重载时产生警告的方法 以下
  • 对齐 GridView 中的行值

    我需要在 asp net 3 5 中右对齐 gridview 列中的值 我怎样才能做到这一点
  • POCO HTTPSClientSession 发送请求时遇到问题 - 证书验证失败

    我正在尝试使用 POCO 库编写一个向服务器发出 HTTPS 请求的程序 出于测试目的 我正在连接到具有自签名证书的服务器 并且我希望允许客户端进行连接 为了允许这种情况发生 我尝试安装InvalidCertificateHandler这是
  • C++ 异步线程同时运行

    我是 C 11 中线程的新手 我有两个线程 我想让它们同时启动 我可以想到两种方法 如下 然而 似乎它们都没有按照我的预期工作 他们在启动另一个线程之前启动一个线程 任何提示将不胜感激 另一个问题是我正在研究线程队列 所以我会有两个消费者和
  • 如何配置 WebService 返回 ArrayList 而不是 Array?

    我有一个在 jax ws 上实现的 java Web 服务 此 Web 服务返回用户的通用列表 它运行得很好 Stateless name AdminToolSessionEJB RemoteBinding jndiBinding Admi
  • 从多个类访问串行端口

    我正在尝试使用串行端口在 arduino 和 C 程序之间进行通信 我对 C 编程有点陌生 该程序有多种用户控制形式 每一个都需要访问串口来发送数据 我需要做的就是从每个类的主窗体中写入串行端口 我了解如何设置和写入串行端口 这是我的 Fo
  • 访问者和模板化虚拟方法

    在一个典型的实现中Visitor模式 该类必须考虑基类的所有变体 后代 在许多情况下 访问者中的相同方法内容应用于不同的方法 在这种情况下 模板化的虚拟方法是理想的选择 但目前这是不允许的 那么 模板化方法可以用来解析父类的虚方法吗 鉴于
  • 检查算术运算中的溢出情况[重复]

    这个问题在这里已经有答案了 可能的重复 检测 C C 中整数溢出的最佳方法 https stackoverflow com questions 199333 best way to detect integer overflow in c
  • 将数据打印到文件

    我已经超载了 lt lt 运算符 使其写入文件并写入控制台 我已经为同一个函数创建了 8 个线程 并且我想输出 hello hi 如果我在无限循环中运行这个线程例程 文件中的o p是 hello hi hello hi hello hi e
  • 在非活动联合成员上使用“std::addressof”是否定义明确[重复]

    这个问题在这里已经有答案了 下面的代码是尝试实现constexpr的版本offsetof在 C 11 中 它可以在 gcc 7 2 0 和 clang 5 0 0 中编译 这取决于申请std addressof工会非活跃成员的成员 这是明确
  • 如何一步步遍历目录树?

    我发现了很多关于遍历目录树的示例 但我需要一些不同的东西 我需要一个带有某种方法的类 每次调用都会从目录返回一个文件 并逐渐遍历目录树 请问我该怎么做 我正在使用函数 FindFirstFile FindNextFile 和 FindClo
  • 在类的所有方法之前运行一个方法

    在 C 3 或 4 中可以做到这一点吗 也许有一些反思 class Magic RunBeforeAll public void BaseMethod runs BaseMethod before being executed public
  • 用于 C# XNA 的 Javascript(或类似)游戏脚本

    最近我准备用 XNA C 开发另一个游戏 上次我在 XNA C 中开发游戏时 遇到了必须向游戏中添加地图和可自定义数据的问题 每次我想添加新内容或更改游戏角色的某些值或其他内容时 我都必须重建整个游戏或其他内容 这可能需要相当长的时间 有没
  • Googletest:如何异步运行测试?

    考虑到一个包含数千个测试的大型项目 其中一些测试需要几分钟才能完成 如果按顺序执行 整套测试需要一个多小时才能完成 通过并行执行测试可以减少测试时间 据我所知 没有办法直接从 googletest mock 做到这一点 就像 async选项
  • 实例化 Microsoft.Office.Interop.Excel.Application 对象时出现错误:800700c1

    实例化 Microsoft Office Interop Excel Application 以从 winforms 应用程序生成 Excel 时 出现以下错误 这之前是有效的 但突然间它停止工作了 尽管代码和 Excel 版本没有变化 我

随机推荐