C语言生成可执行二进制文件的具体过程

描述

Hi,我是小杜。SoC验证中会经常使用C语言,所以需要知道C语言生成可执行二进制文件的具体过程以及如何从生成的中间文件读取有用信息。因为小杜是转行做数字IC验证,SoC的知识需要重头开始学,如果错误,还请批评指正。

C语言源码到生成可执行文件的过程通常包括预处理(Preprocessing)编译(Compilation)汇编(Assembly)链接(Linking)等多个步骤,每个步骤都有其特定的任务和产物。下面,小杜通过一个具体的例子详细讲述这个过程,以及如何通过反汇编(Disassembly)来查看汇编、链接产生的不可读二进制目标文件。

C语言源码准备

首先编写3个文件,一个是主程序 main.c,另一个是功能函数 func.c,外加一个头文件 func.h。具体路径为:

 

project/
│
├── main.c
├── func.c
└── func.h

 

代码如下:

 

// main.c
#include 
#include 


int main() {
    int result = add(5, 3);
    printf("Result: %d
", result);
    return 0;
}
// func.c
#include "func.h"


int add(int a, int b) {
    return a + b;
}
// func.h
#ifndef FUNC_H
#define FUNC_H


int add(int a, int b);


#endif

 

分步生成可执行文件

1. 预处理(Preprocessing)

预处理器处理 #include、#define 等指令,并生成一个预处理后的文件。可以使用 -E 选项运行预处理器。

 

gcc -E main.c -o main.i -I .
gcc -E func.c -o func.i -I .

 

main.i 和 func.i 是预处理后的文件,包含展开后的宏和头文件内容。以main.i为例,预处理后的文件可能会非常长,因为所有的宏、头文件都被展开。预处理器在预处理文件中插入了很多#开头的行,提供了文件和行号信息用于调试和错误报告。下面截图展示了main.i文件中的主要部分:

C语言

源码中所有的注释都会在预处理阶段被去除。如果源码中有条件编译指令,比如`ifdef,`ifndef,`if 等,预处理器会根据条件和结果保留或删除相应的代码。

预处理文件的用途

调试:通过查看预处理后的文件,可以检查宏和头文件是否正确展开,从而帮助调试编译问题。

了解代码结构:预处理文件展示了代码的最终形态,包括所有的头文件和宏定义,这对理解代码的整体结构很有帮助。

性能优化:分析预处理后的代码,有助于识别和优化编译时间和代码冗余问题。

2. 编译(Compilation)

编译器将预处理后的C代码转换为汇编代码。可以使用 -S 选项生成汇编代码。

 

gcc -s main.i -o main.s -I .
gcc -s func.i -o func.s -I .

 

C语言

从生成的汇编代码中我们可以看到文件和段定义函数入口和栈帧设置调用add函数调用printf函数以及函数退出

汇编代码可能是调试过程中接触的最底层部分,SoC验证过程中如果CPU卡死,通过记录的寄存器内容、程序计数器(PC)和堆栈指针(SP),找到对应的汇编指令就可以知道CPU卡死的具体位置,这对调试和找出代码bug十分重要。从汇编代码中我们可以得到如下信息:

函数调用和参数传递 

通过汇编代码可以看到函数是如何调用的,以及参数是如何传递的。例如,在x86-64架构中,前六个整数参数通过寄存器(如EDI、ESI、EDX等)传递。

栈帧管理

汇编代码展示了栈帧是如何创建和销毁的,尤其是通过 pushq %rbp、movq %rsp, %rbp 和 leave 指令。这有助于理解函数调用过程中栈的变化。

变量和寄存器操作
汇编代码展示了如何使用寄存器和内存操作来实现程序逻辑。例如,通过 movl 指令可以看到如何在寄存器和内存之间传递数据。

调试和优化

通过分析汇编代码,可以发现编译器生成的代码是否高效。例如,可以识别不必要的指令或冗余操作,进而优化代码。

3. 汇编(Assembly)与反汇编(Disassembly)

汇编器将汇编代码转换为机器码,生成目标文件。可以使用 -c 选项生成目标文件。这一步开始生成的二进制目标文件已经不能看了,不过我们还是可以通过反汇编来获取有用信息。

 

gcc -c main.s -o main.o -I .
gcc -c func.s -o func.o -I .

 

C语言

main.o 和 func.o 是目标文件,包含机器代码、符号表和调试信息,但它们是二进制格式,不像汇编代码那样直接可读。我们可以使用工具来查看分析目标文件,以获取有用的信息,比如使用objdump。

反汇编

通过反汇编可以查看目标文件的机器码、段信息以及符号表。

 

objdump -d main.o # 反汇编目标文件,显示机器码
objdump -h main.o # 显示目标文件的段信息
objdump -t main.o # 显示符号表

 

比如反汇编产生机器码(工作中一般不会接触这么深,这里只是举例):

C语言

4. 链接(Linking)

链接器将多个目标文件和库文件链接在一起,生成最终的可执行文件。

 

gcc main.o func.o -o myprogram

 

C语言

myprogram 是最终生成的可执行文件。二进制可执行文件不可读,但同样可以使用objdump来查看可执行文件的有用信息,比如:

 

objdump -f myprogram # 查看可执行文件功能

 

该输出展示了可执行文件的基本信息,包括文件格式、架构、标志和程序入口信息。

C语言

一步到位的编译和链接

通常我们会直接使用 gcc 命令来一步到位完成整个过程,而不需要手动执行每个步骤:

 

gcc main.c func. -o myprogram

 

这个命令会自动处理上述所有步骤,并生成最终的可执行文件 myprogram。

总结 让我们来总结一下C语言源码到最终的可执行二进制文件的4个过程分别干了哪些事:

预处理:处理头文件包含和宏定义,生成一个单一的C源文件。

编译:将C源文件转换为汇编代码,这一步会进行语法检查和优化。

汇编:将汇编代码转换为目标文件,目标文件是二进制格式的机器码,但还不是完整的可执行程序。

链接:将多个目标文件和库文件链接在一起,解决符号引用问题(如函数和变量的定义和声明),生成最终的可执行文件。

感谢你看到这里。项目工作中,其实工具链建立好之后一般也就不会去关注这些过程了,写完C代码走脚本流程即可,但如果出现bug,了解这一过程对解决bug就很重要了!

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分