hello.c文件
gcc -E hello.c -o hello.i
hello.i文件(不到10行的代码文件预处理完了变成800多行)
预处理就是头文件直接展开,宏定义直接替换,条件编译处理
编译就是先进行语法检查,语法没问题后将预处理后的文件转换为汇编文件
gcc -S hello.i -o hello.s
hello.s文件
gcc -c hello.s -o hello.o
hello.o文件(二进制文件,ELF开头)
gcc hello.o -o hello
该步骤将printf函数在哪的位置信息给链接进来
gcc hello.o -o hello -static
静态链接文件要比动态链接文件大很多很多
准备材料:3个文件
文件一:hello.c(我们公司自己写的hello.c)
文件二:f1.c(其他公司的机密源文件)
文件二:f2.c(其他公司的机密源文件)
其他公司不能把机密源文件给我们,因此要用库的形式给我们
step1:将f1.c、f2.c这两个源文件制作成二进制文件
gcc -c f1.c f2.c
step2:制作库文件
ar -crv libx.a f1.o f2.o
lib开头,.a是静态库文件后缀,x是自己命名的库名字
libx.a就是我们制作的静态库文件
若我们直接编译hello.c文件,则会跳出错误,找不到f1,f2函数:
正确做法:
gcc hello.c -o hello -static -L . -l x
编译并且运行:
-L表示库所在路径,这里是在当前路径(.),-l表示库名,这里是x
准备材料:3个文件
制作:
gcc -fPIC -shared -o libxx.so f1.c f2.c
生成动态库文件libxx.so
lib开头,.so是动态库文件后缀,xx是自己命名的库名字
编译并运行:
gcc hello.c -o hello -L . -l xx
运行报错,因为它去找这个动态库文件是从默认路径去找
查看它去找的默认动态库路径ldd
发现它去/lib/x86_64-linux-gun/下去找这个动态库文件了
所以我们把这个动态库文件拷贝到那个路径下再运行就可以了
cp libxx.so /lib/x86_64-linux-gun/
代码解耦,重用
对外提供服务,典型应用:exe调用dll。我们打开软件目录,一堆dll,少量exe就是这个道理
静态库:与exe打包成exe(一个文件),windows就是.lib文件,linux就是.a文件。
动态库(共享库):与exe是独立的2个文件,windows就是.dll文件,linux就是.so文件。
wiki解释:Ldd(List Dynamic Dependencies,意译为列出动态库依赖关系)是一款在类Unix系统的实用工具,负责在命令行内输出程序或共享库所依赖的函数库。此工具由罗兰·麦克格拉斯及乌尔里希·德雷佩尔开发。Ldd在指定的程序缺少部分函数库的情况下将无法显示结果。
————————————————
ldd显示可执行模块的dependency的工作原理,其实质是通过ld-linux.so(elf动态库的装载器)来实现的。我们知道,ld-linux.so模块会先于executable模块程序工作,并获得控制权,因此当上述的那些环境变量被设置时,ld-linux.so选择了显示可执行模块的dependency。实际上可以直接执行ld-linux.so模块,如:/lib/ld-linux.so.2 --list program(这相当于ldd program)
which:which查找文件与find方式不同,which从环境变量文件(/etc/profile)中的path目录中查找,且which找的都是可执行文件。
whereis:与which类似查询可执行文件,whereis查询更多,会查找出相关的man文件
locate:这是区别以上两种的方式的一种查找文件方式,locate以一个数据库文件为基础查找。linux系统每天至少自动扫描一次文件,将结果保存到数据库,locate查的是数据库记录。locate查询比find要快,但最新文件如果在数据库中没有记录,会查询不到,次是需要手动更新(updatedb)
下面通过一个实际的例子来测试和体会几个命令的差异:
先通过which找到ls命令的位置
tarena@tedu:/$ which ls
/bin/ls
把ls复制到主目录,并把名称修改为newls
tarena@tedu:/$ cp /bin/ls ~/newls
tarena@tedu:/$ cd ~
尝试用which和whereis命令查找newls,由于主目录不在 P A T H 中(除非你恰巧之前你恰巧把~加入 PATH中(除非你恰巧之前你恰巧把~加入 PATH中(除非你恰巧之前你恰巧把~加入PATH了),所以都无法找到
tarena@tedu:~$ whereis newls
newls:
tarena@tedu:~$ which newls
tarena@tedu:~$
执行以下export命令,把~加入$PATH,然后我们cd到根目录,再次尝试查找newls,发现已经可以找到了
tarena@tedu:~$ export PATH=$PATH:~
tarena@tedu:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/tarena
tarena@tedu:~$ cd /
tarena@tedu:/$ which newls
/home/tarena/newls
tarena@tedu:/$ whereis newls
newls: /home/tarena/newls
我们再cd到~,然后取消newls的可执行权限
tarena@tedu:/$ cd ~
tarena@tedu:~$ chmod u-x newls
然后我们再次尝试使用which和whereis查找newls,我们发现whereis可以找到,而which找不到newls。因为which只能用来查找可执行文件,whereis没有该限制。
tarena@tedu:~$ cd /
tarena@tedu:/$ whereis newls
newls: /home/tarena/newls
tarena@tedu:/$ which newls
这时我们再把newls改名为ls,然后我们尝试用locate命令找出系统中存在的两个ls文件,我们发现会找到大量不是我们要的文件(此处已省略了很多),但这些文件路径中确实包含ls。
tarena@tedu:~$ cd ~
tarena@tedu:~$ mv newls ls
/bin/false
/bin/ls
/bin/lsblk
/bin/lsmod
/bin/ntfsls
/boot/grub/i386-pc/cbls.mod
/boot/grub/i386-pc/command.lst
/boot/grub/i386-pc/crypto.lst
/boot/grub/i386-pc/fs.lst
/boot/grub/i386-pc/ls.mod
/boot/grub/i386-pc/lsacpi.mod
/boot/grub/i386-pc/lsapm.mod
/boot/grub/i386-pc/lsmmap.mod
/boot/grub/i386-pc/lspci.mod
/boot/grub/i386-pc/moddep.lst
/boot/grub/i386-pc/partmap.lst
/boot/grub/i386-pc/parttool.lst
/boot/grub/i386-pc/terminal.lst
/boot/grub/i386-pc/video.lst
...
我们尝试用正则表达式缩小匹配范围
tarena@tedu:~$ locate -r '\bls$'
/bin/ls
/usr/bin/gvfs-ls
/usr/lib/klibc/bin/ls
/usr/share/bash-completion/completions/gvfs-ls
我们发现只找到了一个ls,另外一个可能因为系统还没有纳入索引数据库,所以没有找到,我们执行updatedb命令,强制更新一下系统索引,然后再执行一遍locate试试,发现现在可以找到了
tarena@tedu:~$ sudo updatedb
/bin/ls
/home/tarena/ls
/usr/bin/gvfs-ls
/usr/lib/klibc/bin/ls
/usr/share/bash-completion/completions/gvfs-ls
find命令全盘查找太慢,所以限制下查找路径,也是同样可以找到
tarena@tedu:~$ find ~ /bin/ -name ls
/home/tarena/ls
/bin/ls
1.简介:
ldconfig 命令用于在默认搜寻目录 /lib 和 /usr/lib 以及动态库配置文件 /etc/ld.so.conf 内所列的目录下,搜索出可共享的动态链接库(格式如 lib.so),进而创建出动态链接器(ld.so 或 ld-linux.so)所需的缓存文件。缓存文件默认为 /etc/ld.so.cache,此文件保存已排好序的动态链接库名字列表。为了让动态链接库为系统所共享,需运行动态链接库的管理命令 ldconfig 更新动态链接库的缓存文件。**
ldconfig 位于 /sbin 目录下,通常在系统启动时运行,当用户安装了一个新的动态链接库时,需要手动运行这个命令。
2.常用示例:
3.注意事项:
Linux 系统上有两类不同的可执行程序。第一类是静态链接的可执行程序。静态可执行程序包含执行所需的所有函数和数据 — 换句话说,它们是“完整的”。因为这一原因,静态可执行程序不依赖任何外部库就可以运行。
第二类是动态链接的可执行程序,这意味着它需要依赖外部库才可以运行。
动态链接依赖查看
我们可以用 ldd 命令来确定某一特定可执行程序是否为动态链接,如果是动态链接,还会打印出所依赖的动态库。
victory@ubuntu:~$ ldd `which ls`
linux-vdso.so.1 (0x00007ffc63f5c000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f4521458000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4521230000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f4521198000)
/lib64/ld-linux-x86-64.so.2 (0x00007f45214c8000)
注意:ldd后面必须跟程序的绝对路径。
我们自己来编译一个静态链接和动态链接的C程序:
victory@ubuntu:~$ cat main.c
int main() {
return 0;
}
gcc编译默认是动态链接:
victory@ubuntu:~$ gcc main.c
victory@ubuntu:~$ ldd a.out
linux-vdso.so.1 (0x00007ffc3adfc000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbbd0740000)
/lib64/ld-linux-x86-64.so.2 (0x00007fbbd0988000)
通过–static选项可以静态编译:
victory@ubuntu:~$ gcc main.c --static -o a.out.static
victory@ubuntu:~$
victory@ubuntu:~$ ldd a.out.static
not a dynamic executable
也可以通过file命令查看程序类型:
victory@ubuntu:~$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=d986d2fd8d2dce338f289b4c07dc0753e64c296d, for GNU/Linux 3.2.0, not stripped
看下动态和静态链接的程序大小:
victory@ubuntu:~$ ls -al a.out* -h
-rwxrwxr-x 1 victory victory 16K Feb 5 22:45 a.out
-rwxrwxr-x 1 victory victory 869K Feb 5 22:46 a.out.static
如您所见,a.out.static 的大小超过动态链接 a.out 十倍以上。
动态装载器
如果动态可执行程序不包含运行所需的所有函数和数据,Linux 的哪部分负责将这些程序和所有必需的共享库一起装入,以使它们能正确执行呢?
答案是动态装入器(dynamic loader),它实际上是您在a.out 的 ldd 清单中看到的作为共享库依赖出的 ld-linux.so.2 库。动态装入器负责装入动态链接的可执行程序运行所需的共享库。现在,让我们迅速查看一下动态装入器如何在系统上找到适当的共享库。
动态装载器找到共享库要依靠两个文件 — /etc/ld.so.conf 和 /etc/ld.so.cache。如果您对 /etc/ld.so.conf 文件进行 cat 操作,您可能会看到一个与下面类似的清单:
$ cat /etc/ld.so.conf
/usr/X11R6/lib
/usr/lib/gcc-lib/i686-pc-linux-gnu/2.95.3
/usr/lib/mozilla
/usr/lib/qt-x11-2.3.1/lib
/usr/local/lib
ld.so.conf 文件包含一个所有目录(/lib 和 /usr/lib 除外,它们会自动包含在其中)的清单,动态装入器将在其中查找共享库。
在我的ubuntu系统上,ld.so.conf文件并没有直接包含库目录,而是include了/etc/ld.so.conf.d目录下以conf为后缀的文件:
victory@ubuntu:~$ cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf
victory@ubuntu:~$ ls /etc/ld.so.conf.d/
libc.conf x86_64-linux-gnu.conf
victory@ubuntu:~$
victory@ubuntu:~$ cat /etc/ld.so.conf.d/libc.conf
# libc default configuration
/usr/local/lib
victory@ubuntu:~$ cat /etc/ld.so.conf.d/x86_64-linux-gnu.conf
# Multiarch support
/usr/local/lib/x86_64-linux-gnu
/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu
上面的路径清单是系统自己添加的,也就是说只要我们把自己的动态库放到上述路径下,动态装载器就能找到。
ld.so.cache
但是在动态装入器能“看到”这一信息之前,必须将它转换到 ld.so.cache 文件中。可以通过运行 ldconfig 命令做到这一点:
$ ldconfig
当 ldconfig 操作结束时,将会生成一个新的 /etc/ld.so.cache 文件,它反映您对 /etc/ld.so.conf 所做的更改。从这一刻起,动态装入器在寻找共享库时会查看您在 /etc/ld.so.conf 中指定的所有新目录。
因此当安装完一些库文件,或者修改/etc/ld.so.conf增加了库的新的搜索路径,需要运行一下ldconfig,使所有的库文件都被缓存到文件/etc/ld.so.cache中,如果没做,可能会找不到刚安装的库。
ldconfig 技巧
-p选项可以查看ld.so.cache缓存的所有共享库:
victory@ubuntu:~$ ldconfig -p
1064 libs found in cache `/etc/ld.so.cache'
libzvbi.so.0 (libc6,x86-64) => /lib/x86_64-linux-gnu/libzvbi.so.0
libzvbi-chains.so.0 (libc6,x86-64) => /lib/x86_64-linux-gnu/libzvbi-chains.so.0
libzstd.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libzstd.so.1
libzmq.so.5 (libc6,x86-64) => /lib/x86_64-linux-gnu/libzmq.so.5
...
还有另一个方便的技巧可以用来配置共享库路径。有时候您希望告诉动态装入器在尝试任何 /etc/ld.so.conf 路径之前先尝试使用特定目录中的共享库。在您运行的较旧的应用程序不能与当前安装的库版本一起工作的情况下,这会比较方便。
LD_LIBRARY_PATH
要指示动态装载器首先检查某些目录,请将 LD_LIBRARY_PATH 变量设置成您希望搜索的目录。多个路径之间用冒号分隔;例如:
$ export LD_LIBRARY_PATH="/usr/lib/old:/opt/lib"
导出 LD_LIBRARY_PATH 后,如有可能,所有从当前 shell 启动的可执行程序都将使用 /usr/lib/old 或 /opt/lib 中的库,如果仍不能满足一些共享库相关性要求,则转回到 /etc/ld.so.conf 中指定的库。
交互式模式就是shell等待你的输入,并且执行你提交的命令。这种模式被称作交互式是因为shell与用户进行交互。这种模式也是大多数用户非常熟悉的:登录、执行一些命令、签退。当你签退后,shell也终止了。
shell也可以运行在另外一种模式:非交互式模式。在这种模式下,shell不与你进行交互,而是读取存放在文件中的命令,并且执行它们。当它读到文件的结尾,shell也就终止了。
/etc/profile 此文件为系统的每个用户设置环境信息,当第一个用户登录时,该文件被执行. 并从/etc/profile.d目录的配置文件中搜集shell的设置.
/etc/bashrc 为每一个运行bash shell的用户执行此文件.当bash shell被打开时,该文件被读取。有些linux版本中的/etc目录下已经没有了bashrc文件。
~/. profile每个用户都可使用该文件输入专用于自己使用的shell信息,当用户登录时,该文件仅仅执行一次!默认情况下,它设置一些环境变量,然后执行用户的.bashrc文件.
~/.bashrc 该文件包含专用于某个用户的bash shell的bash信息,当该用户登录时以及每次打开新的shell时,该文件被读取.
另外,/etc/profile中设定的变量(全局)的可以作用于任何用户,而~/.bashrc等中设定的变量(局部)只能继承/etc/profile中的变量,他们是"父子"关系.
如想将一个路径加入到$PATH中,可以像下面这样做:
控制台中,不赞成使用这种方法,因为换个shell,你的设置就无效了,因此这种方法仅仅是临时使用,以后要使用的时候又要重新设置,比较麻烦。
这个只针对特定的shell;
$ PATH=“$PATH:/my_new_path” (关闭shell,会还原PATH)
修改/etc/profile文件,如果你的计算机仅仅作为开发使用时推荐使用这种方法,因为所有用户的shell都有权使用这些环境变量,可能会给系统带来安全性问题。 这里是针对所有的用户的,所有的shell;
$ vi /etc/profile
在里面加入:
export PATH=“$PATH:/my_new_path”
$ vi /root/.bashrc
在里面加入:
export PATH=“$PATH:/my_new_path”
后两种方法一般需要重新注销系统才能生效,最后可以通过echo命令测试一下:
$ echo $PATH
输出已经是新路径了。
本文讨论了一个重要的 Linux shell 环境变量,称为PATH,以及如何向该变量添加目录。
通常,您在 Linux 系统上运行的大多数程序都可以完美运行,并且您的 shell 在执行时不会出现任何错误。但是,有时您可能希望从 Linux 系统上的非标准目录快速运行自定义脚本或程序。
在这种情况下,您需要将它们的源目录添加到 shell 的 PATH 变量中,以便它知道在哪里可以找到它们。
路径环境变量用于什么?
环境变量控制 shell 的行为。如果您曾经 在 Linux 上使用过命令行,系统将依赖 PATH 变量来查找您正在输入的命令的位置。
**PATH 是 Linux 中的一个内置环境变量,它告诉 shell 在哪些目录中搜索可执行文件以响应用户或应用程序发出的命令。**它包含以冒号分隔的目录列表,用于查找您输入的命令。
如何在 Linux 中将目录添加到 PATH
要立即查看 PATH 中的内容,请在终端中输入:
linuxmi@linuxmi:~/www.linuxmi.com$ echo $PATH
如您所见,此变量中的目录由冒号 ( :
) 分隔。目前,shell 在以下目录中搜索二进制可执行文件:
搜索 PATH 的顺序也很重要。因此,如果您有两个共享相同名称的可执行文件位于两个不同的目录中,则 shell 将运行位于 PATH 中第一个目录中的文件。
现在假设您已经创建了一个位于/srv/scripts
目录中的自定义 shell 脚本,并且您希望将此目录添加到您的 PATH 变量中。这是你应该做的:
linuxmi@linuxmi:~/www.linuxmi.com$ export PATH=$PATH:/srv/scripts
为了让事情更清楚,让我们分解一下这个语法。以下是每个参数的含义:
**export**
命令会将修改后的**PATH**
变量导出到 shell 子进程环境。**$**
意思是你指的是它的值。**:/srv/scripts**
部分指定**:**
符号后面的内容应附加到当前 PATH 变量中包含的值。让我们看看 PATH 现在的样子:
linuxmi@linuxmi:~/www.linuxmi.com$ echo $PATH
正如您在上面的输出中看到的,该 /srv/scripts
目录被添加到 PATH 变量的末尾。现在,您存储在 /srv/scripts
目录中的文件可以在任何地方执行,而无需指定它们的完整路径。
此外,如果您认为您的目录应该在其他所有内容之前被搜索,您可以将其添加到$PATH
。
export PATH=/srv/scripts:$PATH
请务必注意,如果您退出终端或从系统注销,PATH 将恢复并且更改将丢失,因为这种设置 PATH 的方法仅在当前终端会话期间临时记住更改。
要使其永久化,请查看以下部分。
要使更改永久生效,您需要在 shell 配置文件中定义 PATH 变量。
默认系统范围的 PATH 值在 /etc/profile
文件中指定。将目录添加到单个用户的路径的最佳位置是修改该用户的.bashrc
文件。
使用文本编辑器打开文件,滚动到文件底部,并在文件末尾添加以下行:
linuxmi@linuxmi:~/www.linuxmi.com$ vim ~/.bashrc
~/.bashrc
export PATH=$PATH:/srv/scripts
source
最后,保存文件并使用以下命令将新的 PATH 加载到当前 shell 会话中:
linuxmi@linuxmi:~/www.linuxmi.com$ source ~/.bashrc
要确认目录已成功添加,请键入以下 echo
命令检查路径:
linuxmi@linuxmi:~/www.linuxmi.com$ echo $PATH
结论
在Linux中向您的用户或全局 PATH 变量添加新目录非常简单。在本文中,我们了解到有两种方法可以做到这一点:暂时的和永久的。
make是一个 构建工具
,主要用于C/C++项目。
Makefile是一个编译脚本,使用 make
工具解释Makefile(makefile)文件中的指令(编译指令)进行我们的项目编译。
在Linux环境下进行开发,工程源文件较少时,直接使用gcc进行编译。源文件较多及工程复杂时,就可以使用 Makefile(makefile)
来对我们的工程进行管理,然后使用 make
工具解释Makefile(makefile)文件中的指令(编译指令)进行我们的项目编译。即借助Makefile可以做到 自动化编译
。
1、基础实例
万年不变helloworld,使用make编译hello.c。对应的Makefile文件:
hello:hello.c
gcc hello.c -o hello
2、开源项目实例
之前我们在实践分享 | 基于framebuffer的lvgl的移植使用中也是使用Makefile来管理工程:
#
# Makefile
#
CC ?= gcc
LVGL_DIR_NAME ?= lvgl
LVGL_DIR ?= ${shell pwd}
CFLAGS ?= -O3 -g0 -I$(LVGL_DIR)/ -Wall -Wshadow -Wundef -Wmaybe-uninitialized -Wmissing-prototypes -Wno-discarded-qualifiers -Wall -Wextra -Wno-unused-function -Wundef -Wno-error=strict-prototypes -Wpointer-arith -fno-strict-aliasing -Wno-error=cpp -Wuninitialized -Wmaybe-uninitialized -Wno-unused-parameter -Wno-missing-field-initializers -Wtype-limits -Wsizeof-pointer-memaccess -Wno-format-nonliteral -Wno-cast-qual -Wunreachable-code -Wno-switch-default -Wno-switch-enum -Wreturn-type -Wmultichar -Wformat-security -Wno-ignored-qualifiers -Wno-error=pedantic -Wno-sign-compare -Wno-error=missing-prototypes -Wdouble-promotion -Wclobbered -Wdeprecated -Wempty-body -Wtype-limits -Wshift-negative-value -Wstack-usage=1024 -Wno-unused-value -Wno-unused-parameter -Wno-missing-field-initializers -Wuninitialized -Wmaybe-uninitialized -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -Wtype-limits -Wsizeof-pointer-memaccess -Wno-format-nonliteral -Wpointer-arith -Wno-cast-qual -Wmissing-prototypes -Wunreachable-code -Wno-switch-default -Wswitch-enum -Wreturn-type -Wmultichar -Wno-discarded-qualifiers -Wformat-security -Wno-ignored-qualifiers -Wno-sign-compare
LDFLAGS ?= -lm
BIN = demo
#Collect the files to compile
MAINSRC = ./main.c
include $(LVGL_DIR)/lvgl/lvgl.mk
include $(LVGL_DIR)/lv_drivers/lv_drivers.mk
include $(LVGL_DIR)/lv_examples/lv_examples.mk
OBJEXT ?= .o
AOBJS = $(ASRCS:.S=$(OBJEXT))
COBJS = $(CSRCS:.c=$(OBJEXT))
MAINOBJ = $(MAINSRC:.c=$(OBJEXT))
SRCS = $(ASRCS) $(CSRCS) $(MAINSRC)
OBJS = $(AOBJS) $(COBJS)
## MAINOBJ -> OBJFILES
all: default
%.o: %.c
@$(CC) $(CFLAGS) -c $< -o $@
@echo "CC $<"
default: $(AOBJS) $(COBJS) $(MAINOBJ)
$(CC) -o $(BIN) $(MAINOBJ) $(AOBJS) $(COBJS) $(LDFLAGS)
clean:
rm -f $(BIN) $(AOBJS) $(COBJS) $(MAINOBJ)
执行一个make命令很方便地就生成了可执行程序demo。
前面分享了两个Makefile实例,大概知道Makefile里面都有些什么内容了。我们学习编程语言的时候,最开始都是先学习一些基本的概念及语法,同样的,对于Makefile,我们想要看懂Makefile文件或者要自己能修改、写出Makefile文件,自然也要学习一些Makefile的基本语法。
Makefile的内容很多,这里我们只专注于一些常用的Makefile知识点,基础知识掌握得牢固之后,其它一些冷门的、高级的用法我们在实践中遇到再去查资料学习也不迟。
就比如我们学习编程语言,有了基础之后,就可以去写代码实践了,而不是把厚厚的一本参考书的所有知识点都啃完之后采取动手,一下子输入太多知识而没有及时去实践巩固,就很容易忘记。
本文分享Makefile的如下几点常用知识:
Makefile文件中最重要的是规则。基本规则的格式如下:
# 基本规则
target:prerequisites
command
其中,target
为目标,prerequisites
为依赖。command
为make需要执行的命令。
Makefile文件使用#进行注释。
,因为make是用过来识别命令行的,进而完成相应的动作。对照着我们上面举例的两个Makefile文件,看看是不是都遵循着这样的规则。
Makefile中的变量与编程语言中的变量的概念有点不一样,跟C语言的宏倒是有些类似,用于记录一些信息,在Makefile被解析执行时,变量调用的地方就可以使用这些信息。特别是多处要用到同样的信息、用到较长的信息时,定义变量就很有优势。
定义变量有赋值的过程,Makefile提供了四种赋值方式,如:
# 直接给变量赋值。
VAR = xxx
# 是在该变量没有被赋值的情况下为其赋值。
VAR ?= xxx
# 将":="右边中包含的变量直接展开给左边的变量赋值。
VAR := xxx
# 追加赋值,可以往变量后面增加新的内容。
VAR += xxx
定义及使用变量:
# 定义变量
VAR = xxx
# 使用变量
$(VAR)
对于以上四种赋值方式,可以看个网上易懂的例子:
例子:
a = 1
b = 2
c := $(a) 3
d = 4
d ?= 5
b += 6
结果:
a=1
c=1 3
d=4
b=2 6
Make命令提供一系列内置变量,常用的有:
变量名 | 说明 |
---|---|
CURDIR | 当前路径 |
CC | C语言编译器的名称 |
CPP | C语言预处理器的名称 |
CXX | C++语言的编译器名称 |
RM | 删除文件程序的名称 |
CFLAGS | C语言编译器的编译选项,无默认值 |
CPPFLAGS | C语言预处理器的编译选项,无默认值 |
CXXFLAGS | C++语言编译器的编译选项,无默认值 |
更多内置变量,可查阅:
https://www.gnu.org/software/make/manual/html_node/Implicit-Variables.html
Makefile中,分支判断有如下几种情况:
使用方法类似C语言中的if用法。格式如:
ifeq (ARG1, ARG2)
...
else
...
endif
例如根据不同的CPU架构选择不同的编译器:
ARCH ?= x86
ifeq ($(ARCH),x86)
CC = gcc
else
CC = arm-linux-gnueabihf-gcc
endif
hello:hello.c
$(CC) hello.c -o hello
@echo $(CC)
在echo前面加上echo可以关闭回显。
上面这一段Makefile中的变量arch的值默认为x86,我们也可以在执行make命令时指定变量的值,就可以在不修改Makefile文件的情况下灵活地切换编译器,如:
make ARCH=x86
make ARCH=arm
有些工程用,各模块有自己的一个Makefile文件,提供给工程总的Makefile文件使用。总的Makfile文件可以使用关键字 include
包含其它Makefile文件,格式如:
include
例如上面实例中的:
include $(LVGL_DIR)/lvgl/lvgl.mk
include $(LVGL_DIR)/lv_drivers/lv_drivers.mk
include $(LVGL_DIR)/lv_examples/lv_examples.mk
显式规则
。显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。如
hello:hello.o
gcc hello.o -o hello
这就是一条显示规则。工程较简单时,常常使用显示规则来编写Makefile。但工程的结构比较复杂时,Makefile文件中常常会穿插着一些隐式规则来简化Makefile。下面看看是什么时隐式规则。
隐式规则
。隐含规则是系统或用户预先定义好的一些特殊规则,主要是一些常用的依赖关系和更新命令。隐含规则中出现的目标文件和依赖文件都只使用文件的扩展名。
如果Makefile 文件里面没有显式给出文件的依赖关系的时候,make 就会根据文件的扩展名找到相应的隐含规则,然后按照隐含规则来更新目标。隐式规则的例子如:
hello:hello.o
$(CC) $^ -o $@
其中,$@
代表了目标文件 ,$<
代表了所有依赖文件。其中, $@
与 $<
也称作系统的 自动化变量
。类似的还有如下几个常用的自动化变量:
Makefile 文件中的目标分为两类:实目标和伪目标。
实目标是真正要生成的以文件形式存放在磁盘上的目标。如:
hello:hello.o
$(CC) $^ -o $@
其中,hello
文件就是实目标。
伪目标不要求生成实际的文件,它主要是用于完成一些辅助操作。如:
clean:
rm -rf hello.o hello
其中的 clean
就是一个伪目标。我们在命令里面输入命令:make clean
就可以执行删除操作:
rm -rf hello.o hello
但是这种书写形式不是很严谨,因为可能在当前目录下面存在文件名为 clean 的文件,因为这时候, 后面没有依赖文件,所以make 就认为这个文件是最新的,所以就不会执行 rm -rf hello.o hello
。所以为了避免这种情况的发生,Makefile使用**.PHONY
** 来区分伪目标,使用如:
.PHONY:clean
clean:
rm -rf hello.o hello
使用 .PHONY
说明clean是一个伪目标。
类似 .PHONY
这样的特殊的内置目标名称还有很多,可查阅:
https://www.gnu.org/software/make/manual/html_node/Special-Targets.html#Special-Targets
Makefile中有很多使用的内置函数,借助这些函数可以使我们的Makefile更为简洁。Makefile函数的调用方式与使用变量的方式类似,如:
$(函数名 参数)
或者:
${函数名 参数}
下面介绍一些常用的函数:
wildcard函数用于获取文件列表,并使用空格分隔开。语法如:
$(wildcard 匹配规则)
例如我们有如下工程:
我们可以使用wildcard函数获取src文件夹下的文件:
SRC_FILES = $(wildcard src/*.c)
target1:
@echo $(SRC_FILES)
patsubst函数功能为模式字符串替换。语法如:
$(patsubst 匹配规则, 替换规则, 输入的字符串)
patsubst函数看起来貌似有点复杂,不做过多解释,看个例子就知道什么意思了。还是使用上面的例子:
OBJ_FILES = $(patsubst %.c, %.o, hello.c)
target1:
@echo $(OBJ_FILES)
我们输入的hello.c符合匹配规则**%.c
,所以按照替换规则%.o
**进行替换得到hello.o。
%是一个通配符,用于匹配任意个字符。
subst函数功能为字符串替换。语法如:
$(subst 源字符串, 目标字符串, 输入的字符串)
例子:
INPUT_STR = hello world
OUTPUT_STR = $(subst hello, HELLO, $(INPUT_STR))
target1:
@echo $(OUTPUT_STR)
subst函数把输入字符串 hello world
中的源字符串hello替换成目标字符串HELLO。
notdir函数用于去除文件路径中的目录部分。语法如:
$(notdir 文件名)
使用上面的例子来演示:
SRC_FILES = $(notdir src/src1.c)
target1:
@echo $(SRC_FILES)
notdir函数去掉了src/src1.c前面的路径src/,输出src1.c。这里只是输出了一个文件,我们可以与上面的wildcard函数结合使用输出多个文件,如:
SRC = $(wildcard src/*.c)
SRC_FILES = $(notdir $(SRC))
target1:
@echo $(SRC_FILES)
先是使用wildcard函数获取带有路径的文件列表,再使用notdir函数去掉前面的路径。
更多内置函数可查阅:
https://www.gnu.org/software/make/manual/html_node/Functions.html
根据我们上面学习的知识,我们可以来练习给如下工程编写一个Makefile文件:
Makefile:
# 默认架构
ARCH ?= x86
# 源文件目录
SRCS_DIR = src
# 头文件目录
INCS_DIR = inc
# 编译输出目录
BUILD_DIR = build
# 源文件
SRCS = $(wildcard $(SRCS_DIR)/*.c)
# 头文件
INCS = $(wildcard $(INCS_DIR)/*.h)
# 目标文件
OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(notdir $(SRCS)))
# C编译选项,显示警告
CFLAGS = -Wall
# 目标APP
TARGET_APP = hello_makefile
# 根据ARCH选择编译器
ifeq ($(ARCH), x86)
CC = gcc
else
CC = arm-linux-gnueabihf-gcc
endif
# 链接
$(BUILD_DIR)/$(TARGET_APP):$(OBJS)
$(CC) -o $@ $^ -I$(INCS_DIR) $(CFLAGS)
# 编译
$(BUILD_DIR)/%.o:$(SRCS_DIR)/%.c $(INCS)
@mkdir -p $(BUILD_DIR)
$(CC) -c -o $@ $< -I$(INCS_DIR) $(CFLAGS)
# 清除
.PHTHY:clean
clean:
rm -rf $(BUILD_DIR)
以上就是本次的分享,学习了这些Makefile基础知识之后,我们就可以看懂很多工程的Makefile文件。
VERSION =1.00
CC =gcc
DEBUG =-DUSE_DEBUG
CFLAGS =-Wall
SOURCES =$(wildcard ./source/*.c)
INCLUDES =-I./include
LIB_NAMES =-lfun_a -lfun_so
LIB_PATH =-L./lib
OBJ =$(patsubst %.c, %.o, $(SOURCES))
TARGET =app
#links
$(TARGET):$(OBJ)
@mkdir -p output
$(CC) $(OBJ) $(LIB_PATH) $(LIB_NAMES) -o output/$(TARGET)$(VERSION)
@rm -rf $(OBJ)
#compile
%.o: %.c
$(CC) $(INCLUDES) $(DEBUG) -c $(CFLAGS) $< -o $@
.PHONY:clean
clean:
@echo "Remove linked and compiled files......"
rm -rf $(OBJ) $(TARGET) output
【要点说明】
【1】程序版本
开发调试过程可能产生多个程序版本,可以在目标文件后(前)增加版本号标识。
VERSION = 1.00
$(CC) $(OBJ) $(LIB_PATH) $(LIB_NAMES) -o output/$(TARGET)$(VERSION)
【2】编译器选择
Linux下为gcc/g++;arm下为arm-linux-gcc;不同CPU厂商提供的定制交叉编译器名称可能不同,如Hisilicon“arm-hisiv300-linux-gcc”。
CC = gcc
【3】宏定义
开发过程,特殊代码一般增加宏条件来选择是否编译,如调试打印输出代码。-D是标识,后面接着的是“宏”。
DEBUG =-DUSE_DEBUG
【4】编译选项
可以指定编译条件,如显示警告(-Wall),优化等级(-O)。
CFLAGS =-Wall -O
【5】源文件
指定源文件目的路径,利用“wildcard”获取路径下所有依赖源文件。
SOURCES =$(wildcard ./source/*.c)
【6】头文件
包含依赖的头文件,包括源码文件和库文件的头文件。
INCLUDES =-I./include
【7】库文件名称
指定库文件名称,库文件有固定格式,静态库为libxxx.a;动态库为libxxx.so,指定库文件名称只需写“xxx”部分,
LIB_NAMES =-lfun_a -lfun_so
【8】库文件路径
指定依赖库文件的存放路径。注意如果引用的是动态库,动态库也许拷贝到“/lib”或者“/usr/lib”目录下,执行应用程序时,系统默认在该文件下索引动态库。
LIB_PATH =-L./lib
【9】目标文件
调用“patsubst”将源文件(.c)编译为目标文件(.o)。
OBJ =$(patsubst %.c, %.o, $(SOURCES))
【10】执行文件
执行文件名称
TARGET =app
【11】编译
%.o: %.c
$(CC) $(INCLUDES) $(DEBUG) $(CFLAGS) $< -o $@
【12】链接
可创建一个“output”文件夹存放目标执行文件。链接完输出目标执行文件,可以删除编译产生的临时文件(.o)。
$(TARGET):$(OBJ)
@mkdir -p output
$(CC) $(OBJ) $(LIB_PATH) $(LIB_NAMES) -o output/$(TARGET).$(VERSION)
@rm -rf $(OBJ)
【13】清除编译信息
执行“make clean”清除编译产生的临时文件。
.PHONY:clean
clean:
@echo "Remove linked and compiled files......"
rm -rf $(OBJ) $(TARGET) output
VERSION =
CC =gcc
DEBUG =
CFLAGS =-Wall
AR =ar
ARFLAGS =rv
SOURCES =$(wildcard *.c)
INCLUDES =-I.
LIB_NAMES =
LIB_PATH =
OBJ =$(patsubst %.c, %.o, $(SOURCES))
TARGET =libfun_a
#link
$(TARGET):$(OBJ)
@mkdir -p output
$(AR) $(ARFLAGS) output/$(TARGET)$(VERSION).a $(OBJ)
@rm -rf $(OBJ)
#compile
%.o: %.c
$(CC) $(INCLUDES) $(DEBUG) -c $(CFLAGS) $< -o $@
.PHONY:clean
clean:
@echo "Remove linked and compiled files......"
rm -rf $(OBJ) $(TARGET) output
【要点说明】
基本格式与“编译可执行Makefile”一致,不同点包括以下。
【1】使用到“ar”命令将目标文件(.o)链接成静态库文件(.a)。静态库文件固定命名格式为:libxxx.a。
VERSION =
CC =gcc
DEBUG =
CFLAGS =-fPIC -shared
LFLAGS =-fPIC -shared
SOURCES =$(wildcard *.c)
INCLUDES =-I.
LIB_NAMES =
LIB_PATH =
OBJ =$(patsubst %.c, %.o, $(SOURCES))
TARGET =libfun_so
#link
$(TARGET):$(OBJ)
@mkdir -p output
$(CC) $(OBJ) $(LIB_PATH) $(LIB_NAMES) $(LFLAGS) -o output/$(TARGET)$(VERSION).so
@rm -rf $(OBJ)
#compile
%.o: %.c
$(CC) $(INCLUDES) $(DEBUG) -c $(CFLAGS) $< -o $@
.PHONY:clean
clean:
@echo "Remove linked and compiled files......"
rm -rf $(OBJ) $(TARGET) output
【要点说明】
基本格式与“编译可执行Makefile”一致,不同点包括以下。
【1】编译选项和链接选项增加“-fPIC -shared ”选项。动态库文件固定命名格式为libxxx.so。
编写测试例程,文件存放目录结构如下,头文件存放在“include”目录,库文件存放在“lib”目录,源文件存放在“source”目录,Makefile在当前目录下。
源码1:
/*头文件*/
#ifndef _FUN0_H_
#define _FUN0_H_
#endif
extern void fun0_printf(void);
extern void fun1_printf(void);
/*源文件*/
#include
#include "fun0.h"
void fun0_printf(void)
{
printf("Call \'fun0\'. \r\n");
}
源码2:
/*头文件*/
#ifndef _FUN1_H_
#define _FUN1_H_
#endif
extern void fun1_printf(void);
/*源文件*/
#include
#include "fun1.h"
void fun1_printf(void)
{
printf("Call \'fun1\'.\r\n");
}
主函数源码:
/*源文件*/
#include
#include "fun0.h"
#include "fun1.h"
#include "fun_lib_a.h"
#include "fun_lib_so.h"
int main(void)
{
#ifdef USE_DEBUG
printf("Debug Application startup.\r\n");
#endif
fun0_printf();
fun1_printf();
fun_lib_a_printf();
fun_lib_so_printf();
return 0;
}
库文件,“./lib”目录下存放两个库文件,一个静态库libfun_a.a,一个动态库libfun_so.so。
Makefile文件即为“2.1节”的Makefile模板。
测试运行:
【如果执行文件提示无“libfun_so.so”,则需拷贝“libfun_so.so”到根目录下的“/lib”或者“/usr/lib”目录下,因为系统执行程序,默认从该路径引脚动态库】
编写测试例程,生产的库文件即为“3.1节”调用的库文件(libfun_a.a)。文件存放目录结构如下:
源文件:
/*头文件*/
#ifndef _FUN_LIB_A_H_
#define _FUN_LIB_A_H_
#endif
extern void fun_lib_a_printf(void);
/*源文件*/
#include
#include "fun_lib_a.h"
void fun_lib_a_printf(void)
{
printf("Call \'fun_lib_a\'.\r\n");
}
Makefile文件即为“2.2节”的Makefile模板。
编译生成静态库:
编写测试例程,生产的库文件即为“3.1节”调用的库文件(libfun_so.so)。文件存放目录结构如下:
源文件:
/*头文件*/
#ifndef _FUN_LIB_SO_H_
#define _FUN_LIB_SO_H_
#endif
extern void fun_lib_so_printf(void);
/*头文件*/
#include
#include "fun_lib_so.h"
void fun_lib_so_printf(void)
{
printf("Call \'fun_lib_so\'.\r\n");
}
编译生成动态库: