Featured image of post Arduino UNO 下的 avr 架构函数调用笔记

Arduino UNO 下的 avr 架构函数调用笔记

字数: 852

大二的单片机课程用的 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 指针:

1
2
3
> sp
SP = 0x08F6
> 

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 弹出了这些寄存器。这些寄存器是要保护的调用者的寄存器,也就是上下文保护。