This article was last updated on <span id="expire-date"></span> days ago, the information described in the article may be outdated.
今天我们对一则简单的 STM32 程序进行分析,通过寄存器的状态以及反汇编指令对 STM32 的指令流水线进行一个简单的了解。
我们今天只对从 main
函数开始的部分进行分析,STM32 的启动过程在 startup_stm32f10x_md-vl.s
文件中以汇编指令进行编写,留作以后分析。
下面给出本次需要分析的 C
代码:
1 |
|
在 μVision
中新建工程,在调试中选择使用模拟器调试,之后对程序进行 ReBuild
,就可以开始我们的分析了。
我们从 main
函数的入口开始分析,所以将函数断点设置在 int main(void)
处,按下 Ctrl + F5
,程序自动为我们执行到了断点处,并给出了 main
函数入口处的反汇编代码:
1 | 57: int main(void) { |
首先第一行,我们看到程序执行了 PUSH
对 r4
至r11
以及lr
指令集进行了入栈,这里我们主要关注两个点:
- 为什么在程序在通用寄存器中仅仅对
r4 - r11
进行了入栈状态保存? - 为什么需要对
lr
寄存器进行状态保存?
我们首先来看第一个问题 - 为什么只对 r4 - r11
进行了状态保存
这个问题与 STM32
中对寄存器的分类有关:
r0 - r3
在 STM32
除了通用的寄存器作用之外,需要用作传入参数的状态保存;而在程序返回值时,他们则负责保存程序的返回值。而在子程序的调用之间,程序可以将 r0 - r3
用作任何用途;这也就说明在被调用函数返回之前不需要恢复他们的值。
而如果调用函数需要再次使用他们的内容,程序反而需要保留他们的内容(最常见的例子一般是递归调用)
而 r4 - r10
寄存器保存的内容根据 STM32
的规定,在函数返回之前必须要恢复这些寄存器的值。
第二个问题 - lr
寄存器的作用以及为什么需要保存它
首先对这个寄存器进行简单的介绍:lr
- Link Register
(链接寄存器)执行子程序调用指令(BL )时,会自动完成将当前的PC的值减去4的结果数据保存到LR寄存器。即将调用指令的下紧邻指令的地址保存到LR。
而如果我们此时观察寄存器中 lr
的值:
会发现它保存了一个地址 - 0x080001AB
;那么这个地址代表什么呢?
可以看到,我们的 main
函数实际上是由 0x080001A6
处的 BL.W
指令跳转执行到的,而紧接着下一条执行的便是位于0x080001AA
的跳转至 BL.W exit (0x080004C4)
指令。
也就是说:lr
寄存器在进入函数之前对函数返回后需要跳转的地址进行保存;当函数返回之后,lr
寄存器的值会被更新,指示流水线下面需要继续执行哪一步的指令。
我们继续,程序执行到了:SUB sp,sp,#0x14
这一行。
有基础编程经验的朋友应该都知道 sp
寄存器 - Stack Pointer
作为栈指针保存了当前栈顶的位置,当在程序中需要为在栈上分配内存时就会推动该指针为程序保留指定大小的栈内存;而在脱离了变量作用域之后则会回推该指针表示释放了指定大小的栈内存。
这里我们的 SUB
指令将栈指针推动了 0x14
- 也就是20字节,这也就意味着在下面程序中变量比较多,其中有20字节的栈内存被使用 - 这点我们下面会做印证。
下面我们的的程序进行到了变量初始化并赋值的阶段:
1 | 59: int a = 1, b = 2, c = 3, d = 4, e = 5, f = 6, g = 7, h = 8, i = 9, j = 10, k = 11, l = 12; |
这段看起来比较多,但大都是重复的,我们有这几个点需要注意:
- 为什么立即数赋值操作一部分使用的是
MOVS
指令,另一部分是MOV
指令? -
MOV + STR
- 栈内存的赋值操作
第一个问题 - 为什么一部分移动使用 MOV
,另一部分使用 MOVS
这个问题同样与 STM32
的寄存器分配有关,根据官方的说明文档:
我们可以看到,官方对 r0 - r7
这8个通用寄存器称为 “低位寄存器”;而相对的 r8 - r12
则为 “高位寄存器”;而在程序中涉及到高位寄存器的四步操作,程序统一使用了 32 位的 MOV
指令(ARM
指令集);
这也就意味着,在低位寄存器进行立即数赋值操作时,程序总是需要使用 16 位的 MOV
指令(thumb
指令集),而如果表示的是符号数的话,则需要使用 MOVS
对其进行符号位扩展。
而在高位寄存器的立即数赋值中 STM32
默认使用了符号位扩展,所以只需要直接调用 MOV
指令即可,这一点在官方文档中也得到了印证:
第二部分 - 栈内存的赋值操作
因为寄存器数量不够,没有办法保存我们程序中要用到的所有变量,编译器向栈内存申请了一部分空间进行保存,这个保存的过程使用 MOVS + STR
两条指令完成:
首先将立即数保存至 r0
寄存器
之后使用 STR
指令将 r0
寄存器的内容保存至栈内存,这个栈内存的地址通过栈指针 sp
加上一个偏移量确认
请大家记住,我们已经使用了4个 int
类型长度的栈内存 - 也就是 16 字节
程序现在运行到了最关键的部分 - 子程序调用以及数组赋值;我们来看生成的反汇编代码:
1 | 63: uiMas[1] = Fun(a, b, c, d, 0xabcd); |
首先,程序将立即数 0xABCD
移动到了 r0
寄存器以备使用(MOVW
表示移动2个字节);而参数中按照顺序调用了 a, b, c, d
这四个参数,可以看到程序按照倒序依次将 d, c, b
三个变量的值移动至了 r3, r2, r1
这三个寄存器作为参数。
按照我们上面对参数的四个寄存器的描述,剩下需要将 a
变量移动至 r0
寄存器,可是现在 r0
寄存器被用来存放临时的立即数 0xABCD
,所以程序使用 STR
将这个立即数保存至栈内存 sp
指针的地址。请注意,这里我们再次使用了 4 个字节的栈内存空间 - 至此,我们在 main
函数开始时分配的 20 字节栈内存被全部使用。
解决了 r0
的冲突问题,程序将 a
变量移动至 r0
寄存器。
下面是这次需要关注的重点:BL.W
指令,我们的程序使用它来进行子程序调用;通过对寄存器状态的关注,我们来看看这条指令在执行时发生了什么:
lr
寄存器被更新至0x8000024B
- 也正是我们刚刚执行函数跳转之后的指令地址pc
寄存器被更新至0x080001BC
- 也就是Fun
函数的入口,指示流水线下面一步该执行的指令位置
我们现在进入了 Fun
函数,关注生成的反汇编代码:
1 | 49: int Fun(unsigned int a, unsigned int b, unsigned int c, unsigned int d, unsigned int e) { |
和进入 main
函数时相同,程序对 r4 - r7
以及 lr
寄存器的值进行了入栈。
之后程序将 r0
寄存器分配给变量 iLoc
使用,为了保存函数参数 a
的值,程序将 r0
的数据移动至寄存器 r4
。
之后程序从数据地址 sp + 0x14
进行了 LDR
操作,可以看到这个地址存储着值 0x0000ABCD
- 也就是我们刚刚在栈内存存储的临时值,并把它移动到 r5
寄存器,以备使用。
由于 Fun
函数内部剩下的代码并不是很难理解,我们将几条语句放在一起分析:
1 | 51: iLoc = a; |
因为 iLoc
存储在 r0
,程序将刚刚备份的参数 a
从寄存器 r4
拷贝回寄存器 r0
。
之后进行了一些普通的算术运算,并将临时的结果存储在 r6
寄存器,当运算结果完成之后将结果重新放回了 r0
寄存器。
好了,现在 r0
寄存器保存着我们的 iLoc
变量 - 也正是 Fun
函数的返回值,函数开始进行返回的操作 - 使用 POP
指令从栈内存中调出函数执行前的程序状态,更新 pc
寄存器到需要执行的指令地址。至此,Fun
函数的调用过程全部完成。
我们现在来关注得到返回值之后,程序是如何对数组进行赋值的:
1 | 0x0800024A 490F LDR r1,[pc,#60] ; @0x08000288 |
程序首先将 0x08000288
处的值拷贝至 r1
寄存器,我们来看看这个地址存储着什么:
这个地址存储着一个地址,指向了 0x20000000
这个地址,而这个地址正是数组 uiMas
的首地址。
之后程序对该地址偏移量 4 的地址进行了 STR
操作,也就是数组的第2位被改变了。
现在程序需要对一串值进行累加操作,我们来看看这部分的反汇编代码:
1 | 65: a += b += c += d += e += g += f += h += i += k += l += j; |
这部分与上部分一样,需要注意的是,刚刚存储在栈上的变量现在仍然需要在栈上更新 - 也就是说,这部分变量的更新需要首先调用 LDR
将值保存至寄存器,之后使用 STR
保存回栈上。
现在进入程序的最后一部分:main
函数的返回:
1 | 67: return 0; |
首先我们看到,因为函数的返回值是 0 ,程序将 r0
寄存器更新至立即数 0。
之后将 sp
寄存器增加了 20 - 也就是推回20个字节,释放了刚刚在 main
函数内申请的栈内存。
在这之后将 r4 - r11
寄存器恢复至执行 main
函数之前的状态,而 lr
寄存器的状态被更新至 pc
寄存器,用于指示下一步执行的函数 - 也就是我们开始所说的 0x080001AA
,这里存储着跳转至 exit
函数的指令。
至此,我们的分析过程全部完成。
从这次的分析过程中我们可以了解到 STM32
指令流水线的一些基本特点;我们还学习了 B
指令的特点,了解了函数传参以及返回值的方式。在这个基础上我们对 STM32
的寄存器有了更加深入的了解。
这两天感冒挺严重的,还是很羡慕室友们一个个都有知冷知热的女朋友…
顺便祝大家身体健康。
Comments