Что такое соглашение о вызовах для 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 для получения более подробной информации. Присвоение переменных регистрам происходит только после всех этих оптимизаций более высокого уровня для оставшихся переменных, поэтому оно не применяется к параметрам в любой фиксированной схеме, если их переменные все еще существуют.
В случае, если вызов не был встроен, существует несколько различных сценариев, каждый из которых, вероятно, имеет свое собственное соглашение о вызовах:
- Интерпретируемое исполнение вызывающего абонента входит в интерпретированное исполнение вызываемого абонента
- Интерпретируемое выполнение вызывающего абонента входит в уже скомпилированный вызываемый
- Нативный код, не вставивший вызываемого, входит в интерпретируемое выполнение вызываемого
- Нативный код, не вставивший вызываемого, входит в уже скомпилированный вызываемый