ARM学习(25)链接装载高阶认识
1、例子引出
笔者先引入几个编译链接的例子来介绍一下:
-
声明无效:declared implicitly?,属于编译错误还是链接错误?
编译阶段的错误,属于编译错误,因为编译器发现这个函数没有声明,声明异常 -
标识符/符号找不到:xxxx is undefined? undefined xxxxx? 无法解析的外部符号?属于编译错误还是链接错误?
第一个是结构体找不到,属于编译阶段错误,相当于类型找不到。
第二个和第三个属于链接错误,找不到对应的函数符号。 -
只编译不实现:会出现什么情况? 编译可以通过?链接可以通过?
编译通过,链接不过,因为链接会关心函数的大小和实现。
补充例子图。 -
如何骗过编译器/链接器
- 骗过编译器:让编译器认为我们写的代码是OK的, 可以编译通过。
- 骗过链接器:让链接器认为我们代码正常,可以链接成完整的可执行文件,比如axf。
- 通常来说,我们为了让代码运行,可以会编译链接出一个完整的axf,此时需要快速解决一些编译问题和链接问题,就需要让编译器和链接器认为我们代码是OK的,就需要快速适配,即“骗过”。
- 比如编译到一个函数test_speed(),找不到对应的函数,test_speed,此时就需要声明一下函数,然后就可以编译过**(下图1)**。
- 然后到链接的时候,发现找不到符号,Error: L6218E: Undefined symbol test_speed (referred from main.o)。(下图2)那么只有声明不行,需要定义一下,所以再加上一个空函数就行,此时就可以链接过。(下图3)
-
编译链接学习的意义:
- 帮助理解代码执行过程
- 提升代码质量 (熟悉处理编译等警告)
- 优化代码性能 (了解编译优化)
- 更好的跨平台开发 (各个平台编译差异)
- 更深入掌握调试技巧 (各自视图 调试不按行)
-
主要有PE和ELF两种可执行文件格式
2、编译链接
整体框图,1、预处理 2、编译 3、汇编 4、链接
由下图可以看到,
- C文件经过预处理可以得到.i文件,编译选项-E
- .i文件经过编译可以得到汇编文件,编译选项-S
- .s文件经过汇编可以得到目标文件,
- .o文件经过打包,可以形成静态库.a文件,也可以经过与库文件链接形成可执行文件,后缀为out或者axf。
2.1 预处理器
预处理的主要内容有如下:
- #define进行替换
- 处理#if #ifdef等预编译指令
- 展开#include
- 删除 // /* */
- 添加行号和文件名
- 保留Progma指令 ……
string.h 文件展开
2.2 编译
编译遵循的语法规则(个人总结):
- 函数需要声明,不能重复声明
- 变量、结构体不能重复定义
- 变量函数定义需要封号结尾
- 定义变量数组需要指明大小,不能为负数 宏与枚举不能重复声明
- 宏需要多行,如果多行,需要\进行链接
- 包含头文件的路径需要指明
- 需要包含正确的头文件
- 函数的声明和定义需要一致
- If whilefor等关键字得正确使用
- 注释的正确使用
2.3 链接
链接:将目标文件粘贴在一起,形成可执行文件。
按.o文件进行地址排序
- Main fun -> Uart1Init fun Main fun -> UartPoll fun
- 每个目标文件为一个section
- 目标文件中首个函数地址均从0开始
- 根据链接顺序,依次向后排
- 向后排的大小按照目标文件所有函数的大小
- 后面的符号地址确定后会在前面地址进行修正
按section进行地址排序(设置了分割section 属性,将每个函数进行section分割)
- Main fun -> Uart1Init fun
- Main fun -> UartPoll fun
- 目标文件每个函数为一个section
- 函数地址均从0开始
- 根据链接顺序,依次向后排
- 想后排的大小按照函数的大小
- 后面的符号地址确定后会在前面地址进行修正
3、目标文件的认识
3.1 简介
目标文件:以.o或者.obj文件结尾,是可重定位文件(下图1中 REL(Relocatable file))。
- 包括了代码和数据 (下图2)
- 入口地址为0 (下图1)
- 包括多个section/Segment (下图2)
- Section中包含符号表/重定位表(下图3)
- 可以被用来链接成可执行文件或者共享库文件
- 遵循ELF文件格式
下图中有365个段,包括了bss以及data段以及重定位段等。
Section: 链接视图中的段
Segemnt:装载视图中的段,合并一定相同属性的段
由下图可以看到Section中定义的段,到了Segment里面,代码都合并成了一段。
比如ER_IROM1 、ER_REGION_HEADER、ER_IROM2 合并了,
这样的好处可以减少段零散,节省内存,同时加载相对简单,不需要每个section都去分散加载。
3.2 目标文件分析
目标文件分析:分割section
分割section的意思,按函数分割为一个段,
UART1Init:Section10,Size 208 Byte,重定位后的地址0x08004C5C(下图1),
UART1Poll:Section11,Size 176Byte,重定位后的地址0x08004D2C(下图1),恰好相差0xD0,也就是208Byte(下图2)。
结论:目标文件确定后,其大小则确定,即链接器按照地址和size依次向后排列,确定地址。
从下图4也可以看出,最终的可执行文件指令代码和目标文件形成的指令代码是一致的。
图1
图2
图3
图4
目标文件分析:文件为section
Uart.o 为一个section,内部函数按顺序地址递增,然后文件之间进行地址排序
Uart.o wifi.o:地址0x08007E68 – 0x08009004(下图2),相差0x119C(4508个byte)(下图3),
结论:目标文件确定后,链接器按照文件地址和size依次向后排列,确定地址,同时size增大(44280 -> 62128)下图5。
图1
图2
图3
图4
图5
3.3 目标文件重定位
目标文件重定位表:记录着哪些位置的值链接器需要进行重定位
表结构:两个成员,一个offset,一个type
typedef struct rel_table_struct
{
u32 offset;
u32 type;
}rel_table_t;
可能是数据重定位,也可能是函数重定位
- 下图1 可以看到是一个重定位表,第一个是函数重定位,其type类型是 R_THM_CALL,符号是DMA_Get_CurrDataCounter
- 下图1中其他是数据,Type是R_ARMC_ABS32,
- 图2 可以看到UART1Poll函数,其数据地址都是0,重定位后,图2可以看到都有了相应的地址。
图1
图2
图3
函数地址进行重定位
- 目标文件中的BL指令F7FFFFFE,经过重定位后,变成F7DFFE94
- BL的修改规则,是通过BL的ARM 指令编码表来计算的,如下图2。例如知道知道当前地址和编码后的指令代码,就可以知道跳转的地址(下图3和图4),当然如果知道当前地址以及跳转地址,可以推断出修订指令编码值。
- 函数跳转地址实现如下面代码所示,根据ARM BL指令编码表,然后计算出S、J1和J2,imm1和imm2,最后再组装在一起,形成最后的值。
图1
图2
图3
图4
int it,pc,offset = 0;
printf("please input Intruction:\r\n");
scanf("%x", &it);
printf("please input pc:\r\n");
scanf("%x", &pc);
int S = (it & 0x04000000) >> 26;
int J1 = (it & 0x00002000) >> 13;
int J2 = (it & 0x00000800) >> 11;
int I1 = (~(J1 ^ S))&0x1;
int I2 = (~(J2 ^ S))&0x1;
int imm10 = (it & 0x03FF0000) >> 16;
int imm11 = (it & 0x000007FF);
if(S == 1)
{
offset = 0xFF000000;
}
offset |= (S<<24);
offset |= (I1<<23);
offset |= (I2<<22);
offset |= (imm10<<12);
offset |= (imm11<<1);
printf("jump addr=0x%x\r\n",(offset + pc + 4));
4、静态链接
4.1 空间地址分配
在链接的时候,如果形成图1的这种可执行文件,那么加载的时候,有一些劣势。
简单地址分配:
- 空间浪费
- 不利于管理
- 不利于加载
4.2 强弱符号和修饰
- 强符号、弱符号与符号修饰:__weak 或者 attribute((weak))
符号:函数和变量,链接器接口
符号名:函数名和变量名
强符号:只允许存在一个
弱符号:允许存在多个(weak修饰)
符号修饰:符号名根据特定规则进行修改
extern “C”:将函数名按照C语言中生成函数名的方式去生成。
对于弱符号,如果只定义不实现,可以编译过?链接过?能执行吗?
可以看到能编译过,同样可以链接过,但是执行报错,地址为空,可能无法访问。
所以GCC编译器:
- 若符号即使没定义,也可以链接
- 有符号名
- 符号地址为空,允许出错
对于ARMCC编译器:
- 若符号即使没定义,也可以链接
- 没有符号名
- 函数引用指令链接成nop,可正常运行
再来说说修饰规则C++调用C:
- GCC:不作任何操作
- VC编译器:符号前面加”_”下划线。
- 不声明extern “C”情况:按照C++的函数命名去修饰
修饰规则C++: - GCC:N或者_N开头,………
- VC编译器:??或者?开头 ,…….
4.3 链接与ABI接口
链接与ABI接口:Application Binary Interface,应用程序二进制接口
-
API与ABI:前者为源码级别的接口(如POSIX), 后者来二进制级别的接口(各大编译器无法兼容的原因,就是ABI不同,比如GCC和VC编译器,C++标准都一样,但是ABI不同,导致无法互相调用)。
-
影响ABI的因素:C角度
- 基本类型大小以及存储方式(大小端)
- 符号修饰方面 函数调用方式(入栈/返回值)
- 寄存器使用约定等
- 堆栈分布方式
-
影响ABI的因素:C++角度
- 继承类体系分布
- 指向成员指针内存分布
- 虚函数的调用
- 模板类的实例化
- 外部符号的修饰
- 全局对象的构造和析构
- 异常产生和捕获机制
4.4 链接过程控制与脚本语法
链接过程控制其实就是链接脚本来控制链接的过程,比如将数据分配到链接脚本指定的段。
加载视图:加载期间,代码和数据的分布情况
运行视图:运行期间,代码和数据的分布情况
存储地址:代码数据存放的位置
加载地址:代码数据加载到内存中(执行代码)的地址
执行地址:代码数据真正执行的地址
来看一个加载过程,具体可参考【Bootloader学习理解学习–加强版】。
链接脚本语法,如下面两张图所示。