编译器会做什么

时间:2016-09-15 16:19:34

标签: c++ c function static compilation

我已经编程了几年,但令人尴尬的是,还有一两件我还不完全清楚的事情。

在下面仅用于示例的基本代码中,当编译器遇到myFunc()时,str1和str2将在哪里存储?

它们是字符串文字的指针,所以我假设字符串文字将存储在只读内存中,但是这种情况在一个指针是静态本地而另一个指针之间有什么区别呢? 另外,我认为局部变量将存储在堆栈中,并且在调用函数之前不会分配它们?这令人困惑。

对于整数var1,它是非静态的,但var2是静态的。编译器是否会在编译时将此var2放在数据段中。我已阅读另一篇文章When do function-level static variables get allocated/initialized?,本地静态变量将在第一次使用时创建并初始化,而不是在编译期间。那么在那种情况下,如果函数永远不被调用怎么办?

提前感谢有经验的知识。

EDITED:从main()调用myFunc()。这是一个错字,因为myFunc()从未被称为

int myFunc()
{
    static char* str1 = "Hello";
    char* str2 = "World";

    int var1 = 1;
    static int var2 = 8;

}

int main()
{

    return myFunc();
}

6 个答案:

答案 0 :(得分:11)

编辑:

其他答案和评论是正确的 - 因为您的变量将被优化,因为它们甚至没有被使用。但是,让我们有一点乐趣,并实际使用它们来看看会发生什么。

我使用gcc -S trial.c按原样编译了op的程序,虽然myFunc从未被调用,但此答案没有任何其他内容发生变化。

我稍微修改了你的程序以实际使用这些变量,这样我们就可以学习更多关于编译器和链接器的功能。这是:

#include <stdio.h>

int myFunc()
{
    static const char* str1 = "Hello";
    const char* str2 = "World";

    int var1 = 1;
    static int var2 = 8;
    printf("%s %s %d %d\n", str1, str2, var1, var2);
    return 0;
}

int main()
{
    return myFunc();
}

我使用gcc -S trial.c编译并获得以下程序集文件:

    .file   "trial.c"
    .section .rdata,"dr"
.LC0:
    .ascii "World\0"
.LC1:
    .ascii "%s %s %d %d\12\0"
    .text
    .globl  myFunc
    .def    myFunc; .scl    2;  .type   32; .endef
    .seh_proc   myFunc
myFunc:
    pushq   %rbp
    .seh_pushreg    %rbp
    movq    %rsp, %rbp
    .seh_setframe   %rbp, 0
    subq    $64, %rsp
    .seh_stackalloc 64
    .seh_endprologue
    leaq    .LC0(%rip), %rax
    movq    %rax, -8(%rbp)
    movl    $1, -12(%rbp)
    movl    var2.3086(%rip), %edx
    movq    str1.3083(%rip), %rax
    movl    -12(%rbp), %r8d
    movq    -8(%rbp), %rcx
    movl    %edx, 32(%rsp)
    movl    %r8d, %r9d
    movq    %rcx, %r8
    movq    %rax, %rdx
    leaq    .LC1(%rip), %rcx
    call    printf
    movl    $0, %eax
    addq    $64, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .def    __main; .scl    2;  .type   32; .endef
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    pushq   %rbp
    .seh_pushreg    %rbp
    movq    %rsp, %rbp
    .seh_setframe   %rbp, 0
    subq    $32, %rsp
    .seh_stackalloc 32
    .seh_endprologue
    call    __main
    call    myFunc
    addq    $32, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .data
    .align 4
var2.3086:
    .long   8
    .section .rdata,"dr"
.LC2:
    .ascii "Hello\0"
    .data
    .align 8
str1.3083:
    .quad   .LC2
    .ident  "GCC: (Rev1, Built by MSYS2 project) 5.4.0"
    .def    printf; .scl    2;  .type   32; .endef

甚至在汇编文件中找不到var1。它实际上只是一个被加载到堆栈中的常量。

在汇编文件的顶部,我们在.rdata部分中看到“World”(str2)。在汇编文件中降低,字符串“Hello”在.rdata部分中,但str1的标签(包含“Hello”的标签或地址)位于.data部分中。 var2也在.data部分。

这里a stackoverflow question深入探讨了为什么会发生这种情况。

Another stackoverflow question指出.rdata部分是.data的只读部分,并解释了不同的部分。

希望这有帮助。

<小时/>

编辑:

我决定尝试使用-O3编译器标志(高优化)。这是我得到的汇编文件:

    .file   "trial.c"
    .section .rdata,"dr"
.LC0:
    .ascii "World\0"
.LC1:
    .ascii "Hello\0"
.LC2:
    .ascii "%s %s %d %d\12\0"
    .section    .text.unlikely,"x"
.LCOLDB3:
    .text
.LHOTB3:
    .p2align 4,,15
    .globl  myFunc
    .def    myFunc; .scl    2;  .type   32; .endef
    .seh_proc   myFunc
myFunc:
    subq    $56, %rsp
    .seh_stackalloc 56
    .seh_endprologue
    leaq    .LC0(%rip), %r8
    leaq    .LC1(%rip), %rdx
    leaq    .LC2(%rip), %rcx
    movl    $8, 32(%rsp)
    movl    $1, %r9d
    call    printf
    nop
    addq    $56, %rsp
    ret
    .seh_endproc
    .section    .text.unlikely,"x"
.LCOLDE3:
    .text
.LHOTE3:
    .def    __main; .scl    2;  .type   32; .endef
    .section    .text.unlikely,"x"
.LCOLDB4:
    .section    .text.startup,"x"
.LHOTB4:
    .p2align 4,,15
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    subq    $40, %rsp
    .seh_stackalloc 40
    .seh_endprologue
    call    __main
    xorl    %eax, %eax
    addq    $40, %rsp
    ret
    .seh_endproc
    .section    .text.unlikely,"x"
.LCOLDE4:
    .section    .text.startup,"x"
.LHOTE4:
    .ident  "GCC: (Rev1, Built by MSYS2 project) 5.4.0"
    .def    printf; .scl    2;  .type   32; .endef

var1现在只是一个放在寄存器(r9d)中的常量1。 var2也只是一个常量,但它放在堆栈上。此外,字符串“Hello”和“World”以更直接(有效)的方式访问。

所以,我决定尝试稍微不同的东西:

#include <stdio.h>

void myFunc()
{
    static const char* str1 = "Hello";
    const char* str2 = "World";

    int var1 = 1;
    static int var2 = 8;
    printf("%s %s %d %d\n", str1, str2, var1, var2);

    var1++;
    var2++;
    printf("%d %d", var1, var2);
}

int main()
{
    myFunc();
    myFunc();
    return 0;
}

使用gcc -O3 -S trial.c

关联的程序集
    .file   "trial.c"
    .section .rdata,"dr"
.LC0:
    .ascii "World\0"
.LC1:
    .ascii "Hello\0"
.LC2:
    .ascii "%s %s %d %d\12\0"
.LC3:
    .ascii "%d %d\0"
    .section    .text.unlikely,"x"
.LCOLDB4:
    .text
.LHOTB4:
    .p2align 4,,15
    .globl  myFunc
    .def    myFunc; .scl    2;  .type   32; .endef
    .seh_proc   myFunc
myFunc:
    subq    $56, %rsp
    .seh_stackalloc 56
    .seh_endprologue
    movl    var2.3086(%rip), %eax
    leaq    .LC0(%rip), %r8
    leaq    .LC1(%rip), %rdx
    leaq    .LC2(%rip), %rcx
    movl    $1, %r9d
    movl    %eax, 32(%rsp)
    call    printf
    movl    var2.3086(%rip), %eax
    leaq    .LC3(%rip), %rcx
    movl    $2, %edx
    leal    1(%rax), %r8d
    movl    %r8d, var2.3086(%rip)
    addq    $56, %rsp
    jmp printf
    .seh_endproc
    .section    .text.unlikely,"x"
.LCOLDE4:
    .text
.LHOTE4:
    .def    __main; .scl    2;  .type   32; .endef
    .section    .text.unlikely,"x"
.LCOLDB5:
    .section    .text.startup,"x"
.LHOTB5:
    .p2align 4,,15
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    subq    $40, %rsp
    .seh_stackalloc 40
    .seh_endprologue
    call    __main
    call    myFunc
    call    myFunc
    xorl    %eax, %eax
    addq    $40, %rsp
    ret
    .seh_endproc
    .section    .text.unlikely,"x"
.LCOLDE5:
    .section    .text.startup,"x"
.LHOTE5:
    .data
    .align 4
var2.3086:
    .long   8
    .ident  "GCC: (Rev1, Built by MSYS2 project) 5.4.0"
    .def    printf; .scl    2;  .type   32; .endef

这看起来更像原版。 var1仍然只是常量优化,但var2现在再次出现在.data部分。 “Hello”和“World”仍然在.rdata部分,因为它们是不变的。

其中一条评论指出,在具有不同编译器的不同平台上,这会有所不同。我鼓励你尝试一下。

答案 1 :(得分:3)

-1

static const char* str1 = "Hello"; 是字符串文字的静态本地指针,它将存储在只读内存中。

str1

const char* str2 = "World"; 是一个本地的,#34;堆栈分配的&#34; 指针到一个字符串文字,它将存储在只读存储器中。

str2str1是他们指向的字符串文字的相应地址。

str2

如果从未达到这些代码行,则永远不会初始化int var1 = 1; static int var2 = 8; 。我不知道编译器是否在编译时在其他地方为它留出了一块内存。

答案 2 :(得分:2)

编译器的作用必须基于代码的语义(假设一个正确工作的编译器),这就是我将要讨论的内容。

首先,一个相当小的一点。通过声明具有()的函数,您可以指定它采用固定但未指定的数字和类型的参数。这是一种过时的声明/定义形式,并且很少有理由使用它。 (空括号在C ++中有不同的含义,但您要询问C。)要指定函数具有 no 参数,请使用(void)而不是()尤其是main,因为它不是100%明确的,int main()必须被符合标准的编译器接受。“

随着这一变化:

int myFunc(void)
{
    static char* str1 = "Hello";
    char* str2 = "World";
    int var1 = 1;
    static int var2 = 8;
}

int main(void)
{
    return myFunc();
}

这个程序什么都不做;它不产生输出,也没有副作用。允许编译器将其编译为几乎没有。但是,让我们忽略它,并假设没有任何东西被丢弃。

需要考虑两个重要概念:范围生存期(也称为存储持续时间)。标识符的范围是程序文本的可见区域。它纯粹是一个编译时的概念。对象的生存期是执行期间该对象存在的持续时间。它纯粹是一个运行时概念。这两者经常混淆,特别是当你使用&#34; local&#34;和&#34;全球&#34;。

具有自动存储持续时间的对象在进入其定义的块时创建,并且在从该块退出时(逻辑上)销毁。在您的计划中,相关的数据块由{}的{​​{1}}定义。

在程序的整个运行时间内存在具有静态存储持续时间的对象。

myFunc()

static char* str1 = "Hello"; 是一个字符串文字。它指定类型为"Hello"静态数组;在整个程序执行期间存在该数组(至少逻辑上)。您不能修改该数组的内容 - 但由于历史原因,它不是char[6],如果您尝试修改它。字符串文字通常存储在只读存储器中(可能不是物理ROM,而是标记为只读的虚拟存储器)。

指针对象const也具有静态存储持续时间,但其名称仅在封闭块中可见(&#34;块范围&#34;)。它被初始化为指向str1的初始字符。在进入"Hello"之前逻辑上进行此初始化。由于字符串文字实际上是只读的,因此最好使用main来避免意外尝试修改它的风险:

const

下一步:

static const char *str1 = "hello";

指针对象char* str2 = "World"; 的名称与str2具有相同类型的块范围,但指针对象本身具有自动存储持续时间。它是在进入封闭区块时创建的,并在退出时被销毁。它被初始化为指向str1的初始字符;初始化在执行到达声明时发生。同样,最好在声明中添加"World"

const

int var1 = 1; static int var2 = 8; 具有块范围和自动存储持续时间。在运行时达到其声明时,它已初始化为var1具有块范围和静态存储持续时间。该对象用于整个程序的执行,并在进入var2之前初始化为8

现在我们遇到了一些问题。您已定义main()以返回myFunc()结果,但您实际上并未返回任何内容。实际上,这本身并不是无效的,但如果调用者使用了结果(就像你的int函数那样),则行为是未定义的。修复很简单:在结束main()之前添加return 0;

假设您已添加,}来电main。在执行myFunc期间,myFuncstr2以某种方式分配,并按照我所描述的方式进行初始化。 (var1str1没有任何结果,因为他们重新var2。)从函数返回时,为staticstr2分配的存储空间是释放,有效地摧毁物体。

但你问的问题是:编译器会做什么?答案就是:它将生成实现我刚刚描述的语义所需的任何代码。这真的是C标准所要求的。

实际上,大多数编译器生成的代码在&#34;堆栈&#34;上分配自动存储持续时间的变量。 &#34;堆栈&#34;通常是一个连续的内存区域,从一些固定的基地址开始,当项目被添加到一个方向时,它在一个方向上增长,在项目被移除时在另一个方向上收缩。它通常通过CPU寄存器管理,#34;堆栈指针&#34;。 (有些CPU也有一个&#34;帧指针&#34;。)但实际上C标准要求的是这些对象以先进后出的方式分配和释放 - 以及实际的分配和只要结果行为相同,就不需要在您预期时进行释放。例如,如果在循环内定义本地对象,则可能在每次迭代时分配和取消分配,或者其分配可能会折叠到周围范围内。 C标准并不关心(在大多数情况下,你也不应该)。甚至有些编译器根本不使用连续的堆栈;而是每个函数调用的存储从堆中分配。连续堆栈是90%以上的最佳解决方案,但并不是必需的。

具有静态存储持续时间的对象通常在程序启动时分配,在调用var1之前。大多数系统将任何初始化的静态对象的初始内容存储在可执行文件中,因此可以将其加载到内存中。 (这可能包含字符串文字。)对于初始值为零的静态对象,可执行文件可能只包含有关要分配的内存量的信息。

对于对此数据进行操作的生成指令,这完全取决于目标CPU,可能还取决于系统ABI。

答案 3 :(得分:1)

如果函数永远不会返回任何与返回类型规范相矛盾的函数,则无法在没有警告的情况下编译代码。

无论如何,在我的机器上它会生成代码。如果不使用任何优化代码,则为函数分配本地str2str1var2在代码的数据部分中分配,以指向相应的值。如果你使用优化显然会发出一个愚蠢的代码,并且未使用的局部变量作为未使用的全局变量消失。

要观察这一点,您至少可以使用nm检查目标代码:

$ gcc -o p p.c
$ nm p
0000000100000f90 T _main
0000000100000f70 T _myFunc
0000000100001000 d _myFunc.str1
0000000100001008 d _myFunc.var2
$ gcc -O3 -o p2 p.c
$ nm p2
0000000100000fb0 T _main
0000000100000fa0 T _myFunc

如果您需要更多详细信息,请使用-S生成汇编代码并观察发生的情况。

答案 4 :(得分:0)

编译器将生成一个不接受输入,什么也不做,然后不发出输出的程序。

所有这些声明完全无关紧要,因为它们对程序的[不存在]结果没有任何贡献。您可能会说他们“已经过优化”,但实际情况是他们在您生成的已编译可执行文件中确实没有模拟。

答案 5 :(得分:0)

static变量,即使是函数范围内的变量,也将存储在全局范围内。函数或范围内的static变量将仅初始化输入函数或范围的第一个时间。输入功能范围时,非static变量将在大多数编译器中分配或存储在堆栈中,并在输入范围时初始化。有些编译器会在其他地方存储局部变量。