Позволяет ли соглашение о вызовах ARM функции не хранить LR в стеке? - PullRequest
1 голос
/ 30 марта 2020

Как видно из названия, у меня проблемы с пониманием соглашения о вызовах для архитектуры ARM. В частности, мне все еще трудно узнать, что происходит с регистром LR при вызове подпрограммы.

Я думаю, что наиболее очевидный и безопасный способ обработки регистра LR при вводе подпрограммы - это сохранить его в стек, но такое поведение не отражено в документации, поэтому я подумал о следующем примере.

Я напишу это в C, потому что я думаю, что это легче объяснить. Представьте, что у вас есть только две функции

void function_1(void){
   //some code here
}

void function_2(void){
   //some code here
   function_1();
   //some code here
}

То, как я бы использовал регистр LR внутри function_1, было бы, как я уже говорил, я бы сохранил его значение в стеке, но если вы посмотрите ближе, function_1 не вызывает никакую другую подпрограмму, поэтому в этом нет необходимости.

Возможно ли, что при использовании компилятора ARM этот компилятор решит не сохранить LR в стеке ?

Я читал о стандарте вызова в этой сети: инфоцентр

1 Ответ

6 голосов
/ 30 марта 2020

Соглашение о вызовах определяет только то, какие регистры сохраняются при вызове, а не о вызове, и где можно найти аргументы стека.

Функция на 100% зависит от того, как она работает, чтобы убедиться, что ее адрес возврата равен доступны где-то, когда он готов вернуться. Самый простой и эффективный способ справиться с этим - просто оставить его в LR все время в функции листьев. (Функция, которая не вызывает других: это лист в графе / дереве вызовов).

На практике компиляторы обычно просто оставляют его в LR в конечных функциях, даже если оптимизация отключена. Например, G CC устанавливает указатель кадра с отключенной оптимизацией, но по-прежнему не сохраняет / перезагружает LR, когда он знает, что ему не нужно так много чистых регистров, что он хотел бы использовать LR.

В противном случае в неконечных функциях обычные компиляторы обычно сохраняют его в стеке, но если они хотят, они могут, например, сохранить R4 в стек и mov r4, lr, затем восстановить LR и перезагрузить R4, когда они ' готов к возврату.

Функция, не являющаяся арендатором / не поддерживающая потоки, теоретически могла бы сохранить свой адрес возврата в хранилище c, если бы захотела.

Источник и GCC8.2 -O2 -mapcs-frame вывод от Godbolt , заставляющий его генерировать кадр стека APCS (стандарт вызова процедуры ARM), даже когда он не нужен. (Похоже, что он имеет эффект, аналогичный -fno-omit-frame-pointer, который включен по умолчанию при оптимизации.)

void function_1(void){
   //some code here
}
function_1:
    bx      lr     @ with or without -mapcs-frame
void unknown_func(void);   // not visible to the compiler; can't inline
void function_2(void){
   function_1();   // inlined, or IPA optimized as pure and not needing to be called.
   unknown_func(); // tailcall
   unknown_func();
}
function_2:              @@ Without -macps-frame
    push    {r4, lr}         @ save LR like you expected
    bl      unknown_func
    pop     {r4, lr}         @ around a call
    b       unknown_func     @ but then tailcall for the 2nd call.

или с APCS:

    mov     ip, sp
    push    {fp, ip, lr, pc}
    sub     fp, ip, #4
    bl      unknown_func
    sub     sp, fp, #12
    ldm     sp, {fp, sp, lr}
    b       unknown_func
int func3(void){
    unknown_func();
    return 1;               // prevent tailcall
}
func3:           @@ Without -macps-frame
    push    {r4, lr}
    bl      unknown_func
    mov     r0, #1
    pop     {r4, pc}

Или с APCS:

func3:
    mov     ip, sp
    push    {fp, ip, lr, pc}
    sub     fp, ip, #4
    bl      unknown_func
    mov     r0, #1
    ldmfd   sp, {fp, sp, pc}

Поскольку взаимодействие большого пальца не требуется (с опциями компиляции по умолчанию), G CC вставит сохраненный LR в P C вместо того, чтобы просто вернуться в LR для bx lr.

При нажатии R4 вместе с LR стек выравнивается на 8, что по умолчанию является IIR C.

...