【问题标题】:Why does initializing a variable `i` to 0 and to a large size result in the same size of the program?为什么将变量“i”初始化为 0 和较大的大小会导致程序大小相同?
【发布时间】:2016-01-19 08:27:04
【问题描述】:

有一个问题让我很困惑。

int main(int argc, char *argv[])
{
    int i = 12345678;
    return 0;
}

int main(int argc, char *argv[])
{
    int i = 0;
    return 0;
}

程序的总字节数相同。为什么?

文字值确实存储在哪里?文字片段还是其他地方?

【问题讨论】:

  • 文字数字将是可执行代码中的立即操作数,准备好被压入堆栈。
  • 是的,我有另一个问题Where the value of variables are stored in C。这给了我答案。
  • 那么 "char *p = "12345"; " 和上面的一样吗?它也存储在代码中?
  • 可能是,但可能在初始化的只读数据中。

标签: c variables size


【解决方案1】:

程序的总字节数相同。为什么?

有两种可能:

  1. 编译器正在优化变量。它没有在任何地方使用,因此没有意义。

  2. 如果 1. 不适用,则程序大小无论如何都是相等的。他们为什么不应该? 0 的大小与 12345678 一样大。 T 类型的两个变量在内存中占用相同的大小。

文字值确实存储在哪里?

在堆栈上。局部变量通常存储在堆栈中。

【讨论】:

  • 我得到Where the value of variables are stored in C。这个问题给了我一个明确的线索,即字面值存储在文本段中。非常感谢。
  • 我的意思是它自己的数字。初始化变量的数字作为立即操作数存储在文本段中。当数字给它存储在堆栈中的变量时。我知道。
  • 如果数字存储在 .text 部分但数字 12345678 肯定大于 .在文本段中存储不是需要更多的位吗?
【解决方案2】:

因为您的程序已经过优化。在编译的时候,编译器发现i没用,就把它去掉了。

如果没有优化,另一个简单的解释是int 与另一个int 大小相同。

【讨论】:

    【解决方案3】:

    考虑一下你的卧室。如果你把它填满了东西或者你把它空了,这会改变你卧室的面积吗? int 的大小是sizeof(int)。不管你在里面存储什么值。

    【讨论】:

      【解决方案4】:

      TL;DR

      第一个问题:它们的大小相同,因为您的程序的instructions 输出大致相同(更多内容见下文)。此外,它们的大小相同,因为您的 ints 的 size(字节数)永远不会改变。

      第二个问题: i 变量存储在您的局部变量框架中,该框架位于函数堆栈中。您设置为i 的实际值在文本段的指令(硬编码)中。


      GDB 和汇编

      我知道您使用的是 Windows,但请考虑这些代码和 Linux 上的输出。我使用了与您发布的完全相同的来源。

      对于第一个,i = 12345678,实际的主要功能是这些计算机指令:

      (gdb) disass main
      Dump of assembler code for function main:
         0x00000000004004ed <+0>: push   %rbp
         0x00000000004004ee <+1>: mov    %rsp,%rbp
         0x00000000004004f1 <+4>: mov    %edi,-0x14(%rbp)
         0x00000000004004f4 <+7>: mov    %rsi,-0x20(%rbp)
         0x00000000004004f8 <+11>:movl   $0xbc614e,-0x4(%rbp)
         0x00000000004004ff <+18>:mov    $0x0,%eax
         0x0000000000400504 <+23>:pop    %rbp
         0x0000000000400505 <+24>:retq   
      End of assembler dump.
      

      至于其他程序,i = 0main是:

      (gdb) disass main
      Dump of assembler code for function main:
         0x00000000004004ed <+0>: push   %rbp
         0x00000000004004ee <+1>: mov    %rsp,%rbp
         0x00000000004004f1 <+4>: mov    %edi,-0x14(%rbp)
         0x00000000004004f4 <+7>: mov    %rsi,-0x20(%rbp)
         0x00000000004004f8 <+11>:movl   $0x0,-0x4(%rbp)
         0x00000000004004ff <+18>:mov    $0x0,%eax
         0x0000000000400504 <+23>:pop    %rbp
         0x0000000000400505 <+24>:retq   
      End of assembler dump.
      

      两个代码之间的唯一区别是存储在变量中的实际值。让我们通过下面这几行一步一步来(我的电脑是 x86_64,所以如果你的架构不同,说明可能会有所不同)。


      操作码

      以及main的实际指令(使用objdump):

      00000000004004ed <main>:
        4004ed:   55                      push   %rbp
        4004ee:   48 89 e5                mov    %rsp,%rbp
        4004f1:   89 7d ec                mov    %edi,-0x14(%rbp)
        4004f4:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
        4004f8:   c7 45 fc 4e 61 bc 00    movl   $0xbc614e,-0x4(%rbp)
        4004ff:   b8 00 00 00 00          mov    $0x0,%eax
        400504:   5d                      pop    %rbp
        400505:   c3                      retq   
        400506:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
        40050d:   00 00 00 
      

      要获得实际的字节差,使用objdump -D prog1 &gt; prog1_dumpobjdump -D prog2 &gt; prog2_dump 以及它们diff prog1_dump prog2_dump

      2c2
      < draft1:     file format elf64-x86-64
      ---
      > draft2:     file format elf64-x86-64
      51,58c51,58
      <   400283: 00 bc f6 06 64 9f ba    add    %bh,-0x45609bfa(%rsi,%rsi,8)
      <   40028a: 01 3b                   add    %edi,(%rbx)
      <   40028c: 14 d1                   adc    $0xd1,%al
      <   40028e: 12 cf                   adc    %bh,%cl
      <   400290: cd 2e                   int    $0x2e
      <   400292: 11 77 5d                adc    %esi,0x5d(%rdi)
      <   400295: 79 fe                   jns    400295 <_init-0x113>
      <   400297: 3b                      .byte 0x3b
      ---
      >   400283: 00 e8                   add    %ch,%al
      >   400285: f1                      icebp  
      >   400286: 6e                      outsb  %ds:(%rsi),(%dx)
      >   400287: 8a f8                   mov    %al,%bh
      >   400289: a8 05                   test   $0x5,%al
      >   40028b: ab                      stos   %eax,%es:(%rdi)
      >   40028c: 48 2d 3f e9 e2 b2       sub    $0xffffffffb2e2e93f,%rax
      >   400292: f7 06 53 df ba af       testl  $0xafbadf53,(%rsi)
      287c287
      <   4004f8: c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
      ---
      >   4004f8: c7 45 fc 4e 61 bc 00    movl   $0xbc614e,-0x4(%rbp)
      

      注意地址0x4004f8你的号码在那里,4e 61 bc 00prog200 00 00 00prog1,两个4字节等于sizeof(int)。字节c7 45 fc 是其余的指令(将一些值移动到rbp 的偏移量中)。另请注意,不同的前两个部分具有相同的字节大小(21)。所以,你去吧,虽然略有不同,但它们的大小是一样的。


      逐步完成组装说明

      1. push %rbp; mov %rsp, %rbp:这称为设置Stack Frame,并且是所有 C 函数的标准(除非您告诉 gcc -fomit-frame-pointer)。这使您可以通过固定寄存器访问堆栈和局部变量,在本例中为rbp

      2. mov %edi, -0x14(%rbp):这会将寄存器edi 的内容移动到我们的局部变量框架中。具体来说,偏移量-0x14

      3. mov %rsi, -0x20(%rbp):这里也一样。但这次它节省了rsi。这是 x86_64 调用约定的一部分(它现在使用寄存器而不是像 x86_32 那样将所有内容压入堆栈),但是我们没有将它们保存在寄存器中,而是通过将内容保存在局部变量框架中来释放寄存器 - 寄存器更快并且是 CPU 实际处理任何东西的唯一方式,所以我们拥有的空闲寄存器越多越好。

      注意:edirsi 寄存器的 4 字节部分,根据 x86_64 调用约定,我们知道 rsi 寄存器用于第一个参数。 main 的第一个参数是 int argc,所以我们使用一个 4 字节的寄存器来存储它是有意义的。 rsi 是第二个参数,实际上是指向字符的指针的地址 (**argv)。因此,在 64 位架构中,这完全适合 64 位寄存器。

      1. &lt;+11&gt;: movl $0xbc614e,-0x4(%rbp):这是实际的行 int i = 12345678 (0xbc614e = 12345678d)。现在,请注意我们“移动”该值的方式与我们存储main 参数的方式非常相似。我们使用偏移量-0x4(%rbp) 将其存储在“局部变量框架”上(这回答了您关于存储位置的问题)。

      2. mov $0x0, %eax; pop %rbp; retq: 再说一遍,清空帧指针并返回(结束程序,因为我们在 main)。

      3. 请注意,在第二个示例中,唯一的区别是行&lt;+11&gt;: movl $0x0,-0x4(%rbp),它有效地存储值零 - 用 C 语言编写,int i = 0

      因此,通过这些说明,您可以看到两个程序的 main 函数以完全相同的方式转换为汇编,因此它们的大小最终相同。 (假设您以相同的方式编译它们,因为编译器还在二进制文件中放入了许多其他内容,例如数据、库函数等。在 linux 中,您可以使用 objdump -D program 获得完整的反汇编转储。

      注意 2:在这些示例中,您看不到计算机如何从 rsp 中减去值以分配堆栈空间,但通常是这样做的。


      堆栈表示

      两种情况下的堆栈都是这样的(只有i 的值会改变,或者-0x4(%rbp) 的值会改变)

      |        ~~~       |  Higher Memory addresses 
      |                  |
      +------------------+ <--- Address 0x8(%rbp)
      | RETURN ADDRESS   | 
      +------------------+ <--- Address 0x0(%rbp) // instruction push %rbp
      | previous rbp     | 
      +------------------+ <--- Address -0x4(%rbp)
      | i=0x11223344     | 
      +------------------+ <---- Address -0x14(%rbp)
      | argc             |
      +------------------+  <---- address -0x20(%rbp)
      | argv             | 
      +------------------+
      |                  |
      +~~~~~~~~~~~~~~~~~~+ Lower memory addresses
      

      注意 3:堆栈增长的方向取决于您的架构。数据如何写入内存还取决于您的架构。


      资源

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2023-04-10
        • 2014-10-08
        • 1970-01-01
        • 2017-02-28
        • 1970-01-01
        • 2019-03-21
        相关资源
        最近更新 更多