【问题标题】:Why compilers creates one variable "twice"?为什么编译器会“两次”创建一个变量?
【发布时间】:2010-06-30 21:51:59
【问题描述】:

我知道这是一个更“重”的问题,但我认为它也很有趣。它是of my previous questions about compiler functions 的一部分,但我解释得非常糟糕,很多人只回答了我的第一个问题,所以它是:

所以,如果我的知识是正确的,现代 Windows 系统使用分页来切换任务并确保每个任务在内存中都有适当的位置。所以,每个进程从 0 开始都有自己的位置。

当多任务生效时,我相信内核必须将所有重要的寄存器保存到任务的堆栈中,而不是保存当前的堆栈指针,更改页面条目以切换到另一个进程的物理地址空间,加载新的进程堆栈指针,弹出保存的寄存器并通过调用弹出的指令指针地址继续。

由于这个很好的特性(分页),每个进程都认为它有很好的平坦内存触手可及。因此,没有远跳转、远指针、内存段或数据段。一切都很好而且是线性的。

但是,当进程没有更多的分段时,为什么编译器仍然在堆栈上创建变量,或者当全局直接在其他内存空间中,而不是直接在程序代码中?

举个例子,我有一个C代码:int a=10;

翻译成(Intel 语法):mov [position of a],#10

但是,实际上您在 RAM 中占用的字节数比需要的多。因为,前几个字节取实际指令,在该指令完成后,有一个包含值 10 的新字节。

为什么,而不是这个,当不需要切换任何段(从而降低处理速度)时,不仅仅是像这样直接将值 10 编码到程序中:

xor eax,eax //just some instruction
10 //the value iserted to the program
call end //just some instruction

因为编译器知道每条指令的确切位置,所以在操作该变量时,它只会使用它的地址。

我知道,const 变量可以做到这一点,但当你无法更改它们时,它们并不是真正的变量。

我希望我能很好地解释我的问题,但我仍在学习英语,所以请原谅我的语法甚至语义错误。

编辑:

我已经阅读了你的答案,看来我可以根据这些修改我的问题:

所以,这里有人说,全局变量实际上是直接附加到程序中的那块值,我的意思是,当变量是全局时,它是附加到程序的末尾,还是只是在执行,而不是直接在堆上的堆栈上?

如果第一种情况 - 附加到程序本身,为什么甚至存在局部变量?我知道,你会因为递归而告诉我,但事实并非如此。调用函数时,可以将任意内存空间压入栈中,因此没有程序存在。

我希望你能理解我,内存的使用总是效率低下,当某个值(甚至 0)从某个指令在堆栈上创建时,因为你需要在程序中为该指令而不是实际 var 留出空间。就像这样:push #5 //instruction that says to create local variable with integer 5 而这条指令只是让数字 5 在堆栈上。请帮助我,我真的很想知道为什么会这样。谢谢。

【问题讨论】:

  • 我认为它是一个变量这一事实意味着它是对可以更改的值的一些引用。如果你做了int a = 10; a = 5;,代码应该是什么样子?或者你只是在值不变的情况下询问编译器优化?
  • 我认为他的意思是:为什么不是所有的局部变量都优化为全局变量(其在内存中的位置是恒定的)。变量在内存中的存储方式似乎也存在一些严重的混淆。

标签: c windows compiler-construction


【解决方案1】:

考虑:

  • 如果一个例程被递归调用(甚至间接地在一个递归体面解析器中)或从多个线程调用,局部变量可能同时存在多个,并且这些情况发生在相同 内存上下文
  • 将程序内存标记为不可写并且将堆栈+堆标记为不可执行是针对某些类型的攻击(堆栈粉碎...)的一种小而有用的防御措施,并且 被某些操作系统使用(不过,我不知道 Windows 是否会这样做)

您的提案不允许上述任何一种情况。

【讨论】:

  • “可能同时存在多个” - 如果相同的可执行代码在多个进程中运行,即使对于可修改的全局变量也是如此。代码只需要在物理 RAM 中存在一次(因为它是 RO,你的另一点),并且可能映射到进程中相同或不同的虚拟地址(我不知道在 Windows 的情况下是哪个)。如果变量被嵌入到它的中间,那么页面将不得不被复制,失去优化。
  • 还有:类的实例变量
  • 基于堆栈的变量仅在函数运行时存在(并占用内存)也很有用。如果它们都存储在绝对地址中,那么它们都会在进程运行的整个过程中占用内存,这是非常低效的。
  • @Steve Jessop:在 Win32 上,代码通常存储在只读的可共享可执行段中。当您运行应用程序的两个实例时,这些段将只有一个物理副本。您可以使用 SysInternals 的流程查看器查看此行为。
  • 嗯,首先,我的方式允许。调用函数时,您可以简单地将变量从任何位置压入堆栈,甚至直接在程序代码中的某个内存单元中。第二,为什么要这样辩护?由于使用了虚拟内存,您无法重写其他进程,并且在您自己的情况下,编译器不会愚蠢地让您覆盖程序本身。
【解决方案2】:

因此,没有远跳转、远指针、内存段或数据段。一切都很好而且是线性的。

是和不是。不同的程序段有不同的用途——尽管它们驻留在平面虚拟内存中。例如。数据段可读写,但不能执行数据。代码段可读可执行,但不能写入。

为什么编译器仍然在堆栈上创建变量,[...] 而不是直接在程序代码中?

简单。

  1. 代码段不可写。首先出于安全考虑。第二, 大多数 CPU 不喜欢将代码段写入 打破了许多用于加速执行的现有优化。
  2. 函数的状态必须是函数私有的,因为 诸如递归和多线程之类的东西。

不只是像这样直接将值 10 编码到程序中

现代 CPU 预取指令以允许并行执行和乱序执行等操作。将垃圾(对于作为垃圾的 CPU)放入代码段只会降低(或完全取消)技术的效果。在过去十年中 CPU 所表现出的大部分性能提升都归功于它们。

当不需要切换任何段时

如果没有切换段的开销,为什么还要把它放到代码段中呢?将其保存在数据段中没有问题。

特别是在只读数据段的情况下,将程序的所有只读数据放在一个地方是有意义的 - 因为它可以由正在运行的应用程序的所有实例共享,从而节省物理 RAM。

因为编译器知道每条指令的确切位置,所以在操作该变量时,它只会使用它的地址。

不,不是真的。大多数代码是可重定位的或与位置无关的。当操作系统将代码加载到内存中时,代码已修补,并带有真实的内存地址。实际上使用特殊技术来实际避免修补代码,以便所有正在运行的应用程序实例也可以共享代码段。

ABI 负责定义编译器和链接器应该如何以及应该做什么,以使程序可以被兼容的操作系统执行。 Windows ABI我没见过,但是Linux使用的ABI很容易找到:搜索“AMD64 ABI”。即使阅读 Linux ABI 也可能会回答您的一些问题。

【讨论】:

  • 现有的 x86 操作系统中没有代码和数据段保护模式。访问控制是按页面而不是按段处理的。数据执行预防实际上是不久前引入的,“NX 位”仅适用于 PAE 和长模式。
  • @Axel。 “无代码和数据段” - 检查 Linux 上的流程图​​。查找文本、数据、rodata 段。还没有看到任何类似的 Windows 工具。 “访问控制是按页面而不是按段处理的。” - 你可以对其进行微观管理,是的;但是操作系统已经自动为您的进程段执行此操作。 “NX 位”:其他 CPU 很久以前就有了。是的,我在“程序的数据段”中使用“段”的概念,而不是在“8086 内存组织的噱头”中。
  • 他说的是x86的内存分段(理论上在保护模式下仍然支持)。这与 ELF 文件中的“数据段”没有任何关系。我知道操作系统会自动执行此操作,但它使用页面,而不是分段。
【解决方案3】:

你说的是优化,那是编译器的事。如果没有任何东西改变这个值,并且编译器可以计算出来,那么编译器完全可以按照你说的去做(除非 a 被声明为 volatile)。

现在,如果您说您看到编译器没有这样做,并且您认为应该这样做,那么您必须与您的编译器编写者交谈。如果您使用的是 VisualStudio,他们的地址是 One Microsoft Way, Redmond WA。在那里敲门祝你好运。 :-)

【讨论】:

    【解决方案4】:

    为什么不只是将值 10 直接编码到这样的程序中:

    xor eax,eax //just some instruction  
    10 //the value iserted to the program  
    call end //just some instruction  
    

    全局变量的存储方式。然而,它们并没有被困在可执行代码中间(现在很混乱,not even possible),它们被存储在内存中的程序代码之后(至少在 Windows 和 Linux 中) ,在所谓的 .data 部分中。

    如果可以,编译器会将变量移动到 .data 部分以优化性能。但是,它可能不会有以下几个原因:

    • 某些变量不能设为全局变量,包括类的实例变量、传递给函数的参数(显然)以及递归函数中使用的变量。
    • 变量仍然存在于内存中的某个地方,并且仍然必须有代码才能访问它。因此,内存使用不会改变。其实在x86(“Intel”)上,根据this page的指令来引用一个局部变量:

      mov eax, [esp+8]
      

      以及引用全局变量的指令:

      mov eax, [0xb3a7135]
      

      两者都需要 1(一个!)时钟周期。

      那么,唯一的优势是,如果每个局部变量都是全局变量,那么您就不必在堆栈上为局部变量腾出空间。

    • 将变量添加到 .data 段实际上可能增加可执行文件的大小,因为该变量实际上包含在文件本身中。

    • caf mentions in the comments 一样,基于堆栈的变量仅在函数运行时存在 - 全局变量在程序的整个执行期间占用内存

    【讨论】:

      【解决方案5】:

      不太清楚你的困惑是什么?

      int a = 10; 意思是在内存中占一个位置,并将值10放在内存地址

      如果你想成为 10

      #define a 10
      

      虽然更常见

      #define TEN 10
      

      【讨论】:

      • 值得看看他之前的问题。他似乎认为编写编译器的人在他们如何使用内存方面无缘无故地愚蠢。
      【解决方案6】:

      变量有存储空间,可以修改。将它们粘贴在无法修改的代码段中是没有意义的。

      如果你有带有int a=10甚至const int a=10的代码,编译器无法将引用'a'的代码直接转换为使用常量10,因为它无法知道'a'是否可以在其后面更改返回(甚至可以更改 const 变量)。例如,可以在编译器不知道的情况下更改“a”的一种方法是,如果您有一个指向“a”的指针。指针在运行时不是固定的,因此编译器无法在编译时确定是否会有一个指针指向并修改'a'。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-11-15
        • 1970-01-01
        • 1970-01-01
        • 2020-06-30
        • 1970-01-01
        • 1970-01-01
        • 2017-02-09
        • 2011-05-26
        相关资源
        最近更新 更多