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

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

4.数据段重定位


参考手册:Using LD, the GNU linker


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


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

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


4.1.1 怎么确定源?


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

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


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

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


4.1.2 怎么确定目的地址?


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

对于这样的代码:

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


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

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


4.1.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"只能增大,不能较小。


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


4.2.1 修改链接脚本


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

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

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


4.2.2 编写程序


修改start.S:

ldr r0, =__data_start   /* 目的: 链接地址 */
/* 计算data段的当前地址:
 * _start的链接地址 - _start的当前地址 = __rodata_start的链接地址 - rodata段的当前地址
 * data段的当前地址 = __rodata_start的链接地址 - (_start的链接地址 - _start的当前地址)
 */
ldr r0, =__rodata_start
ldr r2, =_start  /* link addr */
adr r3, _start   /* load addr */
sub r2, r2, r3
sub r1, r0, r2   /* 源 */
ldr r3, =__bss_start
sub r2, r3, r0
bl memcpy    /* r0: 目的, r1: 源, r2:长度 */


5.清除BSS段


5.1. C语言中的BSS段


程序里的全局变量,如果它的初始值为0,或者没有设置初始值,这些变量被放在BSS段里。

char g_Char = 'A';
const char g_Char2 = 'B';
int g_A = 0;  // 放在BSS段
int g_B;      // 放在BSS段


BSS段并不会放入bin文件中,否则也太浪费空间了。

在使用BSS段里的变量之前,把BSS段所占据的内存清零就可以了。


5.2. 清除BSS段


5.2.1 BSS段在哪?多大?


在链接脚本中,BSS段如下描述:

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


BSS段的起始地址、结束地址,使用__bss_start和__bss_end来获得,它们是链接地址。


5.2.2 怎么清除BSS段


ldr r0, =__bss_start   /* 目的 */
mov r1, #0             /* 值 */
ldr r2, =__bss_end     
sub r2, r2, r1         /* 长度 */
bl memset              /* r0: 目的, r1: 值, r2: 长度 */


6.代码段重定位


6.1. 代码段不重定位的后果


谁执行了数据段的重定位?

谁清除了BSS段?

都是程序自己做的,也就是代码段那些指令实现的。

代码段并没有位于它的链接地址上,并没有重定位,为什么它也可以执行?


因为重定位之前的代码是使用位置无关码写的,后面再说。


如果代码段没有重定位,则不能使用链接地址来调用函数:


汇编中


ldr  pc, =main   ; 这样调用函数时,用到main函数的链接地址,如果代码段没有重定位,则跳转失败


C语言中

void (*funcptr)(const char *s, unsigned int val);
funcptr = put_s_hex;
funcptr("hello, test function ptr", 123);


6.2. 代码段重定位


6.2.1 代码段在哪?多大?


这要看链接脚本,对于MPU的程序,代码段、数据段一般是紧挨着排列的。

所以重定位时,干脆把代码段、数据段一起重定位。


链接脚本

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


对于这样的代码:

.text
.global  _start
_start:


确定目的

ldr r0, =_start


确定源

adr  r1, _start


确定长度

ldr r3, =__bss_start
sub r2, r3, r0


6.2.2 怎么重定位


ldr r0, =_start
adr  r1, _start
ldr r3, =__bss_start
sub r2, r3, r0
bl memcpy


6.3 为什么重定位之前的代码也可以正常运行?


因为重定位之前的代码是使用位置无关码写的:


只使用相对跳转指令:b、bl


不只用绝对跳转指令:

ldr pc, =main


不访问全局变量、静态变量、字符串、数组


重定位完后,使用绝对跳转指令跳转到XXX函数的链接地址去

bl main         // bl相对跳转,程序仍在原来的区域运行
ldr pc, =main   // 绝对跳转,跳到链接地址去运行
ldr r0, =main   // 更规范的写法,支持指令集切换
blx r0


7.重定位的纯C函数实现


7.1. 怎么得到链接脚本里的值


对于这样的链接脚本,怎么得到其中的__bss_start和 __bss_end:

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


7.1.1 汇编代码


ldr  r0, =__bss_start
ldr  r1, =__bss_end


7.1.2 C语言


方法1

声明为外部变量,使用时需要使用取址符:

extern unsigned int __bss_start;
extern unsigned int __bss_end;
unsigned int len;
len = (unsigned int)&__bss_end - (unsigned int)&__bss_start;
memset(&__bss_start, 0, len);


方法2

声明为外部数组,使用时不需要使用取址符:

extern char __bss_start[];
extern char __bss_end[];
unsigned int len;
len = __bss_end - __bss_start;
memset(__bss_start, 0, len);


7.2. 怎么理解上述代码


对于这样的C变量:

int g_a;


编译的时候会有一个符号表(symbol table),如下:

Name Address
g_a xxxxxxxx


对于链接脚本中的各类Symbol,有2中声明方式:

extern unsigned int __bss_start;     // 声明为一般变量
extern char __bss_start[];           // 声明为数组


不管是哪种方式,它们都会保存在符号表里,比如:

Name Address
g_a xxxxxxxx
__bss_start yyyyyyyy


对于int g_a变量

使用&g_a得到符号表里的地址。

对于extern unsigned int __bss_start变量

要得到符号表中的地址,也是使用&__bss_start。

对于extern char __bss_start[]变量

要得到符号表中的地址,直接使用__bss_start[],不需要加&

为什么?`__bss_start本身就表示地址啊

相关文章
|
2月前
|
Ubuntu Linux
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
237 3
|
5天前
|
设计模式 人工智能 算法
编程之旅:从代码到架构的感悟
【9月更文挑战第33天】在编程的世界里,代码不仅是实现功能的工具,更是连接思想与现实的桥梁。本文将通过个人的编程经历,分享从编写第一行代码到设计系统架构的旅程,探索编程背后的哲学和技术演变。我们将一起思考,如何在代码的海洋中找到自己的航向,以及在这个过程中如何不断成长和适应变化。
|
26天前
|
机器学习/深度学习 测试技术 数据处理
KAN专家混合模型在高性能时间序列预测中的应用:RMoK模型架构探析与Python代码实验
Kolmogorov-Arnold网络(KAN)作为一种多层感知器(MLP)的替代方案,为深度学习领域带来新可能。尽管初期测试显示KAN在时间序列预测中的表现不佳,近期提出的可逆KAN混合模型(RMoK)显著提升了其性能。RMoK结合了Wav-KAN、JacobiKAN和TaylorKAN等多种专家层,通过门控网络动态选择最适合的专家层,从而灵活应对各种时间序列模式。实验结果显示,RMoK在多个数据集上表现出色,尤其是在长期预测任务中。未来研究将进一步探索RMoK在不同领域的应用潜力及其与其他先进技术的结合。
67 4
|
15天前
|
存储 缓存 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等语言编写代码并无缝集成。这一框架凭借其强大的功能和广泛的社区支持,已成为软件开发领域的重要工具,适合初学者深入学习以奠定职业生涯基础。
92 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模式。通过这种方式,开发者可以构建更加高效和可扩展的桌面应用程序。
65 0
|
2月前
|
存储 前端开发 数据库
神秘编程世界惊现强大架构!Web2py 的 MVC 究竟隐藏着怎样的神奇魔力?带你探索实际应用之谜!
【8月更文挑战第31天】在现代 Web 开发中,MVC(Model-View-Controller)架构被广泛应用,将应用程序分为模型、视图和控制器三个部分,有助于提高代码的可维护性、可扩展性和可测试性。Web2py 是一个采用 MVC 架构的 Python Web 框架,其中模型处理数据和业务逻辑,视图负责呈现数据给用户,控制器则协调模型和视图之间的交互。
29 0
|
2月前
|
消息中间件 缓存 Java
如何优化大型Java后端系统的性能:从代码到架构
当面对大型Java后端系统时,性能优化不仅仅是简单地提高代码效率或硬件资源的投入,而是涉及到多层次的技术策略。本篇文章将从代码层面的优化到系统架构的调整,详细探讨如何通过多种方式来提升Java后端系统的性能。通过对常见问题的深入分析和实际案例的分享,我们将探索有效的性能优化策略,帮助开发者构建更高效、更可靠的后端系统。