gcc可以说是个伟大的编译器集合。支持c, c++, object-c, java, fortran, pascal, ada等一大堆语言,同时支持几乎所有32位以上的cpu和部分16位、8位cpu。因此几乎所有开源操作系统、商业unix操作系统、嵌入式开发都使用gcc做编译器。风河也使用gcc来编译vxworks的代码。下面介绍些最常用的编译参数(仅以gcc作为c编译器使用时进行说明)。更多的参数请参考gcc的文档。或者开始菜单里的Tornado Online Manual。
-O 优化参数。后面可以跟数字表示优化级别-O0表示不优化,-O1,-O2,-O3优化程度依次递增。大部分平台*别是-O3,也就是说3以后的数字基本都没用。另外建议大家最高只用到-O2。大部分开源操作系统经过千锤百炼都是使用-O2参数编译的。优化级别开得太高对于调试不利。为了优化,编译器可能会打乱部分代码的顺序,造成我们单步跟踪的时候会发现在C源代码里乱跳。
另外还有一个-Os表示为代码大小进行优化,用它可以生成尽量短小的机器码。
-E 表示仅仅对代码进行预处理不编译。也就是仅仅将头文件包含和宏定义展开。如果没有用-o指定输出文件则将预处理结果输出到控制台上。
-c 表示仅仅汇编及编译代码,不进行链接。也就是将源代码编译成.o文件。
-S 表示仅仅汇编而不进行编译及链接。也就是将源代码翻译成汇编指令。gcc -S生成的文件阅读起来不如用objdump反编译.o文件生成的结果舒服。建议想分析汇编指令采用objdump命令反编译。
-o filename 指明输出文件名。一般配合-E -c -S三个命令使用。比如:
gcc -c a.c -o a.o
gcc -E a.c -o a.e
-ansi 表示除了ANSI C标准之外其他特性都认为语法错。比如//单行注释。需要注意的是,这个选项只是严格执行大部分ansi标准。
-std= 后面可以跟c89 c99 gnu89等标准。表示编译器使用哪个标准进行编译。比如使用c89标准就用-std=c89,如果想一些gcc的扩展特性就用-std=gnu89。这个gnu89是默认值,也就是什么都不指定的时候就是用c89标准+gcc的扩展。-ansi就相当于-std=c89
-pedantic 严格执行ANSI C标准。一般与-ansi配合使用可以让源代码严格遵循ANSI C标准。可以打印出-Wall以外更多的告警信息。
-w 关闭所有告警提示
-Wall 虽然是-Wall但是不是打开所有告警提示而是大部分,但下面的-W开头的告警需要单独打开。
-W 对某些告警显示更详细的信息
-Wfloat-equal 浮点数直接使用==判断是否相等时告警
-Wtraditional 如果使用了原始C语言里有而C标准化后被废弃的特性就告警
-Wshadow 对shadow变量进行告警。比如:
有个全局变量nCount,这时你写的函数有个参数也叫nCount。编译器就会告警提示你,说局部参数nCount起作用而全局nCount不起作用。如果你用意不是如此就需要修改代码了。
-Werror 把所有的告警都转化为编译错误。只要有告警就停止编译。
-g 生成调试信息。后面象-O一样用数字表示信息丰富程度。不跟数字默认为-g2,-g1是最基本的。-g3连宏信息都附加进去。有些调试器可以在调试时利用这些信息来展开宏。一般只要用-g。注意附加调试信息将显著增加ELF文件大小,但不会影响生成代码的大小。
-fstrict-aliasing 后面单独介绍
-mlong-calls 对于arm平台,如果想编译出来的.o文件能被shell下ld命令动态加载就需要加上此参数。不然会提示
Relocation value does not fit in 26 bits.
btw:这条是王璐玮试出来的。我偷学来的。
-D 定义宏。比如
gcc -D_DEBUG=1 -Dver=”1.0” -c a.c -o a.o
-fvolatile 强制所有指针访问都是volatile类型的,也就是强制去内存取数据。如果你不确定是否所有指向设备地址空间的指针都是volatile类型声明的建议加上这个参数。会损失优化效果。
-I dirname 指示预处理器把dirname加入到include文件搜索路径里来。比如
gcc -c a.c -I../include -I./include -o a.o
-include filename 在源文件前面包含filename作为头文件。比如
gcc -include ../common.h -c a.c -o a.o
效果和a.c里#include “../common.h”是一样的。注意-include包含的头文件将相当于在代码第一行就插入#include 推荐编译参数-O2 -g -fno-strict-aliasing -Wall -fvolatile -I./
2) 怎样生成依赖关系
手工写Makefile的时候需要确定一个源文件依赖哪些头文件。每个文件都打开看一下include的头文件的话会比较麻烦,并且有些用相对路径include的头文件的绝对路径也不好确定。因此我们可以使用编译器来给我们生成依赖关系。
C:\>ccarm -MM stone.c
stone.o: stone.c e://Tornado2.2/target/h/types/vxArch.h \
e://Tornado2.2/target/h/arch/arm/archArm.h \
e://Tornado2.2/target/h/arch/arm/arm.h \
e://Tornado2.2/target/h/types/vxParams.h \
e://Tornado2.2/target/h/types/vxTypesBase.h \
e://Tornado2.2/target/h/types/vxTypesOld.h \
e://Tornado2.2/target/h/sys/types.h \
e://Tornado2.2/target/h/types/vxTypes.h \
e://Tornado2.2/target/h/tool/gnu/toolMacros.h \
e://Tornado2.2/target/h/vwModNum.h \
e://Tornado2.2/target/h/objLib.h \
e://Tornado2.2/target/h/errno.h \
e://Tornado2.2/target/h/memLib.h \
e://Tornado2.2/target/h/private/objLibP.h \
e://Tornado2.2/target/h/vxWorks.h \
e://Tornado2.2/target/h/objLib.h \
e://Tornado2.2/target/h/classLib.h \
e://Tornado2.2/target/h/private/classLibP.h \
e://Tornado2.2/target/h/vwModNum.h \
e://Tornado2.2/target/h/memLib.h \
e://Tornado2.2/target/h/private/objLibP.h \
e://Tornado2.2/host/x86-win32/lib/gcc-lib/arm-wrs-vxworks/2.9-010413/include/stdarg.h
把 绝对路径处理一下用变量代替tornado所在路径就可以放到Makefile里啦。注意的是生成依赖关系的时候最好保留完整的编译命令行选项,只是将 -c替换成 -MM,比如ccarm -MM -DXXX -DYYY -fno-strict-aliasing -Dzzz=123 stone.c,这样如果有些宏定义会影响include的话我们还是可以生成正确的依赖关系。
3) 非常重要的strict aliasing规则
假设有人问你 (i++)+(++i)+(++i++)结果是多少,你也许会直接回答这和编译器实现有关。不过如果有人问你
int abc()
{
short a[2];
int *b = (int *)a;
a[0]=0x1111;
a[1]=0x1111;
*b = 0x22222222;
printf("%x %x\n", a[0], a[1]);
return 0;
}
这个函数在单板上运行输出结果是多少你可能就毫不犹豫的说是2222 2222了。
事 实上,这段程序和(i++)+(++i)+(++i++)前面一样是不一定的,它的结果与编译器实现、编译参数有关。不信的话你找个arm的单板(别的平 台我没有试过),用ccarm -c -O2 参数编译得到的结果就是1111 1111。如果用参数ccarm -c -O1编译得到的结果就是2222 2222。造成两种结果的参数不是-O2和-O1而是一个叫strict-aliasing的编译参数。这个参数在-O2优化的时候默认是打开的。刚才两 种不同结果是-fstrict-aliasing和-fno-strict-aliasing造成的。也就是说-O2结果就是1111 1111,-O2 -fno-strict-aliasing结果就是程序员预期的2222 2222。
造成这个现象的原因是-fstrict-aliasing的时候编译器会假设不同类型的指针指向的内存不会重叠来进行优化。
以 上面的例子为例分析,由于a和b类型不同,当打开-fstrict-aliasing,编译器给*b赋值的时候就认为不会影响到a数组里面的值。编译器在 优化的时候分析:既然a数组的值在赋值后没被修改,下一次读取a数组值的时候就没必要再从内存里读取了。因此printf这一行在取a[0],a[1]的 时候就被优化成了从寄存器取值。结果打印出来的是1111 1111。
而如果使用了-fno-strict-aliasing,则编译器认为任何 指针都可能指向同一个内存区域。因此对*b赋值,编译器认为有可能会影响a里面的值了。所以编译器给printf那一行传递参数的时候就认为寄存器里的 a[0],a[1]值已经不一定正确了,只有内存里的才可靠,于是只能老老实实从栈里取值了。
以上的分析是在arm平台上做的。我在x86下也做 了比较,但是两种参数编译出来结果都是2222 2222。主要是因为x86下通用寄存器数量很少,优化的时候不一定总能把变量放在寄存器里,所以这个例子影响不了x86。但在研发支撑体系上有人曾经提 出过-O2生成的代码有问题,就是因为strict aliasing影响的。
2007-10-08 gcc编译优化问题请教,牛人请进!
出现问题的代码如下:
XXXX_UINT32 xxxx_map_get_free
(
XXXX_MAP_PTR IO_map_p
)
{
XXXX_VOID_PTR cur_data_p;
XXXX_BUFF_PTR buff_p;
buff_p = IO_map_p->buff_p;
cur_data_p = buff_p->free_data_p;
buff_p->free_data_p = *(XXXX_VOID_PTR*)cur_data_p;
*(XXXX_UINT32*)cur_data_p = 0;
buff_p->used ++;
return XXXX_SUCCESS;
}
知道了这个特性之后我们写代码的时候就需要特别注意不要偷懒用强制指针转化去给buf赋值。上面那个程序安全的版本是:
int abc()
{
union
{
short a[2];
int b;
}U;
U.a[0]=0x1111;
U.a[1]=0x1111;
U.b = 0x22222222;
printf("%x %x\n", U.a[0], U.a[1]);
return 0;
}
这样无论怎么都不会出错了。
到这里你也许会问,为何要有这个特性,既然-fno-strict-aliasing比较安全,那干脆所有地方都这么用算了。实际上出现-fstrict-aliasing是为了提供更好的优化效果。比如下面的程序:
void sum(int *array, short * num, int n)
{
int i = 0;
for (i = 0;i < n;i++)
{
array[i] += (int)num[0];
}
}
如果使用-fno-strict-aliasing参数编译,编译器认为num和array有可能指向同一片区域。由于编译器认为给array[i]赋值有可能会改变num[0],所以循环内部num[0]的值每次都是从内存里取的。
而如果使用-fstrict-aliasing参数编译。编译器则认为num和array不会互相影响。这样在循环外部就把num[0]的值读取到寄存器中。循环内部每次都是寄存器的值去累加。减少了读取内存的次数,从而优化了速度。
要是一个函数内指针、数组数量众多,编译器进行优化后行为就很可能会与程序员期望的。同时编译参数是针对整个文件起作用的,无法针对单个指针变量和单个函 数。为了明确告诉编译器究竟指针所指内存是否会互相覆盖,在C99标准里引入了新的关键字restrict。restrict使用的地方基本和const 差不多。它指明此指针指向的内存仅仅会被这个指针来修改,其他的指针不会修改这部分内存。这样,明确告诉编译器之后,编译器就可以生成既符合程序员期望又 高效的代码了。不过Tornado2.2里所带的gcc版本相当老,对C99标准支持非常差,不支持restrict关键字,在这里就不多说了。更详细的 可以参考C99标准。建议在编译命令行加上-fno-strict-aliasing,虽然优化效果会打折扣,但是安全。
4) 怎样查看预处理器默认定义的宏
不同版本的gcc默认定义的宏是不太一样的。有时候为了兼容不同平台的gcc可能希望在代码里用编译宏区分开。这样就想知道编译器与处理器默认定义了哪些宏可以提供给我们使用。可以建立一个空文件然后使用下面的命令
ccsimpc -dM -E emptyfilename
或者
ccsimpc -dM -E - < NUL
可以得到输出
#define _stdcall __attribute__((__stdcall__))
#define CPU SIMNT
#define __i386__ 1
#define _X86_ 1
#define __i386 1
#define ___stdcall__ __attribute__((__stdcall__))
#define __GNUC_MINOR__ 96
#define __declspec(x) __attribute__((x))
#define __vxworks 1
#define i386 1
#define __stdcall __attribute__((__stdcall__))
#define __GNUC__ 2
#define __cdecl __attribute__((__cdecl__))
#define __STDC__ 1
#define ___stdcall __attribute__((__stdcall__))
ccarm -dM -E filename
或者
ccarm -dM -E - < NUL
可以得到输出
#define __arm__ 1
#define __ARM_ARCH_4__ 1
#define arm 1
#define __GNUC__ 2
#define __APCS_32__ 1
#define __arm_elf__ 1
#define __arm_elf 1
#define __vxworks 1
#define __CHAR_UNSIGNED__ 1
#define __ARMEL__ 1
#define __ELF__ 1
#define arm_elf 1
#define __GNUC_MINOR__ 9
#define CPU ARMSA110