Makefile

在 Linux 上,一个文件能否被运行,通常是看是否有 “x” 权限,但是真正的可执行文件是二进制的文件,如 /usb/bin/passswd,/bin/touch 等。我们常写的 shell script 其实是利用 shell(如 bash)程序的功能进行的一些判断,而最终运行的时候除了 bash 还有在脚本中调用的一些已经编译好的二进制的程序。

$ file /bin/bash
/bin/bash: ELF 64-bit LSB executable, AMD x86-64, version 1 (SYSV), for GNU/Linux 2.6.9, dynamically linked (uses shared libs), for GNU/Linux 2.6.9, stripped

$ file ver.sh
ver.sh: Bourne-Again shell script text executable

如果是二进制运行的时候,会看到 ELF,shared libs 等字样;而普通的脚本则会显示 text exectable 字样。

我们以 hello world 开始:

$ cat hello.c
#include<stdio.h>
int main(void)
{
   printf("hello world\n");
   return 0;

}

$ gcc hello.c
会产生一个 a.out 的二进制文件,这个就是最总的的可执行二进制文件。或者可以先编译产生目标文件(.o),再链接产生最终的二进制文件:

$ gcc -Wall -c hello.c
$ ls hello*
hello.c hello.o
$ gcc -o hello hello.o
$ ls hello*
hello.c hello.o hello
$ ./hello
hello world

程序的编译的过程:
预处理(Pre-Processing) -> 编译(Compliling) -> 汇编(Assesmling) -> 链接(Linking)
以上的四个步骤。从功能上来看,预处理,编译,汇编是三个不同的阶段,但是 gcc 通常将这三个步骤合并成一个步骤来执行。因此通常就成了常说的编译,链接两大步。

在预处理阶段,gcc 会调用 cpp 处理源文件中的 #ifdef,#include,#define 等指令,生成 .i 文件:

$ gcc -E hello.c -o hello.i

在编译阶段,gcc 调用 ccl 将 .i 的中间文件经过编译后生成汇编文件 .s

$ gcc -S hello.i -o hello.s

在汇编阶段,gcc 调用 as 将输入的汇编文件 .s 转换成目标文件,也就是机器语言 .o 文件

$ gcc -c hello.s -o hello.o

最后,在链接阶段,gcc 会调用 ld 将 .o 文件包含一些库文件链接成一个可执行的二进制文件

$ gcc hello.o hello
$ ./hello
hello world

$ file hello*
hello:   ELF 64-bit LSB executable, AMD x86-64, version 1 (SYSV), for GNU/Linux 2.6.9, dynamically linked (uses shared libs), for GNU/Linux 2.6.9, not stripped
hello.c: ASCII C program text
hello.i: ASCII C program text
hello.o: ELF 64-bit LSB relocatable, AMD x86-64, version 1 (SYSV), not stripped
hello.s: ASCII assembler program text

常见的 gcc 参数有:

-c
编译文件,但是没有链接,产生的结果就是目标文件(Object file)

-o
产生输出文件,不管是可执行的二进制文件,目标文件,汇编文件还是预处理文件。如果 -o 没有指明,默认的可执行文件为 a.out;目标文件为 .o;汇编文件为 .s;预处理文件为 .i

-O
在程序编译,链接的过程中,对源代码进行优化,导致的结果就是产生的二进制文件的执行效率会提高,但是编译链接的过程会变慢

-O2
更 -O 相比执行更好的优化,一般选这个

-Wall
产生详细的警告,出错信息,建议开启

下面看看链接:

$ cat sin.c
#include <stdio.h>
int main(void)
{
   float value;
   value = sin ( 3.14 / 2 );
   printf("%f\n",value);
   return 0;
}

$ gcc sin.c
sin.c: In function 'main':
sin.c:5: warning: incompatible implicit declaration of built-in function 'sin'
/tmp/ccsfvijY.o: In function `main':
sin.c:(.text+0x1b): undefined reference to `sin'
collect2: ld returned 1 exit status

可以看到没有定义 sin。而 sin 函数的定义是在 libm.so 这个函数库中的,所以需要在编译的时候将其链接进去:

$ gcc -lm sin.c -L /lib -L /usr/lib sin.c

-lname
加 入某个函数库。也就是链接时,加载名为 libname.a 的库文件。该库文件位于系统默认的 /usr/lib/ 下或者由 -L 选项指定。因此 lm 就代表 libm.a 这个函数库。这个选项跟你在整条命令中放置的位置有关系,因此,foo.o -lz bar.o 会在  foo.o 之后在 bar.o 之前寻找 z 这个库文件

m
在这里也就代表 libm.so 这个库,lib 与 扩展名 .so 或者 .a 不需要写

-L
到哪个库中寻找 libm.so 文件,默认情况下,链接程序 ld 会在 /usr/lib/ 中找到所需的库文件

除了链接函数库之外,在 sin.c 的首行还有个 #include<stdio.h> 这句话,表示将一些定义的数据由 stdio.h 这个文件读入,该文件默认放在 /usr/include/stdio.h 中,如果该文件不在 /usr/include/ 目录下,可以使用 -I 来指明:

$ gcc sin.c -lm -I /path/of/stdio.h

上面我们看到的参数 -Wall,-O 这些非必要的参数都称为 FLAGS,因为使用的是 C 语言,有时称他为 CFLAGS。这些变量会在下面的 make 中用到。

假设我们要编译一个包含 4 个源文件的程序,分别为 main.c haha.c sin_value.c cos_value.c,到这里下载。如果按照普通的做法:

$ gcc -c main.c
$ gcc -c haha.c
$ gcc -c sin_value.c
$ gcc -c cos_value.c
$ gcc -o main main.o haha.o sin_value.o cos_value.o -lm -L /usr/lib -L /lib
$ ./main

可以看到步骤比较多而且繁琐,如果将其写成 Makefile 的话:

$ cat Makefile
main: main.o haha.o sin_value.o cos_value.o
#如果 main.o,haha.o sin_value.o cos_value.o 比 main 新,或者 main 不存在,则执行下面的语句
   gcc -o main main.o haha.o sin_value.o cos_value.o -lm

clean:
   rm -f main main.o haha.o sin_value.o cos_value.o

$ make clean
rm -rf main main.o haha.o sin_value.o cos_value.o

$ make [main]
cc    -c -o main.o main.c
cc    -c -o haha.o haha.c
cc    -c -o sin_value.o sin_value.c
cc    -c -o cos_value.o cos_value.c
gcc -o main main.o haha.o sin_value.o cos_value.o -lm

这样就生成了我们需要的最终的可执行文件 main,使用 make 的好处有:

简化编译时所需要下达的命令
若在编译完成之后,修改了某个原代码文件,则 make 仅会针对被修改了的文件进行编译,其他的目标文件不会被更动
可以依照相依性来升级 (update) 运行文件

通常,Makefile 会将要执行的命令打印出来,如果不想将其打印出来,使用 @ 就可以将其抑制。

$ make clean
rm -rf main main.o haha.o sin_value.o cos_value.o

现在在 Makefile 的 clean 下面的 rm -rf 前面加上 @:
   @rm -rf main main.o haha.o sin_value.o cos_value.o

$ make clean

可以发现面有任何输出了。

当需要让命令执行在上一条命令结束上时,应将命令写在同一行,并使用分号分割:

exec1:
   cd /home
   pwd

exec2:
   cd /home;pwd

$ make exec1
cd /home
pwd
/mnt/lfs/t
$ make exec2
cd /home;pwd
/home

而如果想忽略命令执行出错的提示,可在命令前加上 -:

clean:
   -rm -rf main main.o haha.o sin_value. cos_value.o

还可以在 Makefile 中添加变量:
LIBS = -lm
OBJS = main.o haha.o sin_value.o cos_value.o
main: ${OBJS}
       gcc -o main ${OBJS} ${LIBS}
clean:
       rm -f main ${OBJS}

在 gcc 编译时,会主动读取 CFLAGS 这个环境变量,所以可以直接在 shell 中直接指定,或者在 Makefile 中指定:

$ CFLAGS="-Wall" make clean main
或者
$ cat Makefile
LIBS = -lm
OBJS = main.o haha.o sin_value.o cos_value.o
CFLAGS = -Wall
main: ${OBJS}
   gcc -o main ${OBJS} ${LIBS}
clean:
   rm -f main ${OBJS}

除了上面的自定义变量以及环境变量之外,还有预定义变量以及自动变量。

预定义变量包含了常见的编译器,汇编器等的名称。比如 AR 表示维护库的工具,默认为 ar;AS 表示汇编程序名,默认为 as;CC 表示 C 编译器,默认为 cc;CPP 表示 C 下的预编译器,默认是 $(CC) -E 了;RM 默认表示 rm -f;CFLAGS 表示 C 编译器的选项,没有默认值,需要自行添加。

由于 Makefile 中包含众多的目标文件以及依赖文件,为了进一步的简化,又引入了自动变量这一表示。下面是常见的一些自动变量:

$*        不包含扩展名的目标文件名称
$<     第一个依赖文件的名称
$@     目标文件的完整名称
$^     所有不重复的依赖文件

我们来看看如何在 Makfile 是使用自动变量,这里假设 main.o 文件依赖于 main.c 和 def.h 这两个文件:

LIBS = -lm
OBJS = main.o haha.o sin_value.o cos_value.o
CFLAGS = -Wall
main:${OBJS}
   $(CC) -o $@ $^ ${LIBS}
main.o:main.c def.h
   $(CC) $(CFLAGS) -o $@ -c $<
clean:
   rm -f main ${OBJS}

大部分的 tarball 软件之安装的命令下达方式:

./configure
创建 Makefile 这个文件,通常开发者会写一个脚本来检查你的 Linux 系统、相关的软件属性等等,这个步骤相当的重要, 因为未来你的安装信息都是这一步骤内完成的。另外,这个步骤应该要参考一下该目录下的 README 或 INSTALL 相关的文件。

make clean
make 会读取 Makefile 中关于 clean 的工作。这个步骤不一定会有,但是希望运行一下,因为他可以去除目标文件。因为谁也不确定原始码里面到底有没有包含上次编译过的目标文件 (*.o) 存在,所以当然还是清除一下比较妥当的。

make
make 会依据 Makefile 当中的默认工作进行编译。编译的工作主要是进行 gcc 来将原始码编译成为可以被运行的 object files ,但是这些 object files 通常还需要一些函数库的链接后,才能产生一个完整可执行文件。使用 make 就是要将源代码编译成可执行文件,而这个可执行文件会放置在目前所在的目录之下, 尚未被安装到预定安装的目录中。

make install
通常这就是最后的安装步骤了,make 会依据 Makefile 这个文件里面关于 install 的项目,将上一个步骤所编译完成的数据给他安装到预定的目录中,就完成安装。

最后再来看看库文件。通常,Linux 下的库文件分为两大类,分别为静态函数库,动态函数库

静态函数库特点:扩展名为 .a,通常的函数库名为 libxxx.a;这类函数库在编译的时候会直接整合到程序当中去,所以使用静态函数库编译成的文件会比较大;编译成功可以独立运行;虽然二进制可以独立 运行,但因为函数库是直接整合到运行档中, 因此如果函数库升级时,整个可执行文件必须要重新编译才能将新版的很熟库整合到程序当中。

动态特点:.so,通常的名字为 libxxx.so;跟静态函数库的方式不同,在编译的时候,动态函数库并没有整合程序中去,而只是以指针的形式。当可执行文件要使用到函数库时,程序才 会去调用函数库。由于可执行文件中仅仅是一个指向动态函数库的指针,并不包含实际的库,所以文件通常较小;当函数库升级时,不需要重新编译,因为只是一个 指向。

可以看到两者最大的区别就是程序在执行时所需的代码实在运行时动态的加载,还是在编译时静态的加载。默认情况下,gcc 在链接时优先使用动态链接库,方便升级,只有当动态链接库不存在时才会使用静态链接库。因此 -static 这个选项就是表示强制使用静态链接库。

标准系统库可在目录 /usr/lib 与 /lib 中找到。比如,在类 Unix 系统中 C 语言的数学库一般存储为文件/usr/lib/libm.a。该库中函数的原型声明在头文件 /usr/include/math.h 中。C 标准库本身存储为 /usr/lib/libc.a,它包含 ANSI/ISO C 标准指定的函数,比如‘printf’。对每一个 C 程序来说,libc.a 都默认被链接。

假使在 /home/jaseywang/lib/ 下有链接时需要使用到库文件 libfoo.so 和 libfoo.a,为了让 gcc 在链接时仅使用静态链接库:

$ gcc foo.c -L /home/jaseywang/lib -static -lfoo -o foo

总结一下,Makefile 就是一组有语法规范的规则,通过 Makefile,make 命令可以方便的进行的编译和链接,而不必一条一条命令的敲 gcc。同时 Makefile 中将经常使用的一些变量以宏(marco)的形式出现,方便编写 Makefile 的同学。

参考:
http://vbird.dic.ksu.edu.tw/linux_basic/0520source_code_and_tarball.php

  • kiki

    哟,你也Makefile了,可以cc高哥哥了…

    • http://jaseywang.info jaseywang

      跟他有什么关系~

      • kiki

        他之前感慨”难道现在学校都不教Makefile的么?…“

        • http://ibeatles.me Beetle

          被我看到了。。。

  • http://ibeatles.me Beetle

    像 clean 这样的伪目标还是建议加个 .PHONY 来声明一下

    • http://jaseywang.info jaseywang

      听煤老板教诲!

  • Pingback: Autotools | Jasey Wang()