AVR C 编译器行为。内存管理

AVR C compilers behavior. Memory management

AVR C 编译器是否让程序记住 SRAM 中函数开始将其数据(变量、数组)存储在索引寄存器之一的数据堆栈中的地址,以便通过公式获得局部变量的绝对地址:

absoluteAdr = functionDataStartAdr + localShiftOfVariable.

当由它的长度声明的变量或堆栈指针在函数的 end/start 中增加时,它们会增加数据堆栈点吗?

编译器不管理 RAM,编译器在编译时计算每个数据部分(如 bss、数据、文本、rodata 等)所需的大小,并为每个翻译单元生成可重定位目标文件

链接器随后生成一个目标文件,并将可重定位地址分配给根据链接器配置文件LCF映射的绝对地址。

在运行的时候,机制依赖于架构本身。通常,每个函数调用在堆栈中都有一个框架,其中定义了参数、return 地址和局部变量。堆栈随着变量的创建而扩展,对于低成本的 AVR 微控制器,没有关于堆栈增加或堆栈与另一个内存部分(通常是堆)之间重叠的内存管理保护。即使有 OS 管理防止任务超出其分配的堆栈,没有内存管理单元,OS 所能做的就是断言 RESET 非法内存访问原因。

让我们看看 avr-gcc,它可以免费获得,包括 its ABI:

Do AVR C compilers make program memorize the address in SRAM where function started to store its data (variables, arrays) in data stack in one of index registers in order to get absolute address of local variable by formula:

是,不是,这取决于:

静态存储

对于静态存储中的变量,即

定义的变量
unsigned char func (void)
{
    static unsigned char var;
    return ++var;
}

编译器生成一个像 var.123 这样大小合适的符号(本例中为 1 个字节)。然后链接器/定位器将分配地址。

func:
    lds  r24,var.1505
    subi r24,lo8(-(1))
    sts  var.1505,r24
    ret
    .local  var.1505
    .comm   var.1505,1,1

自动

如果可能,自动变量保存在寄存器中,否则编译器在函数的框架中分配space。甚至可能是变量被优化掉了,在那种情况下它们在程序的任何地方都不存在:

int add (void)
{
    int a = 1;
    int b = 2;
    return a + b;
}

add:
    ldi r24,lo8(3)
    ldi r25,0
    ret

有 3 种类型的实体存储在函数的框架中,所有这些实体都可能存在或不存在,具体取决于程序:

  • 由函数序言保存(PUSH'ed)并由尾声恢复(POP'ed)的被调用者保存的寄存器。当局部变量分配给被调用者保存的寄存器时需要这样做。

  • Space 用于不能分配给寄存器的局部变量。当变量太大而无法保存在寄存器中,自动变量太多,或者变量的地址被获取(并且无法优化获取地址)时,就会发生这种情况。这是因为你不能获取寄存器1的地址。

    void use_address (int*);
    
    void func (void)
    {
       int a;
       use_address (&a);
    }
    

    这些变量的 space 在序言中分配并在尾声中释放。收缩包装未实现:

    func:
        push r28
        push r29
        rcall .
        in r28,__SP_L__
        in r29,__SP_H__
        /* prologue: function */
        /* frame size = 2 */
        /* stack size = 4 */
        movw r24,r28
        adiw r24,1
        rcall use_address
        pop __tmp_reg__
        pop __tmp_reg__
        pop r29
        pop r28
        ret
    

    在这个例子中,a占用2个字节,由rcall .分配(它是为具有16位程序计数器的设备编译的)。然后编译器用堆栈指针的值初始化帧指针 Y (R29:R28)。这是必需的,因为在 AVR 上,您不能通过 SP 访问内存;唯一涉及 SP 的内存操作是 PUSHPOP。然后 Y+1 变量的地址被传入 R24。函数调用后,结语释放帧并恢复R28和R29。

  • 必须在堆栈上传递的参数:

    void xfunc (int, ...);
    
    void call_xfunc (void)
    {
        xfunc (42);
    }
    

    这些参数被压入,被调用者从堆栈中取出它们。这些参数在调用前后被压入/弹出,但也可以通过 -maccumulate-args.

    累积
    call_func:
        push __zero_reg__
        ldi r24,lo8(42)
        push r24
        rcall xfunc
        pop __tmp_reg__
        pop __tmp_reg__
        ret
    

    在此示例中,参数必须在堆栈上传递,因为 ABI 规定可变参数函数的所有参数都必须在堆栈上传递,包括命名的参数。

有关框架布局和参数传递的精确描述,请参阅[框架布局和参数传递] (https://gcc.gnu.org/wiki/avr-gcc#Frame_Layout)。

1 一些 AVR 实际上允许这样做,但您绝不会(就像绝不会)想要传递通用寄存器的地址!