立即注册 找回密码

微雪课堂

搜索

《μC/OS-II卧槽宝典(上)》 【连载 第二章 两类武器】

2016-9-26 15:12| 发布者: waveshare-admin| 查看: 1579| 评论: 0

摘要: 初学者可能会困惑: ·局部变量分配在哪?接下来,我们对程序进行编译,得到以下反汇编代码: ·全局变量又是分配在哪? ·它们与栈、CPU寄存器有什么关系? ...
第二章 两类武器
【保持队形的废话】
初学者可能会困惑:
·局部变量分配在哪?接下来,我们对程序进行编译,得到以下反汇编代码:
·全局变量又是分配在哪?
·它们与栈、CPU寄存器有什么关系?
· 局部变量
软件的一切神秘的运行机制全在反汇编代码里。(网上的一句话,写的中肯,挪来用下)
不错,C代码华丽的外表下,其“内在”是什么,如果不清楚,那么真相就不甚明了。
所以,本节,我们要做的就是揭开C语言的面纱,看下它的反汇编。下面我们做两个实验,并以此进行研究。
【实验一】
『实验内容』
这里给出一个简单的函数testX():

接下来,我们对程序进行编译,得到以下反汇编代码:

再接下来,我们按代码的执行顺序,观察反汇编代码,并分析局部变量如何分配,赋值。
-----------------------------------------------------

入栈r1-r3及lr,因为本程序将改变r1-r3及lr的值。
说明:r0的值在本程序中也被改变,但外部没有使用,所以,不需要入栈
-----------------------------------------------------

将r2赋值为3,并将它的值传给栈顶指向的地址(a[0]的存储地址分配为“栈顶地址”)
将r2赋值为5,并将它的值传给栈顶+0x08指向的地址(a[2]的存储地址分配为“栈顶地址+0x08”)
将r2赋值为4,并将它的值传给栈顶+0x04指向的地址(a[1]的存储地址分配为“栈顶地址+0x04”)
说明:
·本文使用的编译器,每个int占4个字节,两个int则为0x08字节
·根据汇编指令STR的功能,STRr2,[sp, #0x00]是将sp+0x00地址中的值传给r2,但不改变sp的值
-----------------------------------------------------

将栈顶地址中的值(即a[0]的值)传给r2,并判断它是否为3
·如果不是:跳到0x0800325A处(if(a[1]==4)语句的地址)
·如果是:将r2赋值为6,并将它的值传给栈顶地址(即传给a[0])
将r1赋值为4(即传给b);将r0赋值为5(即传给c)
-----------------------------------------------------

将地址“栈顶+0x04”中的值(即a[1]的值)传给r2,并判断它是否为4
·如果不是:跳到0x08003264处(if(a[2]==4)语句的地址)
·如果是:将r2赋值为7,并将它的值传给地址“栈顶+0x04”(即传给a[1])
-----------------------------------------------------

将地址“栈顶+0x08”中的值(即a[2]的值)传给r2,并判断它是否为4
·如果不是:跳到0x0800326E处(“}”语句的地址)
·如果是:将r2赋值为11,并将它的值传给地址“栈顶+0x08”(即传给a[2]);
将r0赋值为5(即传给c);
判断结束后,出栈r1-r3及lr(lr需要放到pc里,故,代码为POP {r1-r3,pc}
实验现象
我们在μVisions中查看memory、core,观察到的现象是
·执行“函数开始符‘{’”到“定义变量”这部分代码:
入栈:r1-r3、lr,SP值减小。(STM32是递减堆栈,当PUSH时,SP值减小)
RAM、SP的情况如下:
0x20002224(r1的值)← SP(PUSH后)  
0x20002228(r2的值)
0x2000222C(r3的值)
0x20002230(lr的值)
0x20002234(???)← SP(PUSH前)
·执行“定义变量”后到“函数退出符‘}’”前这部分代码:
SP值不变。数组a存到了SP及SP之后的地址。
RAM、SP的情况如下:
0x20002224(a[0]的值)← SP
0x20002228(a[1]的值)        
0x2000222C(a[2]的值)
0x20002230(lr的值)
0x20002234(???)
·执行函数退出符‘}’代码:
出栈,并存放到:r1-r3、pc,SP值增大。(STM32是递减堆栈,当POP时,SP值增大)
RAM、SP的情况如下:
0x20002224(a[0]的值)→(存到r1)
0x20002228(a[1]的值)→(存到r2)
0x2000222C(a[2]的值)→(存到r3)
0x20002230(lr的值)  →(存到pc)
0x20002234(???) ← SP
潜在问题
读者将发现,原r1-r3并不能被成功恢复,因为它们的值被a[0]、a[1]、a[2]覆盖了。
确实是这样,笔者遇到这样的问题,也汗了一把。
但,笔者很快就为编译器的圆场,猜想:“是否被覆盖了也不会导致程序出现问题?”
带着这个疑问查看了程序,发现,主程序调用testX()函数后,并没有再使用r1-r3,因而,不会出现问题。
所以,编译器还是蛮聪明的,知道没有再使用r1-r3,则肆无忌惮的覆盖它们。
当然,这种聪明有点水分,既然这样,当初何必对r1-r3压栈,直接不压它们不就得了。
前面,我们提到了“覆盖”。“覆盖”这种东西,难免让人不安:
如果,加大数组a的长度,定义为a[5],会是怎样?该不会连lr(pc)也覆盖了吧,那就完了!
带着这个疑问,我们将进行“实验二”。
【实验二】
实验内容
为了一探究竟,笔者将数组a,加大数组长度,定义长度为5,并进行其它少量改动,改动后的程序如下:

接下来,我们对程序进行编译,得到以下反汇编代码:


由于实验一已进行了详细的分析,所以,这里,我们只给出相应的反汇编代码,而不再进行分析。
实验现象
我们在μVisions中查看memory、core,观察到的现象是
·执行“函数开始符‘{’”到“定义变量”这部分代码:
入栈:r4-r5、lr,为数组a[5]腾出空间:SUB sp,sp,#0x14,SP值减小。
RAM、SP的情况如下:
0x20002210(???)← SP(执行SUB sp,sp,#0x14后)
0x20002214(???)
0x20002218(???)
0x2000221C(???)
0x20002220(???)
0x20002224(r4的值)← SP(执行PUSH {r4-r5,lr}后)
0x20002228(r5的值)
0x2000222C(lr的值)
0x20002230(???)← SP(执行PUSH {r4-r5,lr}前)
·执行“定义变量”后到“函数退出符‘}’”前这部分代码:
SP值不变。数组a存到了SP及SP之后的地址。
RAM、SP的情况如下:
0x20002210(a[0]的值)← SP
0x20002214(a[1]的值)
0x20002218(a[2]的值)
0x2000221C(a[3]的值)
0x20002220(a[4]的值)
0x20002224(r4的值)
0x20002228(r5的值)
0x2000222C(lr的值)
0x20002230(???)
·执行函数退出符‘}’代码:
出栈,并存放到:r4-r5、pc,SP值增大。
RAM、SP的情况如下:
0x20002210(a[0]的值)← SP(执行ADD sp,sp,#0x14前)
0x20002214(a[1]的值)
0x20002218(a[2]的值)
0x2000221C(a[3]的值)
0x20002220(a[4]的值)
0x20002224(r4的值)← SP(执行ADD sp,sp,#0x14后)  →(存到r4)
0x20002228(r5的值)                                →(存到r5)
0x2000222C(lr的值)                                →(存到pc)
0x20002230(???)← SP(执行POP {r4-r5,lr}后)
实验一之潜在问题续
实验一,我们提到了“覆盖”问题,之后,基于此问题,展开了实验二。
现在,我们来回顾下,“覆盖”问题最终如何解决。
由于,代码SUB sp,sp,#0x14的作用,局部变量分配的地址不与使用过的地址重叠,所以,避免了“覆盖”。
同时,由于,代码ADD sp,sp,#0x14的作用,SP的值也恢复到调用函数前的值。
调用一个函数如此,调用N个函数当然也如此。(如果按此方法,调用函数退出后,总能得到正确的SP。)
【总结】
局部变量与“CPU寄存器、栈”之间的关系
·当局部变量不多的时候,将它们分配到CPU寄存器中,这样,读写速度是最快的。
这是μVisions编译器编译STM32程序的规则,并不表示所有的编译器都这么做。
·当局部变量较多的时候(笔者有另外经过测试)或局部变量为数组,“习惯”的将它们分配到RAM中。
这种情况,编译器并不会跳过任何RAM,而是“紧挨着”原SP值的地址分配局部变量。
并不是只有μVisions编译器这样处理,作为一个“正常”的C编译器就必须这样。
其实,变量怎么分配,C程序员不必Care,这活由编译器去干①。但由于我们要研究操作系统,才需要清楚。
由前面的分析,我们知道:局部变量可能存放在CPU寄存器Rn中,也可能存放在与SP相关的RAM中(栈)。
这是个非常重要的特点,后续,我们分析MCU需要做什么操作,就要根据这一特点。
编译器需要怎么做,才能让程序正常运行
·执行“函数开始符‘{’”到“定义变量”这部分代码:
·入栈相应CPU寄存器(如果变量分配在CPU寄存器中,且退出函数后,相应CPU寄存器需要被使用)
·入栈LR(这个不是必须的,笔者经过测试,某些情况,不入栈LR,而之后通过BX LR跳转)
·减小SP的值(如果变量分配到栈中,且栈中数据不可被覆盖)
·执行“定义变量”后到“函数退出符‘}’”前这部分代码:
·堆栈不变,SP值不变。分配局部变量可能会借助SP,但并不会改变SP。
·执行执行函数退出符‘}’代码:
·增大SP的值(如果之前有改变过),使得SP值恢复到进入函数后的值。
·出栈LR(这个不是必须的,见前面说明)
·出栈相应CPU寄存器(如果之前有入栈),使得SP值恢复到进入函数前的值。
总结就到这,后续章节,我们将研究任务切换原理,就需要“学习”这的“MCU需要做什么”。
-------------------------------------------------------------------------------------------------------------------------------------------
① 聪明的编译器分配的好点,愚蠢的编译器分配的傻叉点,有BUG的编译器乱分配,以至于程序出错罢了。
-------------------------------------------------------------------------------------------------------------------------------------------
· 全局变量
编译时,编译器将会为全局变量分配固定的RAM地址,而不会像局部变量那样,临时分配在CPU寄存器或堆栈中。因为,在程序运行的过程,全局变量不能因为退出了某个函数就“失效”,不能被“消灭”。
对于STM32,全局变量分配在哪,可以查看编译后生成的后缀为map的文件(打开文件后,Ctrl+F,可搜索变量)

2-2.png (73.07 KB, 下载次数: 44)

2-2.png

158

顶一下

刚表态过的朋友 (158 人)

相关阅读

最新评论

μCOS-II

微雪官网|产品资料|手机版|小黑屋|微雪课堂. ( 粤ICP备05067009号 )

GMT+8, 2019-11-12 06:19 , Processed in 0.018097 second(s), 19 queries .

Powered by Discuz! X3.2 © 2001-2013 Comsenz Inc & Style Design

返回顶部