1. 内核空间头文件
对于 Linux kernel的程序开发,主要使用C语言,当然汇编语言也是举足轻重,Kernel的开发主要针对哪些方面呢?1). 硬件驱动模块
2). 内核性能的增强
3). 内核的新功能、新特性
4). 内核bug的fix
......
无论是哪一方面的内核开发,使用的都是内核自身的头文件,而内核的头文件主要有两个目录位置:
当然,除了asm和 linux 这两个主要的头文件目录,还有诸如/usr/include/drm,/usr/include/video,/usr/include/sound等驱动相关的头文件目录。
总之,如果是从事内核开发的话,所有引用的头文件均是来自内核本身,绝不可能使用用户空间的头文件,比如"glibc",“libstdc++"等头文件的引用是不可能出现在内核程序中的。
2. 用户空间头文件
用户空间的头文件杂乱纷繁,随便一个应用程序,几乎都有自己维护的头文件,但尽管如此,有一些最基本、相对底层的头文件或库函数是开发应用程序的基础。比如,如果想在用户空间编写C语言程序,那么使用的最基本的C库和头文件是由glibc提供的;想在用户空间编写C++程序,其使用的基本库和头文件又是来自于libstdc++。
对于glibc,不仅仅提供了标准的C库,如fopen(), fclose()等, 而且还提供了和内核空间打交道的相关库,如open(), close()等。
不管是glibc提供的C库,还是由libstdc++提供的C++库,想要编写GUI窗口程序,显然不太现实,于是,有了著名的基于C语言的GTK+库和基于C++语言的QT库,这些相对上层的库所做的事情就是为了方便编写GUI程序,对C和C++封装了一层。
3. 所谓跨平台
1). 跨平台一般分为:跨硬件平台:比如Linux内核,即能在x86上运行,也能在ARM或Android上运行
跨系统平台:即能在Linux系统下运行,也能在Windows系统下运行,比如firefox
跨硬件跨系统:比如Java程序,可以在不同的体系结构,也可以在不同的系统平台运行
2). Window环境中重新编译,才能运行,其原理是使用标准库编写程序,从而实现跨平台:
glibc:Linux标准C库
libstdc++:Linux标准C++库
3). Windows环境中重新编译,才能运行,其原理是统一不同系统平台的链接库,从而实现跨平台:
GTK+:基于C语言的GUI库
Qt: 基于C++的GUI库
4). Windows环境中不需要重新编译,一次编译,到处运行:
Java,使用JVM虚拟机统一不同的链接库和不同的平台,从而实现跨平台。
前面介绍了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 <sys/ioctl.h>等。
所以,如果想要编写能够在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 <stdio.h>
- 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 <stdio.h>
- #include "test.h"
- int main(void)
- {
- printf("==echo main ==\n");
- func();
- return 0;
- }
- /* test.c */
- #include <stdio.h>
- #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已经完全帮你处理了这个复杂的过程。