EDA365电子论坛网

标题: 迅为IMX6ULL终结者开发板Ubuntu下C编程入门 [打印本页]

作者: 孤久厌闹    时间: 2020-5-11 13:56
标题: 迅为IMX6ULL终结者开发板Ubuntu下C编程入门
我们在 Windows 下使用 C 语言开发的时候,一般都会有支持 Windows 的开发工具,比如我们学习 51 单
  t# [( k6 l& _: F) G片机或者 STM32,所使用的的 Keil 开发软件。此类开发工具一般都会集编辑、编译于一体,我们只需要编辑6 t# z7 }9 q) S5 _
好代码,电机开发工具的编译按钮,就可以自动为我们编译出可执行的二进制文件了。Ubuntu 下的 C 语言: E9 f" R. d; {5 V- }4 w
开发与 Windows 下的 C 语言开发是不一样的,并没有图形界面的开发工具,而且编辑和编译是分开的。我
) U5 y. B" I8 t, u% b们需要使用文本编辑软件先编写代码,然后使用编译工具(GCC)来编译,最终生成可执行的二进制文件。9 w  b( j6 h1 ~% t( c0 s! a" y
如果我们的工程中有多个源文件,在编译的时候我们通常会通过 Makefile 文件来管理整个工程的源文件。
3 w, P4 d4 ?8 ?: @+ @. G& r8 n  H; U本章我们来学习如何在 Ubuntu 下进行 C 语言成的编辑,GCC 编译,Makefile 文件的使用。通过本章学习我
1 J' U" S4 t6 P: _4 |3 I: C. x1 e们可以掌握 Linux 下的 C 语言开发基本方法,为后面的学习做好准备。
3 d! j% @* A0 X! O1 g( p4 ]3.1 x Linux  下编写第一个 C C 程序 程序1 @: ?7 P7 H) w7 e7 t- z+ j
本章节开始的部分我们介绍了 Ubuntu 下 C 程序开发分成两部分:编辑和编译。Ubuntu 下有很多种文本
6 A3 `. H  l- w' [编辑的工具,如 vim、Gedit、Emacs,这里我们使用前面章节介绍的 vim 文本编辑工具来编辑 C 程序。相信
: c$ x( ]* y6 Z2 H2 i; L! N  i大家在学习 C 语言的时候都是从“Hello World”这个程序开始的吧,下面我们通过这个程序,来学习下 Linux* ]. B. w) s7 ^% l' d; Z
下 C 程序开发的流程。
: g; t) w% P/ A, c- ^1 i# k) a" j3.1.1  代码编写( D, @) f7 l& [7 Q: \# m
首先我们在用户根目录下建立文件夹“work”,用于保存所有 C 语言的程序,运行结果如下图所示:
7 @! G8 s& }/ y+ u9 I# ?! Y 7 i) U  Q  U& C8 z9 s8 J6 c: ^+ X
然后进入创建的文件夹 work,为了方便管理,我们每个例程都创建单独的文件夹,首先我们创建文件: O1 S+ P) z2 @3 A
夹“hello_world”来保存我们的第一个 C 程序,如下图所示:  }) h  X& f1 p/ q8 Y3 I
& j* k% Q! L5 K; U" l$ ^  y
然后进入上图中的 hello_world 文件夹,使用 vi 命令新建文件“main.c”,然后在里面输入下面的代) l& ]( V$ w5 c8 V- L
码:: X) {  c8 ~$ Q$ w- k3 @4 \; ^. I
#include : d: @; t+ Y4 n4 M
int mian(int argc, char *argv[])
# X7 _8 F) c9 v: `) {{
5 I* z' H) O1 r/ r! x5 Kprintf("Hello World!\n");7 s, Q! r- \7 p4 ^$ K! X
return 0;; Z$ r9 q& {. Q. n
}
. ^) t3 x0 P9 s" h. u1 }+ ~编写完以后保存并退出 vim 编辑器,然后可以使用 cat 命令查看代码是否保存成功,如下图所示:
$ J. F- \$ V3 s' z2 ?' `* l7 C7 J 3 A# ~! O; F0 h0 V& j
通过上图可以看到代码已经编辑完成了。/ C3 p3 {1 i& x: j+ B9 X
3.1.2  代码编译' `  ^- m+ r4 p" G5 P
Ubuntu 下使用 gcc 编译器来编译 C 程序,我们在安装 Ubuntu 系统的时候,gcc 编译器 morning 安装好. t# w1 h- c! M  {% b
了,我们可以在终端输入“gcc -v”来查看下 gcc 的版本,如下图所示:. l8 H+ p9 B$ L/ u8 e; c) W

$ L$ }% c' }: [& i4 B; c$ f通过上图可以看到 gcc 的版本信息,说明我们的 Ubuntu 上已经安装了 gcc 编译器了,下面我们来看看$ X6 ?& R( i, @: M* F+ Q. d( o& A
怎么通过 gcc 编译我们的第一个 C 程序,我们在终端输入“gcc main.c -o main”,然后回车,会生成 main 文
9 b, _8 r0 }1 _) C: r; x8 W件,如下图所示:) _( v/ E6 p% ]# T2 G, i: l$ ^
6 a  L! f" l2 Y
在上面的“gcc main.c -o main”这个命令里面“-o“用来指定编译生成的文件名字,我们指定的是 main,
& x% Q% J$ Q9 F, Z所以在图 3.1.2.2 中可以看到生成了文件“main”,如果我们想生成其它的文件名字,我们只需要修改“-o”0 @! Z* r( z9 w1 w7 ]
后面的 main(修改成您希望生成的文件名字)。经过前面的步骤已经生成了可执行文件 main,现在我们演8 r* W" R# L8 T: Q/ V
示下在终端如何运行一个可行性的程序,我们直接在终端当前目录下(可执行文件所在的目录下)输入
+ E% k0 }. G0 p) j% }“./main”,然后回车,就可以运行可执行文件 main 了,运行结果如下图所示:8 U; `" `. [. {+ Y

0 C- w6 P1 {  V8 y! v+ _在上图中运行的命令“./main”,其中的“./”代表当前目录下。我们可以看到在终端打印出了 Hello3 @; M2 Q  i1 j1 i2 _1 e" ]
World!。至此,Linux 下的 C 语言编辑和编译的一整套流程我们就介绍完了。
4 b+ x% G, s2 [3.2 c gcc 编译器 编译器5 m" ~6 [& b2 e, c* |( G; K/ k! ]9 I
3.2.1 gcc  命令分析( `  t3 g. t! v8 [. a2 G
在 3.1 节我们已经使用 gcc 编译了 Linux 下的第一个 C 程序,gcc 命令的格式如下:
6 x% w  c$ D# y& D6 p3 Zgcc [参数] [文件名]5 n) Q* u) ^  [2 }3 w9 L. G
主要参数说明如下:) G2 g/ H4 \7 r: q
-c 编译、汇编到目标代码(.o),不链接成可执行文件
8 ~( n; \5 R; \- Q+ m% W-g 生成调试信息0 Z7 a' E$ w9 d7 z5 v
-o 编译完成后生成的文件名,如果不使用该选项,默认生成 a.out 文件6 ?! r' k1 T: N6 }1 w" b7 l
-O 对程序进行优化编译,产生的可执行文件执行效率高- d9 C$ S0 }$ T+ x
-w 不生成任何警告1 u( Q. y3 F8 R- I6 \: U
-S 仅编译到汇编语言,不进行汇编和链接+ ?% J* V) Z; ~0 B  D; v
3.2.2  编译警告错误处理
2 W5 C0 g1 [6 `$ W1 D) T我们是 Windows 下使用 Keil 或者其他开发工具,在编译的时候,如果程序有错误,开发工具会提示出
6 j$ T& j7 n4 A/ r具体的错误信息,可以很方便的定位到问题点,快速的修改出现的问题,gcc 同样也有类似的功能,下面我, q4 w  K6 n* `- e, y" E: H% n5 R
们来看下 gcc 的错误提示功能,首先我们在 work 目录下建立文件夹“test2”,然后使用 vim 在 test2 文件
; B0 E- q5 p2 N夹创建 main.c 文件夹,在 main.c 文件输入如下代码:
! Z3 \) x1 m3 j; n& b6 H7 B+ q6 I#include 8 p* M9 l3 \/ n( M' M) W# A) H$ e
int main(int argc, char *argv[])! [1 \7 ]6 e1 b) q  j% g6 [
{, l+ E' S. v4 s- A
int a;' R1 R, m- z7 S+ G# U
a = 18 ~7 x# e+ |6 V. e! T$ E  {3 x
printf("a=\n", a);
" k4 u" l! r, \0 ?4 U6 m3 Mreturn 0;
' W! g3 t/ W( U}
; O5 t" O! Z0 x  w! c. u上面代码有两处错误:7 N- ~6 h3 g2 G! r/ y( i9 ?* w4 _7 H" A
第 7 行 最后缺少“;”
% Z) c; Q% Z- z( e9 _9 J第 9 行 printf 语法不对,应该为:printf("a =%d\n", a);
- f0 _3 s& p2 u" r我们使用 gcc 编译 main.c,可以看到 gcc 会提示错误信息,如下图所示:
; Q" Y2 `7 B/ b. q* H0 X 5 f( ~7 W+ m( u$ c# H* j7 w4 w
从上图中可以看到在 mian.c 文件的第 9 行 printf 前面缺少“;”我们在第 7 行“a = 1”后面加上“;”,
4 v2 f, s- W& B, I1 M然后继续编译,如下图所示:
  S" r* U: p  @8 S7 {; a5 J ' D' V6 z) I" n# [" Q9 ]
从上图可以看出编译提示语法格式不对,我们把第 9 行修改成“printf("a=%d\n", a);”,然后在继续编
0 s* u% \6 w. y译,如下图所示:
, {$ q/ ^4 `  C; R+ E+ i# Z; X & Y! M$ n: F) P  k7 o* ]
我们可以看到这次 gcc 编译通过,最终生成了文件 main。我们在终端执行 main,运行结果如下图所示:# W+ G, t+ J9 w9 s% M2 {

$ P8 t) [' A6 b3 W7 f从上图可以看到运行的结果和我们涉及到额结果一致,通过本例程可以看到 gcc 编译器不仅可以检测出) V6 R# j$ Z3 I' A& y
程序的错误,而且还会标记处错误在哪个文件的哪一行,很方便的帮助我们去修改问题。( p* @9 p2 M1 S9 [$ u
从上图可以看到运行的结果和我们涉及到额结果一致,通过本例程可以看到 gcc 编译器不仅可以检测出
2 u7 |3 t* N! c+ M0 H4 F! i- H程序的错误,而且还会标记处错误在哪个文件的哪一行,很方便的帮助我们去修改问题。
1 ?" x* z6 {+ O2 F9 F3 L3.2.3 gcc  编译流程
  @1 |0 }5 z3 }8 ]; T; ]( egcc 的编译流程可以分成四个步骤:5 }% C1 p) ~4 |3 u( ~, J
1.预处理,生成预编译文件(.文件)+ z( p" s8 w+ x$ A& g* U
2.编译,生成汇编代码(.S 文件)% k: q! ~2 r; O' h, f- I3 g
3.汇编,生成目标文件(.o 文件)
% K  G3 V2 h7 g& r4.链接,生成可执行文件
5 o/ B4 ?9 p  G) f) h7 B2 w3 3.3  初识  Makefile$ }/ f) k# }5 r# h2 X
3.3.1  什么是 Makefile( {6 S6 D( k' V8 g' S3 n, d: {
在 3.2 章节我们了解了在 Ubuntu 系统下通过 gcc 编译器来编译 C 程序,在我们演示的历程中只有一个
% K$ Z6 i) l/ C7 |" n# m- lC 文件,我们直接在终端输入 gcc 的编译命令,就完成了 C 程序的编译。我们在实际开发过程中,如果我们, Y* b% w/ Y! W' ~
的工程有几十个,或者几百几千个 C 文件,我们通过在终端输入 gcc 命令来编译,这显然是不现实的。为
! Q/ p6 m3 t/ s) ~了解决这个问题我们可以使用“make”命令,它会解析 Makefile 文件中的指令(应该说是规则)来编译整
0 B# K/ a0 r* j) D6 R个工程。在 Makefile 文件中描述了整个工程的所有文件的编译顺序,编译规则。& z3 S* `# p/ }6 f
作为一个专业的程序员,一定要掌握 Makefile 的,我们可以通过 Makefile 能了解到整个工程的处理过程  y$ s& D6 B! \! X4 |+ K9 }
的。
$ m( j' z* {- V0 |4 ?0 W1 X# x由于Makefile涉及到很多的知识点,以至于可以单独写本书来讲述,所以本章我们只是讲解下Makefile
: @- v# o7 k6 h  e: B) n! V的基础入门,如果详细的研究 Makefile,可以给大家推荐《跟我一起写 Makefile》这个电子文档,该文档: ]3 [( s; d! [- E$ a0 J! X, @* F
已经放在了:i.MX6UL 终结者光盘资料\09_其它参考资料里面了。6 U8 I# Q7 T; J# Q/ k
3.3.2  第一个 Makefile
' H; B+ G! ?: c4 m0 K在本节我们建立这样一个工程,计算两个整形数的和,并将结果在终端显示出来。在这个工程中一共有
! k3 k4 R# ]2 z2 q4 Pmain.c、calc.c 两个 C 文件和 calc.h 这个头文件。其中 main.c 是主文件,calc.c 负责接收 main.c 传过( O% A. |7 D- k1 C  |: D
来的数据,然后进行相加。main.c 文件的内用如下:
- U% k* j* w0 ]% M! K3 C#include * w# z- v1 m9 K! U3 ]
include "calc.h"
! g- c( _* U- G* L5 T% Iint main(int argc, char *argv[])/ ]4 W, r+ N8 X& I4 l5 R2 J4 }
{4 m8 M4 V0 j( x: E' e- C
int a = 3, b = 6, sum;5 E) d" r2 s1 @. k' U7 [- w1 J
sum = calc(a, b);8 e6 D; i) k- s
printf("%d + %d = %d\n", a, b, sum);
) z4 |9 J) v. j- a" W- ureturn 0;
. E8 v! ~2 i8 B$ [/ f}
  _9 i$ m# \' H$ M( w# V7 @calc.c 文件内容如下:1 G' A3 n- P. |
#include ' h' H8 D1 b8 j% a2 h6 u' ]
int calc(int a, ing b)" p7 l+ v7 W6 O( J
{0 g* {7 x, j3 q7 ]3 F& j
return (a+b);4 D, T3 g- ^6 j& i) i& h2 a
}- f, t" |/ n* J9 }
文件 calc.h 内容如下:
. @( K3 x: e% {" ]% @#ifndef _CALC_H, ^: G( o; c8 G
#define _CALC_H9 P) J. \% }/ O6 k
int calc(int a, int b);$ H" g* f& E4 F8 V9 T
#endif" Y+ t% ~8 y  x
上面就是我们这个工程的所有源文件,我们在终端使用 gcc 编译这个工程,在终端输入“gcc main.c# u6 A6 Q+ w& c. t* |% b1 s
calc.c -o main”,该命令的意思是用 gcc 对 main.c、calc.c 进行编译,然后输出可执行文件 main,运行
! O* C. [, D7 g) u, k: J6 l! {结果如下图所示:' X: s; m6 M$ T5 r/ C
' F, ~; e( p! o# H
通过上图可以看到生成了可执行文件 main,我们在终端运行 main 执行文件,运行结果如下图所示:
3 e% c% R, Y% u' o 9 m7 ?/ c$ V" e( m+ t
我们可以看到上图的运行结果和我们设计的结果是一致的。由于我们的这个工程是有三个文件,如果
% J2 h: `& K" Y/ b6 d& f) {* S9 K工程有几百个,几千个的文件,或者如果有一个文件被修改,使用上面的命令将会编译所有的文件,如果
, `( B) _" r9 ]/ Y3 O" }我们的工程有上万个文件,编译完一次工程所需要的时间就很可怕。最优的方法就是编译过一次以后,如
# i4 [. L- j2 q5 ?1 a3 |. @1 p* V; [6 P  |* u果后面在编译,只编译修改的文件,这样就会节约很多时间,因此我们修改下编译方法,命令如下:
5 [$ V- Q. @7 ], K& Igcc -c main.c6 n) T' T1 l) p9 E- b# m6 X
gcc -c calc.c' H$ U0 t! e: p
gcc main.o calc.o -o main
" k* b0 K6 G7 T/ a) Q我们在终端输入上面的命令,结果如下图所示:
8 q5 y  t1 H5 \  s; p/ Q- E% ~, J
. m; O& @' x, Z+ B, O) V  ^上图的第一条和第二条命令里面使用了参数“-c”是把 main.c 和 calc.c 编译成对应的.o 文件,最后
$ H2 i+ u5 {% y# F一条命令是把编译生成的.o 文件链接成可执行文件 main。假如我们修改了 main.c 这个文件。只需将 main.c
! \9 A6 T8 h/ K( ?5 C& k" O这个一个文件重新编译下,然后在把所有的.o 文件重新链接成可执行文件,对应的命令如下:/ j9 w- a0 n. Q. D, s
gcc -c main.c
& F3 j$ i: P$ }gcc main.o calc.o -o main- p* T! D. ]$ r
可是这样还有一个问题,如果需要修改的文件有很多,这样工作量也会不小,所以我们需要一个工具:9 I" s1 B+ b2 {2 K) P- l0 J
1.如果工程没有编译过,就会把工程中的.c 文件全部编译并连接成可执行文件' Z. _6 \% s) h% G8 u( p6 s! B1 T
2.如果工程中有某些文件修改了,只编译修改的文件并连接成可执行文件
& I7 q7 Q/ A: N3.如果工程中的头文件修改了,那么就要编译所有引用这个头文件的.c 文件,并且连 接成可执
+ z$ W( z- f" h' R8 o7 R行文件
8 M' r7 t+ z5 a6 w+ J; x& J* G+ H我们开头说的 Makefile 就是完成这个功能的,下面我们在工程中建立一个 Makefile 文件来实现这样的功8 J! D; y+ x! i) Y
能(注意:文件名字必须为 Makefile,大小写是区分的)。我们使用 vim 创建 Makefile 文件(Makefile
8 [! [5 \$ q; k8 a6 }: _1 O8 w1 b和我们的 main.c、calc.c 在同一级目录下),然后输入下面的脚本:  J+ r8 |2 P# P; l1 c. b9 J
main:main.o calc.o
0 D% [1 r" {' B9 _+ R( ogcc -o main main.o calc.o
; a. l. E. q# Mmain.o:main.c
( e% D2 ]3 _% [2 `gcc -c main.c
% U  s6 V: j, qcalc.o:calc.c: _* D9 R3 N* k) x' Z, t
gcc -c calc.c+ d! _/ O! Q' s! O& d# ^$ v
clean:/ [+ m$ _- h0 ^8 X9 y; g5 _
rm -rf *.o
+ l/ H) `  m7 [' B2 E3 Q& F9 krm -rf main
: w8 E, Q- k! P. I- C5 A上面脚本缩进的行需要使用“Tab”键缩进,不要使用空格,这是 Makefile 的语法要求,编写完成的脚本
8 r/ q) `! C: c) C, Z+ Q如下图所示:
+ Q2 q( H1 g" p& V3 g; O! z + [2 ~, E: l3 ?- P5 o
编写好 Makefile,保存并退出,然后我们在终端输入“make”命令来编译我们的工程,make 命令会在) i5 B# o7 q, X% l7 u" C# o. G
当前目录下查找“Makefile”文件,如果存在的话就按照 Makefile 里面的规则进行编译,如下图所示:
% ~( \; x$ n4 k6 M 1 B, `4 r/ G' r) ^8 I
通过上图可以看到编译产生了 main.o、calc.o 和 main 执行文件,说明编译成功了。接下来我们修改& M& R8 Y  F  o% `8 i9 n: _+ ~
下 main.c 这个文件,如下图所示:. d/ K2 F. E$ t( K9 o
" E2 B4 E  G' H5 d( Y3 Z
然后保存并退出,然后在终端输入“make”再次编译下工程,如下图所示:
1 O" k+ z2 X+ h! [  v5 ^' P* u# U % e- T% q4 g# N! n
通过上图我们可以看到只重新编译了修改的 main.c,并最终重新链接生成可执行文件 main,我们在终5 a( w! Z+ l$ [' h$ o
端运行可执行文件 main,如下图所示:
, ^! k+ L9 p/ w* o / m# s1 c" m8 z: o5 F
从上图的运行结果可以看到最后的结果等于 10 了,和我们程序的设计结果是一样的。
- [/ \/ n& \( |" v+ U! E3.4 e Makefile 语法 语法
* [$ N- H1 N$ N5 T3.4.1  初识 Makefile
5 l/ B4 m& p9 I7 A9 ~, Y* O# `Makefile 文件是由一些列的规则组合而成的,格式如下:' u+ \  l4 u( V9 i) J: H
target(目标文件) ...: prerequisites(依赖的文件) ...
. J+ e, O& [! a7 R: t: ycommand(命令)
! F5 Q9 I3 n, N$ f+ I...- Z. c$ D1 g& N# b, S1 _
...
! F8 H& ^% K" ?比如 3.3.2 中写的 Makefile 的规则:
4 Y* \  l1 I% d+ A1 i4 Mmain.o:main.c
" }7 T' I$ X. ^- Egcc -c main.c  h0 v+ s( Z3 n# B
这条规则的 main.o 是目标文件(将要生成的文件),main.c 是依赖的文件(生成 main.o 需要的文件),& S0 A5 v' U5 z& O  D/ Y+ u
“gcc -c main.c”是生成 main.o 需要运行的命令。e Makefile  中每行的脚本如果有缩进的情况,必须使用8 X- x" z. [% U# z9 x8 q1 G
“ Tab ” 键缩进,切记不能使用空格缩进(这是 e Makefile  的语法要求),大家一定要切记!
# |9 d9 J/ H4 T0 j+ @  H' r. U下面我们来分析一下图 3.3.2 章节中写的 Makefile 文件,脚本如下:
6 C& C" B+ t6 r. v1 1 main:main.o calc.o" p  A; U  k, r( m. G! `1 A8 d
2 2 gcc -o main main.o calc.o3 ?5 W7 z6 w, G; C
3 3 main.o:main.c- Y* C2 G# M8 m3 I! Z- R
4 4 gcc -c main.c  w' U3 T0 Z8 D. q8 j$ r' ~
5 5 calc.o:calc.c4 ^  r4 I: |" z8 x; _: @" X
6 6 gcc -c calc.c
# Q% z" o  P! j- c5 r, v2 n2 }7 7
# D6 i* w' O0 G& W; V4 y: }8 V8 8 clean:
7 _) x: l$ L6 r1 J/ A; ]  G9 9 rm -rf *.o' l6 f8 `/ d3 Q+ m2 T3 V: h
10 rm -rf main  ?" N5 Q$ D. D8 P! G  f0 e" h: e
该脚本一共有 4 条规则,1、2 行是第一条规则,3、4 行是第二条规则,5、6 是第三条规则 8、9、10
+ S: ]7 ]1 ?; F* `/ }. B是第四条规则。我们在运行 make 命令的时候,会解析当前目录下的这个 Makefile 文件里面的规则,首先
+ b; t6 ?% x9 H# N解析第一条规则,第一条规则中的目标文件是 main,只要完成了该目标文件的更新,整个 Makefile 的功能  W& j2 _) {# N
就完成了。在第一次编译的时候,由于目标文件 main 不存在,则会解析第一条规则,第一条规则依赖文件; B6 Z! ^' A6 o, i  X* q( X
main.o、calc.o,make 命令会检查当前目录下是否有这两个.o 文件,经过检查发现没有,然后 make 会在6 b' o2 s3 G  ^& U6 Q# Y
Makefile 中查找分别以 main.o、calc.o 为目标的规则(第二条,第三条规则)。执行第二条规则依赖的文4 h9 C& t) M+ k9 S$ v/ ], F9 A8 w
件是 main.c,make 命令检查发现当前目录下有这个文件,然后执行第二条规则的命令“gcc -c main.c”
3 w& K, l: _- o3 r; K生成 main.o 文件。然后执行第三条规则,第三条规则的目标文件是 calc.o,依赖的文件是 calc.c,make
$ L; s6 N. }7 v命令检查发现当前目录下存在该文件,然后执行第三条规则的命令“gcc -c calc.c”生成 calc.o 文件,& {( R6 N, E+ L! c  j" @
至此第一条规则依赖的 main.o、calc.o;两个文件已经生成了,然后运行第一条规则的命令“gcc -o main1 A. m9 k% J# y; W
main.o calc.o”生成 main 文件。因为 make 命令运行的时候会从 Makefile 的第一条规则开始解析,然后
9 ?% x- A) N, S3 G, V根据第一条规则的依赖文件去遍历文件中的“对应规则”,然后在根据“对应规则”的依赖文件去遍历“对, I- C* G) m4 g
应的规则”,采用这样递归的方式会遍历出完成第一条规则所需要的所有规则。下面我们来看看第四条规
4 B- ]: J* `, A3 d5 P1 T/ Q则的目标文件是 clean,我们通过查看发现该规则与第一条规则没有关联,所以我们在运行 make 命令的时
% [- L, t' c/ [" |候,不会遍历到该规则。我们可以在终端输入“make clean”命令来运行第四条规则,第四条规则没有依
& P3 C. W5 H( s4 j2 p4 `( y5 J赖的文件,所以执行运行命令“rm -rf *.o”和“rm -rf main”,这两条命令的功能是删除以.o 问结尾的( a  Y; i6 n5 N" K3 Q3 z
所有文件,删除文件 main,运行如下图所示:9 N6 B* f$ e0 H' b0 L9 i9 u& A2 c
3 s5 o! T" V) W
通过上图可以看到 main.o、mcalc.o 和 main 三个文件已经删除了。通过该规则我们可以清除编译产生0 Z" ^& [8 R$ k0 i" |5 d4 M8 Q3 G
的文件,实现工程的清理。
0 }, d; U  H( Y* m3 G* X: K我们再来总结一下 make 命令的执行过程:
  o! e& A0 J) n$ @2 B1.make 命令会在当前目录下查找以 Makefile 命名的文件
- @# p) b+ g' W4 R4 b2.找到 Makefile 文件,就会按照 Makefile 里面的规则去编译,并生成最终文件
- H. U! r7 ]" P8 t4 r9 u3.当发现目标文件不存在或者所依赖的文件比目标文件新(修改时间),就会执行规则对应的命令来更新。. N( ?5 r  W# C" l: Q/ [$ p% B
我们可以看到 make 是一个工具,他会通过 Makefile 文件里面的内容来执行具体的编译过程。! H. Y9 \! v! o* J
3.4.2 Makefile  的变量  M" b% @# Y6 ^+ Y# s
在 3.3.2 章节中的 Makefile 第一条规则:, e8 S" g7 t, ]7 D! \: s& B% ]' @6 s
main:main.o calc.o3 k& Y  `3 o6 |2 U" f2 }
gcc -o main main.o calc.o' f% \8 b( `8 P$ l( g4 x$ F
在该规则中 main.o、calc.o 这两个文件我们输入了两次,由于我们的额 Makefile 文件内容比较少,
& [% a  D7 l4 C3 p5 l8 c如果 Makefile 复杂的情况下,这种重复的输入就会非常占用时间,而且修改起来也会很麻烦,为了解决这( r3 i% x3 y( A1 ^- ?
个问题,Makefile 可以使用变量。Makefile 的变量是一个字符串。比如上面的规则我们声明一个变量,叫
$ z# T+ Q" E# g/ p' [objects,objs 或者是 OBJ,反正不管是什么,只要能够表示 main.o、calc.o 就行了,我们修改上面的规, @9 w: ?7 f6 [! i

6 B! e* t2 k+ B+ p9 q- v) R1 1 objects = main.o calc.o
+ P" o! v& h5 A: Y. L3 q; D2 2 main( objects)
9 p& ~  ]( `; u' W* O+ h3 3 gcc -o main $( objects)
5 H9 n8 I: X1 Z. i/ b5 `2 k4 a我们来分析下修改后的规则,首先第一行是我们定义了一个变量 objects,并给赋值“main.o calc.o”,* M) W* X; S- v4 J) y1 W
第二行、第三行用到了变量 objects。Makefile 中的变量引用方式是“$(变量名)”,变量 objects 的赋值% l0 W6 `9 G4 a  L, V: e
使用“=”,Makefile 中变量的赋值还可以使用“:=”、“?=”、“+=”,这四种赋值的区别如下:2 c; n, V" I8 e+ R' N, |
1. “= = ” 赋值符
9 H, M6 P" m7 g: c. l我们先在用户根目录的 work 目录下创建一个 Makefile 脚本,输入下面的内容:- M  L3 Y* e5 n6 u& e. G
1 ceshi1 = test
! @/ J5 Y' C, ^2 h+ R2 ceshi2 = $(ceshi1)
+ J; E2 }/ {$ ?$ E* Y- X3 ceshi1 = temp5 j6 u  }; L2 q7 c1 x7 M
4
- H3 }- E* f9 m1 c9 ^! Y$ l5 out:
9 s; d7 x' N/ I+ g6 @Echo ceshi2(ceshi2)
& u# t: d5 S; H6 p0 P0 x第一行我们定义了变量并赋值“test”,第二行定义了变量 ceshi2 并赋值变量 ceshi1,第三行修改变量* ^2 w- S$ z! D; M1 @0 g
ceshi1 的值为“temp”,第五行、第六行是输出变量 ceshi2 的值。我们在终端输入“make out”命令,如3 |! U# L( y! \6 Y: {) U
下图所示:
" p% }7 ?* J1 Q 5 ^8 T$ k( c' Y3 i3 q# s
在上图可以看到变量 ceshi2 的值是 temp,也就是变量 ceshi1 最后一次的赋值。2 y4 p9 u! D8 j$ l7 Z# h# H. n! Y
2. “ := ” 赋值符9 A# \, h/ M: F% Y
我们修改“=”赋值符中的代码,第二行的“=”改成“:=”,代码如下:
4 s. z" B* X5 M- v  N. a0 S8 Q' k1 ceshi1 = test
; M! {  q0 a2 K$ }2 B- L2 ceshi2 := $(ceshi1)8 E8 A8 B. q% M- U
3 ceshi1 = temp; o5 @% Q0 ~6 L' ]* q
4
# W! w' s0 Z; @, g" S! Y5 p5 out:4 W- b4 W+ m0 e1 g
6 @echo ceshi2(ceshi2)( `' l, |! s+ K! n2 e
我们在终端输入“make out”命令,如下图所示:  d- c, Y& _' s6 `

1 `& x* g$ [, y2 e  r2 T/ ]我们可以看到上图的运行结果输出变量 ceshi2 的值是 test,虽然在第三行我们修改了变量 ceshi1 的
9 Z1 l% G6 f" z' z( N0 c2 c值,通过本实验我们可以看到“:=”赋值符的功能了。
( f5 D8 U" {" ~1 t- {3. “ ?= ” 赋值符
* i3 g* A' p( E2 Iceshi ?= test  B) b+ Q& ^! F# I' ]
“?=”赋值符的作用是如果前面没有给变量 ceshi 赋值,那么变量就赋值“test”,如果前面已经赋值了,
( b! [! ?* q6 b& j& Q7 S: H4 b就使用前面的赋值。
7 X* q. Y: U' x" a' E4. “ += ” 赋值符2 s7 k7 p, a+ N2 K
objs = main.o' A) ^4 `" K3 J* ~0 r, X6 T
objs += calc.o) X1 ~! }8 X9 c+ H, g# Z
上面的脚本最后变量 objs 的值是“main.o calc.o”,“+=”赋值符的功能是实现变量的追加。
( j" i- I3 Y2 `% O3.4.3  条件判断% ?+ U! X0 j8 f" E; R. j
使用条件判断,可以让 make 根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量/ N& P( L! G; i
的值,或是比较变量和常量的值。其语法有下面两种:
. K# S: c: d( T/ b1.
! U- e3 }  M% F$ u1 A5 C<条件比较>
  c4 ~8 u/ l$ U) T! H[条件为真时执行的脚本]
/ D5 K+ K# S7 |. ], [" Uendif* M' G7 l+ W+ V
2., i/ r6 ~0 ~" Q5 b. K
<条件比较>6 o" U1 d& g3 G1 [/ a- J/ R3 `) u; c3 C( `
[条件为真时执行的脚本]2 h( @/ U. ~  A
else
) h- K+ X5 T+ y  e6 k[条件为假时执行的脚本]; y; |6 H8 u9 f' s2 t1 ~  k6 N* `
endif; o( I+ O9 m6 F5 Q
条件比较用到的比较关键字有:ifeq、ifneq、ifdef、ifndef。- G$ H# w2 ~, [
ifeq 表示如果比较相等,语法如下:8 m. O& ^' ]- W2 ^* m7 B- s* Z9 b
ifeq(<参数 1>, <参数 2>)
: ]. l# V5 W# p& l' \ifneq 表示如果不相等,语法如下:# v3 r2 X& q5 }
ifneq(<参数 1>, <参数 2>)) S* l" \% P1 {+ w: c# j! m
ifdef 表示如果定义了变量,语法如下:
  ^7 D0 f) @, u( G% X$ z& mifdef <变量名>
3 b! M1 A! A3 F, O! ^ifndef 表示如果没有定义变量,语法如下:0 {- ]- {' @4 f& y0 q' m! A0 z3 U. a
ifndef <变量名>+ \( _6 T- Y2 O. W: w
3.4.4  使用函数
, N+ T* f: O6 ~6 c0 n9 p在 Makefile 中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make 所
1 h+ Q2 [$ [/ D& d; `2 [0 s支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。- x2 n  Q: v" X- C! B/ ]0 ?! }: _
函数的调用很像变量的使用,也是以“$”来标识的,语法如下:
' E! w& e; E1 d( c9 v$(<函数名> <参数集合>)2 z' c; y& r& m" K8 A9 K3 `* L& U; @
或者:- ^1 E/ A1 `& m
${<函数名> <参数集合>}
4 F+ `2 |% ^6 N2 I函数名和参数集合之间以空格分隔,参数集合的参数通过逗号分隔。函数调用以“$”开头,以圆括号或花
. B7 p( i5 N  z8 V4 A括号把函数名和参数括起。感觉很像一个变量。函数中的参数可以使用变量。为了风格的统一,函数和变
/ d6 S# t# O7 _* a* r# ~1 [量的括号最好一样,如使用“$(subst a,b,$(x))”这样的形式,而不是“$(subst a,b,${x})”的形式。
: R+ J& x* c0 j; V3 B因为统一会更清楚,也会减少一些不必要的麻烦。. j0 J1 [1 r2 s) c/ P* R& _* p
接下来我们介绍几个常用的函数,其它的函数可以参考文档《跟我一起写 Makefile》。+ u) ^/ i. v' v! [* _( _( n) ?
t 1.subst  函数8 e' u; d0 p/ \  Y# b8 _5 M; q
$(subst ,,)
- K5 B. {) `9 X3 ^5 w- B" x1 v+ o此函数的功能是把字串中的字符串替换成,函数返回被替换过后的字符串。如下示例:
4 z8 }4 L' N$ `, G3 ?$(subst ee,EE,feet on the street)5 F$ y* m4 `3 g% y- o
以上脚本实现把字符串“feet on the street”中的“ee”字符串替换成“EE”字符串,替换后的字符串! \, \4 N) w% {/ d6 S" j
为“feet on the strEEt”。
0 S' h; f# D( p! F8 f+ N; P0 t. 2. t patsubst  函数
: h6 x$ u) |! W7 [6 {$ s& Y$(patsubst ,,)
' d6 t* f" `( T3 H此函数的功能是查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式1 x  e4 [9 q  P. J$ J3 \
,如果匹配的话,则以替换。这里可以包括通配符“%”,表示任意长" |9 K5 X' ^" C7 J/ ?; F; B
度的字串。如果中也包含“%”,那么中的这个“%”将是中的那个
- N  w8 n" w  G6 }7 z8 {“%”所代表的字串。(可以用“\”来转义,以“\%” 来表示真实含义的“%”字符)。函数返回被替换5 a& N! W5 m2 @) y9 N" W6 D, L% |
过后的字符串。如下示例:4 ^  A) |1 n( I+ O% |5 y# M. P" y
$(patsubst %.c,%.o,x.c bar.c)
* H' b: l% R; x  G1 {以上脚本实现把字串“x.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.o bar.o”
6 o* n* F+ Y$ D& ]p 3.strip  函数
, h/ W: V$ [& K7 p1 E. T5 I8 G$(strip )$ k! ~, @0 F) N& i: J5 f: o8 W; J
此函数的功能是去掉字串中开头和结尾的空字符,函数返回被去掉空格的字符串值。如下示例:/ M7 }' g2 h/ h# i2 ^9 L. Y, c
$(strip a b c ); I6 f+ e, ~: p+ U  o% j
以上脚本实现把字串“a b c ”去掉开头和结尾的空格,结果是“a b c”。6 z: G) O* j) g, s! ^' E( n$ Z
. 4. g findstring  函数
' J7 |7 i7 x8 P& v! z! G$(findstring ,)
8 K2 X: T2 Z# h4 r# V) M此函数的功能是在字串中查找字串,如果找到,那么返回,否则返回空字符串,如下示0 h9 F$ p* u" P1 t
例:, t: z0 \8 {8 e8 J) z0 {
$(findstring a,a b c): j1 _  G7 H, K
$(findstring a,b c)5 E# F% @( o0 W
以上脚本,第一个返回“a”字符串,第二个返回空字符串。9 g( `& l: I+ `$ o. z9 Z- o3 y
r 5.dir  函数% q$ _6 }% B) c4 \) o. M# _
$(dir )
3 o6 y7 o- I: R# x4 ?, s8 B此函数的功能是从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部- }8 z# r" o0 Y2 ^. G
分。如果没有反斜杠,那么返回“./”。返回文件名序列的目录部分,如下示例:
) S% o2 A+ U. L$(dir src/foo.c hacks)
% |" M+ [% q; o以上脚本运行结果返回“src/”。: Z# C$ i; ^  ?
. 6. r notdir  函数
5 ?& r& V; x9 F0 C' Z" `$(notdir )
3 R- {. m. w0 E: U此函数的功能是从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠(“/”)之后/ v7 c; R' x- j1 U) X6 T
的部分,返回文件名序列的非目录部分,如下示例:
( o7 J+ o3 d. r  a$(notdir src/foo.c)* ~! z. Y% X/ D. r/ }' E, ?; I7 ~4 M
以上脚本返回字符串“foo.c”- u1 V% f& W" x. G$ J
. 7. h foreach  函数
2 M7 K, I9 P3 z& R7 y4 r2 ~  Z$(foreach ,,)
; C# T' A4 {/ g7 r3 A) o此函数的功能是把参数中的单词逐一取出放到参数所指定的变量中,然后再执行所包含% m- `; t- A/ ~* J* R. W
的表达式。每一次会返回一个字符串,循环过程中,的所返回的每个字符串会以空格分隔,4 c5 I3 F, e6 }1 L! G) e0 ^
最后当整个循环结束时,所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函& B0 ~1 i6 I: w, R! ?
数的返回值。所以,最好是一个变量名,可以是一个表达式,而中一般会使用这
. ~! W& J. f' g个参数来依次枚举中的单词。如下示例:
9 C) y1 |" ^, q( v+ {4 onames := a b c d* l! A5 t: ?8 D5 g- E; Y4 U+ k. J
files := $(foreach n,$(names),$(n).o)
, B- S$ V2 U* _以上脚本实现$(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次根据“$(n)”计算出
& F* ^/ k! {+ J1 T  J2 G9 w* Z* T0 @4 x一个值,这些值以空格分隔,最后作为 foreach 函数的返回,所以$(files)的值是“a.o b.o c.o d.o”。- a' ~; U7 M; d' N; R4 A
(注意,foreach 中的参数是一个临时的局部变量,foreach 函数执行完后,参数的变量将不
, H- G2 Q% n3 w$ t# P& W在作用,其作用域只在 foreach 函数当中)。
* a2 \9 v1 J9 d9 q6 {0 g9 M2 x更多内容关注迅为电子
- }5 s+ `# L" _, f
作者: shapeofyou888    时间: 2020-5-11 16:16
学习了




欢迎光临 EDA365电子论坛网 (https://bbs.eda365.com/) Powered by Discuz! X3.2