前言
从多个.c文件到达一个可执行文件的四步:
预处理–>编译–>汇编–>链接
预处理
预处理过程就是预处理器处理这些预处理指令(要不然编译器完全不认识),最终会生成 main.i的文件
主要做的事情有如下几点:
- 展开头文件
- 展开宏
- 条件编译
- 删除注释
- 添加行号等信息
- 保留parama预处理指令
- 头文件展开—#include指令
- #include <sdtio.h> 和 #include “stdio.h”
对于<> 搜索顺序为- 通过GCC参数gcc-I指定的目录(注:大写的I 让我们自由指定的)。
- 通过环境变量CINCLUDEPATH指定的目录。
- GCC的内定目录。
对于 " "的搜索时顺序 - 项目当前目录(此时也可以用"…/LED/led.h"方式去搜索)
- 通过GCC参数gcc-I指定的目录。
- 通过环境变量CINCLUDEPATH指定的目录。
- GCC的内定目录
- 为什么把声明放在头文件里
- 提供一个接口 方便其他文件通过声明调用对应的函数
- 当我们的led.c 包含了 led.h的时候 也方便编译器做类型的检查
- 头文件多次包含会增加可执行文件的体积吗?
只要是使用了类似#pragma once 或者#ifndef 多次包含是不会的增加可执行文件的体积的
同样的要注意:声明不会增加可执行文件的体积
- #include <sdtio.h> 和 #include “stdio.h”
- 宏展开#define 宏指令
- 宏定义最小
#define MIN(x,y) ((x) > (y) ? (y) : (x))
因为宏只是做了一个替换所以对于如下代码
- 宏定义最小
#include <stdio.h>#define MIN(x,y) ((x) > (y) ? (y) : (x))//因为宏只是做了一个替换所以对于如下代码int main(){int a = 2 ;int b = 5;int c = MIN(a++,b++);printf("c = %d a = %d b = %d\r\n",c,a,b); // a竟然等于4}
在这里可以用GNU C语法中的一些小技巧操作
#define MIN(x,y) ({\typeof(x) _x = (x);\typeof(y) _y = (y);\_x > _y ? _x : _y;})
- 定义一个很大的常数的时候
#define MAX_LONG (100001000010000)UL //指定类型 - ##连接符
高端用法 看了好多代码都用这个 但是分析起来乱乱的,大概就是把两个字母连接到一起
#define contact(x,y) (x##y)int bc = 50;printf("bc = %d\r\n", contact(b,c));
- offset_of与container_of
之前写结构体的时候写过,权当复习一下#define offset_of(type, member) ((size_t)(&((type *)0)->member)) #define container_of(type,member,ptr) (type *)((size_t) ptr - offset_of(type,member)) struct student {int height;char * name; }; int main() {struct student stu;stu.height = 50;stu.name = "123456";char ** tmp_name = &stu.name;struct student* s = &stu;printf("%p %p %ld\r\n",s, tmp_name,offset_of(struct student,height));struct student *new_s = container_of(struct student,name,tmp_name);new_s->height = 60;printf("%d\r\n",stu.height); }
``
- 宏为什么要用 do {} while(0)
如果去看linux源码也好还是RTOS等的代码也好 会有很多时候用到do_while(0) 它的作用是什么呢
假设我们定义了
#define MACRO() foo(); bar()
此时我们写了这样的伪代码
if (condition)
MACRO();
else
baz();
// 宏展开后和我们想要的就完全不一样了 直接就出错了
// do {}while(0)可以保证宏作为一个整体执行 此时就可以定义一些局部变量
- 条件编译 #ifdef等指令
-
条件编译指令
正常用的比较多的就是 #ifndef #define #endif这几个连用
也有 #defined(VAR_X)之类的 -
#error指令
如果发生错误直接中断编译过程
- #pragma 指令
- #pragma pack([n]):指示结构体和联合成员的对齐方式。
- #pragma message(“string”):在编译信息输出窗口打印自己的文
本信息。 - #pragma warning:有选择地改变编译器的警告信息行为。
- #pragma once:在头文件中添加这条指令,可以防止头文件多次
编译。
编译
真要讲编译我也是不配讲的 就我们知道这是在干嘛就行了
编译就是把.c文件变成汇编文件的过程
- 编译过程的6步
词法分析 / 语法分析 / 语义分析 / 中间代码生成 / 汇编代码生成 / 目标代码生成- 语法错误: stynax error: 缺少分号 / {}没扩住 /
- 语义错误: 类型不匹配 未定义的变量
最终的结果就是生成.S文件
- gcc的优化等级 gcc -O
可以参考
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options - 交叉编译
嵌入式开发板一般是ARM架构 然后PC是x86架构
通过交叉编译器进行程序的编译
汇编
汇编就是把汇编代码编程机器码 也就是比较熟悉的 xx.o文件了
汇编过程最终会生成以零地址为链接起始地址的可重定位目标文件
链接
把.o 文件进行组装 需要重定位 因为所有的.o文件的开头都是以0地址开头的
链接主要分为3个过程:分段组装、符号决议和重定位
-
分段组装
不太好讲 基本就是通过一个脚本把多个文件按照段组合到一起
-
符号决议
符号决议的核心就行 如果说变量/函数重名了怎么办- 不允许同时存在两个相同的强符号
初始化的全局变量、函数名默认都是强符号,未初始化的全局变量默认是弱符号
比如
- 不允许同时存在两个相同的强符号
// b.cint i; // 未初始化 是弱符号int main() {printf("%d\r\n",i); //i的值是20}// a.cint i = 20;
__attribute__关键字 可以把强符号强行转换为弱符号 attribute((weak))
- 使用弱符号的好处
-
自定义重名函数
这里在嵌入式里最常见的就是中断服务函数的弱定义了
当我们需要重新定义中断服务函数的时候 只需要保证名字很start.S的名字一致就行,链接的时候就知道链接到哪里了
-
检查该函数是否存在
// b.c
#include <stdio.h>
int global_k;
char global_i;
attribute((weak)) void func()
{
printf(“这被定义为弱符号了\r\n”);
}
int main()
{
printf(“%d\r\n”,global_k);
if(func)
func(); //调用的是强符号的函数
return 0;
}
// a.c
#include <stdio.h>
int global_k = 20;
int global_i;
void func()
{
printf(“这被定义为强符号了 fun\r\n”);
}
-
- 同样都是弱符号 谁体积大谁胜出
- 重定位
因为要把不同的文件链接到一块 所以位置就会发生变化