Конвенции вызова: Microsoft vs Linux

17 Янв 2016

Конвенции вызова: Microsoft vs Linux

Понятие модульности является одним из базовых в программировании. А если есть модули, то должны быть обеспечены эффективные методы их взаимодействия. Когда речь о подпрограммах в составе одного вы­полняемого файла, разработчик имеет право выбрать архитектуру са­мостоятельно. В иных случаях задачи сопряжения модулей, написанных различными командами разработчиков, подключение низкоуровневых ассемблерных библиотек и вызов функций API операционной системы невозможны без стандартизации «с точностью до бита». Сравним кон­венции вызова, применяемые в 64-битных реализациях ОС Microsoft Windows и Linux, подсознательно продумывая вопросы унификации подходов к разработке кроссплатформенных приложений.

Обратимся к систематизирующей таблице из фундаментальной работы Агнера Фога.

Систематизация конвенций вызова
Рис.1 Систематизация конвенций вызова:

  • Scratch registers — регистры, состояние которых может быть изменено вызываемой процедурой.
  • Callee-save registers — регистры, которые вызываемая процедура обязана сохранять неизменными.
  • Registers for parameters transfer — регистры, используемые для передачи параметров от вызывающей к вызываемой процедуре (Input).
  • Registers for return — регистры, используемые для возврата параметров от вызываемой к вызывающей процедуре (Output).

Microsoft x64 Calling Convention

Четыре первых параметра передаются в регистрах, последующие в стеке. В случае целочисленных значений или указателей это регистры RCX,RDX,R8,R9, в случае параметров с плавающей точкой это младшие биты векторных регистров XMM0-XMM3. Отсюда следует интересный вывод: полноценное функционирование 64-битной ОС на процессоре без поддержки SSE, невозможно. Для параметров, передаваемых в регистрах, место в стеке также резервируется, эта особенность называется Parameters Shadow. Отсюда следует, что пятый параметр (первый, передаваемый в стеке) после входа в подпрограмму будет находится по адресу RSP+40. 32 байта затрачено на Parameters Shadow для четырех 64-битных регистров, и 8 байт занимает 64-битный счетчик команд RIP. 32+8=40. Этот пример для внутрисегментного (NEAR) 64-битного вызова подпрограммы. Другая особенность состоит в так называемом пропуске регистра. Это означает, что если передается два параметра, первый целочисленный, второй с плавающей точкой, то будут использованы регистры RCX и XMM1. Пропускается XMM0. Аналогично будет пропущен RCX, если первый параметр с плавающей точкой второй целочисленный. Такое ограничение видимо аргументировано обеспечением регулярности модели передачи параметров,  так как не связано с архитектурой процессора.

Функция может вернуть одно целое число в регистре RAX, либо одно число с плавающей точкой в младших битах регистра XMM0. Указатель стека RSP должен быть выровнен (кратен 16) на момент выполнения инструкции CALL.

Linux x64 Calling Convention

Для передачи целочисленных параметров используются 6 регистров: RDI, RSI, RDX, RCX, R8, R9. Для чисел с плавающей точкой 8 векторных регистров XMM0-XMM7, в младших битах регистров передаются скалярные величины. Предусмотрено расширение протокола для применения большего количества векторных регистров без нарушения совместимости с ранее написанным программным обеспечением. Подход Linux не содержит таких решений, как Parameters Shadow для параметров-регистров и пропуск регистра, поэтому здесь больше шансов обойтись без стековых переменных, снижающих производительность.

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

Указатель стека RSP должен быть выровнен (кратен 16) на момент выполнения инструкции CALL. Здесь подходы Microsoft и Linux аналогичны, так как обусловлены применением SSE-инструкций для доступа к параметрам в стеке.

Linux x64 Calling Convention для системных вызовов

Инструкция системного вызова SYSCALL применяемая для Linux API, использует регистр RCX в качестве альтернативы стеку для быстрого сохранения содержимого счетчика команд RIP (об этом подробнее в следующем разделе). Поэтому регистр RCX не может содержать входной параметр. С учетом сказанного, список регистров хранящих 6 входных целочисленных параметров, для Linux API примет вид: RDI, RSI, RDX, R10, R8, R9.

Инструкции SYSCALL и SYSENTER

Требования безопасности во многом определяют модель взаимодействия программных компонентов. В архитектуре x86 определены 4 уровня привилегий, высший (Ring 0) принадлежит ОС, низший (Ring 3) отдается рядовому пользователю. Численное значение уровня привилегий хранится в двух младших битах регистра селектора сегмента кода CS. Переключение между уровнями требует большого количества проверок и вспомогательных операций. Одним из путей повышения производительности является упрощение этой операции для такого частного случая:

  • Вызывающая процедура всегда является пользовательским приложением с уровнем привилегий Ring 3.
  • Вызываемая процедура всегда является частью ядра ОС с уровнем привилегий Ring 0.
  • Вложенные или рекурсивные вызовы подпрограмм не предусмотрены. Это дает возможность избавится от дополнительных операций с памятью, применяя для сохранения контекста процессора регистры общего назначения, либо Model-Specific регистры вместо стека.

В современных реализациях Linux, использование инструкции SYSCALL определено для вызова привилегированных процедур ОС непосредственно из пользовательских приложений.

Функции WinAPI вызываются обычной внутрисегментной инструкцией CALL, управление при этом передается системным библиотекам DLL, загруженным в адресное пространство приложения, внутри которых и скрыты процедуры межсегментной передачи управления.

Используется ли при этом SYSCALL или похожая инструкция SYSENTER? Интересующимся предлагаем дизассемблировать код Microsoft и ответить на этот вопрос самостоятельно.

Описание инструкции SYSCALL в документации Intel
Рис.2 Описание инструкции SYSCALL в документации Intel. Значения счетчика команд EIP(RIP) и регистра флагов EFLAGS(RFLAGS) сохраняются в регистрах RCX и R11 соответственно. Это быстрее, чем сохранять их в памяти (в стеке). После этого, в указанные регистры а также регистр сегмента кода CS загружается контекст подпрограммы из Model-Specific регистров процессора. Так реализована  высокопроизводительная альтернатива классической инструкции вызова подпрограммы CALL FAR с операндом в памяти.

Описание инструкции SYSENTER в документации Intel
Рис.3 Описание инструкции SYSENTER в документации Intel. Значения регистров сегмента кода CS, счетчика команд EIP(RIP) и указателя стека ESP (RSP) загружаются из Model-Specific регистров процессора. Это  высокопроизводительная альтернатива классической инструкции передачи управления JMP FAR с операндом в памяти.

Резюме

Было бы большой ошибкой, сказать «Linux лучше Windows потому, что использует больше регистров для передачи параметров» или «Windows лучше Linux так как обеспечивает единую модель вызова для системных и пользовательских функций». Спор сторонников двух ОС часто находится далеко за пределами дизайна аппаратно-программных платформ, уходя в политико-философские аспекты существования личности и цивилизации, а иногда и в «аналогии по Фрейду».  А мы всего лишь рассмотрели частный технический аспект.

Литература

[1] Intel 64 and IA-32 Architectures Software Developer's Manual. Combined Volumes: 1, 2A, 2B, 2C, 3A, 3B, 3C and 3D. Order Number: 325462-056US. September 2015.

[2] Calling conventions for different C++ compilers and operating systems. By Agner Fog. Technical University of Denmark. Copyright (C) 2004-2015. Last updated 2015-12-23.

Теги: