【问题标题】:Does gcc actually treat prototypes as functions and do their parameters have memory allocated?gcc 是否实际上将原型视为函数并且它们的参数是否分配了内存?
【发布时间】:2017-07-25 06:45:06
【问题描述】:

我有一个奇怪的具体问题,关于 C 的设计,实际上是关于编程和语言设计的一般性问题。

这是它的基础:如果我调用一个我只是原型化的函数,而不是分配的,我会调用实际的 C 函数数据结构吗?换句话说,这在任何意义上都是真正的函数,还是 gcc 会以某种具有代表性的方式处理原型,可能使用不同的数据结构?该问题中的具体点是关于是否为使用原型声明的参数分配内存以及是否创建空范围。

Gcc 当然不会让你这样做,但如果它会编写与通常相同的机器代码,并且我确实尝试调用仅原型化的函数,那么失败会是:

  1. 原型并不是所有意义上的函数

  2. 参数实际上不是声明,所以它们不是 表示具有正确地址和正常分配的内存 行为

  3. 由于没有大括号,gcc 没有,或者不能, 为要添加到堆栈的这个“函数”生成一个范围, 由于没有范围,因此使参数声明变得荒谬 以便在其中声明它们(因此它们不是 - 因此没有地址)

  4. 创建了一个范围,否则它的内容可能会继续 堆栈,但执行终止,因为没有指令 在内存中推进程序的功能块

  5. 从技术上讲,您可以像对待原型一样思考和对待原型 函数,问题是它们什么都不做!

  6. 其他我完全错过的东西

我不知道为什么这个问题对我很重要 - 但我想如果有什么比一切都重要 - 这有点让我发疯......

谢谢大家!

【问题讨论】:

    标签: c gcc compiler-construction language-design


    【解决方案1】:

    当您使用不带-c 标志的gcc 时,它会做两件事:它将源文件编译为对象和文件,然后将对象文件链接到最终的可执行(或库)文件中。所以在这个意义上你可以把它看作两个工具,事实上对于链接步骤 gcc 确实调用了单独的工具 ld。

    现在当 gcc 看到一个函数原型时会发生什么?它将有关函数签名的信息存储在其内部数据结构中,因此它知道如何对函数的调用进行类型检查以及如何为函数调用生成代码(取决于类型,它可能必须插入代码以进行隐式转换和生成的例如,在调用可变参数函数时,代码看起来会有所不同)。原型不会导致生成任何实际代码。

    当 gcc 看到一个实际的函数定义时,它也会在其内部数据结构中存储相同的信息,但它也会为函数的主体生成代码,并将函数的名称和生成的代码的地址存储在对象的符号中表。

    现在对于函数调用,编译器对仅原型函数的处理与对实际实现的函数的处理相同。事实上,它甚至不知道函数的定义是否存在,因为编译器一次只能看到一个 c 文件(或者更确切地说是一个编译单元),并且定义很可能存在于另一个文件中。那么编译器是做什么的呢?它将参数推送到系统堆栈和/或将它们存储在寄存器中,具体取决于参数的数量和类型以及调用约定。然后它使用函数名作为符号添加对函数的调用。

    无论是否定义了所有功能,这都将起作用。如果您只执行gcc -c,您将不会收到未定义函数的错误。

    现在 ld 做什么?它遍历所有目标文件并将它们的内容一起复制到最终的可执行文件或库文件中。这样做时,它将函数和变量的符号名称替换为它们在可执行文件中的实际地址。如果未定义函数,这就是您得到错误的部分。

    如果它允许你调用未定义的函数会发生什么?好吧,它不能。当您调用未定义的函数作为一种健全性检查时,它不会拒绝创建可执行文件,它会拒绝创建可执行文件因为它不能。当没有函数定义时,就没有用来替换符号的地址。所以无法链接文件。

    所以我猜答案是“a”:原型不是函数,因为它们根本不存在于生成的目标文件中。一个函数只会存在于包含其实际定义的目标文件中,如果这样的文件不存在(或有多个),那就是错误。

    【讨论】:

    • 哇,超级简洁明了-谢谢!在我开始组装之前,我仍在尝试了解 C 生态系统下的一些底层行为......
    【解决方案2】:

    a) 原型并不是所有意义上的函数

    更正它们不是函数,只是帮助编译器的声明。

    b) 参数实际上不是声明,所以它们不是 表示具有正确地址和正常分配的内存 行为

    整个原型是一个声明,一个原型,没有生成代码。

    c) 因为没有大括号,gcc 没有,或者不能, 为要添加到堆栈的这个“函数”生成一个范围, 由于没有范围,因此使参数声明变得荒谬 让它们在其中声明(因此它们不是 - 因此没有地址)

    同样没有花括号它是一个声明或原型,它不生成代码它是一个定义,用于在实际代码调用该函数时帮助编译器。它是一个“函数原型”的函数

    d) 创建了一个范围,否则它的内容可能会继续 堆栈,但执行终止,因为没有指令 在内存中推进程序的功能块

    堆栈与此无关,即使它是真正的代码。这是定义的目标和实现。

    e) 从技术上讲,您可以想到并像对待原型一样对待 函数,问题是它们什么都不做!

    它们是函数定义,以便正确地准备对这些函数的调用。在你用 C 语言从另一个函数调用一个函数之前,你必须定义它,要么完全定义为真实的,要么是原型。

    f) 我完全错过的其他东西

    不,你有它。

    实际上涉及三个工具。编译器制作汇编语言,汇编器将其组装成每个源文件的重复对象,然后链接器将所有这些链接在一起。如果编译器看到一个项目是在该文件及其包含的编译中未定义的全局变量或函数,那么它会为链接器留下信息以将对象链接在一起,以便链接器可以使用它定义的地址解析该外部为那个项目。

    所以

    unsigned int fun1 ( unsigned int x );
    unsigned int fun0 ( unsigned int x )
    {
        return(fun1(x)+1);
    }
    

    我可以使用 extern(见下文)或不使用,有人可能会争辩说这是正确的,但 gcc 似乎并不在意。

    arm-none-eabi-gcc -c -O2 -save-temps fun0.c
    

    arm 更容易阅读,基本上是使用最广泛的指令集。

    通常 gcc 会删除临时文件,即使使用 -c 它也会调用汇编程序

    fun0.s

        .cpu arm7tdmi
        .eabi_attribute 20, 1
        .eabi_attribute 21, 1
        .eabi_attribute 23, 3
        .eabi_attribute 24, 1
        .eabi_attribute 25, 1
        .eabi_attribute 26, 1
        .eabi_attribute 30, 2
        .eabi_attribute 34, 0
        .eabi_attribute 18, 4
        .file   "fun0.c"
        .text
        .align  2
        .global fun0
        .syntax unified
        .arm
        .fpu softvfp
        .type   fun0, %function
    fun0:
        @ Function supports interworking.
        @ args = 0, pretend = 0, frame = 0
        @ frame_needed = 0, uses_anonymous_args = 0
        push    {r4, lr}
        bl  fun1
        pop {r4, lr}
        add r0, r0, #1
        bx  lr
        .size   fun0, .-fun0
        .ident  "GCC: (GNU) 6.2.0"
    

    生成一个反汇编为的目标文件

    00000000 <fun0>:
       0:   e92d4010    push    {r4, lr}
       4:   ebfffffe    bl  0 <fun1>
       8:   e8bd4010    pop {r4, lr}
       c:   e2800001    add r0, r0, #1
      10:   e12fff1e    bx  lr
    

    只是该函数的代码,对于普通读者来说并不明显,但是对 fun1 的调用 (bl) 并不完整,它必须稍后由链接器填写以连接两者。这里根本没有 fun1 代码,它只是一个原型,以便 gcc 可以正确创建 fun0。

    有趣1

    extern unsigned int fun2 ( unsigned int );
    unsigned int fun1 ( unsigned int x )
    {
        return(fun2(x)+2);
    }
    

    这次用的是extern

    00000000 <fun1>:
       0:   e92d4010    push    {r4, lr}
       4:   ebfffffe    bl  0 <fun2>
       8:   e8bd4010    pop {r4, lr}
       c:   e2800002    add r0, r0, #2
      10:   e12fff1e    bx  lr
    

    没有改变它只是一个原型。

    unsigned int fun2 ( unsigned int  x)
    {
        return(x+3);
    }
    

    在此处将其称为行尾并返回一些内容

    00000000 <fun2>:
       0:   e2800003    add r0, r0, #3
       4:   e12fff1e    bx  lr
    

    到目前为止,我们已经为每个 C 源文件编译成汇编语言,然后编译器调用汇编程序来生成目标文件,但是这些目标文件在链接之前并不是真正的程序,在特殊情况下可以使用它们,但是这个工具链的设计是使用全链编译器、汇编器、链接器。

    如果我添加一个引导程序,就足以成为一个真正的程序

    .globl _start
    _start:
        mov sp,#0x8000
        mov r0,#0
        bl fun0
        b .
    

    然后将它们链接在一起

    00008000 <_start>:
        8000:   e3a0d902    mov sp, #32768  ; 0x8000
        8004:   e3a00000    mov r0, #0
        8008:   eb000000    bl  8010 <fun0>
        800c:   eafffffe    b   800c <_start+0xc>
    
    00008010 <fun0>:
        8010:   e92d4010    push    {r4, lr}
        8014:   eb000002    bl  8024 <fun1>
        8018:   e8bd4010    pop {r4, lr}
        801c:   e2800001    add r0, r0, #1
        8020:   e12fff1e    bx  lr
    
    00008024 <fun1>:
        8024:   e92d4010    push    {r4, lr}
        8028:   eb000002    bl  8038 <fun2>
        802c:   e8bd4010    pop {r4, lr}
        8030:   e2800002    add r0, r0, #2
        8034:   e12fff1e    bx  lr
    
    00008038 <fun2>:
        8038:   e2800003    add r0, r0, #3
        803c:   e12fff1e    bx  lr
    

    大部分代码与位置无关,调用(bl,分支链接)也是如此,但需要一个 pc 相对偏移量,链接器完成了修改这些指令的工作,以便它们连接到函数的相对地址被调用。注意这里没有涉及堆栈,除了保留返回地址之外,r4 的推送是为了堆栈对齐,几乎可以使用除 r4 之外的任何寄存器,严格保持堆栈在 64 位边界上。

    原型只是为了让调用正确的原型。如果您关闭原型,那么它将采用整数并声明警告。

    fun1.c: In function ‘fun1’:
    fun1.c:5:12: warning: implicit declaration of function ‘fun2’ [-Wimplicit-function-declaration]
         return(fun2(x)+2);
                ^~~~
    
    00000000 <fun1>:
       0:   e92d4010    push    {r4, lr}
       4:   ebfffffe    bl  0 <fun2>
       8:   e8bd4010    pop {r4, lr}
       c:   e2800002    add r0, r0, #2
      10:   e12fff1e    bx  lr
    

    但是如果它不是一个 int 或者我们没有幸运的东西或者我们的原型错误。

    extern float fun2 ( unsigned int );
    unsigned int fun1 ( unsigned int x )
    {
        return(fun2(x)+2);
    }
    

    生产

    00000000 <fun1>:
       0:   e92d4010    push    {r4, lr}
       4:   ebfffffe    bl  0 <fun2>
       8:   e3a01101    mov r1, #1073741824 ; 0x40000000
       c:   ebfffffe    bl  0 <__aeabi_fadd>
      10:   ebfffffe    bl  0 <__aeabi_f2uiz>
      14:   e8bd4010    pop {r4, lr}
      18:   e12fff1e    bx  lr
    

    gccs 隐式声明以便它可以继续运行将是错误的,并且代码将无法正常工作。

    如果您不将 extern 放在前面,则 gcc(或 gcc 的某些未来版本)之外的某些编译器可能会实际生成代码。由于人们的习惯可能很糟糕,因为许多现有代码会破坏,但除非标准另有说明,否则编译器编写者可能会解释这一点

    void more_fun ( void );
    

    例如作为一个完整的函数,并生成一些代码,但是这个

    unsigned int more_fun ( unsigned int ); 
    

    对于那个编译器来说可能有点难以驾驭它,至少希望他们抱怨没有变量名。

    我不知道我见过从原型生成代码的编译器,并且见过大量代码,如果编译器尝试这样做会产生问题。您将对链接器不知道如何处理的每个函数有多个定义。只是没有意义。

    编辑

    我假设您的意思是一般的编译器/工具链设计,而不是语言设计。有些语言总是要编译的(Pascal、C、C++),有些是要解释的(JAVA、Pyton、Perl、BASIC),但这并不意味着你不能编译它们或不编译它们。 JAVA 和 Python 是编译语言,但设计者同时进行了语言和实现,它们的目的是编译为通用机器代码,虚拟机代码,创建特定于目标的虚拟机来解释。 Pascal 曾经也是这样(可以说 Small C 也是如此)但是例如 gcc 作为一个 JAVA 前端,如果我理解正确,它会生成本地后端代码,也许我错了,但我认为我看到了。 Python 可能有也可能没有编译到目标的方法,在每种情况下,您都需要库来填补虚拟机系统调用的空白(虚拟指令实际上是系统调用,而不是像运行的单个指令那样的 CISC/RISC在内存或寄存器上)。

    设计语言部分是语法,然后是所需的实现。例如,语言本身并没有使其面向对象,与它无关,编译器的实现会或不会这样做。但是当人们着手设计一种语言时,经常/有时也会推动编译器的实现,JAVA 和 python 是很好的例子。其他语言(我认为是 D)旨在被编译并利用 LLVM 或 GCC 添加新语言前端并利用现有后端的能力。

    所以语言设计是一个过于笼统的主题,必须像 C 那样专门关注一个主题。

    【讨论】:

      【解决方案3】:

      其他我完全错过的东西。

      是的。这将是一个链接器错误。与编译器无关。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-07-16
        • 2011-04-02
        • 2019-01-17
        • 2016-05-11
        • 2016-03-04
        • 2012-05-22
        相关资源
        最近更新 更多