文章

C语言编译过程

C语言编译过程

C语言从源代码变成可执行文件,大体上分为四个步骤,分别是:

1.预处理(Preprocessing)

2.编译(Compilation)

3.汇编(Assemble)

4.链接(Linking)

其中各个步骤的编译命令为:

1、预处理:对源文件进行预处理,包括头文件包含,宏定义替换,空格去除,语法分析等等

1
  gcc -E -I./inc test.c -o test.i

gcc是C语言的编译器,-E是让编译器在预处理之后就退出,不进行后续编译过程;-I指定头文件目录,这里指定的是我们自定义的头文件目录;-o指定输出文件名。这里将test.c文件预处理为test.i文件。

2、编译:将C语言代码转换为汇编代码,包括语义分析,内联扩展,死代码删除,循环展开,寄存器分配等等

1
  gcc -S -I./inc test.c -o test.s

-S让编译器在编译之后停止,不进行后续过程,这里将test.c编译为test.s

3、汇编:将汇编代码转成机器码,机器码为二进制文件

1
  gcc -c test.s -o test.o

这个会为每一个源文件产生目标文件

4、链接:将二进制文件链接成为可执行文件

1
  ld -o test.out test.o inc/mymath.o ...libraries...

在嵌入式系统中,链接产生的文件为ELF文件或者DWARF文件,再通过工具将它转换成hexbin文件

内存分配

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int iVar1;                            //未初始化的数据存放在.bss或ZI区
int iVar2 = 10;                       //初始化的数据存放在.data或RW区
const double dVar1 = 1.0;             //传统的编译器放在.data区
//许多现代 C 工具链支持单独的.const/.rodata部分,专门用于常量值。该部分可以与.data部分分开放置(在 ROM 中)。

void aFunc(int p)                     //代码存放在.text或.code区
{
	// Function def'n
}

int main(void)
{
	double loc;                       //临时变量放入.stack区,部分单片机会放入临时寄存器中
	int *p = malloc(sizeof(int));     //内存分配放入.heap区
	free(p);
}

数据初始化

在嵌入式系统上,任何初始化数据都必须存储在非易失性存储器(闪存/ROM)中。启动时,必须将任何非常量数据复制到 RAM。将代码等只读部分复制到 RAM 以加快执行速度也很常见(本例中未显示)。 为了实现这一点,链接器必须创建额外的部分以实现从 ROM 到 RAM 的复制。每个要通过复制初始化的部分都分为两部分,一个用于 ROM 部分(初始化部分),一个用于 RAM 部分(运行时位置)。链接器生成的初始化部分通常称为影子数据部分——在我们的示例中为 .sdata(尽管它可能有其他名称)。

如果不使用手动初始化,链接器也会安排启动代码来执行初始化。

.bss 部分也位于 RAM 中,但在 ROM 中没有影子副本。卷影副本是不必要的,因为 .bss 部分只包含零。该部分可以作为启动代码的一部分通过算法进行初始化。

单片机内分配

  • .cstartup——系统引导代码——明确位于闪存的开头。
  • .text.rodata 位于 Flash 中,因为它们需要持久化
  • .stack.heap 位于 RAM 中。
  • 在这种情况下,.bss 位于 RAM 中,但此时(可能)是空的。它将在启动时初始化为零。
  • .data 部分位于 RAM(用于运行时),但其初始化部分 .sdata 位于 ROM 中。

图像

链接器将执行检查以确保您的代码和数据部分适合指定的内存区域。

定位过程的输出是一个独立于平台的格式的加载文件,通常是 .ELF 或 .DWARF(尽管还有许多其他格式)

调试器在执行源代码调试时也使用 ELF 文件。

局部变量以及内存分配

当声明一个自动对象(即局部变量)时,编译器负责管理该对象的生命周期(该对象的内存可用多长时间)。如果可以的话,现代编译器会尝试为对象使用寄存器,但有一些原因可能无法使用寄存器——例如,如果对象太大而无法放入寄存器;或者如果你想获取该对象的地址。在这种情况下,编译器必须使用 RAM 来存储对象。为这些对象指定的部分是 .stack。然而,编译器直接对 .stack 部分一无所知——它通过一个特殊的寄存器访问所有自动对象:堆栈指针 (SP)。

由于编译器对 .stack 部分一无所知,因此无法知道要为其分配多少内存。这就是为什么您,程序员,必须在链接器控制文件中指定 .stack 部分的大小。一些编译器可以协助进行这种大小计算,但它仍然只是一个估计。为嵌入式系统正确分配 .stack 部分是嵌入式系统设计的“魔法”之一!弄错可能会导致一些可怕的调试问题。

.heap 部分保留供 malloc 使用。当您调用 malloc(显然是 free)时,对象(称为“动态对象”)是从 .heap 部分分配的。编译器对这些对象的生命周期一无所知——它只看到函数调用;它不知道他们在做什么!动态对象的生命周期必须在程序员的控制之下。由于动态对象的生命周期可以跨越多个范围(例如,在一个函数中创建,在另一个函数中销毁),因此由程序员来跟踪这一点。有证据表明程序员在这项工作中表现不佳。

请注意,malloc 实际上确实知道 .heap 部分的大小;并跟踪已消耗的数量。如果您尝试分配超出 .heap 部分中可用的内容,malloc 将在运行时通过返回 NULL 指针来通知您。

因此,也许您这样写可能更准确: .stack 部分由编译器消耗(自动对象);.heap 由程序员(动态对象)使用。但是这两个部分都必须由程序员分配

参考:

link1

link2

本文由作者按照 CC BY 4.0 进行授权