GCC工具链详解

举报
樊心昊 发表于 2020/06/23 17:12:09 2020/06/23
【摘要】 摘要:GCC全称GNU Compiler Collection,是一套由 GNU 开发的编程语言编译器。它是一套以 GPL 及 LGPL 许可证所发行的自由软件。我们使用IoT Studio编译程序时,最终被调用的编译器也是GCC的一个分支,arm-none-eabi-工具链,注意这里所说的“链”,指一系列工具,编译器、汇编器、链接器等工具,被组装到了一起,形成了一个链子。本篇主要讲解他们的...

摘要:GCC全称GNU Compiler Collection,是一套由 GNU 开发的编程语言编译器。它是一套以 GPL 及 LGPL 许可证所发行的自由软件。我们使用IoT Studio编译程序时,最终被调用的编译器也是GCC的一个分支,arm-none-eabi-工具链,注意这里所说的“链”,指一系列工具,编译器、汇编器、链接器等工具,被组装到了一起,形成了一个链子。本篇主要讲解他们的用法。

 

一个C文件到hex/bin文件的过程

其实我们常说的“编译”一个程序,其中“编译“步骤包括了预处理、编译、汇编、链接等步骤,不过常见的IDE(Keil,IAR等)或者Makfile,为了方便用户的使用,将这些步骤封装起来了,我们只需点击“build”或者输入“make”,即可拿到烧录文件hex/bin,为了能提高大家在实际开发中的排错能力,我今天必须将这些知识都拿出来说一说。

下面我就按照顺序“预处理à编译à汇编à链接à将elf文件转换为hex/bin文件”在Ubuntu系统下逐步向大家演示并讲解,我用gcc工具链替代arm-none-eabi-gcc工具链,因为ubuntu默认带有gcc工具链,如果你有兴趣可以跟着我做一做下面的实验从而加深理解和记忆。

       首先创建两个文件:hello.c和hello.h。

image.png image.png

预处理

     预处理的作用就是将注释替换为空格和处理宏定义,你看到的#include,#ifndef,#define,#endif都是宏定义的标识符,这一步是将宏定义替换为编译器可以识别的符号,我们可以使用如下命令,让GCC工具链只进行预处理操作,将预后的数据生成到hello.E文件中:

       gcc -E -o hello.E hello.c

       我们现在可以来查看一下hello.E文件中的内容,我这里只截取最后15行的内容,因为前面的内容是#include <stdio.h>语句引入的内容,我们暂时不关心。

       image.png

       请注意第726行到732行,这是hello.h文件中的内容,被预处理器拷贝到了hello.c文件中,str的定义就有了。

编译

       编译是将预处理过的c语言文件编译为汇编语言的文件,等待汇编时转换为机器码,我使用如下指令来进行只编译不汇编不链接将汇编内容生成到hello.S文件中。

       gcc -S -o hello.S hello.c

       我们来查看一些hello.S文件中的内容:

       image.png

汇编

       将汇编代码汇编为二进制代码,这里的二进制代码并不能执行,因为还没有链接,没有运行时地址等等信息,所以无法运行,但是可以用于反汇编分析错误和问题。

       我是用如下指令,让编译器只预处理、编译、汇编,不链接,将内容生成到hello.o文件中。

       gcc -c -o hello.o hello.c

       我们来看看文件中有些什么,注意:这里如果直接使用普通编辑器打开会出现乱码情况,如下图:

image.png

       我这里有vim编辑器打开,首先使用vim -b hello.o以二进制形式打开该文件,在切换到“命令行模式”(按ESC并输入:),然后输入     %!xxd -g 1 切换为16进制,最终可以看到如下显示:

       image.png

       这是二进制数据和汇编指令一一对应(伪指令除外),最早的程序员就是直接编写这些二进制数据(纸上打孔),但是数字类的东西不便于人类处理,所以就有了“助记符”(汇编)来进行编程。

       这里在引入一个小知识:如果你不知道当前文件是什么类型的文件,可以使用file指令查看:

       我这里分别查看了.c C语言源文件,.S汇编语言文件,.o二进制待链接文件,hello可执行文件。

image.png

链接

       每个.c文件都会通过编译生成一个.o文件,最终的可执行文件由ld链接器根据链接脚本(没有指定链接脚本采用默认链接脚本)将多个.o文件和库文件(.so)链接并生成可执行文件。

       使用如下指令即可完成链接操作,并将可执行文件名生成为hello。

       gcc -o hello hello.o

       这时,执行./hello,即可输出如下信息:

       image.png

       这里再说一个知识点:这样的链接叫做“动态链接”。

       什么是动态链接?比如我们使用了printf函数,如果采用动态链接,printf函数的实现并未放到可执行文件中,而是等待执行hello可执行文件时(./hello),通过加载器去寻找printf所在的动态库并调用,从而输出。这样做的好处是可执行文件占用空间少,坏处是不便于移植,如果移植到没有支持printf函数动态库的机器上就无法运行了,所以单片机的程序都是采用静态链接。

将elf文件转换为hex/bin文件

       这里可以参考我的另外一篇帖子:https://bbs.huaweicloud.com/forum/thread-58379-1-1.html

 

总结:

       gcc的参数:

              -o 指定生成文件名

              -E 只预处理,不编译,不汇编,不链接  文件名一般以.E结尾

              -S 只编译,不汇编,不链接     文件名一般以.S结尾

              -c 只编译(汇编),不链接       文件名一般以.o结尾

 

       gcc工具链中的常用组件:

              size:显示可执行文件或者二进制文件中各个段的大小,如下图:

              image.png

              ar:用于处理库相关的指令,比如刚才提到的动态库,或者我们为了让程序保密,可以将自己写好的某个模块的代码制作库,只给别人一个头文件,库中都是二进制,无法看到源码,如果大家对制作库感兴趣,可以给我留言,改天专门说一说。

              nm:列出一个二进制文件(可执行文件和库文件)中有哪些符号(函数和全局变量),实例如下图:

       image.png

              as:该指令可以将汇编语言文件编译为二进制文件,其实就是gcc -c -o hello.o hello.S指令调用的工具示例如下图:

image.png

              ld:链接器,就是将hello.o和动态库链接到一起的工具。

              objdump:用于反汇编二进制文件为汇编代码的工具,使用示例如下图:

image.png

       -D参数:反汇编所有段

       >符号:因为objdump是直接打印输出信息,所以我们要把输出信息通过>重定向到hello.dis文件中方便查看。

       我这里只截图main函数反汇编的代码块给大家看看:

image.png

objdump工具在实际开发中作用挺大的,大家可以学习下,后面我还给出了一个利用objdump工具排错的案例。

objcopy:该指令一般用于单片机的程序开发,当我们编译好了生成了一个elf文件,但是要求文件格式为hex或者bin才能进行烧录,我们这时就可以采用该指令进行转换,详细操作请参考:https://bbs.huaweicloud.com/forum/thread-58379-1-1.html

 

 

编译器优化导致错误的排错思路

当程序出现错误时,我们怀疑是编译器的优化导致的错误,我们就可以去反汇编汇编后未链接的文件来看看。使用objdump,这里顺便说个小知识:为什么二进制文件永远无法还原为和原来的.c文件一模一样?就是因为编译器的优化导致的。

什么是编译器的优化呢?比如我们执行如下语句,用于延时:

       while(time)

              ;

特别聪明的编译器,就会认为这个语句没有用,自作主张地把这个语句删除了,导致我们的延时失效,如何解决呢?用volatile关键词修饰time,编译器就不敢帮我们优化这个语句了。

再来举一个例子,比如我们定义一个指针,将GPIO的状态寄存器的地址赋值给该指针,我们去解析该指针指向地址处的内容时,就可以获取GPIO的状态,假设0为低电平、1为高电平,假设GPIO状态寄存器的地址为0x4000_0000,下面我用伪代码给大家展示:

       LED_statr = *((char *)0x40000000)

while(1)

       {

       if (LED_state)

              printf(“LED ON\r\n”);

       else

              printf(“LED OFF\r\n”);

}

       编译时,如果编译器特别聪明,发现我们从0x4000_0000地址处读取数据特别慢,它就会把第一次读到的值写到一个局部变量中,获取LED_state时并不会从0x4000_0000地址去读,而是直接读取局部变量,所以无论LED的状态是什么LED_state永远是第一次获取到的值,这时我们可以用volatile去修饰,防止此类情况产生。

 


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。