距离上次写博文又有差不多两个多月了,想了一下,鉴于柯南快完结了,头像必须换成柯南的。这次开发是需要在x86下搭建一个交叉编译开发环境,简而言之,就是在windows平台上开发运行于其他平台的程序(项目内容还是要保密的~~)。在某宾馆封闭开发两个月左右,虽然不是从一开始就和整个团队在一起做,但我的工作量还是挺大的,而且也学到了不少东西,还是用程序员自己表达方式来抒发一下感受:
1 while(StayAlive) 2 { 3 keepLearning(); 4 stayCoding(); 5 rethinkAndSummarize(); 6 }
不总结就不能提高,打算写三篇左右来做个小结。项目中涉及的内容特别多,主要是三条主线:
1.程序构建过程;
2.eclipse插件开发与CDT项目;
3.关于项目开发流程的问题。
这篇就主要写我对程序构建过程的一些理解吧,也欢迎大家补充斧正。
一、程序构建流程
程序构建过程的确是博大精深,就算接触了一些,也只能浅浅地谈谈。编译器的种类众多,但原理上都是大同小异,以linux下的gcc为例,程序构建流程主要分为四步:
预处理→编译→汇编→链接
预处理:完成宏定义的替换,去掉所有注释信息及条件编译信息,形式上是由.c文件生成.i文件,想查看这个过程,可以用-E参数来生成.i文件。这个时候,包含的头文件和宏定义都会展开。为查看这个过程,用以下代码测试,环境是CentOS下gcc编译器。使用命令:
$gcc -E hello.c -o hello.i
将结果导出到文件中查看,可以看到前面的#include<stdio.h>已经被替换成头文件了(由于太长,我就只截取了最后一部分),使得行数达到了860行,而且我们自己定义的#define MAX 100也已经被替换成了100这个数值了:)
编译:形式上就是.c文件变成.S文件的过程。经过词法分析、语法分析、语义分析,将高级语言代码转换成汇编语言指令。现在的gcc都是把预处理和编译两个步骤合成一个了,由后台的cc1程序完成这些过程处理,实际上gcc也就是一系列后台程序包装而成的。
$gcc -S hello.i -o hello.S
来看看我们生成的汇编代码,首先是前面将"hello world"字符串放在.LC0处,之后将寄存器ebp的值入栈,用ebp保存esp的值,将栈顶指针减32(相当于开辟空间供局部变量使用),将值100赋给地址为28的空间(4个字节),再将LC0的地址入栈,之后调用puts输出信息,接着nop延迟,leave指令恢复之前的ebp和esp,ret返回。
汇编:汇编器as将汇编代码转化为机器可以执行的机器指令,形式上是将.S文件转换为.o的目标文件。
$gcc -c hello.s -o hello.o
$objdump -h hello.o
查看.o目标文件的内容,可以看到代码段.text大小为32个字节,数据段为0,只读数据段大小为12字节(就是字符串的大小),.bss段大小也为0
用$readelf -h hello.o查看ELF文件内容:
链接:.o文件还不能够被计算机所执行,必须转换为可执行的二进制文件才能被计算机装载到内存中执行。而.o文件转换为可执行二进制文件的过程就叫做链接。可执行二进制文件的形式有很多(比如Windows下的.exe,一般的.ELF文件等),这个过程中,由于各个.o文件中所需要的符号定义可能在其他.o文件中,所以要将这些.o文件一起合并起来,同时还需要和一些库文件(比如.a文件)链接在一起,这样才能组合成可执行二进制文件,这个过程由连接器ld控制完成。
如要要让这个hello程序能够执行,则需要链接进libc.a,因为printf的符号定义在printf.o里面,而printf.o里又需要其他符号定义,所以必须将需要的所有.a和.o文件都链接进去才能生成一个可执行的二进制文件,这里我就没列出来了。
链接中又有静态链接和动态链接的概念。链接的过程需要知道链接脚本作用,静态链接库和动态链接库,符号表是怎么生成,做什么用的。.o文件如何变成elf文件。
动态链接库:多个应用程序可以使用同一个动态库,启动多个应用程序的时候,只需要将动态库加载到内存一次即可。动态链接库的编译方法:gcc -shared -fPIC -o libhello.so hello.c hello1.c,这样就会在当前目录下生成一个libhello.so文件。
静态链接库:代码的装载速度快,执行速度也比较快,因为编译时它只会把你需要的那部分链接进去,应用程序相对比较大。但是如果多个应用程序使用的话,会被装载多次,浪费内存。我们的项目中,由于是嵌入式平台(资源相对较少,程序一般固化在硬件平台上),所以编译出来的可执行文件实际上是静态链接了很多系统库文件,一个helloworld的程序就有2.76M(当然也附带了一些调试信息)。
静态链接库的编译方法:先用gcc -c hello.c hello1.c得到hello.o、hello1.o文件,然后再将.o文件打包成.a文件,即ar -r libhello.a hello.o hello1.o。在当前目录下就生成了libhello.a文件。
可执行程序的格式:ELF等,一般主要分为三段:.text、.data、.bss(图中.rdata为只读段)。其中.text段存放程序代码,已初始化的全局变量和静态变量存放在.data段中、未初始化的全局变量和静态变量存放在.bss段中,而.rdata段存放只读的变量。
二、相关编译选项
编译过程需要知道各种编译选项的作用,我只写了我在项目中碰见的一些编译选项(交叉编译的目标机是mips架构的)。项目里要求开发一个IDE,所以必须要搞清楚其中涉及到的所有编译选项作用,并且将其加入基于eclipse框架的IDE(就是eclipse插件开发相关的东西,扩展工程属性中的编译选项和路径指定)中去,当时在这里也是花了相当长时间研究怎么生成一个正确的可执行文件(凡是搞过交叉开发的都懂的~~)。
-c:只编译不链接
-o:指定目标输出文件
-M:明确依赖关系
-E: 只进行预处理
-g:编译时生成调试信息(g1/g2/g3)
-mips3:指定架构
-EL:指定小端对齐模式
-fomit-frame-pointer:采用
-O0/O1/O2/O3:优化选项
-I:指定头文件包含路径
-L:指定链接库查找路径
-specs:指定使用的bsp文件的名称
-Xlinker:用于将选项或参数传递给连接器
在eclipse CDT插件中增加编译选项的option。(由于项目要求,就把关键词打码了,见谅~)
程序的构建过程由Makefile来控制,linux下构建一个由多个文件组成的程序一般都用Makefile来完成,将所有的编译命令按Makefile的格式写入Makefile就可以大大提高程序的构建效率,避免重复地输入编译命令。只需要在对应目录下输入make命令就可以调用make工具自动完成对Makefile的解析并执行Makefile中的命令。其实大家常用的VS构建程序时也是用了Makefile的,只不过VS下构建的Makefile格式和linux下不一样,而且VS自动帮你写了Makefile而且没在项目中生成,所以就找不到Makefile文件了。
做了这个项目才知道Makefile还分PosixMakefile和GNUMakefile,不过有哪些区别暂时还没搞清楚。Makefile生成过程,分为Makefile,subDir.mk,Objects.mk,sources.mk。先生成Makefille文件,再向Makefile文件中写入各种包含的Makefile文件,之后写入编译命令和文件依赖规则等。
说到这里就不得不吐槽一下,在我之前接收这个项目的两个开发人员由于完全没有面向对象的思想,也没有抱着认真负责的态度来开发,基本上是以调试方式在开发,见到出问题的地方就随便加、随便改代码,运气好出来正确效果了就行了,运气不好就不知道怎么办了,各种Dead Code,完全没有弄懂为什么别人的代码要这么写,是什么原理;更糟的是没有代码更改记录,仅有的注释也就是自己的名字和某些被注掉的代码,使得我花了大把的时间去定位他们修改过的位置(真是给我挖了一个大坑!),然后再挨个改回来。一个.java文件多的就有3000~5000行代码,一个package里一般有十几个.java文件,而一个插件里面又有十几package,总共有几十个插件。当然,我想这应该也不是个别现象,中国做开发的好多企业或者单位肯定都有这种通病。反观国外的做法,拿eclipse和CDT说吧,一个开源项目那么多人都动过,IBM、Wind River、甚至很多贡献过的程序员的注释都十分规范,易读,代码整洁干净,命名清晰,起码能够给二次开发的人员相当大的支持。
先写这么多吧~搞了两个月的项目,几乎都没什么时间认真总结、也没时间看书,东西很多很混乱,还是要多多学习总结,下一篇准备写eclipse 和 CDT的一些介绍和开发总结。