gcc 实际上是否将原型视为函数并且它们的参数是否分配了内存?

Does gcc actually treat prototypes as functions and do their parameters have memory allocated?

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

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

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

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

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

  3. 因为没有花括号,gcc 没有,或者不能, 为这个 "function" 生成一个范围,将其添加到堆栈中, 使参数声明荒谬,因为没有范围 用于声明它们(因此它们不是 - 因此没有地址)

  4. 已创建范围,否则其内容可能会继续 栈,但是执行死了,因为里面没有指令 在内存中推进程序的功能块

  5. 从技术上讲,您可以完全按照原型来思考和处理原型 功能,问题是他们什么都不做!

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

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

谢谢大家!

当你在没有 -c 标志的情况下使用 gcc 时,它会做两件事:它将源文件编译为对象和文件,然后 link 将目标文件放入最终的 executable(或库)文件。因此,从这个意义上说,您可以将其视为两个工具,实际上对于 linking 步骤,gcc 确实调用了单独的工具 ld.

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

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

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

无论是否定义了所有函数,这都将起作用。如果你只做 gcc -c.

,你不会得到未定义函数的错误

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

那么如果它让你调用未定义的函数会发生什么?好吧,它不能。当您调用未定义的函数作为一种健全性检查时,它不会拒绝创建 executable,它拒绝创建 executable 因为它不能.当没有函数定义时,就没有用于替换符号的地址。所以 link 文件是不可能的。

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

a) prototypes aren't in all senses functions

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

b) the parameters aren't actually declarations, so they don't represent allocated memory with proper addresses and normal behaviors

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

c) since there were no curly brackets, gcc didn't, or couldn't, generate a scope for this "function" to be added to the stack, making the parameter declarations absurd since there is no scope for them to be declared in (thus they aren't - thus no addresses)

再次没有花括号它是一个声明或原型,它不生成代码它是一个定义,用于在实际代码调用该函数时帮助编译器。它是一个函数,虽然 "function prototype"

d) there is a scope created, it's contents could otherwise go on the stack, but execution dies because there were no instructions in the function block to advance the program in memory

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

e) you can technically think of, and treat prototypes exactly like functions, problem is they don't do anything!

它们是函数定义,因此可以正确准备对这些函数的调用生成。在 C 中从另一个函数调用一个函数之前,您必须对其进行定义,或者完全定义它是真实的还是原型。

f) something else I have completely missed

不,你有。

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

所以

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

只是该函数的代码,对一般人来说并不明显 reader 但是对 fun1 的调用 (bl) 未完成,必须由 linker 稍后填写才能连接他们俩。这里根本没有 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);
}

在这里称它为行尾,只是 return 一些东西

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

到目前为止,我们已经为每个C源文件编译成汇编语言,然后编译器调用汇编程序生成目标文件,但是这些目标文件直到linked才真正是程序,有在特殊情况下使用它们的方法,但是这个工具链的设计是使用整个链编译器,汇编器,linker.

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

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

然后 link 一起

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,分支 links)也是如此,但需要一个 pc 相对偏移量,linker 做了修改那些的工作指令,以便它们连接到被调用函数的相对地址。请注意,这里不涉及堆栈,除了保留 return 地址之外,压入 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 ); 

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

我不知道我见过从原型生成代码的编译器,也见过如果编译器尝试生成大量代码会产生问题的情况。您将对 link 用户不知道如何处理的每个函数有多个定义。它只是没有意义。

编辑

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

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

所以语言设计是一个太笼统的话题,必须像你对 C 所做的那样专门关注一个话题。

something else I have completely missed.

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