嵌入式中常用`static inline`避免链接问题-C底层研究
嵌入式中常用static inline避免链接问题-C底层研究
static
// add.h
int add(int a, int b) {
return a + b;
}
// utils.c
#include "add.h"
void foo() {
add(3, 4);
}
// main.c
#include "add.h"
int main() {
return add(1, 2);
}预处理后

内容变成了

可以看见头文件被展开了
编译之后

内容变成了

汇编之后
两个.o文件各自都有一份T(已定义)状态的add
正好对应"头文件被多次#include,等于在两个.c文件里各写了一遍完整的函数体"

链接

链接器先在main.o里认了一个add("first defined here"),接着又在utils.o里碰到第二个同名add,直接罢工报错
add.h 加上static,就没问题了(小写代表内部链接,只在本文件内可见)

这就是static的作用,把add限制在了单个的文件作用域
inline
作为内联声明,函数体的实现必须要在预处理阶段被包含进来,才能实现内联的替换作用
预处理
只是把.h文件里的内容复制过来,不会动对应.c里面的东西
- 就算写了
inline关键字,编译器也无能为力——因为它没见过函数体,没法展开。 - 只有把函数体暴露在
.h里,编译器在编译main.c时,才能亲眼看到return a+b或GPIO_ODR ^=的代码,从而决定展开成内联指令
普通函数

内联函数

编译
普通函数
可以看见汇编里面没有关于add的任何信息,只有一个冷冰冰的调用call add,就算加了inline,也无济于事

内联函数
默认gcc是O0优化,不会优化,这里用O2优化

不仅成功触发了内联,还触发了比内联更狠的一层优化。现在编译器已经不是“把 add 函数展开成 add 指令”了,而是直接把数学题算出了答案,把整个函数调用连带加法运算一起“消灭”了!
优化的极致是直接把代码逻辑(甚至函数本身)从最终二进制文件中抹除,只保留最终结果

汇编
普通函数
注意,左侧的地址全是 0。这是因为 main.o 是一个未重定位(未链接)的目标文件。它的代码起始地址还没有被分配,只是从 0 开始偏移。等链接器把多个 .o 合并成 .exe 时,这个 0 会被替换成 main 在最终 .exe 里的真实偏移量(比如 0000000140001000)
把链接上add.c才会显示T

内联函数
- 没有
U add:这意味着main.o里没有任何地方需要去外部找add这个函数。因为call add已经被替换成了movl $3, %eax,外部依赖被彻底斩断。 - 没有
T add:这意味着main.o的代码段(.text)里没有生成add函数的独立机器码。因为它是static inline,并且在编译时已经被展开并消化掉了。 - 结论:
add这个符号在编译阶段就被“物理毁灭”了——它的逻辑被吸收进了main,它的名字被从符号表中抹去。这是链接器在最终生成.exe时能进一步减小体积的基础。

链接
链接两个.c文件

链接里面的结果nm 看符号表:看的是“这件事被安排在了哪个门牌号(地址)
在 nm 里看到 main 在 0x140002660,在反汇编里你就能跳到那个地址,亲眼看到那几行机器码。

反汇编
nm 是地图,反汇编是实地照片。

通过 nm,是在 “看链接器的施工图纸”(地址分配、段合并、导入表构造);而通过反汇编,是在 “看施工完毕后的实体楼房”(机器指令长什么样)。两者结合起来,就能把 .c 源文件到 .exe 执行文件的每一步都看得清清楚楚
总结
若想要用内联函数,就要将它,对应.c来引用该头文件即可。但是如果static保护的话,,不然会有重复定义的报错,上面已经看了汇编之后的代码了。
所以要想实现,必须static inline一起用

