外观
04.揭开链接器的面纱、汇编语言的内嵌编程
约 2149 字大约 7 分钟
嵌入式Linux单片机个人
2022-06-17
七、揭开链接器的面纱(上)
1. 问题
源文件被编译后生成目标文件,这些目标文件如何生存最终的可执行程序?
2. 链接器的意义
链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接。

3. 目标文件的秘密
- 各个段没有具体的起始地址,只有段大小信息
- 各个标识符没有实际地址,只有段中的相对地址
- 段和标识符的实际地址需要链接器具体确定
4. 链接器的工作内容
- 将目标文件和库文件整合为最终的可执行程序
- 合并各个目标文件中的段( .text, .data, .bss )
- 确定各个段和段中标识符的最终地址(重定位)
- 链接前后对比


5. 问题
main()函数是第一个被调用执行的函数吗?
6. 默认情况下( gcc )
程序加载后,_start()是第一个被调用执行的函数
_start()函数准备好参数后立即调用__libc_start_main()函数
__libc_start_main()初始化运行环境后调用main()函数执行
- _start()函数的入口地址就是代码段( .text )的起始地址!

7. __libc_start_main()函数的作用
- 调用__libc_csu_init()函数(完成必要的初始化操作)
- 启动程序的第一个线程(主线程),main()为线程入口
- 注册__libc_csu_fini()函数(程序运行终止时被调用)
8. 程序的启动过程

9. 自定义程序入口函数
- gcc提供-e选项用于在链接时指定入口函数
- 自定义入口函数时必须使用-nostartfiles选项进行链接


10. 思考
链接选项-nostartfiles的意义是什么?
链接时不要使用标准的系统启动文件。标准的系统库通常使用,除非使用-nostdlib或-nodefaultlibs。

八、揭开链接器的面纱(中)
1. 思考
链接器根据什么原则完成具体的工作?
2. 链接脚本的概念和意义
- 链接脚本用于描述链接器处理目标文件和库文件的方式
- 合并各个目标文件中的段
- 重定位各个段的起始地址
- 重定位各个符号的最终地址
3. 链接脚本的本质

4. 链接脚本初探( Round 1 )

5. 注意事项
- 各个段的链接地址必须符合具体平台的规范
- 链接脚本中能够直接定义标识符(源代码的变量)并指定存储地址
- 链接脚本中能够指定源代码中标识符的存储地址
在Linux中,进程代码段(.text)的合法起始地址为[0x08048000,0x08049000]
6. 编程实验:链接脚本初体验
8-1.lds



7. MEMORY命令
默认情况下
- 链接器认为程序应该加载进入同一个存诸空间
嵌入式系统中(可能不存在MMU)
- 如果存在多个存储空间,必须使用MEMORY 进行存储区域定义

8. MEMORY命令的使用( Round 2 )

9. MEMORY命令的属性定义

10. ENTRY命令指定入口点( Round 3 )

11. 编程实验:链接脚本指定入口函数
8-2.Ids


查看默认的链接脚本 ld --verbose > default.lds
默认进入 _start()

12. 小结
- 链接器根据链接脚本中的描述完成具体的工作
- 链接脚本用于指定各个段的地址和标识符的地址
- SECTIONS命令确定可执行程序中的段信息
- MEMORY命令对存储区域进行重定义
- ENTRY命令指定可执行程序的入口函数
九、汇编语言的内嵌编程
1. C 语言中的内嵌汇编
内嵌汇编的语法格式:

2. 内嵌汇编示例

- mov 类似于 赋值操作符
- 占位符 %1 %0 需要被后面的参数所替换。(%0 ==>result)
- asm … ==> result = input;
3. 编译器做了什么?
- 将result关联到某个适合的寄存器
- 将input关联到另一个适合的寄存器
- 通过通用寄存器间接操作变量

= 限制符 意味着后面是输出参数
4. 常用限制符的说明

5. 编程实验:内嵌汇编初体验
9-1.c






6. 问题
如何在不使用printf()的情况下打印字符串?
7. 通过INT 80H使用内核服务
INT指令用于使用Linux内核服务(中断指令)
80H是一个中断向量号,用于执行系统调用
如何指定具体的系统调用(如∶sys_write )以及调用参数?
- 通过寄存器指定具体的系统调用及参数。
8. INT 80H使用示例一

限制符r 让编译器决定用什么寄存器与s和l关联,但是不能使用保留列表的寄存器。
9. INT 80H使用示例二

10. 注意事项
- 嵌入汇编时,除汇编模板外,其余参数可以全部省略
- 当省略的参数在中间时,对应分隔符":"不可省略
- 当省略保留列表时,对应分隔符":"可省略
- 当省略可选参数时,寄存器前使用单个%作为前缀
- 当存在可选参数时,寄存器前使用两个%作为前缀
11. 编程实验:深入体验内嵌汇编

打印刚退出进程的进程码
echo $?
12. 小结
- C 程序中支持直接嵌入汇编语言进行编程
- 通过寄存器到变量的关联完成汇编到C语言的交互
- 内嵌汇编代码时,通过占位符指定交互的变量
- 限制符指示编译器将适合的寄存器关联到变量
- 通过内嵌汇编能够直接使用系统服务
十、揭开链接器的面纱(下)
1. 课程实验(模拟嵌入式开发)
- 编写一个"体积受限"的可执行程序
- 通过makefile完成代码编译
- 运行后在屏幕上打印"D.T.Software"

2. 深度分析

3. 解决方案设计
- 通过内嵌汇编自定义打印函数和退出函数( INT 80H )
- 通过链接脚本自定义入口函数(不依赖任何库和GCC内置功能)
- 删除可执行程序中的无用信息(无用段信息,调试信息,等)
4. 打印函数设计

5. 退出函数设计

6. 链接脚本设计

.rodate和.text都是只读属性,放在一起达到减少段的目的。
.rodate只读数据区
7. 最后的准备(一)
ld命令
- GNU的链接器,将目标文件链接为可执行程序
- GCC编译器集中的一员,重要的幕后工作者
ld -static
- -static表示ld使用静态链接的方式来产生最终程序,而不是默认的动态链接方式
8. 最后的准备(二)
- - gcc -fno-builtin
- -fno-builtin参数用于关闭GCC内置函数的功能
GCC提供了很多内置函数(Built-in Function),它会把一些常用的C库函数替换成编译器的内置函数,以达到优化的目的。
9. 编程实验:模拟嵌入式开发
program.out
10. 小结
- 对于资源受限的嵌入式设备,需要考虑可执行程序的大小
- 通过内嵌汇编直接使用系统服务能够避开相关库的使用
- 可以通过如下方法控制可执行程序的体积大小
- 最小化库的使用(必要情兄下考虑自己实现相关函数)
- 自定义链接脚本,删除无用段信息
