IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> ARM汇编:MOV/LDR陷阱导致offset out of range -> 正文阅读

[嵌入式]ARM汇编:MOV/LDR陷阱导致offset out of range

错误现象

先来说一下最近碰到的问题。在编译阶段,编译器报如下错误信息:

C:\Users\Admin\AppData\Local\Temp\1\ccQUTPlr.s: Assembler messages:
C:\Users\Admin\AppData\Local\Temp\1\ccQUTPlr.s:35: Error: offset out of range

看到...out of range,我第一反应是链接文件出问题了。但立马反应过来,这还没有到链接阶段呢,在编译obj的时候就报错了。老实说,碰到这种问题是比较棘手的,要么你碰到过类似问题,有经验的情况下可能会比较快的得到解决。如果没有经验,这类问题难就难在导致它的原因比较多,除了代码本身,可能与你的编译器(参数)设置,工程配置可能都有关系,加上错误信息就寥寥几个字。

不过这次最先碰到这个问题的不是我本人,所以在拿到这个问题的时候我已经掌握了一些有用信息(感谢信息提供者Mr Zhao,我又节约了很多时间), 比如这个问题与代码量有关。当尝试在某个文件里面屏蔽一些代码后就可以编译了,也就是说问题范围圈缩小了,基本上定位到这个文件或者是与这个文件关系比较紧密的文件,比如(间接)引用的文件等。过了几天后,我又得到反馈说这个问题解决了。方法就是添加了一个编译器参数。由于拿到这个问题的时间段,工作比较忙,所以没来得及第一时间处理,听到解决了的时候,心里暗喜(毕竟又可以偷懒了,😄,还学了东西)。但是再过几天我稍为空的时候来想这个问题,这个解决方法似乎并不靠谱,从添加的这个参数解释来说好像有些地方解释不通,也就是说根本原因还是云里雾里的。所以我又重新分析这个我问题,最后发现根本原因的确不是这个编译选项导致,这个过程中让我重新认识了一遍曾经以为很简单的汇编指令,很有必要记录一下,所以有了本篇文章。

问题原因

经过一系列测试,调试(说的比较高大上,其实就是不断屏蔽代码来缩小范围😢 ),最后发现导致该问题的罪魁祸首就是下面这行代码:

__asm volatile("LDR R2,= 0xE000EF38")

LDR, MOV回锅

说实话,在这之前,我真没有觉得LDR,MOV指令有什么难的(其实是太孤陋寡闻了,还停留在8051时代)。结果就在这上面上当了,今天重头开始学习,回锅肉多炒炒。

首先我们来普及一下这两个命令的作用(虽然可能大家都很熟悉了)。

这里需要强调一点,本文是在ARM架构下面来讲解的,不同架构下面,指令作用不一样。

MOV指令的作用就是在寄存器之间传递数据,或者将立即数拷贝到寄存器里面。而LDR指令类似于C语言里面的指针。我们C语言里面经常使用的将某个内存区域里的数据拷贝到寄存器里面,只能通过LDR来实现,MOV指令是无法完成的。

直接看示例代码:

MOV R0, R1
MOV R0, #1
LDR R0, 0X11223344

上面第一行MOV的作用就是把R1寄存器里面的内容拷贝到R0里面。第二行则是将立即数1赋值到R0里面。最后一行LDR的意思是将0X11223344这个地址里面的值拷贝到R0中,注意0X11223344是作为地址,而不是它本身,这不就是C语言里面的指针了么。MOV是没有LDR这种功能的,MOV后面第二个操作数是立即数。那反过来,LDR能不能实现MOV的功能呢?答案是肯定的。并且它还引入另一个概念:LDR伪指令。区别于LDR伪指令,上面我们提到的LDR指令叫加载指令。那么伪指令有什么区别呢?

直接看代码:

LDR R0, =0X11223344

看到区别没有?数字前面多了一个=号。这就是伪指令,它代表的含义就不一样了,它的意思是将0X11223344写到R0中,这一点看就和MOV一样了(至少功能上相似了)。

但是,LDR伪指令和MOV在使用上还是有区别的。MOV指令对后面的立即数实际上是有要求的,从ARM官方cortex-m7的手册来看,MOV后面的立即数/常量范围限制是8bits。但是这里并非是说真的只能跟一个小于等于8bits的数据。32bits数据也是可以的,只是说8bits范围内的所有整数都是可以的,但是大于8bits的数据不是所有数据都可以,这里面涉及到另外一个机制:移位。简单来说,如果你的这个32bits的数据可以通过一个8bits数据移位得到,那么这个数据也是可以的。这个过程编译器在编译翻译为机器码的时候自动完成。另外这里稍微提一下另外一个指令,MOVW,这个指令后面可以跟16bits立即数,但是好像没有移位这个机制。具体MOV指令的其他语法以及移位机制的详细原理这里就不讲解了,请看官方文档。这里还是贴一下图。
请添加图片描述

我们再来看看LDR的语法。

请添加图片描述

我给大家把重点的地方拎出来翻译一下。重点看expression下面两段话。如果表达式的值在MOV指令范围内,则汇编器直接生成指令。这里所谓的MOV指令范围内,其实就是说的上面描述的MOV指令对立即数的那些限制。重点,重点,重点。如果表达式的值不在MOV指令范围内,则汇编器会将这个立即数放入文字池/字符池(literal pool)并生成程序相关LDR指令,这个指令会从文字池中读这个立即数。pc指针到这个立即数的偏移必须小于4K。你必须保证在4K范围内有一个文字池。

原文...program-relative LDR instruction...处翻译有点怪。这里有两个问题,首先什么是文字池?什么是program-relative LDR instruction? 文字池我们可以理解为map文件里面的各个section。链接文件里面我们也会定义各个section,每个section就是一个文字池,至于编译器怎么产生的,怎么和它交互的,我们不用关心(其实我也讲不明白😢)。第二个问题,所谓的那个什么指令,在代码层面来看,如果那个值的范围符合MOV的范围,那么比较好理解。如果不符合,编译器在翻译的时候会将这个值转换一下,不是直接去访问这个值了,而是去访问当前pc指针到文字池的偏移,(这个偏移肯定是小于4K的),然后从这个偏移所指向的地方里面去读。这里其实又和C语言里面的指针有点类似。可能你还是有点不知所云,我将两者情况用编译后的汇编来表示就清晰了。

立即数在MOV范围内(假如立即数是255):

mov.w r2, #255

立即数不在MOV范围内(假如立即数是0xe000ef38):

0:		f8df 2fcc 	ldr.w	r2, [pc, #4044] ;FCC
1:  	......
fce:	0000      	.short	0x0000
fd0:	e000ef38 	.word	0xe000ef38

上面第一种情况最直接。重点来看看第二种情况。这里的汇编我全部贴出来了,前面0,1,fce,fd0是地址信息,后面是机器码,再后面才是对应的汇编。由于0xe000ef38这个数不符合MOV指令的条件,于是在翻译的时候你看汇编器不是直接去访问这个数了,而是使用偏移的方式去访问,看r2后面的东西,[pc, #4044],也就是pc指针往后偏移4044这么多的地方去取值。当前指令所在地方是0,往后偏移4044+4,注意这里我加了4,因为是pc指针基础上偏移4044,这不就变成4048(fd0)了么,你看最后一行fd0的地方不就是放的原始值么?是不是就是和C语言的指针一样,纯粹是为了让第一行后面那个值变一下以满足MOV的规则。最后fd0这里就是文字池。至于前面fce那里,这里编译器自动填充了两字节,否则就不对齐了(M7里面只有部分指令支持不对齐访问,比如LDR,LDRT,STR等)。

你是不是有个小小的疑问?我们不是再说LDR指令么,为何又是MOV指令的范围?没错,LDR指令和MOV联系非常紧密。当立即数符合MOV指令条件时,其实LDR指令最后是翻译为MOV指令来执行的。如上面第一种情况:

;原始指令
__asm("LDR R2,=255")
; 编译之后的汇编代码
0:	f04f 02ff 	mov.w	r2, #255	;0xff 

解决方法

再继续说说这个文字池,当立即数不符合条件时,解决方法其实官方文档已经给出了。那就是引入文字池,方法就是在合适的地方插入LTORG伪指令,当编译器碰到该指令的时候就让汇编器立即汇编文字池。那么什么地方才是合适的地方呢?通常情况下在无条件分支或子程序返回指令之后。因为只有这样,程序才不会将这个常量当作指令去执行,你看我们上面翻译后的汇编,它就是在某个地方放入了一个常量( .word 0xe000ef38 ),如果把这个语句当作指令执行肯定会出错的,因为它本身就不是一条合法的指令,一旦执行,会产生错误(Hard Fault),而放在上述地方,它不会被执行到。一个大型程序可能需要加入多个LTORG伪指令。该指令加入的地方也要保证与LDR指令所在地方的偏移小于4K(事实上这里的4K是指在ARM指令集下,Thumb指令集的限制是1K)。如上面的例子,我们加入该指令的地方与LDR指令所在地方的偏移是fd0/4048,是小于4K的,如果我们往后放一些,使其大于4096,那么一样会出错。

如下面是官方给的示例:

                AREA            Example, CODE, READONLY
start           BL              func1
func1                                                           ; function body
                ; code
                LDR             r1,=0x55555555                          ; => LDR R1, [pc, #offset to 
                                                                ; Literal Pool 1]
                ; code
                MOV             pc,lr                           ; end function
                LTORG                                           ; Literal Pool 1 contains 
                                                                ; literal &55555555.
data            %               4200                            ; Clears 4200 bytes of memory, 
                                                                ; starting at current location.
                END                                             ; Default literal pool is empty.

插入LTORG伪指令的方法在全汇编的使用场景下还是比较方便的,但是如果是在C语言和汇编混合编程的情况下,其实是不太适用的,尤其是大部分都是C语言函数,偶尔插入LDR指令这种情况。因为C语言的函数返回对于编程人员来说都是黑盒子,由编译器帮我们处理的,程序员无法在return后面去添加伪指令,但是条件分支倒是可以,也就是将这个伪指令适用条件语句包裹起来,但是这样需要引入额外的条件变量,程序通过保证这个变量不满足条件,也就不会执行里面的伪指令。这种方法除了引入额外的变量之外,有时候编译器优化可能会把这种条件语句给优化掉。所以也不是百分百能成功的,需要小心检查。

除了上面说的方法外,当然还有第二种方法,根本原因我们找到了,就是那个立即数不合法,当然这里肯定不是让你去该掉这个立即数,要是这样就不用我花这么多时间来敲文字了,我们讨论的情况肯定是这个值不能改。继续哈,不合法,并且下一个文字池偏移超过了4K。那如果我将文字池偏移缩小使其小于4K不久好了么。对的,完全没错,最开始我说的添加一个编译器参数。这个编译器参数就是去修改这个文字池的。默认情况下,如果不加这个参数,文字池是以一个文件为单位的。如下图所示:

 *(.text)
 .text          0x00401614       0x10 ./Project_Settings/Startup_Code/system.o
                0x00401614                Sys_GetCoreID
 .text          0x00401624     0x1254 ./src/Os_Intvet.o
                0x00401624                SysTick_Handler
                0x0040162a                MSCM_MSI_IRQHandler
                0x00402210                SysTick_Handler111
 .text          0x00402878       0x20 ./src/main.o
                0x00402878                MSCM_MSI0_IRQHandler

如上map文件所示,默认情况下,每个文件,不管你文件里面有多少函数,都是统一编译到一个section(.text)的。很明显,如果你某个文件里面内容比较多,就会导致这个文字池比较大。

编译器为我们提供了一个参数:-ffunction-sections。添加这个参数后,我们再看看map文件变成什么样了。

 .text.Siul2_Port_Ip_SetGPDO
                0x00405220       0x14 ./RTD/src/Siul2_Port_Ip.o
                0x00405220                Siul2_Port_Ip_SetGPDO
 .text.Siul2_Port_Ip_PinInit
                0x00405234       0xc0 ./RTD/src/Siul2_Port_Ip.o
 .text.Siul2_Port_Ip_Init
                0x004052f4       0x2c ./RTD/src/Siul2_Port_Ip.o
                0x004052f4                Siul2_Port_Ip_Init
 .text.SysTick_Handler
                0x00405320        0x8 ./src/Os_Intvet.o
                0x00405320                SysTick_Handler
 .text.startup.main
                0x00405328       0x3c ./src/main.o
                0x00405328                main
 .text.MSCM_MSI0_IRQHandler
                0x00405364       0x20 ./src/main.o
                0x00405364                MSCM_MSI0_IRQHandler
                0x00405384                . = ALIGN (0x4)

看到没有,添加之后就变成以每个函数为单位进行了,section的名字也是:.text.xxx,xxx代表函数名。这样是不是每个section的大小就大大缩小了。这就是为何大部分时候添加这个编译器参数后就可以我们的问题了。这里为何说大部分时候,而不是一定能解决。假如某个函数特别大,大小超过了4K,正好在里面也适用了LDR汇编指令,那么一样会报错。这种情况你还是得通过前面的方法来解决。

-ffunction-sections是针对函数的,同样针对数据也有单独的参数:-fdata-sections。添加-ffunction-sections这种选项会增加文字池大小,很明显,现在section的名字比以前要多很多了,肯定会占用更多的空间(导致二进制文件变大)。为了节省空间,我们可以通过添加:-Wl,--gc-sections这种参数告诉链接器把我们没有使用到的给优化掉。但是需要注意的是这些选项在某些情况下会破坏程序。

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2021-08-25 12:22:20  更:2021-08-25 12:23:47 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/28 17:46:29-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计