参考书籍
1、编译原理
2、嵌入式Linux应用开发
当我们开发目标是一个嵌入式设备时,便需要在PC机上编译出能在该嵌入式设备上运行的可执行文件,这里编译主机与目标运行主机不是同一个设备,那么该过程就称为交叉编译;而编译是指一个源代码文件(这里指的是编译性程序源文件,与之对应的是解释性程序),如C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)和链接(linking)等4步才能变成可执行文件,注意在日常交流中通常使用“编译”统称这4个步骤。
当我们在 Windows下利用IDE工具,即集成开发环境(比如 Visual studio、keil、IAR、Eclipse等等)进行开发时,只需要单击几个按钮即可编译,因为IDE工具已经将各种编译工具的使用封装好了。
Linux下也有很优秀的集成开发工具,但是更多的时候是直接使用编译工具;比如嵌入式开发中,运行在PC平台上的编译工具链为gcc、ld、objcopy、objdump等,它们编译出来的程序在x86平台上运行。要编译出能在 ARM 平台上运行的程序,则须使用交叉编译工具 arm-linux-gcc、arm-linux-ld、arm-linux-objcopy、arm-linux-objdump等;PC与ARM的编译工具使用方法一致,单纯就是名字不一样。
小插曲:关于开发工具的使用,搭建开发环境和熟悉工具使用是进行开发的第一步,也是一道难关,尤其面对多种多样的工具和环境,让人抓狂;其实这也无可厚非,因为各种系统、产品本身具有自己的特点以及软件工具的限制(知识产权),显然要做到一个工具通吃是不现实的,而一般在学习网络教程/公司企业工作时所使用的工具,有可能会直接采用某官方发布的软件,也有可能经过二次开发/封装而来,面对这种情况,我们其实不用去抗拒,学习开发工具本身就是开发过程中必要的一部分,熟练掌握一套工具后,同类的工具上手也会很快的,因此要积极对待。
上图更好理解:
先来简单认识下编译工具的使用,我们先看操作,再看原理,以gcc为例子(与arm-linux-gcc使用一致),查询使用帮助:
gcc选项很多,一般大型开发项目都会使用很多控制选项,这里仅介绍简单常用选项:
-v:查看gcc编译器的版本,显示gcc执行时的详细过程
-o <file> Place the output into <file> (指定输出文件名为file,这个名称不能跟源文件名同名)
-E Preprocess only; do not compile, assemble or link(只预处理,不会编译、汇编、链接)
-S Compile only; do not assemble or link(只编译,不会汇编、链接)
-c Compile and assemble, but do not link(编译和汇编,不会链接 )
以hello.c文件为例
1)方式1
gcc -E -o hello.i hello.c
gcc -S -o hello.s hello.i
gcc -c -o hello.o hello.s
gcc -o hello hello.o
gcc会对.c文件默认进行预处理操作,-c再来指明了编译、汇编,从而得到*.o文件(object file 即我们常说的目标文件),再通过gcc -o hello hello.o将.o文件进行链接,得到可执行应用程序。
小结:
1)输入文件的后缀名和选项共同决定gcc到底执行哪些操作。
2)在编译过程中,除非使用了-E、-S、-c选项(或者编译出错阻止了完整的编译过程),否则最后的步骤都是链接。
2)方式2 - 开发项目中常用的方式
gcc -c -o hello.o hello.c
gcc -o hello hello.o
3)方式3
gcc -o hello hello.c 输出hello,然后./hello来执行该应用程序。
注:如果不指定输出文件名,则默认生成a.out
gcc hello.c 输出一个a.out,然后./a.out来执行该应用程序。
C/C++源文件中,以“#“开头的命令被称为预处理命令,如包含命令“#include”、宏定义命令“#define”、条件编译命令“#if”、“#ifdef”等。预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些代码输出到一个".i"文件,也就是说还是源代码文件;
一般预处理都是些简单的替换、拷贝和选择,这些涉及多个文件,预处理的结果是将每个源文件所需要的代码都放在自己文件里,然后方便下一步处理(ps:因为编译时,编译器每次读入一个文件,输出一个文件,不支持同时处理多个文件 - 来源于“编译原理”);
对比hello.i与hello.c文件内容:
对比hello.i与hello.c大小:
明显hello.i大很多,原因是从其他文件拷贝了很多代码过来。
编译就是把C/C++代码(比如上述的“.i”文件)翻译成汇编代码,这部分涉及复杂的编译器原理,有兴趣可以自行去看书深究;
1、其一由于最开始的底层开发语言是汇编,而高级语言是在底层语言基础上发展的,自然而然会将成熟的工具(汇编器)利用起来,同时实现软件分层可以有效地减弱编译器编写的复杂性,“编译”所拆分的四大步骤也是如此道理;
2、其二有一个好处是方便优化和调试,汇编语言是机器指令的助记 符,一个汇编指令就对应一条机器指令,因此汇编语言更贴近机器特性,因此比高级语言调试起来更有优势;
查看hello.s文件
汇编(注意这里的汇编指的是编译器的一个编译动作,不是汇编语言)是利用汇编器将第二步输出的汇编代码翻译成符合一定格式的机器代码,就是我们熟悉的目标文件(*.o),在Linux系统上一般表现为ELF格式文件;如果开发代码是汇编,则汇编+链接就可以生成可执行文件了;
查看hello.o,需要用readelf工具查看
反汇编文件是由可执行文件逆向解析而来,内容是按照实际内存分布来排布的,包含地址信息,一般用来调试分析用,非常有用;
生成反汇编文件
查看
反汇编文件比汇编文件多了调试信息,如物理地址、机器码等,而汇编文件单纯只是汇编代码;
链接就是将汇编生成的OBJ文件、系统库的OBJ文件、库文件链接起来,即将各个ELF文件重新排序成一个ELF文件,最终生成可以在特定平台运行的可执行程序。
查看hello,需要用readelf工具
系统库文件:一个应用程序要运行在系统上,就需要系统标准启动文件,提供给系统用的;注意裸机bootloader、linux内核等程序是不能使用启动文件以及标准库文件(因为启动文件和库文件的使用是需要系统支持)。
一般gcc自动加入的系统标准启动文件有:crt1.o、crti.o、crtbegin.o、crtend.o、crtn.o
对于一般应用程序,这些启动是必需的。
通过查看gcc详细编译过程可以看到
很好理解,即库文件,如果代码用到标准库函数,而gcc集成了常用的库,链接时自动检索加入;
动态链接使用动态链接库进行链接,生成的程序在执行的时候需要加载所需的动态库才能运行。
动态链接生成的程序体积较小,但是必须依赖所需的动态库,否则无法执行。
gcc默认使用动态库链接
静态链接使用静态库进行链接,生成的程序包含程序运行所需要的全部库,可以直接运行,
不过静态链接生成的程序体积较大。
gcc静态链接需加入选项-static
很明显静态链接比动态链接生成的可执行文件大很多。
最后,编译原理还有很多值得深究的问题,比如ELF格式是什么?系统如何支持动态链接的?边实践边学习,由浅及深,通过现象理解本质才能不急不躁。