本篇内容根据《C程序设计新思维》编写,作者水平有限,难免存在疏漏和错误,有问题请指出
C、UNIX、POSIX的存在是紧密相连的
C和UNIX都是在20世纪70年代由贝尔实验室的设计,而贝尔有一项与美国政府达成的协议:贝尔将不会把自身的研究扩张到软件领域,所以UNIX被免费发放给学者进行研究、重建;UNIX商标则被在数家公司之间专卖。在这个过程中,一些黑客们改进了UNIX,并增加了很多变体,于是在1988年IEEE建立了POSIX标准,提供了一个类UNIX操作系统的公共基础
规定了shell script如何工作、常用的命令行工具如何工作、能够提供哪些C库等等
除了微软的Windows系列操作系统,几乎所有操作系统都建立在POSIX兼容的基础上
特别地,加州大学伯克利分校的一些黑客们对UNIX进行了几乎翻天覆地的改进(重写UNIX的基础代码),产生了伯克利软件发行版(Berkeley Software istribution)BSD——苹果的MacOS正建立在这一发行版上
GNU工程即GNU’s Not UNIX工程,由笔者很敬佩的理查德斯托曼主持开工,大多数的Linux发行版都使用了GNU工具(这就是为什么Linux的全称是GNU/Linux),GNU工程下所有软件都是“自由软件”(想了解自由软件或“Free Software”的详情,推荐阅读理查德斯托曼传记《若为自由故》),这就意味着GPL!
GNU工程下属的GNU C Compiler就是为大家所熟知的C编译器gcc
由Dennis Ritchie和Ken Thompson以及其他的开发者共同发明的最原始的C标准
更被人所熟知的名字是简称“ANSI C”,这个版本是C的第一个成熟、统一的版本
在ANSI C成为主流这段时间内,分离出了C++
当下的POSIX规定了必须通过提供C99命令来提供C编译器
吸收了单行注释、for(int i=0;i
在2011年新定义的版本,做出了泛型函数、安全性提升等**“离经叛道”的改变**
GCC以光速支持了这个标准
C开发环境=包管理器+C库+C编译器+调试器+代码编辑器+C编译器辅助工具+打包工具+shell脚本控制工具+版本控制工具+C接口
看上去很复杂,实际上也很复杂=)
为什么不用IDE呢?当你用C开发某些小众嵌入式设备程序时就明白了(包括但不仅限于目前的MIPS、xtensa、RISC-V、你自己花三年用verilog写出来的CPU(可能还莫名其妙移植了一个操作系统和对应的C编译器,也许会有这样的dalao吧)),IDE?TMD!
IDE?狗都不用!真男人都是记事本+编译器!
每个系统都具有不同的软件包组织方式,所以你的软件很可能被安装在某个犄角旮旯,这就需要包管理器来帮助安装软件;虽然说很奇怪,但windows下也有包管理器
安装完包管理器后,就能用它安装gcc或clang这种编译器、GDB调试器、Valgrind内存使用错误检测器、Gprof(一个运行效率评测软件)、make工具、(如果你很nb还可以安装cmake工具)、Pkg-config(查找库的工具)、Doxygen(用于生成程序文档的工具)、你喜欢的文本编辑器(包括但不仅限于Emacs、Vim、VSCode、Sublime、记事本),除此之外,还能安装一些跨平台的IDE(虽然不太推荐,但eclipse就是最大众的选择,XCode需要有钱人才能买得起(指苹果电脑),Code::blocks在win下工作有点拉跨),还有必要的git工具、autoconf、automake、libtool,以及最重要的增加程序猿B格的Z shell、oh-my-zsh
包管理器还能管理C库,用一些新C库(libcURL、libGLib、libGSL、libSQLite3、libXML2),你可以实现很炫酷的现代C语言开发以及防止重新造轮子
对于一个包管理器,常会提供供用户使用的包和供开发者使用的包,在安装时应该选择带有-dev或-devel的包
在Linux下,包管理器分为两大阵营:
目前而言,两边其实都很好用、易上手,不过根据程序猿的性格不同,选择yum的程序猿多少沾点(我使用apt,就是要引战!),另外还有一个叫的Arch的发行版,因为我一直没有能把它安装上,所以不知道那个pacman -S
是什么东西
在Windows下,微软很nt(逆天,指微软很厉害)地提供了方便的软件安装方式:让你的C盘变红。不过Windows还是勉为其难地提供了一个POSIX兼容的东西——Cygwin是许多自由软件的集合,最初由Cygnus Solutions开发,用于各种版本的Microsoft Windows上,运行UNIX类系统。Cygwin的主要目的是通过重新编译,将POSIX系统(例如Linux、BSD,以及其他Unix系统)上的软件移植到Windows上,可以在Cygwin网站上下载包管理工具,配合一个终端(Terminal)即可实现在Win下进行Linux开发(虽然很蛋疼)。安装方法参考百度(笑)
微软最近还开发了一个叫WSL(Windows Subsystem of Linux,最新版本是跑在windows自带虚拟机Hyper-V上的WSL2)的东西,这个东西可以让你在windows下进行不完全的linux使用(用的发行版是ubuntu),笔者目前使用的就是这个软件,安装很方便——开启Hyper-V和虚拟化、打开Microsoft应用商店搜索WSL点击下载安装即可,不过所有东西就被塞进了C盘。
在POSIX环境下,一切都很方便,apt install能解决一切问题,想安装什么就sudo apt install xxx
非POSIX环境下,可以使用MinGW来实现标准C编译器和一些基础工具,或者使用很好用的WSL
可以使用命令行下的编译器执行编译:
完整的gcc编译器命令如下:
gcc test.c -o test -lm -g -Wall -O3 -std=gnu11
该命令告诉编译器通过程序中的#include包含库函数,并告诉链接器通过命令行中的-lm链接库文件
-o用于指定输出文件的名字,否则会得到一个默认的a.out作为可执行文件名
-g表示加入调试负好,如果没有这个选项,调试器就不会显示变量或者函数的名字
-std=gnu11是gcc的特有选项,允许使用c11和POSIX标准的代码;同理可以使用-std=gnu99来使用c99标准
-O3表示优化等级为3级,尝试已知的所有方法去建立更快的代码
-Wall用于添加编译器警告
微软顽强地抗拒C语言,所以一般在Windows下编译c程序都使用MinGW或cygwin环境,在这些环境中编译后可以得到Windows本地的二进制代码(当然也可以使用宇宙第一IDE VS,在此不讨论)
但伴随的缺点就是缺少使用体验很爽的C库!
安装编译器后,链接工具会被自动安装好
GCC可以自动完成优化-编译-链接一条龙
连接函数库之前需要注意的就是路径:编译器需要知道在哪个目录去查找正确的头文件和目标文件。典型的库存放位置可能有三种:
使用以下命令行来指定头文件搜索目录
gcc -I 头文件所在目录 需要链接的.c文件名 -o 生成的.o文件名 -L 某个库所在的目录
参数-I用于添加指定的路径到头文件的搜索路径范围内,编译器会在这个路径范围内搜索放在代码中用#include
指定的头文件
参数-L用于添加指定的路径到库的搜索路径范围内
注意:最原始的依赖库放在参数最后面,引用了很多库且被当前编译文件引用的子库紧跟在-L之后
因为链接器的工作方式是:
正是因为这种工作方式,很多时候安装依赖库的顺序和编译的顺序不当就会导致“玄学错误”
以上方式就是常规的静态库链接方式,当然现在的软件没什么用静态库的了,嵌入式软件倒是个例外(不过也有些RTOS在嵌入式设备上应用了动态链接极制)。共享库用于动态链接,使用以下Makefile参数来告诉编译器从哪里寻找共享库来进行动态链接
LDADD=-L libpath: xxxxxx -Wl,-R libpath: xxxxxx
-L参数告诉编译器到哪里找到库来解析符号
-Wl参数从编译器传递后面的路径到链接器,链接器会将给定的-R嵌入到所连接的库的运行时搜索路径
Make工具就是天!——沃·兹基硕德
Make工具是一个可以自动执行编译、链接等工作的程序,它也有POSIX标准化,需要通过读入Makefile作为指令和变量,很多嵌入式开发(包括交叉编译和底层软件编译)都要和它打交道
一般来说使用gcc编译一个文件需要像下面这样:
gcc hello.c -o hello.o
对于几个文件的编译还算简单,但是如果是一个很大的工程,包含了数不清的文件,每个文件都互相依赖,那么写shell script就会很绝望了;使用makefile就会让编译工作轻松一点
最小的Makefile如下所示
P=helloworld
OBJECTS=
CFLAGS = -g -Wall -O3
LDLIBS=
CC=c99
$(P):$(OBJECTS)
现在一般都使用GNU Make工具(GNU:没错还是我),需要将编写的Makefile命名为Makefile
并将其与.c文件放在同一目录下,在命令行中输入
make
就可以进行自动化编译了
当然可以在后面添加参数-jn
其中n表示你使用的cpu线程数——这就是多线程编译,可以提高编译速度;但是相对应的就不会生成每个.c文件对应的warning,并且会自动在第一个error处停下,并且不会告诉你error发生在哪里,所以如果是第一次编译请不要使用这个参数
makefile的语法有两种流派:POSIX型和C shell型,现在的makefile大多使用POSIX型或其近似的语法;而CMake工具的cmakelist则选择了类似C shell的语法
make和shell一样使用$指代变量的值,但shell要求使用$var
,make则要求使用$(var)
形式
$(P):$(OBJECTS) #相当于program_name=
有以下几种方法让make工具识别变量:
在C代码中可以使用getenv函数获取环境变量信息,这需要调用stdlib.h库
make工具也提供了一些内置的变量,如下所示:
$@
返回完整的目标文件名(目标文件就是从.c文件中编译得到的.o文件)
$*
返回不带文件名后缀的目标文件
$<
返回触发和制作该目标文件的文件的名称
Makefile的标准形式如下:
target:dependencies
script
输入命令make target
时,对应的target就会被调用,检查dependencies是否是较早(target文件比dependencies更新)的文件,如果检查通过则运行所有的dependencies,结束后再运行target部分的脚本;如果检查不通过则对script的处理会被暂停
Makefile基本上就是目标target、依赖dependencies、脚本script三者所组成的一系列规则。make工具就是根据Makefile的规则决定如何编译和链接程序或者执行其它操作(包括但不仅限于自动下载远程程序、解压缩包、打补丁、设定参数、编译并将软件安装到系统中指定位置等)
在编写小工程,特别是只有不多文件的情况下,在shell里使用gcc就可以了,如果工程数量继续增长,自行编写Shell Script也很简单;但是如果是依赖条件复杂的大工程,直接使用编译器就会很繁琐,这时候通常使用构建工具来辅助
构建工具 (software construction tool) 是一种软件,它可以根据一定的规则或指令,将源代码编译成可执行的二进制程序。这是构建工具最基本也是最重要的功能。实际上构建工具的功能不止于此,通常这些规则有一定的语法,并组织成文件。这些文件用来控制构建工具的行为,在完成软件构建之外,也可以做其他事情。
Linux的内核、Linux上运行的大多数Qt软件、洗衣机的主控、ESP32的底层文件…都是用Make工具完成构建的,这是历史最为悠久的开源构建工具,但是正因如此它的语法比较混乱,且不被Windows支持(除非使用上文提到的Cygwin环境)。除此之外,只要外部条件稍微变化一些,就需要修改软件的Makefile。为了让安装软件更加容易,程序员开发出了几个神奇的生成工具:Ninja、Automake、SCons、CMake。利用这些改善过的工具,编程者只需要写一些预先定义好的宏,提交给程序自动处理,就会生成一个可以供Make工具使用的Makefile文件,再配合使用工具产生的自动配置文件configure即可产生一份符合GNU-Makefile规定的Makefile;或者直接通过自己独有的软件编译方式对工程进行生成。
除了Make工具外,Ninja也是一个自动化编译的构建工具。它由Google的一名程序员开发,通过将编译任务并行化大大提高编译速度
它并不使用Makefile,而是用一套自己组织的.ninja
脚本
现在Android Studio等都选用了Ninja作为编译工具,也有许多嵌入式设备厂商自己的SDK使用了ninja
ninja的安装方法和make一样
sudo apt install ninja
或者可以从github上拉取最新版本的ninja源码,然后自行编译安装
只不过make支持POSIX标准,可以直接在linux上运行;但是ninja需要事先安装一些依赖,具体情况可以参考官网
这里不多介绍ninja的使用方法
SCons是另一套由Python编写的开源构建系统,它和Make、Ninja一样可以生成脚本进行快速编译,但是SCons有一个最厉害的地方:它使用的是python脚本,能够使用标准的Python语法编写构建工程
详情可以参考官网
SCons的脚本称为SConscript,支持多种编译器,包括但不仅限于gcc、clang,甚至支持很多公司专有的交叉编译工具
它的安装也比较简单:
sudo apt install python #安装依赖——这玩意是用python写的
sudo apt install scons
直接在程序目录使用下面的指令就可以轻松完成很多原本需要Makefile写很多才能完成的任务
scons #相当于make
scons -c #相当于make clean
# 上面的就是个开胃菜,和make没差很多;下面才是scons的神奇之处
touch SConstruct
vim SConstruct #新建一个SCons脚本
Program('testPro1', Glob('*.cpp')) #写完以后直接保存退出
scons #直接回到shell来一个执的行,testPro1就构建完毕了
除了直接使用scons
命令生成外,SCons还支持一些扩展功能
RT-Thread就将它扩展为了keil MDK/IAR/VS/CubeIDE工程生成器:
scons --target=xxx #生成某个IDE的工程
SConscript还能执行一些方便的内置函数
GetCurrentDir() #获取当前路径
Glob() #获取当前目录下的所有文件,支持在里面使用表达式匹配
Glob('*.c') #获取当前目录下的.c文件
Split(str) #将字符串str分割成一个列表
具体的SCons使用还要更复杂一些,所有使用方法都可以参考官网或者使用到SCons的SDK说明,即用即查,不需要特意学习
Automake是正统的GNU软件,它用来生成Make构建系统。
这是一个套娃:gcc/clang是编译器,或者说构建工具;Make/Ninja/SCons是构建系统;Automake/CMake则是生成构建系统的系统
CMake是比Automake更常用一些的系统,它通过读取CMakeLists.txt
中的规则来构建编译系统
它的脚本格式大致如下:
cmake_minimum_required(VERSION 3.9) #注明需要的cmake工具版本
project(HelloWorld) #工程名
set(CMAKE_CXX_STANDARD 11) #设置要使用编译器版本
add_executable(HelloWorld main.cpp) #指定源文件
#下面是负责程序安装的指令
install(TARGETS HelloWorld DESTINATION bin) #指定将生成的程序文件安装到/usr/local/bin
install(FILES HelloWorld.h DESTINATION include) #指定将库文件保存到/usr/local/include
在shell中使用
mkdir build #惯用方式,将所有生成的文件保存至build目录
cd build
cmake .. #对上一级也就是工程项目主目录执行cmake,前提是CMakeLists.txt保存在工程项目主目录
#所有生成的文件会被保存到build目录,包括Makefile
make #执行make
就可以完成所有编译任务
它可以将Makefile的编写完全转化成CMakeLists的编写,虽然看上去多此一举,但实际上增强了编译时的可靠性和程序的可移植性
半个可视化也是可视化——伽利略(?)
一般来说一个工程完全可以依靠CMake-Make-GCC的工具链完成编译,但实际上很多与c语言打交道的底层程序需要适配各种不同的情况,这就需要使用代码模块化思想:将一套代码分成多个模块以适应不同情景
最典型的就是Linux内核了:有的设备是嵌入式的,有的设备是高性能的,有的设备带摄像头、网口,有的设备只有WiFi,有的设备自带一堆硬件加速器,有的设备使用RISC-V架构——而Linux内核需要针对这些情况进行优化,因此它使用了“宏内核”思想,内核接管一切驱动程序的管理,将底层所有设备归类交给不同的驱动程序管理。这时Linux的内核编译就需要事先确定目标设备上都有什么,预先选好要加入工程的组件再生成源代码(这也使用了C预编译器的一部分功能)
负责灵活配置编译单元——说人话就是从一堆预先写好的代码里挑选出合适的代码的工具就是Kconfig
Kconfig工具生成CMakeList的设置参数;之后调用make menuconfig
配置.cmake
文件用于将Kconfig生成的参数赋值给CMake变量、.mk
配置文件用于保存make工具的编译变量、.h
文件用于提供C语言的编译基础;再然后使用CMake工具生成Makefile;最后使用Make工具进行编译,就可以生成一个bin文件了
这里着重强调一下Kconfig的使用——因为它是Linux内核编译的最重要辅助工具之一——在编译内核时,需要用到make menuconfig
指令,而这个指令并没有直接调用make、cmake工具,而是先借助Kconfig工具打开一个蓝色的配置菜单界面,如下所示
这个蓝色的菜单可以说是最经典的多级菜单程序,但这里我们不说多级菜单的实现,仅仅谈它使用的脚本工具Kconfig
使用config语句定义一组新的配置选项
config CONFIG_A
bool "Enable MyConfig"
select SELECTED
default y
help
My Config
bool表示配置选项的类型,每个config菜单项都要有类型定义,变量有5种类型:bool
布尔,也就是二选一(y或n)、tristate
三态,也就是三选一、string
字符串,也就是用户从给出的一堆字符串里选择一个、hex
十六进制,也就是用户要选择一个十六进制数、int
整型,也就是用户需要选择一个整数,需要注意:这里的用户选择是不严谨的,实际上选项类型决定了Kconfig会生成什么样的#define语句提供给C源代码;"Enable MyConfig"表示该选项的名称;select表示如果当前配置选项被选中,则SELECTED就会被选中;default后面跟的参数表示配置选项的默认值;help后面跟的语句会作为帮助信息提供给用户
使用menu/endmenu块来生成一个菜单,这个菜单里面可以包含复数个config语句,也可以包含子菜单
使用if/endif块来实现条件判断
使用menuconfig语句表示带菜单的配置项,也就是可展开的菜单
使用choice/endchoice语句将多个类似的配置选项组合在一起,供用户选择一组配置项,也就是“弹窗”子菜单
使用comment语句定义帮助信息,这些东西会出现在界面第一行
使用source语句读取另一个文件中的Kconfig文件
Kconfig
CMake
Automake
Make
Ninja
SCons
GDB是目前最常用的调试器(没有之一),它支持多平台、多目标的调试过程。
调试器,顾名思义就是用于调试程序的软件。它能够给出底层的变量变化和回溯的信息,同时可以追踪内存和寄存器中数值的变化,一般的调试器可以提供断点功能和追踪功能。编译时,在gcc指令后加入-g参数即可启用调试功能,不过这会导致程序变大一些
gdb可以直接在命令行模式中使用,但是一般来说各种代码编辑器和IDE会将gdb嵌入,并提供可视化的调试功能,下面是一些使用命令行模式gdb时会用到的指令
gdb <file> # 使用gdb调试文件
#--下面会进入gdb命令行--
(gdb) r # 正常执行程序直到遇到错误或断点
(gdb) l # 显示运行到的那一行代码
(gdb) l <部分代码> # 显示以某行为中心的部分代码
(gdb) b <部分代码> # 在某行代码处打断点
(gdb) p <变量/指针> # 输出某变量/指针的值
(gdb) bt # 列举出堆栈帧
(gdb) info threads # 获取程序使用线程列表
(gdb) n # 重复单步运行程序
(gdb) b <行号> # 在某一行打断点
(gdb) r # 重新开始一个循环
(gdb) c # 继续运行程序直到完成当前循环或运行到return
(gdb) disp <变量> # 将某变量的变化在调试过程中自动显示
(gdb) <Enter> # 重复上一步命令
(gdb) f <帧号> # 查看某一帧
任何函数都会在内存中占据一个空间,称为函数帧,函数帧会使用特殊的数据结构保存与这个函数有关的所有信息,gdb则可以直接查阅堆栈帧并获取信息。
现在的调试过程已经基本不需要使用gdb的命令行模式了(除非是对跑在某些极少有人使用的、没有开源社区提供gdb调试工具的莫名其妙的cpu上的程序),VSCode、Sublime、Vim、Emacs里面都内置了gdb的(半)图形化调试只需要知道如何打断点、单步调试、查看汇编等基本操作就可以进行有效调试了