目录
一. GCC命令语句大全
二. GCC编译4个阶段
三. makefile的使用
四. CMake
五. GNU工具链开发流程图
六. Keil中的地址段
七. 静态库和动态库
一. GCC命令语句大全
-c | 只编译源文件,生成目标文件(.o 文件),不进行链接。 |
-o <file> | 指定输出文件的名称。 |
-g | 生成调试信息,用于调试程序 |
-Wall | 打开所有警告提示。 |
-I <dir> | 添加头文件搜索路径。 |
-L <dir> | 添加库文件搜索路径。 |
-l <library> | 链接指定的库文件。 |
-std=<standard> | 指定使用的语言标准,如 -std=c99 。 |
-0<level> | 优化级别,如 -O0 (无优化)、-O1 (基本优化)、-O2 (更高级别优化)。 |
-D<macro> | 定义预处理宏。 |
-E | 只进行预处理,输出预处理结果。 |
-S | 只进行编译,生成汇编代码。 |
-shared | 生成共享库(动态链接库)。 |
-static | 生成静态可执行文件,使用静态链接。 |
-pthread | 链接 POSIX 线程库。 |
-lm | 链接数学库。 |
-fopenmp | 启用 OpenMP 并行编程支持。 |
二. GCC编译4个阶段
GCC不仅仅是一个编译器,它还包括预处理器、汇编器以及链接器,可以处理从代码编写到可执行程序生成的整个流程。
三. makefile的使用
先新建一个main.c
#include "stdio.h"
int add(int a, int b);
int main()
{
printf("num:%d\r\n",add(1,2));
return 0;
}
再新建一个math.c
int add(int a, int b)
{
return (a + b);
}
编译永远都是以单个源文件为单位的。这里我们先编译一下mian.c文件。编译生成的.o文件是一个二进制文件,文件格式是ELF(Linux下所有可执行文件的通用格式),Windows使用的是PE格式,(都是对二进制代码的封装)
我们可以在文件头部找到可执行文件的基本信息比如支持的操作系统,机器类型等。
查看一系列的区块,.text是代码区,.data是数据区(里面保存了我们初始化的全局变量,局部静态变量等等),
查看main.o这个目标文件中的内容,这里call指令是之前调用的printf和add函数,但是它们的跳转地址都被设为了0,而这里的0会在后面链接的时候被修正。
另外为了让编译器能够定位到这些需要被修正的地址,在代码块中我们还可以找到一个重定位表,比如在.text区块中,找到被重定位的两个函数printf和add。
接下来编译math.c且连同main.o一起链接生成一个独立的可执行文件
gcc main.o math.o -o main
链接其实就是将编译之后的所有目标文件,连同用到的一些静态库,运行时库,组合拼装成一个独立的可执行文件,其中就包括之前说到的地址修正,在这个时候,连接器会根据我们的目标文件或者静态库中的重定位表,找到那些需要被重定位的函数,全局变量,从而修正它们的地址。
但如果我们每次都要手动编译和链接就不高效,所以我们就得使用makefile,而makefile的核心是对"依赖"的管理,所以makefile其实就是在定义一颗依赖树。
新建一个makefile文件,具体makefile可以查看二. MakeFile-CSDN博客
堆makefile文件进行一定优化
1. 使用makefile变量
2. “%”表示长度任意的非空字符串,比如“%.c”就是所有的以.c 结尾的 文件,类似与通配符,a.%.c 就表示以 a.开头,以.c 结束的所有文件。如 %.o : %.c
3. makefile自动化变量
4. Makefile 伪目标
自动化变量 | 描述 |
$@ | 规则中的目标集合,在模式规则中,如果有多个目标的话,“$@”表示匹配模 式中定义的目标集合。 |
$% | 当目标是函数库的时候表示规则中的目标成员名,如果目标不是函数库文件, 那么其值为空。 |
$< | 依赖文件集合中的第一个文件,如果依赖文件是以模式(即“%”)定义的,那么 “$<”就是符合模式的一系列的文件集合。 |
$? | 所有比目标新的依赖目标集合,以空格分开。 |
$^ | 所有依赖文件的集合,使用空格分开,如果在依赖文件中有多个重复的文件, “$^”会去除重复的依赖文件,值保留一份。 |
$+ | 和“$^”类似,但是当依赖文件存在重复的话不会去除重复的依赖文件。 |
$* | 这个变量表示目标模式中"%"及其之前的部分,如果目标是 test/a.test.c,目标模 式为 a.%.c,那么“$*”就是 test/a.test。 |
all:main#我们最终需要的mian文件
objects = main.o math.o
main:$(objects) #main文件是由main.o math.o而来
gcc -o main $(objects)
%.o : %.c
gcc -c $<
#main.o:main.c #main.o由main.c而来
# gcc -c main.c
#math.o:math.c #math.math.c而来
# gcc -c math.c
.PHONY : clean
clean:
rm *.o
rm main
接下来我们直接make就能生成最后的可执行文件.
四. CMake
CMake主要功能
1.配置和生成各大平台的工程
2.生成makefile文件
因为makefile的配置与当前系统有关,如果要开发一个跨平台的软件所以我们使用cmake。
CMakeList.txt文件
cmake_minimum required(VERSION 3.10)
project(hello)
add_executable(hello main.cpp factorial.cpp printhello.cpp)
cmake . 之后,也能生成makefile文件。这时候我们再make,就能生成可执行文件了。
五. GNU工具链开发流程图
GNU工具链包括了一系列重要的组件,如GCC(GNU编译器集合)、GNU Binutils、GNU Make和GDB等。
编译:编译器是armcc和armasm ,每个c/c++和汇编源文件编译成对应的以“.o”为后缀名的对象文件。
链接:链接器armlink把各个.o文件及库文件链接成一个映像文件“.axf”或“.elf”;
格式转换:一般来说Windows或Linux系统使用链接器直接生成可执行映像文件elf后,内核根据该文件的信息加载后,就可以运行程序了,但在单片机平台上,需要把该文件的内容加载到芯片上,所以还需要对链接器生成的elf映像文件利用格式转换器fromelf转换成“.bin”或“.hex”文件,交给下载器下载到芯片的FLASH或ROM中。
六. Keil中的地址段
程序组件 | 所属类别 |
机器代码指令 | Code |
常量 | RO-data(Read Only data) |
初值非0的全局变量 | RW-data(Read Write data) |
初值为0的全局变量 | ZI-data(Zero Initialie data) |
局部变量 | ZI-data的栈空间 |
使用malloc动态分配的空间 | ZI-data的堆空间 |
初值非0的全局变量和初值为0的全局变量其实就是数据区,而值为0的全局变量区又称为bss段。
七. 静态库和动态库
简单的来说二者的区别:
-
静态库:就是在编译的时候直接将需要的代码连接进可执行程序中去;
-
动态库:就是在需要调用其中的函数时,根据函数映射表找到该函数然后调入堆栈执行。当动态库被加载到内存之后,一旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址,也就是重定位,这些地址在程序加载之前不过只是一堆占位符而已,如果我们直接去修改代码段中的跳转地址,由于代码段的内容被修改,自然就不能被其他进程所共享了,因为我们需要在内存中保存多个不同的副本,这刚好和节约内存的目标就背道而驰了,为了解决这个问题,动态链接采用了一种聪明的做法,不再修改代码段,而是在数据段中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表
查看全局偏移表readelf -S ./libmath.so
got里面专门用来存放全局变量和函数的跳转地址,于是我们在调用函数的时候,会首先查表,然后根据表中的地址来进行跳转,这些地址会在动态库加载的时候,被修改成为真正的地址,而查表的过程也很容易实现,由于全局偏移表与代码段的相对位置是固定的,我们完全可以利用CPU的相对寻址来实现,有了全局偏移表,我们不再需要修改代码段,因此代码可以被所有进程共享,而全局偏移表虽然在每一个进程中保留一份副本,但由于占用空间很小,所以完全没有问题,采用这种方式实现的动态链接也被叫做 PIC(地址无关代码),换句话说我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定 -fPIC 参数的原因,另一方面由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步是非常耗时的,为了进一步降低开销,我们的操作系统还做了一些其他的优化,比如延迟绑定或者也叫PLT,与其在一开始就对所有函数进行重定位,不如将这个过程推迟到函数第一次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。大概思路是,GOT中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码(Stub),在我们第一次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表,于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。动态链接实际上将链接的整个过程,比如符号查询、地址的重定位从编译时推迟到了程序的运行时,更关键的是,它实现了二进制级别的代码复用。
从上面的描述可以知道,静态库是我们MCU开发者常用的一种,而动态库常用于Linux、Windows等开发场合
接下来我们创建一个动态链接库。还是先创建一个main.c和math.c.
gcc -shared -fPIC math.c -o libmath.so
//-shared 表明这是一个共享对象(共享对象),libmath.so是linux下动态库的扩展名,windows下的动态库就是我们熟悉的各种.dll文件,
gcc main.c -lmath -L. -o main
我们在编译主程序的时候,我们需要指定一个-l参数,它告诉编译器与之前建的libmath.so进行动态链接,这里在指定动态库的时候,我们省略了前缀lib和扩展名.so,最后我们通过-L来指定动态库所在的路径,
在运行main时,会提示找不到libmath这个动态库,这是因为linux默认只会去系统路径下搜索动态库
./main: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
解决办法有
第一种,将动态库拷贝到系统路径下,但之后还得删除这个文件所以不方便
另外一种是使用环境变量,将当前目录添加到 LD_LIBRARY_PATH 环境变量中,这样操作系统会先到我们指定的路径下搜索,找不到的话再去搜索系统路径。
下图可以看到它成功调用了动态库中的add函数并返回了正确的结果,那之前我们提到动态链接的一大优势是允许我们单独更新动态库本身。
这样我们更新一下动态库,对主程序不需要做任何修改,直接运行我们就可以看到刚才我们对动态库的更新