Что такое соглашение о вызовах для Java-кода на платформе Linux?

Мы знаем соглашение о вызовах, согласно которому "первые шесть целочисленных или указательных аргументов передаются в регистрах RDI, RSI, RDX, RCX (R10 в интерфейсе ядра Linux [17]:124), R8 и R9" для кода c/ C++ в платформа Linux основана на следующей статье. https://en.wikipedia.org/wiki/X86_calling_conventions

Однако каково соглашение о вызовах для кода Java на платформе Linux (предположим, что JVM - это горячая точка)? Ниже приведен пример, в каких регистрах хранятся четыре параметра?

protected void caller( ) {
callee(1,"123", 123,1)
}

protected void callee(int a,String b, Integer c,Object d) {

}

2 ответа

Решение

Не указано, как JVM вызывает методы Java внутри. Различные реализации JVM могут следовать различным соглашениям о вызовах. Вот как это работает в HotSpot JVM на Linux x64.

  • Java-метод может выполняться в интерпретаторе или JIT-компилироваться.
  • В интерпретируемом и скомпилированном коде используются разные соглашения о вызовах.

1. Ввод метода интерпретатора

Каждый метод Java имеет точку входа в интерпретатор. Эта запись используется для перехода от интерпретируемого метода к другому интерпретируемому методу.

  • Все аргументы передаются в стеке снизу вверх.
  • rbx содержит указатель на Method* структура - внутренние метаданные вызываемого метода.
  • r13 держит sender_sp указатель стека метода вызывающего. Может отличаться от rsp + 8 если c2i используется адаптер (см. ниже).

Подробнее о записях интерпретатора в исходном коде HotSpot: templateInterpreter_x86_64.cpp.

2. Скомпилированная запись

Скомпилированный метод имеет свою точку входа. Скомпилированный код вызывает скомпилированные методы через эту запись.

  • В регистры передается до 6 первых целочисленных аргументов: rsi, rdx, rcx, r8, r9, rdi, Нестатические методы получают this ссылка в качестве первого аргумента в rsi,
  • До 8 аргументов с плавающей запятой передаются в xmm0... xmm7 регистры.
  • Все остальные аргументы передаются по стеку сверху вниз.

Это соглашение хорошо иллюстрируется на ассемблере_x86.hpp:

    |-------------------------------------------------------|
    | c_rarg0   c_rarg1  c_rarg2 c_rarg3 c_rarg4 c_rarg5    |
    |-------------------------------------------------------|
    | rcx       rdx      r8      r9      rdi*    rsi*       | windows (* not a c_rarg)
    | rdi       rsi      rdx     rcx     r8      r9         | solaris/linux
    |-------------------------------------------------------|
    | j_rarg5   j_rarg0  j_rarg1 j_rarg2 j_rarg3 j_rarg4    |
    |-------------------------------------------------------|

Вы можете заметить, что соглашение о вызовах Java похоже на соглашение о вызовах C, но смещено на один аргумент вправо. Это сделано намеренно, чтобы избежать дополнительной перестановки регистров при вызове методов JNI (вы знаете, методы JNI имеют дополнительные JNIEnv* аргумент перед параметрами метода).

3. Адаптеры

Методы Java могут иметь еще две точки входа: c2i а также i2c адаптеры. Эти адаптеры являются частями динамически генерируемого кода, который преобразует скомпилированное соглашение о вызовах в макет интерпретатора и наоборот. с2i а также i2c точки входа используются для вызова интерпретируемого метода из скомпилированного кода и скомпилированного метода из интерпретируемого кода соответственно.


PS Обычно не имеет значения, как JVM вызывает методы внутри, потому что это просто детали реализации, непрозрачные для конечного пользователя. Более того, эти детали могут измениться даже при незначительном обновлении JDK. Однако я знаю по крайней мере один случай, когда знание соглашения о вызовах Java может оказаться полезным - при анализе аварийных дампов JVM.

Особых правил не существует, так как и вызывающая сторона, и вызываемая сторона находятся под контролем JVM, поэтому нет необходимости придерживаться соглашения.

Особенно, когда вы рассматриваете случай, когда оба метода были скомпилированы в нативный код, поскольку это обычно срабатывает, когда этот путь к коду оказывается горячей точкой. В этом случае весьма вероятно, что код вызванного метода будет встроен в код вызывающего, что позволит последующее преобразование кода, которое превратит его во что-то похожее на исходный код, который вы написали. Вместо ссылки на переменные параметров встроенная версия вызванного метода может ссылаться на значения или константы, из которых первоначально были получены аргументы вызова. (Это особенно относится к вашему примеру, где все аргументы являются постоянными значениями)

См. Форму статического одиночного присвоения, Глобальную нумерацию и Распространение условной константы Sparse для получения более подробной информации. Присвоение переменных регистрам происходит только после всех этих оптимизаций более высокого уровня для оставшихся переменных, поэтому оно не применяется к параметрам в любой фиксированной схеме, если их переменные все еще существуют.

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

  • Интерпретируемое исполнение вызывающего абонента входит в интерпретированное исполнение вызываемого абонента
  • Интерпретируемое выполнение вызывающего абонента входит в уже скомпилированный вызываемый
  • Нативный код, не вставивший вызываемого, входит в интерпретируемое выполнение вызываемого
  • Нативный код, не вставивший вызываемого, входит в уже скомпилированный вызываемый
Другие вопросы по тегам