程序的编译与链接——ARM可执行文件ELF

news2024/11/20 9:18:18

读书《嵌入式C语言自我修养》笔记

目录

读书《嵌入式C语言自我修养》笔记

ARM编译工具

使用readelf命令查看ELF Header

使用readelf命令查看ELF section header

程序编译

预处理器

 编译器

(1)词法分析。

(2)语法分析。

(3)语义分析。

(4)中间代码生成。

汇编器

(5)汇编代码生成。

链接器

(6)目标代码生成。


ARM编译工具

linux安装gcc-arm-linux-gnueabi交叉编译器

apt-get install gcc-arm-linux-gnueabi-gcc   //Ubuntu

yum install gcc-arm-linux-gnueabi gcc  //Fedora

使用ARM交叉编译器将C源程序编译生成ARM格式的二进制可执行文件a.out

arm-linux-gnueabi-gcc -o a.out main.c sub.c

使用readelf命令查看ELF Header

readelf -h a.out

使用readelf命令查看ELF section header

readelf -S a.out

 一个可执行文件通常由不同的段(section)构成:代码段(.text)、数据段(.data)、BSS段、只读数据段等。每个section用一个section header来描述,包括段名、段的类型、段的起始地址、段的偏移和段的大小等。一个可执行文件中的每一个section都有一个section header,将这些section headers集中放到一起,就是section header table

函数翻译成二进制指令放在代码段中

初始化的全局变量和静态局部变量放在数据段中。

BSS段比较特殊,一般来讲,未初始化的全局变量和静态变量会放置在BSS段中,但是因为它们未初始化,默认值全部是0,其实没有必要再单独开辟空间存储,为了节省存储空间,所以在可执行文件中BSS段是不占用空间的。但是BSS段的大小、起始地址和各个变量的地址信息会分别保存在节头表section header table和符号表.symtab里,当程序运行时,加载器会根据这些信息在内存中紧挨着数据段的后面为BSS段开辟一片存储空间,为各个变量分配存储单元。

程序编译

gcc在程序编译过程中会分别调用它们,常见的工具有预处理器、编译器、汇编器、链接器。

● 预处理器:将源文件main.c经过预处理变为main.i。

● 编译器:将预处理后的main.i编译为汇编文件main.s。

● 汇编器:将汇编文件main.s编译为目标文件main.o。

● 链接器:将各个目标文件main.o、sub.o链接成可执行文件a.out。最后生成的可执行文件a.out其实也是目标文件(object file)

目标文件一般可以分为3种。

● 可重定位的目标文件(relocatable files)。

● 可执行的目标文件(executable files)。

● 可被共享的目标文件(shared object files)。

预处理器

预处理就是在编译源程序之前,先处理源文件中的各种预处理命令。编译器是不认识预处理指令的。编译器一般为开发人员提供一些预处理命令,使用#标识。我们常见的预处理命令如下。

● 头文件包含:#include。                实现模块化编程

● 定义一个宏:#define。                  提高程序的可读性

● 条件编译:#if、#else、#endif。      让代码兼容不同的处理器架构和平台,以最大限度地复用公用代码

● 编译控制:#pragma。   让代码兼容不同的处理器架构和平台,以最大限度地复用公用代码

                        #pragma pack([n]):指示结构体和联合成员的对齐方式。

                        #pragma message("string"):在编译信息输出窗口打印自己的文本信息。

                        #pragma warning:有选择地改变编译器的警告信息行为。

                        #pragma once:在头文件中添加这条指令,可以防止头文件多次编译。

预处理操作。

● 头文件展开:将#include包含的头文件内容展开到当前位置。

● 宏展开:展开所有的宏定义,并删除#define。

● 条件编译:根据宏定义条件,选择要参与编译的分支代码,其余的分支丢弃。

● 删除注释。

● 添加行号和文件名标识:编译过程中根据需要可以显示这些信息。

● 保留#pragma命令:该命令会在程序编译时指示编译器执行一些特定行为。

  预处理后   

 编译器

 编译过程可以分为以下6步。

(1)词法分析。

         词法分析是编译过程的第一步,主要用来解析C程序语句。

         词法分析一般会通过词法扫描器从左到右,一个字符一个字符地读入源程序,通过有限状态机解析并识别这些字符流,将源程序分解为一系列不能再分解的记号单元——token。

         token是字符流解析过程中有意义的最小记号单元,常见的token如下。

        ● C语言的各种关键字:int、float、for、while、break等。

        ● 用户定义的各种标识符:函数名、变量名、标号等。

        ● 字面量:数字、字符串等。

        ● 运算符:C语言标准定义的40多个运算符。

        ● 分隔符:程序结束符分号、for循环中的逗号等。

        sum = a + b / c ;        👉    分解成了8个token:“sum”  “=”  “a”   “+” “b”   “/”  “c”  “;”

(2)语法分析。

         对前一阶段产生的token序列进行解析,看是否能构建成一个语法上正确的语法短语(程序、语句、表达式等)。语法短语用语法树表示,是一种树型结构,不再是线性序列。

         语法分析工具在对token序列分析过程中,如果发现不能构建语法上正确的语句或表达式,就会报语法错误:syntax error。

(3)语义分析。

           语义分析主要对语法分析输出的各种表达式、语句进行检查,看看有没有错误。如果你传递给函数的实参与函数声明的形参类型不匹配,或者你使用了一个未声明的变量,或者除数为零了,break在循环语句或switch语句之外出现了,或者在循环语句之外发现了continue语句,一般都会报语义上的错误或警告。

(4)中间代码生成。

          中间代码是编译过程中的一种临时代码,常见的有三地址码、P-代码等

          中间代码是一维线性序列结构,类似伪代码,编译器很容易将中间代码翻译成目标代码。

          中间码一般和平台是无关的

生成三地址码

arm-linux-gnueabi-gcc -fdump-tree-gimple main.c 

汇编器

(5)汇编代码生成。

       汇编器的主要工作就是参考ISA指令集,将汇编代码翻译成对应的二进制指令,分析汇编语言中各个section的信息,收集各种符号,生成符号表,将各个符号在section内的偏移地址也填充到符号表内,以section的形式组装到可重定位目标文件(.o)中,后面的链接过程会用到这些信息。

查看符号表信息

readelf -s sub.o

符号表主要用来保存源程序中各种符号的信息,包括符号的地址、类型、占用空间的大小等。这些信息一方面可以辅助编译器作语义检查,看源程序是否有语义错误;另一方面也可以辅助编译器编译代码的生成,包括地址与空间的分配、符号决议、重定位等。符号表本质上是一个结构体数组,在ARM平台下,定义在Linux内核源码的/arch/arm/include/asm/elf.h文件中。

typedef struct elf32_sym{
 Elf32_Word st_name;     //符号名
 Elf32_Addr st_value;    //符号对应的值
 Elf32_Word st_size;     //符号大小
 unsigned char st_info;   //符号类型
 unsigned char st_other;
 Elf32_Half st_shndx;      //符号所在段
} Elf32_Sym;

符号的类型主要有以下几种。

● OBJECT:对象类型,一般用来表示我们在程序中定义的变量。

● FUNC:关联的是函数名或其他可引用的可执行代码。

● FILE:该符号关联的是当前目标文件的名称。

● SECTION:表明该符号关联的是一个section,主要用来重定位。

● COMMON:表明该符号是一个公用块数据对象,是一个全局弱符号,在当前文件中未分配空间。

● TLS:表明该符号对应的变量存储在线程局部存储中。

● NOTYPE:未指定类型,或者目前还不知道该符号类型

如果在当前文件中没有找到符号的定义,也会将这些符号搜集在一起并保存到一个单独的符号表中,以待后续填充,这个符号表就是重定位符号表。如:在main.o的符号表中,会看到add和sub这两个符号的信息处于未定义状态(NOTYPE),需要后续填充。同时,在main.o中会使用一个重定位表.rel.text来记录这些需要重定位的符号

查看重定位表

readelf -r main.o

链接器

(6)目标代码生成。

分段组装

链接器将编译器生成的各个可重定位目标文件重新分解组装:将各个目标文件的代码段放在一起,作为最终生成的可执行文件的代码段;将各个目标文件的数据段放在一起,作为可执行文件的数据段。其他section也会按照同样的方法进行组装

段的组装顺序

何指定程序的链接地址和各个段的组装顺序呢?很简单,通过链接脚本就可以了。链接脚本本质上是一个脚本文件。在这个脚本文件里,不仅规定了各个段的组装顺序、起始地址、位置对齐等信息,同时对输出的可执行文件格式、运行平台、入口地址等信息做了详细的描述。链接器就是根据链接脚本定义的规则来组装可执行文件的,并最终将这些信息以section的形式保存到可执行文件的ELF Header中。

可以使用下面的命令来查看链接器使用的默认链接脚本

arm-linux-gnueabi-ld --verbose

在嵌入式裸机环境下编译程序,尤其是编译ARM底层代码,很多时候我们要根据开发版的不同硬件配置、内存大小和地址,灵活指定链接地址,或者显示指定链接脚本,有时候甚至自己编写链接脚本。U-boot源码编译的链接脚本U-boot.lds一般放在U-boot源码的顶层目录下。Linux内核编译的链接脚本vmlinux.lds一般放在arch/arm/boot/compressed/目录下面。而对于ARM裸机程序开发,大多数IDE都会提供一些设置接口。

符号决议

各个文件中定义了相同的全局变量名或函数名,发生了符号冲突,那么最终的可执行文件中到底该使用哪一个呢?编译器为了解决这种符号冲突,引入了强符号和弱符号的概念:函数名、初始化的全局变量是强符号,而未初始化的全局变量则是弱符号。在一个多文件的工程中,强符号不允许多次定义,否则就会发生重定义错误。强符号和弱符号可以在一个项目中共存,当强弱符号共存时,强符号会覆盖掉弱符号,链接器会选择强符号作为可执行文件中的最终符号。链接器也允许一个项目中出现多个弱符号共存。在程序编译期间,编译器在分析每个文件中未初始化的全局变量时,并不知道该符号在链接阶段是被采用还是被丢弃,因此在程序编译期间,未初始化的全局变量并没有被直接放置在BSS段中,而是将这些弱符号放到一个叫作COMMON的临时块中,在符号表中使用一个未定义的COMMON来标记,在目标文件中也没有给它们分配存储空间。在链接期间,链接器会比较多个文件中的弱符号,选择占用空间最大的那一个,作为可执行文件中的最终符号,此时弱符号的大小已经确定,并被直接放到了可执行文件的BSS段中。

sub .c

int i=20;
int a;

main.c

int i;
char a;
int main(void)
{
    printf("i = %d\n",i);
    return 0;
}

编译并执行

#arm-linux-gnueabi-gcc main.c sub.c -o a.out
#./a.out
i=20
#arm-linux-gnueabi-gcc -c main.c sub.c -o a.out
#readelf -s main.o | grep i
 8:00000001   1   OBJECT   GLOBAL   DEFAULT   COM i
#readelf -s sub.o | grep i
 7:00000004   4   OBJECT   GLOBAL   DEFAULT   COM i
#readelf -s main.o | grep i
63:00804a04   4   OBJECT   GLOBAL   DEFAULT   26 i

通过readelf命令分别查看目标文件main.o和sub.o中的符号i,你会发现它们都被放置在了COMMON块中,大小分别标记为1和4,而最终生成的可执行文件a.out中,变量i则被放置在.bss段中,大小标记为4字节。

GNU C编译器在ANSI C语法标准的基础上扩展了一系列C语言语法,如提供了一个__attribute__关键字用来声明符号的属性。通过下面的命令,可以将一个强符号转化为弱符号。

__attribute__((weak))  int n = 100;

重定位

解决了多文件符号冲突的问题,可执行文件的符号表中的每个符号虽然都确定下来了,但是符号表中的每个符号值,也就是每个函数、全局变量的地址,还是原来各个目标文件中的值,还都是基于零地址的偏移。链接器将各个目标文件重新分解组装后,各个段的起始地址都发生了变化。

链接器怎么知道哪些符号需要重定位呢?在各个目标文件中还有一个重定位表,专门记录各个文件中需要重定位的符号。重定位的核心工作就是修正指令中的符号地址 

无论是代码段,还是数据段,只要这个段中有需要重定位的符号,编译器都会生成一个重定位表与其对应:.rel.text或.rel.data。这些重定位表记录各个段中需要重定位的各种符号,并以section的形式保存在各个目标文件中。我们可以通过readelf或objdump命令来查看一个目标文件中的重定位表信息

查看重定位表

#arm-linux-gnueabi-readelf -r main.o

重新定位新地址 = 新的段基址 +  段内偏移

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/156813.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

班级人员可视化项目

页面分布文件分布index.html(搭建页面)index.css (修饰页面)fonts (放图标)images (放图片)jsjquery.js (调整页面的js)flexible.js (尺寸大小的js)echarts.min.js (charts图表的js)chinaMap…

论文投稿指南——中文核心期刊推荐(中国医学 2)

【前言】 🚀 想发论文怎么办?手把手教你论文如何投稿!那么,首先要搞懂投稿目标——论文期刊 🎄 在期刊论文的分布中,存在一种普遍现象:即对于某一特定的学科或专业来说,少数期刊所含…

普元PAS部署springboot项目访问500

背景 项目需要从东方通部署迁移到普元PAS部署。记录一下遇到的问题 问题一 WebSocket启动异常: Error creating bean with name ‘serverEndpoint’ defined in class path resource 因为SpringBoot默认使用的容器是tomcat 对应的Websocket实现 PAS中直接使用ServerEndpoin…

虹科新品丨什么是光纤微动开关?(下)

HK-Micronor光纤微动开关 HK-MR386光纤微动开关和HK-MR380系列控制器搭配使用,提供了一种全新的创新型信号解决方案,可以长距离部署在困难和危险环境中。该开关传感器采用光中断的方法,通过双工62.5/125μm光纤链路,进行可靠的信号…

虹科方案|使用直接连接的阵列创建 SAN

一、引言通过将直连环境转换为共享存储,用户可以体验到物理主机之间或主机与存储之间更快的数据传输,从而使vMotion 实时迁移等 VMware 功能能 够在更短的时间内完成。二、关于VMWARE VSPHEREvSphere 平台是您的应用程序、云和业务 的最佳基础。 vSphere…

C语言_字符串旋转结果_C语言字符串旋转结果

上一篇博文讲了字符串左旋http://t.csdn.cn/8zbRf 这篇文章将讲解判断一个字符串是不是逆序过。 目录 一、问题描述 二、设计与分析 三、代码实现 一、问题描述 写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串。 例如:给定s1 AAB…

【零基础】学python数据结构与算法笔记9

文章目录前言53.栈和队列的应用:迷宫问题54.使用栈解决迷宫问题55.使用队列进行迷宫问题:介绍56.使用队列进行迷宫问题:实现:总结前言 学习python数据结构与算法,学习常用的算法, b站学习链接 53.栈和队列…

【学习笔记之Linux】工具之vim配置

配置文件的位置: 在目录/etc/下面有一个名为vimrc的文件,这是系统中公共的vim配置文件,对所有用户都有效;   每个用户可以在自己的主目录下创建一个私有的配置文件,命名为“.vimrc”,这个配置只对自己有效…

如何利用MOS管实现双向电平转换

前面讲过的三极管和MOS管电平转换电路都是单向的,就是信号只能是从A输出到B输入。其实单个MOS管也能实现双向电平转换,即信号即能从A输出到B输入,也能从B输出到A输入。实际电路就是这个,包含一个MOS管和两个电阻,芯片1…

自定义启动器

🍁博客主页:👉不会压弯的小飞侠 ✨欢迎关注:👉点赞👍收藏⭐留言✒ ✨系列专栏:👉SpringBoot专栏 🔥欢迎大佬指正,一起学习!一起加油! …

D. Meta-set(组合数学)

Problem - D - Codeforces 你喜欢纸牌棋盘游戏“集合”。每张牌包含k个特征,每个特征都等于集合{0,1,2}中的一个值。这副牌包含所有可能的纸牌变体,也就是说,总共有3k张不同的纸牌。 如果三张牌的某个特征与这三张牌相同或两两不同&#xff0…

[LeetCode算法->双指针]

在算法中,双指针的问题较为常见,应用也比较广泛,双指针问题能够降低时间复杂度和空间复杂度,有必要掌握这一内容。下面通过LeetCode的题目来说明双指针。1.给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的…

Minecraft 1.19.2 Fabric模组开发 03.动画生物实体

我们本次尝试在1.19.2 Fabric中添加一个能够具有各种动画效果动作的生物实体。 效果展示效果展示效果展示 1.首先,为了实现这些动画效果,我们需要首先使用到一个模组:geckolib(下载地址) 找到项目的build.gradle文件,在repositories和depen…

vue实现网页端企业微信扫码登录功能(前端部分)

时至今日,企业微信在企业日常工作中的使用越来越频繁也越来越重要,不少企业已使用企业微信进行着日常的工作安排管理。在这种背景下,各类系统和企业微信对接的需求也不断增加,今天要说的就是一个比较常见的需求:在网页…

第08讲:Docker中的网络类型bridge和host

Docker 中的网络模式早先是 3 种,后来又加了 1 种,一共是 4 种。这里,我们只涉及、介绍使用其中 2 种网络模式:bridge 模式和 host 模式。 bridge 是驱动( Driver )类型为 bridge 的默认网络;h…

『 MySQL篇 』:库操作、数据类型

目录 目录 一、初识数据库 数据库 数据库管理系统 SQL语言 二、详解MySQL MySQL 存储引擎 三、简单的库操作 设置数据库的编码字符集 创建数据库 显示数据库 使用数据库 删除数据库 四、MySQL数据类型 数值类型 字符串类型 日期类型 一、初识数据库 学习 MyS…

SpringBoot整合ELK教程

SpringBoot整合ELK教程 1 基础概念 ELK 即 Elasticsearch、Logstash、Kibana,组合起来可以搭建线上日志系统,本文主要讲解使用 ELK 来收集测试框架产生的日志。 Elasticsearch:用于存储收集到的日志信息;Logstash:用于…

如何成为优秀合格的管理者之角色定位

目录 导语 一、管理者的角色误区和角色定位 (一)管理者对上级 (二)管理者对下属 *如何要做好“老师”的角色? (三)管理者跨部门协作 (四)管理者对自己 二…

springboot:接手老项目,领导让更新数据库说明文档,如何3分钟完成任务

0 引言 最新在重新整理老项目的文档,其中数据库说明文档上一版更新还是在1年多前,文档中的数据结构说明与当前数据库严重脱节,所以更新数据库说明文档已经是迫在眉睫的事情了。 因为项目是一个比较大型且“年长‘的项目,涉及了多…

谷粒商城-基础篇-Day09-整合Ware服务

整合Ware服务 将服务注册到nacos中 spring:cloud:nacos:discovery:server-addr: 127.0.0.1:8848application:name: gulimall-wareMapperScan("com.atguigu.gulimall.ware.dao")//mybatis包扫描 SpringBootApplication EnableDiscoveryClient//开启服务发现 EnableT…