почему во многих процедурах arm64 непосредственно перед концом процедуры есть инструкция b (ветвь)?

Это из исходного кода Linux arch/arm64/kernel/head.S, показывающего запуск ядра. Код сначала вызывает preserve_boot_args, а затем вызывает el2_setup, используя bl (ветка и ссылка). Я также показал процедуру preserve_boot_args.

SYM_CODE_START(primary_entry)
        bl      preserve_boot_args
        bl      el2_setup                       // Drop to EL1, w0=cpu_boot_mode
        adrp    x23, __PHYS_OFFSET
        and     x23, x23, MIN_KIMG_ALIGN - 1    // KASLR offset, defaults to 0
        bl      set_cpu_boot_mode_flag
        bl      __create_page_tables
        /*
         * The following calls CPU setup code, see arch/arm64/mm/proc.S for
         * details.
         * On return, the CPU will be ready for the MMU to be turned on and
         * the TCR will have been set.
         */
        bl      __cpu_setup                     // initialise processor
        b       __primary_switch
SYM_CODE_END(primary_entry)

SYM_CODE_START_LOCAL(preserve_boot_args)
        mov     x21, x0                         // x21=FDT

        adr_l   x0, boot_args                   // record the contents of
        stp     x21, x1, [x0]                   // x0 .. x3 at kernel entry
        stp     x2, x3, [x0, #16]

        dmb     sy                              // needed before dc ivac with
                                                // MMU off

        mov     x1, #0x20                       // 4 x 8 bytes
        b       __inval_dcache_area             // tail call
SYM_CODE_END(preserve_boot_args)

Насколько я понимаю, bl для вызова процедуры (после процедуры возврат по адресу, хранящемуся в lr - регистре ссылок, x30), а b просто идет по помеченному адресу без возврата. Но в процедуре preserve_boot_args выше, в самом конце, есть инструкция b __inval_dcache_area, которая просто переходит к __inval_dcache_area без возврата. Тогда как он возвращается к исходному коду (где bl el2_setup)? А чем завершается сама процедура? Определение SYM_CODE_END выглядит следующим образом:

#define SYM_END(name, sym_type)                         \
        .type name sym_type ASM_NL                      \
        .size name, .-name
#endif

Я не могу понять, как этот код заставляет его вернуться по адресу в lr. Разве мы не должны сделать что-то вроде mv pc, lr?


person Chan Kim    schedule 14.11.2020    source источник


Ответы (1)


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

Как работает эта оптимизация, вызывающая сторона A вызывает функцию B, которая вызывает другую функцию C. Если B собирался вернуться непосредственно к A после вызова C, тогда B может вместо этого перейти к C! Не зная об этом, C возвращается к своей вызывающей стороне, которой, как представляется, является A. Делая это, B не нуждается в кадре стека и не должен сохранять регистр ссылки — он просто передает свой адрес возврата в C.


Эта оптимизация пропускает обычный возврат C к B, заставляя C возвращаться непосредственно к A. Это преобразование разрешено (то есть правильно) только при определенных обстоятельствах:

  • Если после возвращения C у B нет работы, B может настроить C так, чтобы он возвращался непосредственно к A.
  • From a logical perspective (e.g. in C or pseudo code), this means that either:
    • B and C are both void functions, or,
    • B игнорирует возвращаемое C значение, или,
    • B возвращает значение C, возвращаемое A, без изменений.
  • B также не может очистить кадр стека после возврата C, так как C возвращается непосредственно к A; если у B есть кадр стека, его нужно освободить перед вызовом C.  (Также см. комментарий @PeterCordes ниже.)

С аппаратной точки зрения, когда используется оптимизация (она закодирована в B, а затем вызывается B), это выглядит так, как если бы B и C были объединены: функция A вызывает BC, если хотите. Динамически есть один bl (A->BC) и один ret (BC->A) — хорошо сбалансированные, что хорошо для обработки стека вызовов аппаратным предиктором ветвления.


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


В вызовах A вызовы B вызовы C, B и C являются функциями, но A может быть функцией, а может и не быть (это может быть просто некоторый ассемблерный код — хотя он вызывает B, саму A не нужно вызывать или вызывать, поскольку функция. Хотя цепочка вызовов может быть глубокой, первый код в самом верху цепочки вызовов не является функцией (например, это _start, а иногда и main), и ему некуда возвращаться (поэтому не используется ret для выхода ; он не имеет параметра адреса возврата, предоставленного вызывающей стороной).  (Если в коде есть место для возврата, то есть адрес возврата для использования, то по определению он не является вершиной цепочки вызовов (номинально это функция ))

Этот исходный код может играть роль A, но не B или C в образце. Хвостовой вызов исключен для вызова A к B, когда A не является функцией, потому что нет вызывающей стороны A, к которой B мог бы вернуться. Вот почему шаблон должен быть следующим: A вызывает B вызывает C, B и C должны быть функциями, и мы рассматриваем возможность применения оптимизации к B. Если A — функция, у нее должен быть вызывающий объект, поэтому она может играть роль среднего функция в шаблоне (как и C, например, если C вызывает D).

person Erik Eidt    schedule 14.11.2020
comment
это похоже на то. Проверил в конце __inval_dache_area, там ret инструкция. Спасибо. (но интересно, есть и bl __inval_dcache_area, и b __inval_dcache_area, потому что в конце __inval_dcache_area есть ret, разве не всегда должно быть bl __inval_dcache_area?) - person Chan Kim; 14.11.2020
comment
Вызывающий (здесь B) решает, следует ли выполнять оптимизацию хвостового вызова, поэтому один вызывающий может сделать это, а другие нет. В моем примере C ничем не лучше и будет работать с различными вызывающими объектами. - person Erik Eidt; 14.11.2020
comment
Понятно, поэтому программист должен выбрать bl междуb при вызове функции. если это первоначальный вызывающий абонент bl, если нет (откладывание возврата к другой следующей функции) b. Спасибо. материалы в книгах или веб-сайтах не так добры. :) - person Chan Kim; 14.11.2020
comment
См. редактирование, описывающее, когда оптимизация хвостового вызова уместна. - person Erik Eidt; 14.11.2020
comment
@ChanKim: Да, bl foo / восстановить обратный адрес из стека / ret можно оптимизировать до restore return address (при необходимости) / b foo. Это все. - person Peter Cordes; 16.11.2020
comment
Если в функции, выполняющей хвостовой вызов, нужно выполнить другую очистку стека, она может сделать это до хвостового вызова. (@Erik, вы предположили, что это будет демонстрация. Это проблема только в том случае, если вы хотите, например, передать адрес локального устройства в функцию, вызываемую хвостом. Например, char scratch_space[16]; return bar(x, y, scratch_space, 16);. Или если целевой функции требуется больше аргументов стека для вызывающей стороны. Если это меньше или равны, вы можете перезаписать входящие аргументы аргументами для следующей функции и по-прежнему b для нее. Соглашения о вызовах регистра-аргумента отлично подходят для этого; большинство функций не имеют аргументов стека.) - person Peter Cordes; 16.11.2020