又是一篇关于编译器的日记.以前已经讨论过编译器的问题,现在又拿出来的谈,我觉得是有很必要的.工欲善其事,必先利其器.在我做GBA过程中,遇到最困难的问题并非写代码,而是配置编译器.这篇日记的内容很零散,记录我遇到的问题以及解决问题的方法,希望对现在正在做GBA的朋友有所帮助.
探索日记(2)外篇中说到两个编译器,gcc,ArmSDT
ArmSDT编译速度比gcc快许多,但是任天堂提供的官方开发包是用的gcc.而且gcc是个很流行的编译器,所以我打算以gcc为主.同时这些编译器的命令选项的用法也通常适合现在大多数编译的c语言编译(包括ArmSDT).
1.gcc的编译命令
gcc是国际上很流行的c,c++编译器.它是免费的,而且可以被分割成许多部分,因此许多开发商把自己的产品编译器做到gcc上了.除了GBA,我知道还有Palm OS也是如此.可见这些开发商都是十分信任gcc的编译能力的.
不再费话了,下面先看看怎样使用gcc把c源代码编译成可执行文件.
先举个例子.我要编译一个a.c文件
那么我一直接在dos或linux,unix下打入命令:
gcc a.c
就可以得到a.exe或a.out(linux下的执行文件,相当于dos下的a.exe)
这是最简单的编译方式.
但是通常的方法是将*.c文件编译成*.o中间文件,然后再编译成可执行文件
gcc -c a.c -o a.o
gcc a.o -o a
然后就会得到a.exe或a.out
第一行命令是编译,-c后面跟的是源代码文件,-o后面跟的是编译出来的中间文件的文件名.整个命令也可以这么写:gcc -o a.o -c a.c
第二行命令是连接,-o后面跟的是目标执行文件的文件名,前面跟的是要连接的*.o文件名
整个两行命令也可以写成:gcc -o a.exe a.c
接下来来就讲一些你使用AgbLib时需要用的一些选项
-c
编译*.c文件的时候使用
Compile or assemble the source files, but do not link. The linking stage simply is not done. The ultimate output is in the form of an object file for each source file.
编译或者集合这些源代码文件,但是不连接.连接过程将不进行.最终输出文件是以一种object文件(对于每一个源文件)
-S
编译*.c文件的时候使用
把*.c文件翻译成*.s汇编代码
Stop after the stage of compilation proper; do not assemble. The output is in the form of an assembler code file for each non-assembler input file specified.
在编辑过程后停止,不进行装配.输出文件是汇编代码
-I
无论是在编译还是连接都可以使用(需要注意的是gcc的选项是区分大小写的).
它是指定include目录的.
比如你在使用cygwin中的gcc,那么include目录是c:\cygwin\include,所以你在编译的时候就应该这么写
gcc -c a.c -o a.o -I"C:\cygwin\include"
否则很可能在#include一些文件的时候找不到头文件,不过在一般安装好cygwin后,include目录是自动给你配置好了的,所以你即使不用-I"c:\cygwin\include"它也找得到需要的头文件.但是麻烦的问题是当你把你好好的gcc变成armgcc后,你就需要重新去配置这些东西了.
你可以同时给出多个include目录,比如c:\agb\include
gcc -c a.c -o a.o -I"C:\cygwin\include" -I"C:\agb\include"
-w
禁止所有警告信息
Inhibit all warning messages.
这个选项在ArmSDT变成大写的-W
-L
这是个连接*.o文件时使用的参数.
它是指定lib目录的.
例如
gcc -o a.exe a.o -L"C:\cygwin\lib"
同样,它依然可以指定多个lib目录
-l
依然是连接*.o文件时所使用的参数
它是指定所需的lib文件
例如,我在c:\agb\lib中有个叫libxxxx.a的库文件,那么我输入:
gcc -o a.exe a.o -I"C:\agb\include" -L"C:\agb\lib" -lxxxx
在gcc中对于lib文件的名字有些特殊的要求.
第一,文件名必须以lib三个字母开头.
第二,文件名的扩展名必须是a,及文件必须是*.a文件
在AgbLib中,如果你想使用一些c语言常规库函数,就必须把libm.a包含进去.
所以应该有-lm这个参数.
-nostartfiles
连接*.o时的选项
Do not use the standard system startup files when linking. The standard system libraries are used normally, unless -nostdlib or -nodefaultlibs is used.
不使用标准开始连接文件.
这里主要指的是C语言的标准库文件.这些库文件通常都普遍地在使用,除非你用了-nostdlib 或 -nodefaultlibs这些选项.
AgbLib编译的samples里都使用了这个选项.
-nostdlib
连接*.o时的选项
不是使用stdlib等标准库文件
Do not use the standard system startup files or libraries when linking. No startup files and only the libraries you specify will be passed to the linker. The compiler may generate calls to memcmp, memset, and memcpy for System V (and ISO C) environments or to bcopy and bzero for BSD environments. These entries are usually resolved by entries in libc. These entry points should be supplied through some other mechanism when this option is specified.
AgbLib的samples里也使用了该选项
2.ARM compiler options
这些都是针对Armgcc的选项,它并非属于GNU C的部分.
但是AgbLib中使用其中的一些选项,所以还是那它一部分出来讲一讲
-mthumb
Generate code for the 16-bit Thumb instruction set. The default is to use the 32-bit ARM instruction set.
产生的代码是16位的Thumb模式.通常缺省的都是用的32位Arm模式
-mthumb-interwork
Produce assembly code which supports calls between the ARM instruction set and the THUMB instruction set.
提供支持Arm模式和THUMB模式的汇编代码
至于Arm和THUMB模式是什么,其实我也不清楚,但是现在市面上有一本叫Arm Soc的书籍,它专门讲解了Arm CPU已经基于Arm CPU的System等原理.那些东西似乎有点扯远了,我们的目的不是设计Arm Soc,而只是做GBA游戏而已, 所以你在AgbLib看到他们使用了这个选项,我们也就跟着使用就是了
-mcpu=XXXX
Produce assembly code specifically for the indicated processor. The ‘XXXX’ variable can be one of the following processors.
提供汇编代码明确CPU,'XXXX'就是下面的CPU名字
arm2
arm250
arm3
arm6
arm600
arm610
arm620
arm7
arm7m (the default setting)
arm7d
arm7dm
arm7di
arm7dmi
arm70
arm700
arm700i
arm710
arm710c
arm7100
arm7500
arm7500fe
arm7tdmi
arm8
strongarm
strongarm110
这个选项在AgbLib使用gcc环境下很少见,但是如果你使用的ArmSDT.那么你就肯定要用到这个了. 不过需要注意的是ArmSDT中的zarmcc中不再是用-mcpu,而变成了-cpu
比如下面的命令:
zarmcc -c -Wall -Otime -fpu none -Littleend -cpu ARM7TDMI -apcs /narrow/noswst main.c -o main.o -errors log.txt
2.makefile
打开AgbLib官方开发包里的samples.可以发现它的代码都是配置了makefile文件的.makefile是个帮助你编译多个程序文件的有用的工具,无论是在Visual C++开发还是GCC开发,这个工具的使用是很常见的.
这里要十分感谢EyeOnMe,关于makefile的配置的帮助是他提供给我的.
下面就是一篇关于makefile配置的文章.
GNU make 指南
翻译: 哈少
译者按: 本文是一篇介绍 GNU Make 的文章,读完后读者应该基本掌握了 make 的用法。而 make 是所有想在 Unix (当然也包括 Linux )系统上编程的用户必须掌握的工具。如果你写的程序中没有用到 make ,则说明你写的程序只是个人的练习程序,不具有任何实用的价值。也许这么说有点 儿偏激,但 make 实在是应该用在任何稍具规模的程序中的。希望本文可以为中国的 Unix 编程初学者提供一点儿有用的资料。中国的 Linux 用户除了学会安装红帽子以外, 实在应该尝试写一些有用的程序。个人想法,大家参考。
0) 介绍
~~~~~~~~~~~~~~~
本文将首先介绍为什么要将你的C源代码分离成几个合理的独立档案,什么时 候需要分,怎么才能分的好。然后将会告诉你 GNU Make 怎样使你的编译和连 接步骤自动化。对于其它 Make 工具的用户来说,虽然在用其它类似工具时要 做适当的调整,本文的内容仍然是非常有用的。如果对你自己的编程工具有怀 疑,可以实际的试一试,但请先阅读用户手册。
1) 多文件项目
~~~~~~~~~~~~~~~~~~~~~~
1.1为什么使用它们?
首先,多文件项目的好处在那里呢?
它们看起来把事情弄的复杂无比。又要 header 文件,又要 extern 声明,而且如果需要查找一个文件,你要在更多的文件里搜索。
但其实我们有很有力的理由支持我们把一个项目分解成小块。当你改动一行代码,编译器需要全部重新编译来生成一个新的可执行文件。但如果你的项目是分开在几个小文件里,当你改动其中一个文件的时候,别的源文件的目标文件(object files)已经存在,所以没有什么原因去重新编译它们。你所需要做的只是重现编译被改动过的那个文件,然后重新连接所有的目标文件罢了。在大型的项目中,这意味着从很长的(几分钟到几小时)重新编译缩短为十几,二十几秒的简单调整。
只要通过基本的规划,将一个项目分解成多个小文件可使你更加容易的找到一段代码。很简单,你根据代码的作用把你的代码分解到不同的文件里。当你要看一段代码时,你可以准确的知道在那个文件中去寻找它。
从很多目标文件生成一个程序包 (Library)比从一个单一的大目标文件生成要好的多。当然实际上这是否真是一个优势则是由你所用的系统来决定的。但是当使用 gcc/ld (一个 GNU C 编译/连接器) 把一个程序包连接到一个程序时,在连接的过程中,它会尝试不去连接没有使用到的部分。但它每次只能从程序包中把一个完整的目标文件排除在外。因此如果你参考一个程序包中某一个目标档中任何一个符号的话,那么这个目标文件整个都会被连接进来。要是一个程序包被非常充分的分解了的话,那么经连接后,得到的可执行文件会比从一个大目标文件组成的程序包连接得到的文件小得多。
又因为你的程序是很模块化的,文件之间的共享部分被减到最少,那就有很多好处--可以很容易的追踪到臭虫,这些模块经常是可以用在其它的项目里的,同时别人也可以更容易的理解你的一段代码是干 什么的。当然此外还有许多别的好处……
1.2 何时分解你的项目
很明显,把任何东西都分解是不合理的。象“世界,你们好”这样的简单程序根本就不能分,因为实在也没什么可分的。把用于测试用的小程序分解也是没什么意思的。但一般来说,当分解项目有助于布局、发展和易读性的时候,我都会采取它。在大多数的情况下,这都是适用的。(所谓“世界,你们好”,既 'hello world' ,只是一个介绍一种编程语言时惯用的范例程序,它会在屏幕上显示一行 'hello world' 。是最简单的程序。)
如果你需要开发一个相当大的项目,在开始前,应该考虑一下你将如何实现它,并且生成几个文件(用适当的名字)来放你的代码。当然,在你的项目开发的过程中,你可以建立新的文件,但如果你这么做的话,说明你可能改变了当初的想法,你应该想想是否需要对整体结构也进行相应的调整。
对于中型的项目,你当然也可以采用上述技巧,但你也可以就那么开始输入你的代码,当你的码多到难以管理的时候再把它们分解成不同的档案。但以我的经验来说,开始时在脑子里形成一个大概的方案,并且尽量遵从它,或在开发过程中,随着程序的需要而修改,会使开发变得更加容易。
1.3 怎样分解项目
先说明,这完全是我个人的意见,你可以(也许你真的会?)用别的方式来做。这会触动到有关编码风格的问题,而大家从来就没有停止过在这个问题上的争论。在这里我只是给出我自己喜欢的做法(同时也给出这么做的原因):
i) 不要用一个 header 文件指向多个源码文件(例外:程序包 的 header 文件)。用一个 header定义一个源码文件的方式 会更有效,也更容易查寻。否则改变一个源文件的结构(并且 它的 header 文件)就必须重新编译好几个文件。
ii) 如果可以的话,完全可以用超过一个的 header 文件来指向同 一个源码文件。有时将不可公开调用的函数原型,类型定义 等等,从它们的C源码文件中分离出来是非常有用的。使用一 个 header 文件装公开符号,用另一个装私人符号意味着如果 你改变了这个源码文件的内部结构,你可以只是重新编译它而 不需要重新编译那些使用它的公开 header 文件的其它的源文 件。
iii) 不要在多个 header 文件中重复定义信息。 如果需要, 在其中一个 header 文件里 #include 另一个,但 是不要重复输入相同的 header 信息两次。原因是如果你以后改 变了这个信息,你只需要把它改变一次,不用搜索并改变另外一 个重复的信息。
iv) 在每一个源码文件里, #include 那些声明了源码文件中的符 号的所有 header 文件。这样一来,你在源码文件和 header 文件对某些函数做出的矛盾声明可以比较容易的被编译器发现。
1.4 对于常见错误的注释
a) 定义符 (Identifier) 在源码文件中的矛盾:在C里,变量和函数的缺 省状态是公用的。因此,任何C源码档案都可以引用存在于其它源 码档中的通用 (global) 函数和通用变量,既使这个档案没有那个变 量或函数的声明或原型。因此你必须保证在不同的两个档案里不能 用同一个符号名称,否则会有连接错误或者在编译时会有警告。
一种避免这种错误的方法是在公用的符号前加上跟其所在源文件有 关的前缀。比如:所有在 gfx.c 里的函数都加上前缀“gfx_”。如果 你很小心的分解你的程序,使用有意义的函数名称,并且不是过分 使用通用变量,当然这根本就不是问题。
要防止一个符号在它被定义的源文件以外被看到,可在它的定义前 加上关键字“static”。这对只在一个档案内部使用,其它档案都 都不会用到的简单函数是很有用的。
b) 多次定义的符号: header 档会被逐字的替换到你源文件里 #include 的位置的。因此,如果 header 档被 #include 到一个以上的源文件 里,这个 header 档中所有的定义就会出现在每一个有关的源码文件 里。这会使它们里的符号被定义一次以上,从而出现连接错误(见 上)。
解决方法: 不要在 header 档里定义变量。你只需要在 header 档里声明它们然后在适当的C源码文件(应该 #include 那个 header 档的那个)里定义它们(一次)。对于初学者来说,定义和声明是 很容易混淆的。声明的作用是告诉编译器其所声明的符号应该存在, 并且要有所指定的类型。但是,它并不会使编译器分配贮存空间。 而定义的做用是要求编译器分配贮存空间。当做一个声明而不是做 定义的时候,在声明前放一个关键字“extern”。
例如,我们有一个叫“counter”的变量,如果想让它成为公用的, 我们在一个源码程序(只在一个里面)的开始定义它:“int counter;”,再在相关的 header 档里声明它:“extern int counter;”。
函数原型里隐含着 extern 的意思,所以不需顾虑这个问题。