第二章 两类武器 【保持队形的废话】 初学者可能会困惑: ·局部变量分配在哪?接下来,我们对程序进行编译,得到以下反汇编代码: ·全局变量又是分配在哪?
·它们与栈、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,可搜索变量)
|