单片机 MCU 中 stack 使用的探讨
stack 的使用,是单片机开发中影响最大,但是最少被讨论的问题。而提及这个问题的地方,都是对这个问题含糊其辞。
今天花了点时间,使用最笨的办法,直接阅读汇编代码,来对这个问题就行探究,这里做一下记录。
下面是本次实验使用的代码,代码本身没有意义,仅作探讨 stack 相关问题使用:
short c; short g(short a) { short b[10] = {0x03}; short i = 0; for(i = 0; i<10; i++) { b[1] += b[i] + a; } if(b[1] < 200) g(b[1]); return b[1]; } int main(void) { c=g(1); }
使用 COSMIC 针对 s12z 的编译器 cxs12z 对代码进行编译,得到的汇编如下:
1 .const: section .text 2 L3_b: 3 dc.w 3 4 ds.b 18 5 switch .text 6 _g: 7 psh d2 8 lea s,(-25,s) 9 OFST: set 25 10 .dcall "30,2,_g" 11 lea x,(OFST-25,s) 12 ld y,#L3_b 13 ld d2,#5 14 L4: 15 mov.l (y+),(x+) 16 dbne d2,L4 17 clr.w (OFST-2,s) 18 lea x,(OFST-25,s) 19 ld d2,(OFST-23,s) 20 L5: 21 ld d3,(x+) 22 add d3,(OFST+0,s) 23 add d2,d3 24 inc.w (OFST-2,s) 25 ld d4,(OFST-2,s) 26 cmp d4,#10 27 blt L5 28 st d2,(OFST-23,s) 29 st x,(OFST-5,s) 30 cmp d2,#200 31 bge L31 32 jsr _g 33 .dcall "_g:_g" 34 ld d2,(OFST-23,s) 35 L31: 36 lea s,(27,s) 37 rts 38 _main: 39 .dcall "3,0,_main" 40 ld d2,#1 41 jsr _g 42 .dcall "_main:_g" 43 st d2,_c 44 rts 45 xdef _main 46 xdef _g 47 switch .bss 48 _c: 49 ds.b 2 50 xdef _c 51 end
阅读汇编代码:
1. 第 1-5 行,存储数组 b 的初始化值到 const 区。b[0]为3,b[1]-b[9] 为 0。
2. 第 6 行,函数 g 的标签。
3. 第 41 行调用了函数 g,并使用寄存器 d2 传递 short 型参数给函数 g。根据 Compiler 的文档,调用函数时如果需要传递参数,优先使用 cpu 的 register,符合下面要求的第一个参数将被放入相应寄存器,不符合的话,会合其它参数一起,被放入 stack。参数会按从右到左的数序压入 stack。这里显然 short 是可以放入 d2 的,而且只有一个参数,没有用到 stack。
char arguments are passed in d0 and d1, short, int and short _Fract arguments are passed in d2, d3, d4 and d5, long, long _Fract and float arguments are passed in d6 and d7, double and long long arguments are passed in d6:d7,
这里需要注意,jsr 调用,本身是会将当前 PC 压入 stack 的(3 个 byte),所以,只是传递参数没有使用 stack。
4. 第 7 行,因为后面的运算可能用到 register d2,所以,先将 d2 压入 stack。
5. 第 8-9 行,s12z 的 stack 操作方式相关,直接对 s 寄存器操作,在 stacks 上为当前函数预留 25 个 bytes 使用。
6. 第 11 行,将 (OFST-25, s )这个地址赋给 x 寄存器,事实上,也就是 b[0] 的地址。
7. 第 12-16 行,对数组 b 进行初始化。每次从 ROM 拷贝 4 个 bytes 到 stack 上,共拷贝了 5 次。
8. 第 17 行,对变量 i 进行了初始化,变量 i 地址为 (OFST-2, s).
9. 第 18 行,再次读取 b[0] 地址到 x 寄存器。
10. 第 19 行,读取 b[1] 内容到 d2 寄存器。c 代码中第二个循环有个累加操作,累加的值会被暂存在 d2 中。
11. 第 21 行,第 25-27 行的判断跳转,这中间的代码完成了 for 循环。这里是加载 b[i] 到寄存器 d3;x 寄存器从(OFST-25,s)开始自加,历遍 b[i]。
12. 第 22.行,b[i] + a
13. 第 23 行,d2 实施上存储的是 b[1] 的值。b[1] += b[i] + a
14. 第 24 行,i++
15. 第 28 行,for 循环已经结束。再保存 d2 到 b[1] 在 stack 上的存储空间;和第 19 行相反的操作。
16. 第 29 行,将地址 x (3 byte,24 位)保存到 (OFST-5, s) 开始的 stack 空间。
17. 第 30-32 行,if(b[1] < 200) g(b[1])。这里有两个分支:
- a. 当 b[1] 小于 200 时,再次用 jsr 调用函数 g。现实使用 3bytes 的 stack 空间保存了当前的 PC 值,然后进入 g 之后,重新分配了 27 bytes 给下一次调用使用!!!仍然使用 d2(b[1])传递参数给函数 g。
- b. 当 b[1] 大于等于 200 时,跳到分支 L31 执行。第 35-37 行,释放掉本次调用占用的 stack 空间,共 27 bytes。第 37 行的 rts 返回的位置,是上一次 jsr _g 的下一行;之前申请的 stack 空间,在迭代达到最深后,随着一个个 rts,先后被释放。
18. 第 34 行,这里,函数迭代调用结束;即,当 jsr _g 的 rts 达到值后,会继续从这里执行。所以,最后一次的 b[1] 被重新赋值给 d2,通过 d2 将返回值传给 mian 函数。在第 43 行,返回值被从 d2 传输到变量 c 的 RAM 空间。
19. 第 40 行,将立即数 1 存入 d2 寄存器。
20. 第 41 行,调用函数 g。
21. 第 42 行,将返回值赋值赋值给 c。
22. 第 45-46 行,声明全局变量,供 link 使用。
23. 第 49-50 行,声明全局变量 c,大小为 2 bytes。
另外,还是用 arm-noneabi-gcc 编译了上面 C 代码,产生的汇编也是大同小异。可见,大家翻译 C 语言和翻译为汇编时,大的讨论是差不多的。
上面的代码涉及了 stack 调用情况的:
1. 函数跳转时,保存 PC 指针 3 bytes;
2. 函数内部变量,即局部变量被放在 stack 上,在这里是。
上面的代码没有涵盖的使用:
1. 使用 stack 传递参数;
2. 使用 stack 传递返回值。
3. 中断跳转和中断返回自动进行 stack 操作的情形。
此外,编译完成后,该编译器会在 map 文件中汇总分析 stack 的使用情况;在使用 arm-noneabi-gcc 时,产生的每个汇编函数都有进行 stack 使用报告。