Системный вызов Intel x86 vs x64

Я читаю о различии в сборке между x86 и x64.

В x86 номер системного вызова помещается в eax, затем выполняется int 80h для генерации программного прерывания.

Но на x64 номер системного вызова помещается в rax, тогда выполняется syscall.

Мне говорят, что syscall легче и быстрее, чем генерация программного прерывания.

Почему он быстрее на x64, чем x86, и могу ли я сделать системный вызов на x64 с помощью int 80h?

Ответы

Ответ 1

Общая часть

РЕДАКТИРОВАТЬ: Linux несущественные части удалены

Хотя это не совсем неправильно, сужение до int 0x80 и syscall упрощает вопрос, так как с sysenter есть по крайней мере третий вариант.

Использование 0x80 и eax для номера системного вызова, ebx, ecx, edx, esi, edi и ebp для передачи параметров является лишь одним из многих других возможных вариантов реализации системного вызова, но эти регистры - те, которые 32-битный Linux ABI выбрал из этих регистров.,

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

Другим выбором из представленных здесь, предлагаемых архитектурой x86, было бы использование шлюза вызовов (см.: http://en.wikipedia.org/wiki/Call_gate).

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

(Забавный факт: некоторые операционные системы i386 использовали исключение недопустимой инструкции для входа в ядро для системных вызовов, потому что это было на самом деле быстрее, чем инструкция int на 386 ЦП. См. Инструкции OsDev syscall/sysret и sysenter/sysexit, включающие сводку возможные механизмы системного вызова.)

Программное прерывание

Что именно происходит после срабатывания прерывания, зависит от того, требует ли переключение на ISR изменение привилегии:

(Руководство для разработчиков программного обеспечения Intel® 64 и IA-32)

6.4.1 Операции вызова и возврата для процедур обработки прерываний или исключений

...

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

....

Если происходит переключение стека, процессор выполняет следующие действия:

  1. Временно сохраняет (внутренне) текущее содержимое регистров SS, ESP, EFLAGS, CS и> EIP.

  2. Загружает селектор сегмента и указатель стека для нового стека (то есть стека для вызываемого уровня привилегий) из TSS в регистры SS и ESP и переключается в новый стек.

  3. Выдвигает временно сохраненные значения SS, ESP, EFLAGS, CS и EIP для стека прерванных процедур в новый стек.

  4. Добавляет код ошибки в новый стек (при необходимости).

  5. Загружает селектор сегмента для нового сегмента кода и новый указатель команд (из шлюза прерывания или шлюза прерывания) в регистры CS и EIP соответственно.

  6. Если вызов осуществляется через шлюз прерывания, очищает флаг IF в регистре EFLAGS.

  7. Начинает выполнение процедуры-обработчика на новом уровне привилегий.

... вздох, кажется, нужно много сделать, и даже когда мы закончим, это не станет намного лучше:

(отрывок взят из того же источника, что и упомянутый выше: Руководство для разработчиков программного обеспечения для архитектуры Intel® 64 и IA-32)

При выполнении возврата из обработчика прерываний или исключений с уровнем привилегий, отличным от прерванной процедуры, процессор выполняет следующие действия:

  1. Выполняет проверку привилегий.

  2. Восстанавливает регистры CS и EIP до их значений до прерывания или исключения.

  3. Восстанавливает регистр EFLAGS.

  4. Восстанавливает регистры SS и ESP до их значений до прерывания или исключения, что приводит к переключению стека обратно в стек прерванной процедуры.

  5. Возобновляет выполнение прерванной процедуры.

SYSENTER

Еще одна опция на 32-битной платформе, которая вообще не упоминается в вашем вопросе, но тем не менее используется ядром Linux, - это инструкция sysenter.

(Руководство для разработчиков программного обеспечения Intel® 64 и IA-32, том 2 (2A, 2B и 2C): справочник по набору инструкций, AZ)

Описание Выполняет быстрый вызов системной процедуры или процедуры уровня 0. SYSENTER - это сопутствующая инструкция к SYSEXIT. Инструкция оптимизирована для обеспечения максимальной производительности системных вызовов от кода пользователя, выполняющегося на уровне привилегий 3, до операционной системы или исполнительных процедур, выполняющихся на уровне привилегий 0.

Одним из недостатков использования этого решения является то, что оно присутствует не на всех 32-битных машинах, поэтому метод int 0x80 все еще должен быть предоставлен на тот случай, если процессор не знает об этом.

Инструкции SYSENTER и SYSEXIT были введены в архитектуру IA-32 в процессоре Pentium II. Доступность этих инструкций на процессоре указывается с помощью флага функции SYSENTER/SYSEXIT present (SEP), возвращаемого в регистр EDX инструкцией CPUID. Операционная система, которая квалифицирует флаг SEP, должна также квалифицировать семейство и модель процессора, чтобы гарантировать, что инструкции SYSENTER/SYSEXIT действительно присутствуют

Системный вызов

Последняя возможность, инструкция syscall, в значительной степени допускает ту же функциональность, что и инструкция sysenter. Наличие обоих связано с тем, что один (systenter) был представлен Intel, а другой (syscall) - AMD.

Специфичный для Linux

В ядре Linux для реализации системного вызова может быть выбрана любая из трех упомянутых выше возможностей.

Смотрите также Полное руководство по системным вызовам Linux.

Как уже говорилось выше, метод int 0x80 является единственной из 3 выбранных реализаций, которая может работать на любом процессоре i386, так что это единственная возможность, которая всегда доступна для 32-разрядного пользовательского пространства.

(syscall - единственный, который всегда доступен для 64-битного пространства пользователя, и единственный, который вы должны когда-либо использовать в 64-битном коде; ядра x86-64 могут быть CONFIG_IA32_EMULATION без CONFIG_IA32_EMULATION, а int 0x80 прежнему вызывает 32-битный ABI, который усекает указатели до 32-разрядных.)

Чтобы разрешить переключение между всеми тремя вариантами, каждому запускаемому процессу предоставляется доступ к специальному общему объекту, который дает доступ к реализации системного вызова, выбранной для работающей системы. Это странно выглядящий linux-gate.so.1 вы уже могли столкнуться как неразрешенная библиотека при использовании ldd или чего-то подобного.

(Арка /x86/vdso/vdso32-setup.c)

 if (vdso32_syscall()) {                                                                               
        vsyscall = &vdso32_syscall_start;                                                                 
        vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start;                                       
    } else if (vdso32_sysenter()){                                                                        
        vsyscall = &vdso32_sysenter_start;                                                                
        vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start;                                     
    } else {                                                                                              
        vsyscall = &vdso32_int80_start;                                                                   
        vsyscall_len = &vdso32_int80_end - &vdso32_int80_start;                                           
    }   

Чтобы использовать его, все, что вам нужно сделать, это загрузить все номера системных вызовов регистров в eax, параметры в ebx, ecx, edx, esi, edi, как в случае реализации системного вызова int 0x80, и call основную подпрограмму.

К сожалению, не все так просто; чтобы минимизировать риск безопасности фиксированного предопределенного адреса, местоположение, в котором vdso (виртуальный динамический общий объект) будет виден в процессе, рандомизировано, поэтому сначала вам нужно будет определить правильное местоположение.

Этот адрес индивидуален для каждого процесса и передается процессу после его запуска.

Если вы не знали, что при запуске в Linux каждый процесс получает указатели на параметры, переданные после его запуска, и указатели на описание переменных среды, в которых он запущен, и передаются в его стеке - каждый из них завершается NULL.

В дополнение к этому третий блок так называемых эльфийских вспомогательных векторов передается в соответствии с упомянутыми ранее. Правильное местоположение закодировано в одном из них, содержащем идентификатор типа AT_SYSINFO.

Таким образом, макет стека выглядит следующим образом (адреса растут вниз):

  • Параметр-0
  • ...
  • Параметр-м
  • НОЛЬ
  • среда-0
  • ...
  • среда-н
  • НОЛЬ
  • ...
  • вектор вспомогательного эльфа: AT_SYSINFO
  • ...
  • вектор вспомогательного эльфа: AT_NULL

Пример использования

Чтобы найти правильный адрес, вам нужно сначала пропустить все аргументы и все указатели среды, а затем начать сканирование на AT_SYSINFO как показано в примере ниже:

#include <stdio.h>
#include <elf.h>

void putc_1 (char c) {
  __asm__ ("movl $0x04, %%eax\n"
           "movl $0x01, %%ebx\n"
           "movl $0x01, %%edx\n"
           "int $0x80"
           :: "c" (&c)
           : "eax", "ebx", "edx");
}

void putc_2 (char c, void *addr) {
  __asm__ ("movl $0x04, %%eax\n"
           "movl $0x01, %%ebx\n"
           "movl $0x01, %%edx\n"
           "call *%%esi"
           :: "c" (&c), "S" (addr)
           : "eax", "ebx", "edx");
}


int main (int argc, char *argv[]) {

  /* using int 0x80 */
  putc_1 ('1');


  /* rather nasty search for jump address */
  argv += argc + 1;     /* skip args */
  while (*argv != NULL) /* skip env */
    ++argv;            

  Elf32_auxv_t *aux = (Elf32_auxv_t*) ++argv; /* aux vector start */

  while (aux->a_type != AT_SYSINFO) {
    if (aux->a_type == AT_NULL)
      return 1;
    ++aux;
  }

  putc_2 ('2', (void*) aux->a_un.a_val);

  return 0;
}

Как вы увидите, взглянув на следующий фрагмент /usr/include/asm/unistd_32.h в моей системе:

#define __NR_restart_syscall 0
#define __NR_exit            1
#define __NR_fork            2
#define __NR_read            3
#define __NR_write           4
#define __NR_open            5
#define __NR_close           6

Системный вызов, который я использовал, - это номер 4 (запись), переданный в регистре eax. Принимая в качестве аргументов filedescriptor (ebx = 1), указатель данных (ecx = & c) и размер (edx = 1), каждый из которых передается в соответствующем регистре.

Короче говоря

Сравнение предположительно медленного выполнения системного вызова int 0x80 на любом процессоре Intel с (надеюсь) гораздо более быстрой реализацией с использованием (действительно изобретенной AMD) инструкции syscall сравнивает яблоки с апельсинами.

ИМХО: Скорее всего, здесь будет sysenter инструкция sysenter вместо int 0x80.

Ответ 2

Есть три вещи, которые должны произойти при вызове ядра (системный вызов):

  1. Система переходит из "пользовательского режима" в "режим ядра" (кольцо 0).
  2. Стек переключается из "пользовательского режима" в "режим ядра".
  3. Прыжок сделан в подходящую часть ядра.

Очевидно, что, оказавшись внутри ядра, код ядра должен будет знать, что вы на самом деле хотите, чтобы ядро делало, следовательно, помещая что-то в EAX, и часто больше вещей в другие регистры, так как есть такие вещи, как "имя файла, который вы хотите открыть" "или" буфер для чтения данных из файла в "и т.д. и т.д.

Различные процессоры имеют разные способы для достижения вышеупомянутых трех шагов. В x86 есть несколько вариантов, но два наиболее популярных для рукописного asm: int 0xnn (32-битный режим) или syscall (64-битный режим). (Существует также 32-битный режим sysenter, представленный Intel по той же причине, по которой AMD представила 32-битную версию syscall: в качестве более быстрой альтернативы медленному int 0x80. 32-битный glibc использует любой эффективный механизм системных вызовов., используя медленный int 0x80 если нет ничего лучшего.)

64-разрядная версия инструкции syscall была представлена в архитектуре x86-64 как более быстрый способ ввода системного вызова. Он имеет набор регистров (использующих механизмы MSR x86), которые содержат адрес RIP, к которому мы хотим перейти, какие значения селектора загружать в CS и SS, а также для перехода с Ring3 на Ring0. Он также сохраняет адрес возврата в ECX/RCX. [Пожалуйста, прочитайте руководство по набору инструкций для всех деталей этой инструкции - это не совсем тривиально!]. Поскольку процессор знает, что это переключится на Ring0, он может делать правильные действия.

Одним из ключевых моментов является то, что syscall только манипулирует регистрами; это не делает никаких грузов или магазинов. (Вот почему он перезаписывает RCX с сохраненным RIP и R11 с сохраненным RFLAGS). Доступ к памяти зависит от таблиц страниц, и записи таблицы страниц имеют бит, который может сделать их действительными только для ядра, но не для пространства пользователя, поэтому при доступе к памяти при изменении уровня привилегий может потребоваться ожидание, а не только запись регистров. swapgs в режиме ядра, ядро обычно использует swapgs или другой способ поиска стека ядра. (syscall не изменяет RSP; он все еще указывает на стек пользователя при входе в ядро.)

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

(Процессоры AMD поддерживают инструкцию syscall из 32-разрядного пользовательского пространства; процессоры Intel - нет. X86-64 изначально был AMD64; поэтому мы использовали syscall в 64-разрядном режиме. AMD переработала сторону ядра syscall для 64-разрядного режим, поэтому точка входа 64-битного ядра syscall значительно отличается от точки входа 32-битного syscall в 64-битных ядрах.)

Вариант int 0x80 используемый в 32-битном режиме, решает, что делать, основываясь на значении в таблице дескрипторов прерываний, что означает чтение из памяти. Там он находит новые значения CS и EIP/RIP. Новый регистр CS определяет новый уровень "кольца" - в этом случае Ring0. Затем он будет использовать новое значение CS для просмотра сегмента состояния задачи (на основе регистра TR), чтобы выяснить, какой указатель стека (ESP/RSP и SS), и затем, наконец, перейдет на новый адрес. Поскольку это менее прямое и более общее решение, оно также медленнее. Старые EIP/RIP и CS хранятся в новом стеке вместе со старыми значениями SS и ESP/RSP.

При возврате, используя инструкцию IRET, процессор считывает адрес возврата и значения указателя стека из стека, а также загружает новые значения сегмента стека и сегмента кода из стека. Опять же, процесс является общим и требует довольно много операций чтения из памяти. Так как он является общим, процессор также должен будет проверить "меняем ли мы режим с Ring0 на Ring3, если это так, изменим эти вещи".

Итак, в итоге, это быстрее, потому что это должно было работать именно так.

Для 32-битного кода, да, вы можете использовать медленный и совместимый int 0x80 если хотите.

Для 64-битного кода int 0x80 медленнее, чем syscall и syscall ваши указатели до 32-битного, поэтому не используйте его. См. Что произойдет, если вы используете 32-битный int 0x80 Linux ABI в 64-битном коде? Кроме того, int 0x80 недоступен в 64-битном режиме во всех ядрах, поэтому он небезопасен даже для sys_exit который не принимает аргументов указателя: CONFIG_IA32_EMULATION может быть отключен, а особенно отключен в подсистеме Windows для Linux.