前面介绍了Linux下头文件的一些基础知识,有内核头文件和用户空间头文件,但之后的讲解,都是基于用户空间的程序开发说起,而重点讲一些gcc相关的编译知识,以及后续会对 库文件,ELF文件,Makefile, AutoMake等作进一步的讲解。 有一点需要明确一下,在文章中,不会对代码或程序作过多的讲解,而是主要讲方法和整理思路,对于程序算法或结构之类的,相信大家可以从书上或网上的长篇大论中去学习研究,甚至直接去研究kernel或glibc中的程序算法足矣。
4. 不同的C标准
1). C语言的标准也是五花八门,那么,为什么要有各种不同的标准来约束我们编写C程序呢? 试想,如果把一个程序从平台A移植到平台B,但两个平台所支持的是两套不同的C函数接口,这样的程序从A移植到B,其难度是很大的,需要根据B平台自己的函数接口重新改写原程序。所以,需要有那么一套标准,来约束大家,基于这样的一个标准写出来的程序,可以轻易的移植到支持这个标准的所有平台。 C语言标准大概的演变过程如下: 2). Linux中最常见的两套标准是ANSI C和 POSIX C,那么,这两套标准有什么不同呢? 在Linux中,glibc是编写C程序的主要库,而glibc对于C语言标准的支持,除了上面的ANSI C系列标准,还有另外一套标准POSIX C,POSIX C在所有类Unix系统中几乎都能被支持,glibc中系统调用相关的接口都是符号POSIX C标准的,如:#include 等。 所以,如果想要编写能够在Windows和Linux之间方便移植的程序,尽量使用符合ANSI C标准的C语言;如果想要编写能够方便在Unix族系统之间移植的程序,尽量使用符合POSIX C标准的C语言。
5. GCC编译工具
GCC是Linux系统中一个功能强大的编译器,不仅可以对C,C 等程序进行编译,还支持交叉编译,所谓交叉编译是指针对不同硬件平台的编译,如ARM, MIPS等。
GCC常用的参数有:
-c
生成目标文件,但不做链接
-O{n}
优化代码,n为0, 1, 2, 3几个等级
-Wall
显示所有可能的警告信息
-w
不显示任何警告信息
-g
生成gdb必要的调试信息
-I{dir}
添加头文件搜索路径 (字母i的大写)
-include filename
包含名为filename的头文件
-L{dir}
添加 -L库搜索路径(字母l的大写)
-l{name}
链接库文件,比如-lm表示链接libm.so
-lpthread
链接线程库
-fPIC
生成位置无关代码,通常是共享库
-share
使用动态库
6. GCC编译过程
以下面一个简单的源代码为例:
/* demo.c */
#include
int main(void)
{
printf("==echo main ==\n");
return 0;
}
1). 完整的编译过程:源文件 => 预处理 => 翻译 => 汇编 => 链接 => 可执行文件
# gcc -E demo.c -o demo.i
# gcc -S demo.i -o demo.s
# gcc -c demo.s -o demo.o
# gcc demo.o -o demo
gcc -E 参数对源程序demo.c作预处理,生成一个 demo.i 文件
gcc -S 参数对demo.i 文件进行汇编翻译,生成一个demo.s文件
gcc -c 参数对demo.s 文件进行目标转换,生成一个demo.o文件
最后使用gcc把demo.o这个目标文件链接成可执行程序即可
值得注意的是,gcc生成目标文件其实是调用 as命令来处理的,而链接的过程又是使用ld这个链接器来生成可执行程序,gcc对ld进行了一定的封装,后面会详细介绍ld的用法。
对这么一段简单的源代码,如果每次编译都要使用如此复杂的编译过程,显然是很浪费时间的,gcc可以对上面的完整过程进行简化。
2). 简化为两步:源文件 => 目标文件 => 可执行程序
# gcc -c demo.c -o demo.o
# gcc demo.o -o demo
上面步骤直接使用 -c 参数来生成目标文件 demo.o,然后再使用gcc将目标文件链接成可执行程序。
3). 简化为一步:源文件 => 可执行程序
# gcc demo.o -o demo
虽说是简化,但其实gcc内部还是会完成上面的整个编译过程。
7. 多个源文件的编译
上面是针对一个源文件的编译过程,如果是多个源文件,gcc又如何进行编译呢? 以下面的三个源文件为例:
/* demo.c */
#include
#include "test.h"
int main(void)
{
printf("==echo main ==\n");
func();
return 0;
}
/* test.c */
#include
#include "test.h"
void func()
{
printf("==echo func ==\n");
}
/* test.h */
#ifdef TEST_H_
#define TEST_H_
void func();
#endif
上面的源代码非常简单,就是在main()函数里调用了由test.c实现的func()函数,针对这种多个源文件的编译,其关键在于把每一个相关的源文件先生成目标.o文件,然后再把所有的.o目标文件链接成可执行程序即可:
# gcc -c test.c -o test.o
# gcc -c demo.c -o demo.o
# gcc test.o demo.o -o demo
当然,也可以忽略上面生成目标文件的过程,直接一步到位的生成可执行程序:
# gcc demo.c test.c -o demo
8. ld链接器
上面使用gcc来将目标文件链接成可执行程序的过程,实际上是调用ld这个链接器来实现的,使用strace跟踪发现,在gcc链接的过程中,首先调用了一个collect2的程序来链接成生可执行文件,而进一步跟踪发现,collect2实际是ld链接器的一个封装。
ld 是一个GNU链接工具,用来把各种目标文件或库文件链接在一起,形成可执行文件,gcc之所以对ld进行封装,是因为如果我们单纯的使用ld命令来链接生成可执行程序,其过程或写法相对比较复杂。
假如我们已经使用gcc将上面的test.c和demo.c生成了目标文件test.o和demo.o,那么,如果纯粹的使用ld来链接这两个目标文件的过程如下:
# ld -dynamic-linker /lib/ld-linux.so.2 \
> /usr/lib/crt1.o \
> /usr/lib/crti.o \
> /usr/lib/crtn.o \
> demo.o test.o\
> -lc \
> -o demo
ld命令输入文件顺序一般是:
# ld [dynamic-linker] [crt1.o] [crti.o] [crtn.o] [user_obj] [user_lib] [sys_lib] [ -o bin]
首先,需要使用ld-linux.so.2这个linux的动态加载器(dynamic loader),来定位和加载程序所需的动态库,大多数的linux应用程序都是用这个加载器来加载的动态共享库的。
然后,需要指定程序启动的入口以及一些初始化工作,大家都知道一个C程序的入口点是main()函数,其实是不准确的,真正的入口点是_start,ld会把目标程序和crt1.o链接在一起,先调用crt1.o里面的_start,再通过_start调用main函数进入真正的程序主体。
接下来,就是指定所需要处理的目标文件,以及相关的库文件,最终生成可执行程序。
所以,相对ld命令,使用gcc的一个明显的好处就是无需手动的去指定链接过程中所需的各种文件,gcc已经完全帮你处理了这个复杂的过程。