ARM架构与编程(基于I.MX6ULL): 代码重定位(八)(上)

简介: ARM架构与编程(基于I.MX6ULL): 代码重定位(八)

1.段的概念_重定位的引入


1.1. 问题的引入


led.imx = 头部 + led.binled.stm32 = 头部 + led.bin

头部里含有位置信息(addr):固件要把led.bin复制到哪里去

链接程序时,指定了链接地址,一般来说头部信息的addr就等于链接地址

如果,偏要修改头部信息的addr,让它不等于链接地址,会发生什么是?

头部里含有长度信息(len):led.bin多大

在串口程序中添加全局变量,把它打印出来,看看会发生什么事。


1.2. 段的概念


1.2.1 程序直接烧写在ROM上


代码段、只读数据段、可读可写的数据段、BSS段。

char g_Char = 'A';           // 可读可写,不能放在ROM上,应该放在RAM里
const char g_Char2 = 'B';    // 只读变量,可以放在ROM上
int g_A = 0;   // 初始值为0,干嘛浪费空间保存在ROM上?没必要
int g_B;       // 没有初始化,干嘛浪费空间保存在ROM上?没必要


所以,程序分为这几个段:


代码段(RO-CODE):就是程序本身,不会被修改

可读可写的数据段(RW-DATA):有初始值的全局变量、静态变量,需要从ROM上复制到内存

只读的数据段(RO-DATA):可以放在ROM上,不需要复制到内存

BSS段或ZI段:

初始值为0的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以

未初始化的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以

局部变量:保存在栈中,运行时生成

堆:一块空闲空间,使用malloc函数来管理它,malloc函数可以自己写


1.2.2 片内固件功能强大,理解段的概念麻烦一点


看视频


1.3. 重定位


保存在ROM上的全局变量的值,在使用前要复制到内存,这就是数据段重定位。


想把代码移动到其他位置,这就是代码重定位。


2.重定位要做的事


2.1. 程序中含有什么?


代码段:如果它不在链接地址上,就需要重定位

只读数据段:如果它不在链接地址上,就需要重定位

可读可写的数据段:如果它不在链接地址上,就需要重定位

BSS段:不需要重定位,因为程序里根本不保存BSS段,使用前把BSS段对应的空间清零即可


2.2. 谁来做重定位?


程序本身:它把自己复制到链接地址去

一开始,程序可能并不位于它的链接地址上,为什么它可以执行重定位的操作?

因为重定位的代码是使用“位置无关码”写的

什么叫位置无关码:这段代码扔在任何位置都可以运行,跟它所在的位置无关

怎么写出位置无关码:

跳转:使用相对跳转指令,不能使用绝对跳转指令

只能使用branch指令(比如bl main),不能给PC直接复制,比如ldr pc, =main

不要访问全局变量、静态变量

不使用字符串


2.3. 怎么做重定位和清除BSS段?


核心:复制

复制的三要素:源、目的、长度

怎么知道代码段/数据段保存在哪?(加载地址)

怎么知道代码段/数据段要被复制到哪?(链接地址)

怎么知道代码段/数据段的长度?

怎么知道BSS段的地址范围:起始地址、长度?

这一切

在keil中使用散列文件(Scatter File)来描述

在GCC中使用链接脚本(Link Script)来描述


2.4. 加载地址和链接地址的区别


程序运行时,应该位于它的链接地址处,因为:


使用函数地址时用的是"函数的链接地址",所以代码段应该位于链接地址处

去访问全局变量、静态变量时,用的是"变量的链接地址",所以数据段应该位于链接地址处

但是: 程序一开始时可能并没有位于它的"链接地址":


比如对于STM32F103,程序被烧录器烧写在Flash上,这个地址称为"加载地址"

比如对于IMX6ULL/STM32MP157,片内ROM根据头部信息把程序读入内存,这个地址称为“加载地址”

当加载地址 != 链接地址时,就需要重定位。


3.链接脚本使用与分析


参考手册:Using LD, the GNU linker


3.1. 重定位的实质: 移动数据


把代码段、只读数据段、数据段,移动到它的链接地址处。

也就是复制!

数据复制的三要素:源、目的、长度。


数据保存在哪里?加载地址


数据要复制到哪里?链接地址


长度


这3要素怎么得到?

在GCC中,使用链接脚本来描述。

在keil中,跟链接脚本对应的是散列文件,散列的意思就是"分散排列",在STM32F103这类资源紧缺的单片机芯片中:


代码段保存在Flash上,直接在Flash上运行(当然也可以重定位到内存里)

数据段保存在Flash上,使用前被复制到内存里

但是,在资源丰富的MPU板子上:


内存很大,几十M、几百M,甚至几G


可能没有XIP设备(XIP: eXecute In Place,原地执行)


没有类似STM32F103上的Flash,代码无法在存储芯片上直接运行

基于这些特点,在MPU板子上


代码段、数据段、BSS段等等,运行时没有必要分开存放

重定位时,把整个程序(包括代码段、数据段等),一起复制到它的链接地址去


3.2. 链接脚本示例


3.2.1 链接脚本示例


SECTIONS {
    . = 0xC0200000;   /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */
    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }
    . = ALIGN(4);
    .rodata : { *(.rodata) }
    . = ALIGN(4);
    .data : { *(.data) }
    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}


3.2.2 链接脚本语法


3.2.2.1 完整的语法


一个链接脚本由一个SECTIONS组成。

一个SECTIONS里面,含有一个或多个section。

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}


section是链接脚本的核心,它的语法如下:

secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill


3.2.2.2 几个例子


实际上不需要那么复制,不需要把语法里各项都写完。


示例1

SECTIONS { 
  .text : { *(.text) }            /* secname为".text",里面是所有文件的".text"段 */ 
  .data : { *(.data) }            /* secname为".data",里面是所有文件的".data"段 */
  .bss :  { *(.bss)  *(.COMMON) } /* secname为".bss",里面是所有文件的".bss"段和".COMMON"段 */
}


示例2

还可以按文件指定

SECTIONS {
  outputa 0x10000 :     /* secname为"outputa",链接地址为0x10000 */ 
    {
    first.o                 /* 把first.o整个文件放在前面 */
    second.o (.text)        /* 接下来是second.o的".text"段 */
    }
  outputb :             /* secname为"outputb",链接地址紧随outputa */ 
    {
    second.o (.data)        /* second.o的".data"段 */
    }
  outputc :             /* secname为"outputc",链接地址紧随outputb */  
    {
    *(.bss)                 /* 所有文件的".bss"段 */
    *(.COMMON)              /* 所有文件的".COMMON"段 */
    }
}


示例3

SECTIONS { 
  .text 0x10000 : AT (0)       /* secname为".text",链接地址是0x10000,加载地址是0 */ 
  { *(.text) }  
  .data 0x20000 : AT (0x1000)  /* secname为".data",链接地址是0x20000,加载地址是0x1000 */
  { *(.data) } 
  .bss :                       /* secname为".bss",链接地址紧随.data段,加载地址紧随.data段 */
  { *(.bss)  *(.COMMON) } 
}


3.3. 怎么获得各个段的信息


数据复制3要素:源、目的、长度。

怎么知道某个段的加载地址、链接地址、长度?


3.3.1 怎么确定源?


可以用ADR伪指令获得当前代码的地址,对于这样的代码:

.text
.global  _start
_start: 
    ......
    adr r0, _start


adr是伪指令,它最终要转换为真实的指令。它怎么获得_start代码的当前所处地址呢?

实际上,adr r0, _start指令的本质是r0 = pc - offset,offset是在链接时就确定了。


3.3.2 怎么确定目的地址?


也就是怎么确定链接地址?可以用LDR伪指令。

对于这样的代码:

.text
.global  _start
_start: 
    ......
    ldr r0, =_start


ldr是伪指令,它最终要转换为真实的指令。它怎么获得_start的链接地址呢?

_start的链接地址在链接时,由链接脚本确定。


3.3.3 如何获得更详细的信息


在链接脚本里可以定义各类符号,在代码里读取这些符号的值。

比如对于下面的链接脚本,可以使用__bss_start、__bss_end得到BSS段的起始、结束地址:

__bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;


上述代码里,有一个".“,它被称为"Location Counter”,表示当前地址:可读可写。

它表示的是链接地址。

. = 0xABC;       /* 设置当前地址为0xABC */ 
_abc_addr = . ;  /* 设置_abc_addr等于当前地址 */
. = . + 0x100;   /* 当前地址增加0x100 */
. = ALIGN(4);    /* 当前地址向4对齐 */


注意:"Location Counter"只能增大,不能较小。


3.4. 编写程序重定位数据段


3.4.1 修改链接脚本


我们故意只重定位数据段,在后面的课程再来重定位代码段并引入更多知识。

数据段要被复制到哪去?需要在链接脚本里确定一下:增加了__data_start

SECTIONS {
    . = 0xC0200000;   /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */
    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }
    . = ALIGN(4);
    __data_start = .;
    .rodata : { *(.rodata) }
    . = ALIGN(4);
    .data : { *(.data) }
    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}


3.4.2 编写程序


修改start.S:

ldr r0, =__data_start   /* 目的: 链接地址 */
/* 计算data段的当前地址:
 * _start的链接地址 - _start的当前地址 = __data_start的链接地址 - data段的当前地址
 * data段的当前地址 = __data_start的链接地址 - (_start的链接地址 - _start的当前地址)
 */
adr r1, _start
ldr r2, =_start
sub r2, r2, r1
sub r1, r0, r2
/* 计算data段的长度 */
ldr r2, =__bss_start
ldr r3, =__data_start
sub r2, r2, r3
bl memcpy    /* 需要3个参数: dest, src, len */
相关文章
|
2月前
|
Ubuntu Linux
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
215 3
|
20天前
|
机器学习/深度学习 测试技术 数据处理
KAN专家混合模型在高性能时间序列预测中的应用:RMoK模型架构探析与Python代码实验
Kolmogorov-Arnold网络(KAN)作为一种多层感知器(MLP)的替代方案,为深度学习领域带来新可能。尽管初期测试显示KAN在时间序列预测中的表现不佳,近期提出的可逆KAN混合模型(RMoK)显著提升了其性能。RMoK结合了Wav-KAN、JacobiKAN和TaylorKAN等多种专家层,通过门控网络动态选择最适合的专家层,从而灵活应对各种时间序列模式。实验结果显示,RMoK在多个数据集上表现出色,尤其是在长期预测任务中。未来研究将进一步探索RMoK在不同领域的应用潜力及其与其他先进技术的结合。
60 4
|
2月前
|
机器学习/深度学习 算法 数据库
阿里云服务器架构区别解析:从X86计算、Arm计算到高性能计算架构的区别参考
在我们选择阿里云服务器的架构时,选择合适的云服务器架构对于提升业务效率、保障业务稳定至关重要。阿里云提供了多样化的云服务器架构选择,包括X86计算、ARM计算、GPU/FPGA/ASIC、弹性裸金属服务器以及高性能计算等。本文将深入解析这些架构的特点、优势及适用场景,以供参考和选择。
阿里云服务器架构区别解析:从X86计算、Arm计算到高性能计算架构的区别参考
|
9天前
|
存储 缓存 Java
JAVA并发编程系列(11)线程池底层原理架构剖析
本文详细解析了Java线程池的核心参数及其意义,包括核心线程数量(corePoolSize)、最大线程数量(maximumPoolSize)、线程空闲时间(keepAliveTime)、任务存储队列(workQueue)、线程工厂(threadFactory)及拒绝策略(handler)。此外,还介绍了四种常见的线程池:可缓存线程池(newCachedThreadPool)、定时调度线程池(newScheduledThreadPool)、单线程池(newSingleThreadExecutor)及固定长度线程池(newFixedThreadPool)。
|
2月前
|
XML 开发框架 .NET
.NET框架:软件开发领域的瑞士军刀,如何让初学者变身代码艺术家——从基础架构到独特优势,一篇不可错过的深度解读。
【8月更文挑战第28天】.NET框架是由微软推出的统一开发平台,支持多种编程语言,简化应用程序的开发与部署。其核心组件包括公共语言运行库(CLR)和类库(FCL)。CLR负责内存管理、线程管理和异常处理等任务,确保代码稳定运行;FCL则提供了丰富的类和接口,涵盖网络、数据访问、安全性等多个领域,提高开发效率。此外,.NET框架还支持跨语言互操作,允许开发者使用C#、VB.NET等语言编写代码并无缝集成。这一框架凭借其强大的功能和广泛的社区支持,已成为软件开发领域的重要工具,适合初学者深入学习以奠定职业生涯基础。
90 1
|
2月前
|
设计模式 算法 PHP
深入理解PHP中的数组操作探索编程之美:从代码到架构的思维转变
【8月更文挑战第24天】在PHP编程中,数组是基础且强大的数据结构。本文将通过浅显易懂的方式,介绍如何在PHP中高效地操作数组,包括创建、遍历、排序和过滤等常见任务。无论你是初学者还是有经验的开发者,这篇文章都会带给你新的启示。 【8月更文挑战第24天】在编程的世界中,代码不仅仅是冰冷的字符排列,它承载着思想、解决问题的智慧和创新的灵魂。本文将通过个人的技术感悟,带领读者从编写单一功能的代码片段出发,逐步深入到整个软件架构的设计哲学,探索如何将代码块转化为高效、可维护和可扩展的系统。我们将一起见证,当代码与架构思维相结合时,如何引发技术实践的革命性飞跃。
|
1月前
ARM64架构提供的Cache操作
ARM64架构提供的Cache操作
|
2月前
|
前端开发 开发者 C#
WPF开发者必读:MVVM模式实战,轻松实现现代桌面应用架构,让你的代码更上一层楼!
【8月更文挑战第31天】在WPF应用程序开发中,MVVM(Model-View-ViewModel)模式通过分离应用程序的逻辑和界面,提高了代码的可维护性和可扩展性。本文介绍了MVVM模式的三个核心组件:Model(数据模型)、View(用户界面)和ViewModel(处理数据绑定和逻辑),并通过示例代码展示了如何在WPF项目中实现MVVM模式。通过这种方式,开发者可以构建更加高效和可扩展的桌面应用程序。
57 0
|
2月前
|
存储 前端开发 数据库
神秘编程世界惊现强大架构!Web2py 的 MVC 究竟隐藏着怎样的神奇魔力?带你探索实际应用之谜!
【8月更文挑战第31天】在现代 Web 开发中,MVC(Model-View-Controller)架构被广泛应用,将应用程序分为模型、视图和控制器三个部分,有助于提高代码的可维护性、可扩展性和可测试性。Web2py 是一个采用 MVC 架构的 Python Web 框架,其中模型处理数据和业务逻辑,视图负责呈现数据给用户,控制器则协调模型和视图之间的交互。
28 0
|
2月前
|
消息中间件 缓存 Java
如何优化大型Java后端系统的性能:从代码到架构
当面对大型Java后端系统时,性能优化不仅仅是简单地提高代码效率或硬件资源的投入,而是涉及到多层次的技术策略。本篇文章将从代码层面的优化到系统架构的调整,详细探讨如何通过多种方式来提升Java后端系统的性能。通过对常见问题的深入分析和实际案例的分享,我们将探索有效的性能优化策略,帮助开发者构建更高效、更可靠的后端系统。
下一篇
无影云桌面