ARM《1》_回顾gcc、动态编译和静态编译、MakeFile的使用
0、前言:
- 对于语言的学习,如果要落地作为项目实例,重点还是在于集成与整合的开发工具的使用,比如学习了python以及很多python的库,那么要开发一个落地工具,就得有一个框架能够把这些工具都放进去,让你施展,flask就可以作为python的一种开发框架,对于c++而言,Qt就是一个很好的开发框架;
- 本来学习一种技术,首先就是要知道这种技术是用来干什么的,其此就是怎么使用,再深一点就是这种技术背后的实现逻辑。在开始ARM阶段总结学习之前,既定的技术笔记目标,就是先见过,知道某个知识的概念,具体用来做什么,至于技术背后的实现逻辑,浅尝辄止,后续深入;
1、补充:
a、add_gcc编译文件的四个阶段:
a.1、概览:
- 源码 → 预编译(添加头文件生成纯文本.i) → 编译(生成汇编代码.s) → 汇编(生成机器码.o) → 链接(链接main函数需要的所有机器码,生成可执行文件不加后缀) → 可执行
a.2、实例:
- 文件结构:
- 源码【没有math.c实现add,什么时候加math.c,往后看】
// math.h
#ifndef MATH_H
#define MATH_H
int add(int, int); /* 只有声明,没有实现 → 空头支票 */
#endif// main.c
#include <stdio.h>
#include "math.h" /* 预编译阶段把头文件内容搬进来 */
int main(void)
{int a = 1, b = 2;int c = add(a, b); /* 使用未实现的函数 */printf("%d\n",c);
}
-
预处理:预编译阶段,就是 预编译器 把纯文本 → 带声明的文本,预处理器只负责“搬文字”,添加头文件;【gcc -E xx.c -o xx.i】
此时文件夹中的内容:
-
编译:文本 → 汇编语言,这个阶段 编译器 检查声明与调用签名一致,签发“空头支票”合格,但仍缺实现。【gcc -S xx.i -o xx.s】
-
汇编:汇编语言 → 机器码,这个阶段,汇编器 把汇编语言转成机器码,在main.o文件中有调用add的指令,但是此时add的地址是0,等待链接器“填入add地址”【gcc -c xx.s -o xx.o】
-
链接:机器码 → 可执行文件,链接器 把main.o与math.c链接在一起,在这里就必须创建 math.c 在其中可以只写个add函数,gcc 会自动先把它编译成 math.o,再和 main.o 一起链接,最终生成可执行文件;链接器 的核心作用是将多个目标文件(.o)、库文件组合成一个可执行文件,并确定程序的起始执行地址。对于标准 C 程序,链接器默认约定main函数为用户代码的入口点,这个阶段必须要有主函数main,否则会判定为 “未定义入口”,从而链接失败。
一起编译并链接:
-
注意:如果有一个a.c文件,gcc a.c -o a,就会直接生成a.c的可执行文件。
b、add_动态编译和静态编译
- 不论是动态库,还是静态库,都是通过.o文件(机器码)生成的;
b.1、编译静态库:
- 静态库:在linux下以后缀.a结尾,编译程序时(汇编码 → 机器码)可以链接静态库文件,使用库中的函数或变量。链接静态库和其它目标文件构成可执行文件,静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库,因此体积较大。
- 编译静态库:假设有源文件foo.c,要生产foo.a静态库
gcc -c foo.c -o foo.o
ar rcs libfoo.a foo.o # ar 是“归档工具”,把多个 .o 压成一个 .a,不做链接
- 编译主函数并链接静态库: main.c → 预处理 → 编译 → 汇编(生成临时 main.o,链接完之后删除)→ 链接(与 libfoo.a 中依赖的 .o 合并)→ 可执行文件 main。
gcc main.c -L路径 -lfoo -o main # lfoo 展开成 libfoo.a
b.2、编译动态库:
- 动态库(共享库):在linux下一般以后缀.so结尾,在编译时不会链接到可执行程序,只在程序运行时动态加载。【编译时链接,运行时加载】
- 编译动态库:假设有源文件foo.c,要产生foo.os动态库
gcc -c -fPIC foo.c -o foo.o # 动态库会被映射到任意进程地址,代码里所有地址引用必须相对偏移,-c表示编译到生成.o文件这一步,-fPIC 让编译器生成“与位置无关”的代码。foo.c位置可以写相对路径、foo.o位置可以写相对路径
gcc -shared -o libfoo.so foo.o # -shared 告诉链接器不要生成可执行文件,而是生成 ELF 共享目标文件。libfoo.so位置可以写相对路径
- 编译主函数并链接动态库:main.c → 临时 main.o,链接完删除 → 再把临时 main.o + libmath.so → main
# 如果.os文件在当前路径下:
gcc main.c -L.\ -lfoo -Wl,-rpath,'$ORIGIN' -o main
#-L.\ :在.\路径下找库,
#-lfoo :把库名展开为libfoo.so
#-Wl,-rpath,'$ORIGIN': 在 ELF 头里写死运行时搜索动态库的路径:“可执行文件自己所在目录”,'$ORIGIN'可以是一个相对路径'./lib/';
# 通过 (-Wl) 把 -rpath '$ORIGIN' 这个参数传递给链接器 ld。# 如果.os文件在lib文件夹下:
gcc main.c -Llib -lfoo -Wl,-rpath,'$ORIGIN/lib' -o main
- 上面的方法是编译时指定运行时动态库查找路径,是动态库编译最常用的方式。此外,生成动态库后,main.c 用gcc链接动态库生成 main可执行文件,运行时,动态链接器 会从默认的一些路径(环境变量中的路、系统默认路径)中查找,根据这个思路产生了以下两种不用在编译时指定运行时动态库查找路径的方法:
一、方法1:添加临时环境变量后运行
1、先按照上面步骤编译动态库
2、在生成 main.c 的可执行文件时,用指令:gcc main.c -L.\ -lfoo -o main ,在编译阶段,动态库还是可以被加载的;
3、用临时环境变量运行,执行指令export LD_LIBRARY_PATH=.\ (也就是说LD_LIBRARY_PATH=后面写的是一个动态库所在路径,让之后shell启动的所有程序都继承这个路径。)然后调用 ./main 就可以在运行时找到动态库了。
注意:它只影响当前 shell 会话(以及从这个 shell 启动的子进程),不会永久写进系统;关闭终端或 unset LD_LIBRARY_PATH 就失效。
二、方法2:系统默认路径法
1、2步骤同上生成动态库.os,编译生成 main 可执行文件,然后就是把动态库放到路径 /usr/lib/ 下,再执行就可以执行了,删除 /usr/lib/ 路径下的动态库,运行时就没法绑定了。
b.3、案例1:测试生成动态库和静态库
-
单独写一个排序算法在sort.c中,在main.c中调用该排序算法,尝试静态编译,让main.c可以加载;
-
首先创建目录结构
-
main.c源码
#include <stdio.h> extern void bubble_sort(int arr[], int n); // extern声明这个函数是外部的,到了链接阶段,自己去找;
extern void print_array(const int arr[], int n);int main(void) {int a[] = {64, 34, 25, 12