GCC、Makefile编程学习

相关学习资料

http://gcc.gnu.org/
https://gcc.gnu.org/onlinedocs/
http://zh.wikipedia.org/zh/GCC
http://blog.csdn.net/casularm/article/details/316149
http://www.bccn.net/Article/kfyy/cyy/jc/200409/9.html
http://linux.chinaunix.net/techdoc/develop/2008/12/16/1053006.shtml
http://www.91linux.com/html/article/program/cpp/20071203/8745.html
http://www.cnblogs.com/hnrainll/archive/2012/07/05/2578277.html
http://www.cnblogs.com/ggjucheng/archive/2011/12/14/2287738.html
http://www.chinaunix.net/old_jh/23/408225.html
http://blogimg.chinaunix.net/blog/upfile/070907021605.pdf

 

目录

1. GCC简介
2. GCC的编译
3. GCC的命令行参数
4. GCC编译中遇到的常见错误
5. Makefile
6. Makefile Example

 

1. GCC简介

GCC(GNU Compiler Collection GNU 编译器家族)。GCC目前可以支持:

1. C 
2. Ada  
3. C++  
4. Java  
5. Objective C 
6. Pascal 语言
7. COBOL
8. 函数式编程
9. 逻辑编程的Mercury等等

GCC目前几乎支持所有的硬件(处理器)架构

 

 

2. GCC的编译

一个程序编译过程是分为四个阶段进行的,即:

1. 预处理(也称预编译,Preprocessing)
2. 编译(Linking)
3. 汇编 (Assembly)
4. 连接(Linking)

使用gcc指令可以一步完成,也可以单步独立进行,方便程序员获取中间代码代码,进行调试

0x0: 编写源代码

0x1: 预处理(也Preprocessing)

预处理是C语言程序从源代码变成可执行程序的第一步,主要是C语言编译器对各种预处理命令进行处理,包括:

1. 头文件的包含: #include
2. 宏定义的扩展: 
    1) #define: 定义一个预处理宏
    #define MACRO_NAME(args) tokens(opt)
    之后出现的MACRO_NAME将被替代为所定义的标记(tokens). 宏可带参数, 而后面的标记也是可选的. 
    2) #undef: 取消宏的定义
#define除了可以独立使用以便灵活设置一些参数外,还常常和条件编译语法结合使用,以便灵活地控制代码块的编译与否,也可以用来避免同一个头文件的多次包含
3. 条件编译的选择: 
    1) #ifdef: 判断某个宏是否被定义, 若已定义, 执行随后的语句
    2) #ifndef: 与#ifdef相反, 判断某个宏是否未被定义 
    3) #if: 编译预处理中的条件命令, 相当于C语法中的if语句
    4) #elif: 若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-if
    5) #else: 与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else
    6) #endif: #if, #ifdef, #ifndef这些条件命令的结束标志.
    7) #line: 标志该语句所在的行号
4. 编译器指示指令(给编译器看的指令)
    1) #pragma: 说明编译器信息
    #pragma用编译器用来添加新的预处理功能或者显示一些编译信息. #pragma的格式是各编译器特定的, gcc的如下:
    #pragma GCC name token(s)
    2) #warning: 显示编译警告信息
    #warning, #error分别用于在编译时显示警告和错误信息
    #error tokens
    3) #error: 显示编译错误信息
    #warning tokens

gcc预处理指令:

gcc -E test.c -o test.i(输出预处理内容到文件)
gcc -E test.c(输出预处理内容到命令行)

0x2: 编译(Linking)

C语言编译器会进行如下步骤进行编译过程:

1. 词法分析
2. 语法分析: 通过-std指定遵循哪个标准
3. 代码优化和存储分配: 根据优化(-O)要求进行
4. 接着会把源代码翻译成中间语言,即汇编语言(因为C/C++是编译性语言)

gcc编译指令:

gcc -S test.i -o test.s(-S选项,表示在程序编译期间,在生成汇编代码后,停止,-o输出汇编代码文件)

0x3: 汇编(Assembly)

把作为中间结果的汇编代码翻译成了机器代码,即目标代码(可以在目标机器运行的机器码),这一步其实就是我们在进行汇编编程中的汇编编译过程。这一步产生的所谓机器代码还不可以直接运行,还需要下一步的链接程序进行链接后才可以

对于这一步的工作来说,我们可以使用:

1. as汇编工具来汇编AT&T格式的汇编语言,因为gcc产生的中间代码就是AT&T格式的
2. 或者直接用gcc的汇编编译器来进行汇编编译

这一步执行后,得到的就已经是机器码了

1. 用as把汇编语言汇编成目标代码
//注意,这里的.s文件是上一步编译后的文件
as -o test.o test.s       

2. gcc汇编指令
gcc -c test.s -o test.o

0x4: 链接(Linking)

链接的目的是处理可重定位文件(.so、.dll),重定位是将"符号引用"与"符号定义"进行链接的过程。这句话可以这么理解,我们在写代码中会使用到很多的函数和变量,这些"函数名"、和"变量名"最终需要被转换为对应的虚拟内存地址,CPU才可以执行,这个过程就是链接过程
链接又分为:

1. 静态链接
程序开发阶段程序员用ld(gcc实际上在后台调用了ld)静态链接器手动链接的过程
例如:
如果链接到可执行文件中的是静态连接库libmyprintf.a,那么. rodata节区在链接后需要被重定位到一个绝对的虚拟内存地址,以便程序运行时能够正确访问该节区中的字符串信息

2. 动态链接
动态链接则是程序运行期间系统调用动态链接器(ld-linux.so)自动链接的过程
例如:
对于puts,因为它是动态连接库libc.so中定义的函数,所以会在程序运行时通过动态符号链接找出puts函数在内存中的地址,以便程序调用该函数 

关于交叉编译

交叉编译通俗地讲就是在一种平台上编译出能运行在体系结构不同的另一种平台上,比如在我们地PC平台(X86 CPU)上编译出能运行在sparc CPU平台上的程序,编译得到的程序在X86 CPU平台上是不能运行的,必须放到sparc CPU平台上才能运行。当然两个平台用的都是linux。这种方法在异平台移植和嵌入式开发时用得非常普遍。
相对与交叉编译,我们平常做的编译就叫本地编译,也就是在当前平台编译,编译得到的程序也是在本地执行。
用来编译这种程序的编译器就叫交叉编译器,相对来说,用来做本地编译的就叫本地编译器,一般用的都是gcc,但这种gcc跟本地的gcc编译器是不一样的,需要在编译gcc时用特定的configure参数才能得到支持交叉编译的gcc。
为了不跟本地编译器混淆,交叉编译器的名字一般都有前缀,比如sparc-xxxx-linux-gnu-gcc、sparc-xxxx-linux-gnu-g++等等
交叉编译器使用方法跟本地的gcc差不多,但有一点必须要注意的是:必须用-L和-I参数指定编译器用sparc系统的库和头文件,不能用本地(X86)的库,例如

sparc-xxxx-linux-gnu-gcc test.c -L/path/to/sparcLib -I/path/to/sparcInclude

 

 

3. GCC的命令行参数

Usage: gcc [options] file...
Options:
    1) -combine: 一次传给gcc多个源文件
    2) -save-temps: 保留中间文件 
    3) -B <directory>: 添加gcc搜索的源文件路径 
    4) -E 源代码file: 只单独进行预处理(预编译)这一步  
    5) -S 预处理后的file: 只单独进行编译这一步
    6) -c 编译后待汇编的文件: 只进行编译和汇编这两步 
    7) -o <file>: 输出文件 
    
    8) -l参数
    -l参数就是用来指定程序要链接的库,-l参数紧接着就是库名,库名和真正的库文件名之间有一个对应关系呢,例如数学库,他的库名是m,他的库文件名是libm.so,把库文件名的头lib和尾.so去掉就是库名了
    值得注意的是,-l参数在搜索指定库名的时候会从gcc默认的"库路径"下去查找
        1. /lib
        2. /usr/lib
        3. /usr/local/lib
    所以,如我们自已要用到一个第三方提供的库名字叫libtest.so,那么我们只要把libtest.so拷贝到这些路径中(例如/usr/lib)
    example:
    编译时加上-ltest参数,我们就能用上libtest.so库了(我们还需要与libtest.so配套的头文件,对我们需要使用的函数进行声明,我们才能在代码中使用)
    9) -L参数
    我们都知道,放在里的库直接用-l参数就能链接了,但如果库文件没放在这三个目录里,而是放在其他目录里,这时我们只用-l参数的话,链接还是会出错,出错信息大概是:"/usr/bin/ld: cannot find -lxxx",也就是链接
程序ld在那3个目录里找不到
-L参数跟着的是库文件所在的目录名。再比如我们把libtest.so放在/aaa/bbb/ccc目录下,那链接参数就是-L/aaa/bbb/ccc -ltest 10) -include -include用来包含头文件,但一般情况下包含头文件都在源码里用#include xxxxxx实现,所以-include参数很少用 11) -I参数 -I参数是用来指定头文件目录。/usr/include目录一般是不用指定的,gcc知道去那里找(默认路径),但是如果头文件不在/usr/include里我们就要用-I参数指定了,比如头文件放在/myinclude目录里,那编译命令行就要加上
"-I/myinclude"参数了,如果不加你会得到一个"xxxx.h: No such file or directory"的错误 -I参数可以用相对路径,比如头文件在当前目录,可以用-I.来指定 12) -O参数 这是一个程序优化参数,一般用-O2就是,用来优化程序用的,比如 gcc test.c -O2 -o test 优化得到的程序比没优化的要小,执行速度可能也有所提高。关于编译原理、优化的相关知识又是另一类庞大的体系了,以后有时间可以研究一下 13) -shared参数 编译动态库时要用到,比如gcc -shared test.c -o libtest.so

 

 

4. GCC编译中遇到的常见错误

0x1: undefined reference to 'xxxxx'错误

这是链接(Linking)错误,不是编译(Linking)错误,已经到了第4步出错的,也就是说如果只有这个错误,说明你的程序源码本身没有问题,是你用编译器编译时参数用得不对

reason:
可能是由于没有指定链接程序要用到得库,比如你的程序里用到了一些数学函数,那么你就要在编译参数里指定程序要链接数学库

solution: 在编译命令行里加入程序要用到的库,

0x2: libxxxx.so未建立软链接报错

大部分libxxxx.so只是一个链接,比如

libm.so它链接到/lib/libm.so.x
/lib/libm.so.6链接到/lib/libm-2.3.2.so

如果没有这样的链接,还是会出错,因为gcc ;loader只会找libxxxx.so,所以如果你要用到xxxx库,而只有libxxxx.so.x或者libxxxx-x.x.x.so,做一个链接就可以了,例如

ln -s libxxxx-x.x.x.so libxxxx.so

很多库开发包提供了生成链接参数的程序,名字一般叫xxxx-config,一般放在/usr/bin目录下,比如gtk1.2的链接参数生成程序是gtk-config,执行gtk-config --libs就能得到以下输出

"-L/usr/lib -L/usr/X11R6/lib -lgtk -lgdk -rdynamic -lgmodule -lglib -ldl -lXi -lXext -lX11 -lm"

这就是编译一个gtk1.2程序所需的gtk链接参数

 

 

5. Makefile

0x1: Makefile规则格式

makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则(rules)来指定

1. 哪些文件需要先编译
2. 哪些文件需要后编译
3. 哪些文件需要重新编译
4. 甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令

makefile带来的好处就是"自动化编译",一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率
make是一个linux下的命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make

/*
1. 变量声明
在Makefile中声明变量
var = value;
在需要使用value的地方直接使用$(var)就可以了
*/

/*
2. Makefile的隐含规则
在使用make编译.c源文件时,编译.c源文件规则的命令可以不用明确给出。这是因为make本身存在一个默认的规则,能够自动完成对.c文件的编译并生成对应的.o文件。它执行命令"cc -c"来编译.c源文件。在Makefile中我们只需要
给出需要重建的目标文件名(一个.o文件),make会自动为这个.o文件寻找合适的依赖文件(对应的.c文件。对应是指:文件名除后缀外,其余都相同的两个文件),而且使用正确的命令来重建这个目标文件
*/ TARGET... : PREREQUISITES... COMMAND ... ... /* 3. 清理工作 Makefile规则除了完成源代码编译之外,也可以完成其它任务。例如实现清除当前目录中编译过程中产生的临时文件 1) 通过".PHONY"特殊目标将"clean"目标声明为伪目标。避免当磁盘上存在一个名为"clean"文件时,目标"clean"所在规则的命令无法执行 2) 在命令行之前使用"-",意思是忽略命令"rm"的执行错误 值得注意的是,目标"clean"没有出现在终极目标"target"依赖关系中(终极目标的直接依赖或者间接依赖),所以我们执行"make"时,目标"clean"所在的规则将不会被处理。当需要执行此规则,要在make的命令行选项中明确指定这
个目标,即执行"make clean"
*/ .PHONY:clean clean: -rm target $(var) 1. target: 规则的目标 通常是最后需要生成的文件名或者为了实现这个目的而必需的中间过程文件名。可以是: 1) .o文件 2) 最后的可执行程序的文件名 3) 一个make执行的动作的名称,如目标"clean",我们称这样的目标是"伪目标" 2. prerequisites: 规则的依赖 生成规则目标所需要的文件名列表。通常一个目标依赖于一个或者多个文件。当规则的目标是一个文件,在它的任何一个依赖文件被修改以后,在执行"make"时这个目标文件将会被重新编译或者重新连接 3. command: 规则的命令行 规则所要执行的动作(任意的shell命令或者是可在shell下执行的程序),它限定了make执行这条规则时所需要的动作。一个规则可以有多个命令行,每一条命令占一行。 需要注意的是:每一个命令行必须以[Tab]字符开始,[Tab]字符告诉make此行是一个命令行。make按照命令完成相应的动作 命令(command)就是在任何一个目标的依赖文件发生变化后重建目标的动作描述。一个目标可以没有依赖而只有动作(指定的命令)。比如Makefile中的目标"clean",此目标没有依赖,只有命令。它所定义的命令用来删除make过程产生
的中间文件(进行清理工作)

从概念上来说,一个Makefile文件是由一个个的规则组成,每个规则包括: target、prerequisites、command
make程序根据规则的依赖关系,决定是否执行规则所定义的命令的过程我们称之为"执行规则"

0x2: Makefile运行流程

1. 当在shell提示符下输入"make"命令以后。make读取当前目录下的Makefile文件,并将Makefile文件中的第一个目标作为其执行的"终极目标",开始处理第一个规则,默认的情况下,make执行的是Makefile中的第一个规则,此
规则的第一个目标称之为"最终目的"或者"终极目标"。结合Makefile的工作原理就会很好理解为什么第一个规则会是"最终规则",因为Makefile的规则是递归依赖的,最终规则递归地依赖于下面的其他规则 2. make在执行这个规则所定义的命令之前,首先处理目标"终极目标"的所有的依赖目标,可能是: 1) 伪目标 2) .so文件 对.o文件为目标的规则处理有下列三种情况: 2.1) 目标.o文件不存在,使用其描述规则创建它 2.2) 目标.o文件存在,目标.o文件所依赖的.c源文件、.h文件中的任何一个比目标.o文件"更新"(在上一次make之后被修改),则根据规则重新编译生成它 2.3) 目标.o文件存在,目标.o文件比它的任何一个依赖文件的(c源文件、.h文件)"更新"(它的依赖文件在上一次make之后没有被修改),则什么也不做 3) 可执行文件 对于Makefile的工作流程的理解,我们一定要牢记递归的概念,即make会将终极目标的依赖目标从右到左一次回退全部执行完,出现在"终极目标"的依赖列表中的目标一定要全部执行完之后,终极目标才能被最终完成 3. 如果在处理终极目标的依赖目标的时候,又出现了新的依赖目标,则继续递归地进行处理(类似函数栈调用原理) 4. . 在Makefile中一个规则的目标如果不是"终极目标"所依赖的(或者"终极目标"的依赖文件所依赖的),那么这个规则将不会被执行,除非明确指定执行这个规则(可以通过make的命令行指定重建目标,那么这个目标所在的规则就会
被执行,例如"make clean") 5. 更新(或者创建)终极目标的过程中,如果任何一个规则执行出现错误make就立即报错并退出。 整个过程make只是负责执行规则,而对具体规则所描述的依赖关系的正确性、规则所定义的命令的正确性不做任何判断。 就是说,一个规则的依赖关系是否正确、描述重建目标的规则命令行是否正确,make不做任何错误检查。 因此,需要正确的编译一个工程。需要在提供给make程序的Makefile中来保证其依赖关系的正确性、和执行命令的正确性。

0x3: Example

edit : main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o       /*注释:如果后面这些.o文件比edit可执行文件新,那么才会去执行下面这句命令*/
    cc -o edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o

main.o : main.c defs.h
    cc -c main.c
kbd.o : kbd.c defs.h command.h
    cc -c kbd.c
command.o : command.c defs.h command.h
    cc -c command.c
display.o : display.c defs.h buffer.h
    cc -c display.c
insert.o : insert.c defs.h buffer.h
    cc -c insert.c
search.o : search.c defs.h buffer.h
    cc -c search.c
files.o : files.c defs.h buffer.h command.h
    cc -c files.c
utils.o : utils.c defs.h
    cc -c utils.c
clean :
    rm edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o

Relevant Link:

http://wiki.ubuntu.org.cn/%E8%B7%9F%E6%88%91%E4%B8%80%E8%B5%B7%E5%86%99Makefile:MakeFile%E4%BB%8B%E7%BB%8D#.E4.B8.80.E4.B8.AA.E7.A4.BA.E4.BE.8B

 

6. Makefile Example

hellomake.chellofunc.chellomake.h
#include <hellomake.h>

int main() {
  // call a function in another file
  myPrintHelloMake();

  return(0);
}
#include <stdio.h>
#include <hellomake.h>

void myPrintHelloMake(void) {

  printf("Hello makefiles!\n");

  return;
}
/*
example include file
*/

void myPrintHelloMake(void);

0x1: Makefile 1

hellomake: hellomake.c hellofunc.c
    gcc -o hellomake hellomake.c hellofunc.c -I.

0x2: Makefile 2

CC=gcc
CFLAGS=-I.

hellomake: hellomake.o hellofunc.o
    $(CC) -o hellomake hellomake.o hellofunc.o -I.

0x3: Makefile 3

CC=gcc
CFLAGS=-I.
DEPS = hellomake.h

%.o: %.c $(DEPS)
    $(CC) -c -o $@ $< $(CFLAGS)

hellomake: hellomake.o hellofunc.o 
    gcc -o hellomake hellomake.o hellofunc.o -I.

0x4: Makefile 4

CC=gcc
CFLAGS=-I.
DEPS = hellomake.h
OBJ = hellomake.o hellofunc.o 

%.o: %.c $(DEPS)
    $(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
    gcc -o $@ $^ $(CFLAGS)

0x5: Makefile 5

IDIR =../include
CC=gcc
CFLAGS=-I$(IDIR)

ODIR=obj
LDIR =../lib

LIBS=-lm

_DEPS = hellomake.h
DEPS = $(patsubst %,$(IDIR)/%,$(_DEPS))

_OBJ = hellomake.o hellofunc.o 
OBJ = $(patsubst %,$(ODIR)/%,$(_OBJ))


$(ODIR)/%.o: %.c $(DEPS)
    $(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
    gcc -o $@ $^ $(CFLAGS) $(LIBS)

.PHONY: clean

clean:
    rm -f $(ODIR)/*.o *~ core $(INCDIR)/*~

Relevant Link:

http://www.cs.colby.edu/maxwell/courses/tutorials/maketutor/

 

Copyright (c) 2014 LittleHann All rights reserved

 

posted @ 2014-07-20 16:41  郑瀚Andrew.Hann  阅读(2518)  评论(0编辑  收藏  举报