如果您对嵌入式系统不熟悉,显然已经有人问过这个问题。程序的编译过程和内存布局分别是什么阶段。因此,在本教程中,我们将讨论C程序的编译步骤和内存布局。
C程序的编译步骤和内存布局
C的编译步骤
通常,编译C程序是一个多阶段的过程,并使用不同的“工具”。
- Preprocessing 预处理
- Compilation 汇编
- Assembly 部件
- Linking 连结
在本文中,我将逐步完成编译以下C程序的四个阶段:
/*
* "Hello, World!": A classic.
*/
#include <stdio.h>
int main(void)
{
puts("Hello, World!");
return 0;
}
预处理或预编译
编译的第一阶段称为预处理。在此阶段,以#
字符开头的行被预处理器解释为预处理器命令。在解释命令之前,预处理器会进行一些初始处理。这包括连接连续的行(以\
结尾的行)和删除注释。
要打印预处理阶段的结果,请将-E
选项传递给cc
:
cc -E hello_world.c
鉴于Hello, World!
在上面的示例中,预处理器将生成stdio.h
头文件的内容以及thehello_world.c
文件的内容,并从其前导注释中删除:
[lines omitted for brevity]
extern int __vsnprintf_chk (char * restrict, size_t,
int, size_t, const char * restrict, va_list);
# 493 "/usr/include/stdio.h" 2 3 4
# 2 "hello_world.c" 2
int main(void) {
puts("Hello, World!");
return 0;
}
汇编
编译的第二阶段令人困惑,称为编译。在此阶段,预处理后的代码将转换为目标处理器体系结构专用的汇编指令。这些构成了人类可读的中间语言。
此步骤的存在允许C
代码包含内联汇编指令,并允许使用不同的assemblers 汇编器。
一些编译器还支持使用集成汇编程序,在该汇编程序中,编译阶段直接生成机器代码,从而避免了生成中间汇编指令和调用汇编程序的开销。
要保存编译阶段的结果,请将-S
选项传递给cc
:
cc -S hello_world.c
这将创建一个名为hello_world.s
的文件,其中包含生成的汇编指令。在Mac OS 10.10.4
上,其中cc
是clang
的别名,将生成以下输出:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 10
.globl _main
.align 4, 0x90
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
subq $16, %rsp
leaq L_.str(%rip), %rdi
movl $0, -4(%rbp)
callq _puts
xorl %ecx, %ecx
movl %eax, -8(%rbp) ## 4-byte Spill
movl %ecx, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello, World!"
.subsections_via_symbols
组合
在汇编阶段,使用汇编器将汇编指令翻译为机器代码或目标代码。输出包含要由目标处理器运行的实际指令。
要保存汇编阶段的结果,请将-c
选项传递给cc
:
cc -c hello_world.c
运行上面的命令将创建一个名为hello_world.o
的文件,其中包含程序的目标代码。该文件的内容为二进制格式,可以通过运行以下命令之一使用hexdump
或od
进行检查:
hexdump hello_world.o
od -c hello_world.o
链接
在汇编阶段生成的目标代码由处理器可以理解的机器指令组成,但是程序的某些部分混乱或丢失。要生成可执行程序,必须重新排列现有片段,并填充缺少的片段。此过程称为链接。
链接器将排列目标代码段,以便某些段中的函数可以成功调用其他段中的函数。它还将添加一些片段,其中包含程序使用的库函数的说明。在“Hello, World!” 的情况下程序,链接器将添加puts
功能的目标代码。
该阶段的结果是最终的可执行程序。如果不带选项运行,cc
将将此文件命名为a.out
。要为文件命名,请将-o
选项传递给cc
:
cc -o hello_world hello_world.c
供您快速参考:
C程序的内存布局
当您运行任何C
程序时,其可执行映像都会以一种有组织的方式加载到计算机的RAM
中,这称为进程地址空间或C
程序的内存布局。
此内存布局以以下方式组织:
- 文字或代码段
- 初始化的数据段
- 未初始化的数据段(bss)
- 栈段
- 堆段
- 未映射或保留的细分
文字或代码段
代码段,也称为文本段,包含已编译程序的机器代码。可执行目标文件的文本段通常是只读段,可以防止意外修改程序。它将是.bin
或.exe
或.hex
等。
作为内存区域,可以在堆或栈下方放置一个文本段,以防止堆和栈溢出覆盖它。
数据段
数据段存储程序数据。该数据可以采用已初始化或未初始化变量的形式,并且可以是局部或全局的。数据段进一步分为四个子数据段(初始化数据段,未初始化或.bss数据段,栈和堆),以根据变量是局部变量还是全局变量以及已初始化还是未初始化来存储变量。
初始化数据段
初始化数据或简单数据段存储所有预先初始化的全局变量,静态变量,常量变量和外部变量(用外部关键字声明)。
请注意,数据段不是只读的,因为可以在运行时更改变量的值。
该段可以进一步分为初始化的只读区域和初始化的读写区域。
除const变量外,所有全局,静态和外部变量都存储在已初始化的读写存储器中。
//This will stored in initialized read-only memory
const int i = 100;
//This will stored in initialized read-write memory
int j=1;
char c[12] = "EmbeTronicX"
int main()
{
}
未初始化的数据段
未初始化的数据段也称为BSS段。 BSS代表以古老的汇编程序操作员命名的“由符号开始的块”。未初始化的数据段包含所有已初始化为零或在源代码中没有显式初始化的全局变量和静态变量。
//This will stored in uninitialized read-only memory
static int i = 0;
int j;
int main()
{
}
栈段
栈,用于存储自动变量,以及每次调用函数时保存的信息。每次调用函数时,返回位置的地址和有关调用者环境的某些信息(例如某些机器寄存器)都保存在堆栈中。然后,新调用的函数在堆栈上为其自动和临时变量分配空间。这就是C中的递归函数如何工作的方式。每次递归函数调用自身时,都会使用一个新的堆栈框架,因此一组变量不会干扰该函数另一个实例的变量。
因此,堆栈框架包含一些数据,例如返回地址,传递给它的参数,局部变量以及被调用函数所需的任何其他信息。
一个“栈指针(SP)”通过调整到下一个或上一个地址的堆栈指针来跟踪每次堆栈上的堆栈操作。
传统上,堆栈区与堆区相邻,并且方向相反。当堆栈指针遇到堆指针时,可用内存就用完了。 (借助现代的大型地址空间和虚拟内存技术,它们几乎可以放置在任何地方,但它们通常仍会朝相反的方向生长。)
堆段
堆是通常进行动态内存分配的段。
堆区域从BSS段的末尾开始,并从那里增长到更大的地址。堆区域由malloc,realloc和free管理,它们可以使用brk和sbrk系统调用来调整其大小(请注意使用不需要brk / sbrk和单个“堆区”来满足malloc / realloc / free的约定;它们也可以使用mmap来实现,以将虚拟内存的潜在不连续区域保留到进程的虚拟地址空间中) 。堆区域由进程中的所有共享库和动态加载的模块共享。
未映射或保留的细分
未映射或保留的段包含命令行参数和其他与程序相关的数据,例如可执行映像的低地址-高地址等。
请看下面的例子。我将通过实际示例告诉您内存布局。
Example
- 我们将看到以下程序的内存布局。
#include <stdio.h>
int main(void)
{
return 0;
}
编译并检查内存
[embetronicx@linux]$ gcc memory-layout-test.c -o memory-layout-test
[embetronicx@linux]$ size memory-layout-test
text data bss dec hex filename
960 248 8 1216 4c0 memory-layout-test
Step 2:
- 让我们在程序中添加一个全局变量,现在检查bss的大小。
#include <stdio.h>
int embetronicx; //uninitialized global variable. It will stored in bss
int main(void)
{
return 0;
}
编译并检查内存
[embetronicx@linux]$ gcc memory-layout-test.c -o memory-layout-test
[embetronicx@linux]$ size memory-layout-test
text data bss dec hex filename
960 248 12 1220 4c4 memory-layout-test
- 让我们添加一个静态变量,该变量也存储在bss中。
#include <stdio.h>
int embetronicx; //uninitialized global variable. It will stored in bss
int main(void)
{
static int i; //Uninitialized static variable stored in bss
return 0;
}
编译并检查内存
[embetronicx@linux]$ gcc memory-layout-test.c -o memory-layout-test
[embetronicx@linux]$ size memory-layout-test
text data bss dec hex filename
960 248 16 1224 4c4 memory-layout-test
- 让我们将静态变量初始化为非零,然后将其存储在“初始化数据段”(DS)中。
#include <stdio.h>
int embetronicx; //uninitialized global variable. It will stored in bss
int main(void)
{
static int i = 10; //Initialized static variable stored in Initialized Data Segment
return 0;
}
编译并检查内存
[embetronicx@linux]$ gcc memory-layout-test.c -o memory-layout-test
[embetronicx@linux]$ size memory-layout-test
text data bss dec hex filename
960 252 12 1224 4c4 memory-layout-test
- 让我们将全局变量初始化为非零,然后将其存储在“初始化数据段”(DS)中。
#include <stdio.h>
int embetronicx = 100; //Initialized Global variable stored in Initialized Data Segment
int main(void)
{
static int i = 10; //Initialized static variable stored in Initialized Data Segment
return 0;
}
编译并检查内存
[embetronicx@linux]$ gcc memory-layout-test.c -o memory-layout-test
[embetronicx@linux]$ size memory-layout-test
text data bss dec hex filename
960 256 8 1224 4c4 memory-layout-test
在本教程中,我们讨论了C程序及其各种段(文本或代码段,数据,.bss段,堆栈和堆段)的编译和内存布局所涉及的步骤。希望您喜欢阅读本文。谢谢阅读!