6_Makefile与GCC
Posted 韦东山
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了6_Makefile与GCC相关的知识,希望对你有一定的参考价值。
第六章 Makefile与GCC
6.1 交叉编译器
6.1.1 什么是交叉编译
简单地说,我们在PC机上编译程序时,这些程序是在PC机上运行的。我们想让一个程序在ARM板子上运行,怎么办?
ARM板性能越来越强,可以认为ARM板就相当于一台PC,当然可以在ARM板上安装开发工具,比如安装ARM版本的GCC,这样就可以在ARM板上编译程序,在ARM板上直接运行这个程序。
但是,有些ARM板性能弱,或者即使它的性能很强也强不过PC机,所以更多时候我们是在PC机上开发、编译程序,再把这个程序下载到ARM板上去运行。
这就引入一个问题:
1) 我们使用工具比如说gcc编译出的程序是给PC机用的,这程序里的指令是X86指令。
2)那么能否使用同一套工具给ARM板编译程序?
显示不行,因为X86的指令肯定不能在ARM板子上运行。所以我们需要使用另一套工具:交叉编译工具链。
为何叫“交叉”?
首先,我们是在PC机上使用这套工具链来编译程序;
然后再把程序下载到ARM板运行;
如果程序不对,需要回到PC机修改程序、编译程序,再把程序下载到ARM板上运行、验证。如此重复。
在这个过程中,我们一会在PC上写程序、编译程序,一会在ARM板上运行、验证,中间来来回回不断重复,所以称之为“交叉”。对于所用的工具链,它是在PC机上给ARM板编译程序,称之为“交叉工具链”。
有很多种交叉工具链,举例如下:
1) Ubuntu平台:交叉工具链有arm-linux-gcc编译器
2) Windows 平台:利用ADS(ARM开发环境),使用armcc编译器。
3) Windows平台:利用cygwin环境,运行arm-elf-gcc编译器。
6.1.2 为什么需要使用交叉编译
1) 因为有些目的平台上不允许或不能够安装所需要的编译器,而我们又需要这个编译器的某些功能;
2) 因为有些目的平台上的资源贫乏,无法运行我们所需要编译器;
3) 因为目的平台还没有建立,连操作系统都没有,根本谈不上运行什么编译器。
6.1.3 验证实例
下面这个例子,我们准备源文件main.c,然后我们采用gcc编译后可执行程序放在目标板上运行看看是否能运行起来,如下:
main.c
01 #include <stdio.h>
02
03 int main()
04
05 printf("100ask\\n");
06 return 0;
07
在虚拟机编译运行:
$ gcc main.c –o 100ask
$ ./100ask
100ask
$
在上面的运行结果,没有任问题,然后我们将这个可执行程序放到目标板上,如下:
$ chmod 777 100ask
$ ./100ask
./100ask: line 1: syntax error: unexpected “(”
$
报错无法运行。说明为X86平台制作的可执行文件,不能在其他架构平台上运行。交叉编译就是为了解决这个问题。
为了方便实验,我们在Ubuntu中使用gcc来做实验,如果想使用交叉编译,参考章节《第二章1.2 安装SDK、设置工具链》,安装好工具链,设置好环境变量后,将所有的gcc替换为arm-linux- gcc就可以完成交叉编译。
其中:
gcc是在x86架构指令用的。
arm-linux- gcc是RSIC(精简指令集)ARM架构上面使用。
他们会把源程序编译出不同的汇编指令然后生成不同平台的可执行文件。
6.2 gcc编译器1_gcc常用选项__gcc编译过程详解
6.2.1 gcc编译过程详解
一个C/C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)和连接(linking)等4步才能生成可执行文件,编译流程图如下。
6.1.2.1 预处理:
C/C++源文件中,以“#”开头的命令被称为预处理命令,如包含命令“#include”、宏定义命令“#define”、条件编译命令“#if”、“#ifdef”等。预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些东西输出到一个“.i”文件中等待进一步处理。
6.1.2.2 编译:
对预处理后的源码进行词法和语法分析,生成目标系统的汇编代码文件,后缀名为“.s”。
6.1.2.3 汇编:
对汇编代码进行优化,生成目标代码文件,后缀名为“.o”。
6.1.2.4 链接:
解析目标代码中的外部引用,将多个目标代码文件连接为一个可执行文件。
编译器利用这4个步骤中的一个或多个来处理输入文件,源文件的后缀名表示源文件所用的语言,后缀名控制着编译器的缺省动作
后缀名 | 语言种类 | 后期操作 |
---|---|---|
.c | C源程序 | 预处理、编译、汇编 |
.C | C++源程序 | 预处理、编译、汇编 |
.cc | C++源程序 | 预处理、编译、汇编 |
.cxx | C++源程序 | 预处理、编译、汇编 |
.m | Objective-C源程序 | 预处理、编译、汇编 |
.i | 预处理后的C文件 | 编译、汇编 |
.ii | 预处理后的C++文件 | 编译、汇编 |
.s | 汇编语言源程序 | 汇编 |
.S | 汇编语言源程序 | 预处理、汇编 |
.h | 预处理器文件 | 通常不出现在命令行上 |
其他后缀名的文件被传递给连接器(linker),通常包括:
.o:目标文件(Object file,OBJ文件)
.a:归档库文件(Archive file)
在编译过程中,除非使用了“-c”,“-S”或“-E”选项(或者编译错误阻止了完整的过程),否则最后的步骤总是连接。在连接阶段中,所有对应于源程序的.o文件,“-l”选项指定的库文件,无法识别的文件名(包括指定的“.o”目标文件和“.a”库文件)按命令行中的顺序传递给连接器。
6.2.2 gcc命令
gcc命令格式是:
gcc [选项] 文件列表
gcc命令用于实现c程序编译的全过程。文件列表参数指定了gcc的输入文件,选项用于定制gcc的行为。gcc根据选项的规则将输入文件编译生成适当的输出文件。
gcc的选项非常多,常用的选项,它们大致可以分为以下几类 。并且使用一个例子来描述这些选项,创建一个mian.c源文件,代码为如下:
main.c:
01 #include <stdio.h>
02
03 #define HUNDRED 100
04
05 int main()
06
07 printf("%d ask\\n",HUNDRED);
08 return 0;
09
注明: 代码目录在裸机Git仓库 NoosProgramProject/ (6_Makefile与GCC/001_gcc_01001_gcc_01)文件夹下。
6.2.2.1 过程控制选项
过程控制选项用于控制gcc的编译过程。无过程控制选项时,gcc将默认执行全部编译过程,产生可执行代码。常用的过程控制选项有:
(1)预处理选项(-E)
C/C++源文件中,以“#”开头的命令被称为预处理命令,如包含命令“#include”、宏定义命令“#define”、条件编译命令“#if”、“#ifdef”等。预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些东西输出到一个“.i”文件中等待进一步处理。使用例子如下:
$ gcc -E main.c -o main.i
运行结果,生成main.i,main.i的内容(由于头文件展开内容过多,我将截取部分关键代码):
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 942 "/usr/include/stdio.h" 3 4
# 2 "main.c" 2
# 5 "main.c"
int main()
printf("%d ask\\n",100);
return 0;
你会发现头文件被展开和printf函数中调用HUNDRED这个宏被展开。
(2)编译选项(-S)
编译就是把C/C++代码(比如上述的“.i”文件)“翻译”成汇编代码。使用例子如下:
$ gcc -S main.c -o main.s
运行结果,生成main.s,main.s的内容:
1 .file "main.c"
2 .text
3 .section .rodata
4 .LC0:
5 .string "%d ask\\n"
6 .text
7 .globl main
8 .type main, @function
9 main:
10 .LFB0:
11 .cfi_startproc
12 pushq %rbp
13 .cfi_def_cfa_offset 16
14 .cfi_offset 6, -16
15 movq %rsp, %rbp
16 .cfi_def_cfa_register 6
17 movl $100, %esi
18 leaq .LC0(%rip), %rdi
19 movl $0, %eax
20 call printf@PLT
21 movl $0, %eax
22 popq %rbp
23 .cfi_def_cfa 7, 8
24 ret
25 .cfi_endproc
26 .LFE0:
27 .size main, .-main
28 .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
29 .section .note.GNU-stack,"",@progbits
(3)汇编选项(-c)
汇编就是将上述的“.s”文件汇编代码翻译成符合一定格式的机器代码,在Linux系统上一般表现为ELF目标文件(OBJ文件)
$ gcc -c main.c -o main.o
运行结果,生成main.o(将源文件转为一定格式的机器代码)。
6.2.2.2 输出选项
输出选项用于指定gcc的输出特性等,常用的选项有:
(1)输出目标选项(-o filename)
-o选项指定生成文件的文件名为filename。使用例子如下
$ gcc main.c -o main
运行结果,生成可执行程序main,如下:
$ ls
main.c main
$ ./main
$ 100 ask
其中,如果无此选项时使用默认的文件名,各编译阶段有各自的默认文件名,可执行文件的默认名为a.out。使用例子如下:
$ gcc main.c
运行结果,生成可执行文件a.out,如下:
$ ls
a.out main.c
$ ./a.out
$ 100 ask
(2)输出所有警告选项(-Wall)
显示所有的警告信息,而不是只显示默认类型的警告。建议使用。我们见上面的main.c稍微修改一下,b此节代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_gcc_02)文件夹下,如下:
main.c:
01 #include <stdio.h>
02
03 #define HUNDRED 100
04
05 int main()
06
07 int a = 0;
08 printf("%d ask\\n",HUNDRED);
09 return 0;
10
编译不添加-Wall选项编译,没有任何警告信息,编译结果如下:
$ gcc main.c -o main.c
编译添加-Wall选项编译,现实所有警告信息,编译结果如下:
$ gcc main.c -Wall -o main.c
main.c: In function ‘main’:
main.c:7:6: warning: unused variable ‘a’ [-Wunused-variable]
int a=0;
^
6.2.2.3 头文件选项
头文件选项(-Idirname)
将dirname目录加入到头文件搜索目录列表中。当gcc在默认的路径中没有找到头文件时,就到本选项指定的目录中去找。在上面的例子中创建一个目录,然后创建一个头文件test.h。然后main.c里面增加#include“test.h”,代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_gcc_03) 文件夹下,使用例子如下:
$ tree
.
├── inc
│ └── test.h
└── main.c
1 directory, 2 files
$
test.h:
01 #ifndef __TEST_H
02 #define __TEST_H
03 /*
04 code
05 */
06 #endif
运行结果,这样就可以引用指定文件下的目录的头文件,如下:
$ gcc main.c -I inc -o main
如果不添加头文件选项,编译运行结果,如下:
$ gcc main.c -o main
main.c:2:18: fatal error: test.h: No such file or directory
compilation terminated.
会产生错误提示,无法找到test.h头文件。
6.2.2.3 链接库选项
(详细使用方法查看下一节:gcc编译器2_深入讲解链接过程)
1) 添加库文件搜索目录(-Ldirname)
将dirname目录加入到库文件的搜索目录列表中。
2) 加载库名选项(-lname)
加载名为libname.a或libname.so的函数库。例如:-lm表示链接名为libm.so的函数库。
3) 静态库选项(-static)
使用静态库。注意:在命令行中,静态库夹在的库必须位于调用该库的目标文件之后。
6.2.2.4 代码优化选项
gcc提供几种不同级别的代码优化方案,分别是0,1,2,3和s级,用-Olevel选项表示。默认0级,即不进行优化。典型的优化选项:
(1)-O :基本优化,使代码执行的更快。
(2)-O2:胜读优化,产生尽可能小和快的代码。如无特殊要求,不建议使用O2以上的优化。
(3)-Os:生成最小的可执行文件,适合用于嵌入式软件。
6.2.2.5 调试选项
代码目录在**git仓库(6_Makefile与GCC/001_gcc_02)**文件夹下
gcc支持数种调试选项:
-g 产生能被GDB调试器使用的调试信息。
调试例子如下,首先需要编译,操作步骤如下:
$ gcc main.c -g -o main
GDB调试示例:
(1)run命令
调试运行,使用run命令开始执行被调试的程序,run命令的格式:
run [运行参数]
$ gdb -q main <---进入调试程序
Reading symbols from output...done.
(gdb) run <---开始执行程序
Starting program: /home/100ask/makefile/
100 ask
[Inferior 1 (process 7425) exited normally]
(gdb)
(2)list命令
列出源代码,使用list命令来查看源程序以及行号信息,list命令的格式:
list [行号]
(gdb) list 1 <---列出地一行附近的源码,每次10行
#include <stdio.h>
#define HUNDRED 100
int main()
int a = 100;
printf("%d ask\\n",HUNDRED);
return 0;
(gdb) <Enter> <---按Enter键,列出下10行源码
(gdb)
(3)设置断点
1)break命令,设置断点命令,break命令的格式: break <行号> | <函数名>
(gdb) break 7
Breakpoint 1 at 0x40052e: file main.c, line 7.
(gdb)
2)info break命令,查 看断点命令:
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040052e in main at main.c:7
(gdb)
3)delete breakpoint命令,删除断点命令, delete breakpoint命令的格式: delete breakpoint <断点号>
(gdb) delete breakpoint 1
(gdb) info break
No breakpoints or watchpoints.
(gdb)
(4)跟踪运行结果
1)print命令,显示变量的值,print命令的格式:print[/格式] <表达式>
2)display命令,设置自动现实命令,display命令的格式: display <表达式>
3)step和 next命令,单步执行命令,step和next命令的格式:step <行号> 或 next <行号>
4)continue命令,继续执行命令。
(gdb) break 7
Breakpoint 1 at 0x40052e: file main.c, line 7.
(gdb) break 9
Breakpoint 2 at 0x400535: file main.c, line 9.
(gdb) run
Starting program:/home/100ask/makefile/
Breakpoint 1, main () at main.c:7
7 int a = 100;
(gdb) continue
Continuing.
Breakpoint 2, main () at main.c:9
9 printf("%d ask\\n",HUNDRED);
(gdb) print a
$1 = 100
(gdb)
6.2.3 编译错误警告
在写代码的时候,其实应该养成一个好的习惯就是任何的警告错误,我们都不要错过,
编译错误必然是要解决的,因为会导致生成目标文件。但是警告可能往往会被人忽略,但是有时候,编译警告会导致运行结果不是你想要的内容。接下来我们来简单分析一下gcc的编译警告如何处理,例子如下:
main.c
01 #include <stdio.h>
02 #include "hander.h"
03
04 int main()
05
06 float a = 0.0;
07 int b = a;
08 char c = 'a'
09
10 printf("100ask: \\n",a);
11
12 return 0;
13
上面文件中有三处错误:
第2行:包含了一个不存在的头文件。
第8行:语句后面没有加分号。
第10行:书写格式错误,变量a没有对应的输出格式。
我们对上面的文件进行编译,还记得上面我们讲的编译警告选项吗?我们在编译的时候加上它(-Wall),如下:
$ gcc main.c -Wall -o output
main.c: In function ‘main’:
main.c:2:20: fatal error: hander.h: No such file or directory
compilation terminated.
错误警告信息分析:在展开第二行的hander.h头文件的时候,产生编译错误,没有hander.h文件或者目录。接着我们把hander.h头文件去掉,在编译一次:
$ gcc -Wall main.c -o output
main.c: In function ‘main’:
main.c:10:2: error: expected ‘,’ or ‘;’ before ‘printf’
printf("100ask: \\n",a);
^
main.c:8:7: warning: unused variable ‘c’ [-Wunused-variable]
char c = 'a'
^
main.c:7:6: warning: unused variable ‘b’ [-Wunused-variable]
int b = a;
^
错误警告信息分析:有一个错误和两个警告。一个错误是指第10行prntf之前缺少分号。两个警告是指第7行和第8行的变量没有使用。那么我继续解决错误信息和警告,将两个警告的变量删除和printf前添加分号,然后继续编译,如下:
$ gcc -Wall main.c -o output
main.c: In function ‘main’:
main.c:8:9: warning: too many arguments for format [-Wformat-extra-args]
printf("100ask: \\n",a);
^
错误警告信息分析:还是有警告信息,该警告指的是printf中的格式参数太多,也就是没有添加变量a的输出格式,继续解决错误信息和警告,添加变量a的输出格式,然后继续编译,如下:
$ gcc -Wall main.c -o output
$ tree
.
├── main.c
└── output
最终编译成功,输出目标文件。
6.3 gcc编译器2_深入讲解链接过程
你会发现,可执行文件文件会比源代码大了。这是因为编译的最后一步链接会解析代码中的外部应用。然后将汇编生成的OBJ文件,系统库的OBJ文件,库文件链接起来。它们全部链接生成最后的可执行文件,从而使可执行文件比源代码大。我们用一个例子来说明上面描述,代码使用**(代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_gcc_01)文件夹下)**如下:
$ gcc main.c -c
$ gcc -o output main.o
$ gcc -o output_static main.o --static
$ ls -alh
drwxrwxr-x 2 tym tym 4.0K 2月 20 07:27 .
drwxrwxr-x 6 tym tym 4.0K 2月 20 07:25 ..
-rw-rw-r-- 1 tym tym 96 2月 20 07:25 main.c
-rw-rw-r-- 1 tym tym 1.5K 2月 20 07:26 main.o
-rwxrwxr-x 1 tym tym 8.5K 2月 20 07:27 output
-rwxrwxr-x 1 tym tym 892K 2月 20 07:27 output_static
从上面的例子可以看出output_static比output大很多。
6.3.1 动态链接库和静态链接库使用例程
静态库和动态库,是根据链接时期的不同来划分。
静态库:在链接阶段被链接的,所以生成的可执行文件就不受库的影响,即使库被删除,程序依然可以成功运行。链接静态库从某种意义上来说是一种复制粘贴,被链接后库就直接嵌入可执行程序中了,这样系统空间有很大的浪费,而且一旦发现系统中有bug,就必须一一把链接该库的程序找出来,然后重新编译,十分麻烦。静态库是不是一无是处了呢?不是的,如果代码在其他系统上运行,且没有相应的库时,解决办法就是使用静态库。而且由于动态库是在程序运行的时候被链接,因此动态库的运行速度比较慢。
动态库:是在程序执行的时候被链接的。程序执行完,库仍需保留在系统上,以供程序运行时调用。而动态库刚好弥补了这个缺陷,因为动态库是在程序运行时被链接的,所以磁盘上只需保留一份副本,一次节约了空间,如果发现bug或者是要升级,只要用新的库把原来的替换掉就可以了。
下面我们创建三个文件main.c,add.c,add.h,讲解静态库链接和动态库链接,如下:
main.c:
#include <stdio.h>
#include "add.h"
int main(int argc, char *argv[])
printf("%d\\n",add(10, 10));
printf("%d\\n",add(20, 20));
return 0;
add.c:
#include "add.h"
int add(int a, int b)
return a + b;
add.h:
#ifndef __ADD_H
#define __ADD_H
int add(int a, int b);
#endif
**注明:**代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_gcc_04)文件夹下。
6.3.1.1 静态库链接
静态库名字一般为“libxxx.a”。利用静态库编译生成的可执行文件比较大,因为整个函数库的所有数据都被整合进了可执行文件中。
优点:
1.不需要外部函数库支持。
2.加载速度快。
缺点:
1.静态库升级,程序需要重新编译。
2.多个程序调用相同库,静态库会重复调入内存,造成内存的浪费。
静态库的制作,如下:
$ gcc add.c -o add.o -c
$ ar -rc libadd.a add.o
静态库的使用,例子如下:
$ gcc main.c -o output -ladd -L.
运行结果:
$ ./output
20
40
6.3.1.2 动态库链接
动态库名字一般为“libxxx.so”,又称共享库。动态库在编译的时候没有被编译进可执行文件,所以可执行文件比较小。需要动态申请并调用相应的库才能运行。
**优点:**多个程序可以使用同一个动态库,节省内存。
**缺点:**加载速度慢。
动态库的制作,如下:
$ gcc -shared -fPIC lib.c -o libtest.so
$ sudo cp libtest.so /usr/lib/
动态库的使用,如下:
$ gcc main.c -L. -ltest -o output
运行结果:
$ ./output
20
40
6.4 Makefile的引入及规则
6.4.1 为什么需要Makefile?
在上一章节对GCC编译器描述,以及如何进行C源程序编译。在上一章节的例子中,我们都是在终端执行gcc命令来完成源文件的编译。感觉挺方便的,这是因为工程中的源文件只有一两个,在终端直接执行编译命令,确实快捷方便。但是现在一些项目工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,如果仍然使用在终端输入若干条命令,那显然不切实际,开发效率极低。程序员肯定不会被这些繁琐的事情,影响自己的开发进度。如果我们能够编写一个管理编译这些文件的工具,使用这个工具来描述这些源文件的编译,如何重新编译。为此“make”工具就此诞生。并且由Makefile负责管理整个编译流程,Makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 Makefile就像一个Shell脚本一样,也可以执行操作系统的命令,极大的提高了软件开发的效率。
6.4.2 Makefile的引入
Makefile的引入是为了简化我们编译流程,提高我们的开发进度。下面我们用一个例子来说明Makefile如何简化我们的编译流程。我们创建一个工程内容分别main.c,sub.c,sub.h,add.c,add.h五个文件。sub.c负责计算两个数减法运算,add.c负责计算两个数加法运算,然后编译出可执行文件。其源文件内容如下:
main.c:
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main()
printf("100 ask, add:%d\\n", add(10, 10));
printf("100 ask, sub:%d\\n", sub(20, 10));
return 0;
add.c:
#include "add.h"
int add(int a, int b)
return a + b;
add.h:
#ifndef __ADD_H
#define __ADD_H
int add(int a, int b);
#endif
sub.c:
#include "sub.h"
int sub(int a, int b)
return a - b;
sub.h:
#ifndef __SUB_H
#define __SUB_H
int sub(int a, int b);
#endif
代码目录在裸机Git仓库 NoosProgramProject/(6_Makefile与GCC/001_Makefile_01)文件夹下。
我们使用gcc对上面工程进行编译及生成可执行程序,在终端输入如下命令,如下:
$ gcc main.c sub.c add.c -o ouput
$ ls
add.c add.h main.c output sub.c sub.h
$ ./output
<以上是关于6_Makefile与GCC的主要内容,如果未能解决你的问题,请参考以下文章
libstdc++.so.6:未找到版本 GLIBCXX_3.4.20
gcc/make/makefile/cmake/qmake的区别与联系