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 那样专门关注一个主题。