大二的单片机课程用的 Arduino UNO,靠的 Arduino IDE 上手蛮简单,但封装好的框架可玩性不高,所以就用 avr GNU 套件做交叉编译直接写裸机 C 语言。
先前基于 UART 实现了 shell,可以做输入输出,在此基础研究函数调用。
Shell 代码是 #71783bd commit。这个 commit 实现了 cmd_sp 和 cmd_stack,前者可以打印当前的栈指针 SP 后者做了两层函数调用并打印每一层的栈指针地址,由此配合 avr-objdump 反编译可以分析了。
代码分析
Shell 整体就不说了,这套我叫 GooseHeap Shell,中文名就是鹅堆(谐音梗哈哈哈哈)。 用 UART 中断和环形缓冲区实现了 uart_putc() 和 uart_puts()。因为看地址用十六进制比较舒服,基于 uart_putc() 实现打印十六进制的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
void uart_put_hex_digit(uint8_t n)
{
if (n < 10)
{
uart_putc('0' + n);
}
else
{
uart_putc('A' + (n - 10));
}
}
void uart_put_hex16(uint16_t n)
{
uart_puts("0x");
uart_put_hex_digit((n >> 12) & 0xF);
uart_put_hex_digit((n >> 8) & 0xF);
uart_put_hex_digit((n >> 4) & 0xF);
uart_put_hex_digit(n & 0xF);
}
|
打印 16 位二进制的 hex 表示就已够 SP 用,完成基础设施后首先实现 SP 的打印:
avr-libc 提供了 SP 宏直接读取 SP 寄存器对应地址的值。
1
2
3
4
5
6
7
8
|
void cmd_sp(void)
{
uint16_t sp = SP;
uart_puts("SP = ");
uart_put_hex16(sp);
uart_puts("\r\n");
}
|
接下来设计函数调用,设计一下的调用链:
1
|
cmd_stack -> foo -> bar
|
为了防止编译器做内联优化函数声明前用了 __attribute__ 宏修饰:
1
|
__attribute__((noinline))
|
并且防止尾调用优化 foo 调用 bar 后还需要再写个语句使 bar 不作为尾调用。
最后实现的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
__attribute__((noinline)) void bar(void)
{
uint16_t sp = SP;
uart_puts("bar: ");
uart_put_hex16(sp);
uart_puts("\r\n");
}
__attribute__((noinline)) void foo(void)
{
uint16_t sp = SP;
uart_puts("foo: ");
uart_put_hex16(sp);
uart_puts("\r\n");
bar();
uart_puts("bar returned\r\n");
}
void cmd_stack(void)
{
uint16_t sp = SP;
uart_puts("cmd: ");
uart_put_hex16(sp);
uart_puts("\r\n");
foo();
uart_puts("foo returned\r\n");
}
|
栈
使用 sp 命令即可打印当前的 SP 指针:
Arduino UNO 只有 2KB 的内存,也就是 0x800 不过前 0x100 映射给寄存器了,所以实际内存对应地址是 0x8FF ~ 0x100。
再用 stack 指令查看 SP 的变化:
1
2
3
4
5
6
|
> stack
cmd: 0x08F6
foo: 0x08F2
bar: 0x08EE
bar returned
foo returned
|
可见每一次调用 SP 都减去 4 字节,因为栈是从高地址向低地址变化。
UNO 的硬件在每一次 call 的时候都会将当前 PC 弹入栈,但 PC 寄存器实际只占了 2 字节(16位) 还有 2 字节需要看反汇编了。

以 foo 为例,在函数开头弹入了 r28, r29 寄存器,这两寄存器是 1 字节的,所以总共 4 字节。对应输出。在函数返回的时候就 pop 弹出了这些寄存器。这些寄存器是要保护的调用者的寄存器,也就是上下文保护。