Связь между API системных вызовов, инструкцией syscall и механизмом исключения (прерывания) - PullRequest
0 голосов
/ 25 апреля 2018

Я пытаюсь понять взаимосвязь между C системными вызовами языка API, syscall инструкцией на ассемблере и механизмом исключения (прерывания), используемым для переключения контекстов между процессами.Мне есть над чем поработать, поэтому, пожалуйста, потерпите меня.

Правильно ли я понимаю, что системные вызовы языка C реализованы компилятором как syscall с соответствующим кодом в ассемблере, которыйВ свою очередь, ОС реализованы как механизм исключений (прерываний)?

Таким образом, вызов функции write в следующем C коде:

#include <unistd.h>

int main(void)
{
    write(2, "There was an error writing to standard out\n", 44);
    return 0;
}

компилируется в сборку как syscall инструкция:

mov eax,4       ; system call number (sys_write)
syscall 

А инструкция, в свою очередь, реализована ОС как механизм исключений (прерывания)?

Ответы [ 4 ]

0 голосов
/ 26 апреля 2018

Правильно ли я понимаю, что системные вызовы языка C реализуются компилятором как системные вызовы с соответствующим кодом в сборке […]?

Нет.

Компилятор C обрабатывает системные вызовы так же, как он обрабатывает вызовы любой другой функции:

; write(2, "There was an error writing to standard out\n", 44);
mov    $44, %edx
lea    .LC0(%rip), %rsi  ; address of the string
mov    $2, %edi
call   write

Реализация этих функций в libc (библиотеке C вашей системы), вероятно, будет содержать syscallинструкция или что-то подобное в архитектуре вашей системы.

0 голосов
/ 25 апреля 2018

EDIT

Да, приложение C вызывает функцию библиотеки C, которая скрыта в решении библиотеки C - это системный вызов или набор вызовов, которые используют архитектурно-специфический способ доступа к операционной системе, в которой есть настройка обработчика исключений / прерываний иметь дело с этими системными вызовами. На самом деле, он не должен быть специфичным для конкретной архитектуры, он может просто переходить / вызывать по общеизвестному адресу, но при современном стремлении к режимам безопасности и защиты простой вызов не будет иметь этих добавленных функций, хотя все еще функционально корректен.

Способ реализации библиотеки определяется реализацией. И то, как компилятор связывает ваш код с этой библиотечной средой выполнения или временем компоновки, имеет множество комбинаций относительно того, как это может произойти, нет единственного способа, которым это может или должно произойти, поэтому это также определяется реализацией. До тех пор, пока он является функционально корректным и не мешает стандартам C, он может работать.

С нашими операционными системами, такими как windows, linux и другими, на наших телефонах и таблицах очень хочется изолировать приложения от системы, чтобы они не могли наносить ущерб различными способами, поэтому требуется защита, и вам необходимо иметь архитектурно конкретный способ сделать вызов функции в операционной системе, который не является нормальным вызовом, поскольку он переключает режимы. Если в архитектуре есть несколько способов сделать это, то операционная система может выбрать один или несколько способов как часть своего дизайна.

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

0 голосов
/ 25 апреля 2018

TL; DR

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

До x86_64 использовались два других механизма: инструкция int и инструкция sysenter.
Они имеют разные точки входа (все еще присутствующие сегодня в 32-разрядных ядрах и в 64-разрядных ядрах, которые могут запускать 32-разрядные программы пользовательского пространства).
Первый использует механизм прерываний x86 и может быть перепутан с диспетчеризацией исключений (которая также использует механизм прерываний).
Однако исключения являются ложными событиями, в то время как int используется для генерации программного прерывания, опять же, прославленного скачка.


Язык C не занимается системными вызовами, он использует среду выполнения C для выполнения всех взаимодействий со средой будущей программы.

Среда выполнения C реализует вышеупомянутые взаимодействия через механизм, специфичный для среды.
Могут быть различные уровни программных абстракций, но в конце API-интерфейсы ОС вызываются.

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

В Linux ядро ​​предоставляет набор служб, доступных из пространства пользователя, эти точки входа называются системными вызовами .
В Windows службы ядра (доступ к которым осуществляется по тому же механизму аналогов Linux) считаются частными в том смысле, что они не обязаны быть стабильными во всех версиях.
Вместо этого в качестве точек входа используется набор экспортируемых функций DLL / EXE (например, ntoskrnl.exe, hal.dll, kernel32.dll, user32.dll), которые в свою очередь используют службы ядра через (частный) системный вызов.
Обратите внимание, что в Linux большинство системных вызовов имеют оболочку POSIX, поэтому можно использовать эти оболочки, которые являются обычными функциями C, для вызова системного вызова.
Базовый ABI отличается, как и для отчетов об ошибках; Обертка переводит между двумя мирами.

Среда выполнения C вызывает API-интерфейсы ОС, в случае Linux системные вызовы используются напрямую, поскольку они являются общедоступными (в том смысле, что они стабильны в разных версиях), в то время как для Windows обычные библиотеки DLL, такие как kernel32.dll, являются помечены как зависимости и используются.

Мы дошли до того, что программе пользовательского режима, являющейся частью среды выполнения C (Linux) или частью DLL API (Windows), необходимо вызывать код в ядре.

Архитектура x86 исторически предлагала различные способы сделать это, например, call gate .
Другой способ - через инструкцию int , у нее есть несколько преимуществ:

  • Это то, что BIOS и DOS делали в свое время.
    В реальном режиме целесообразно использовать инструкции int, поскольку номер вектора (например, 21h) легче запомнить, чем удаленный адрес (например, 0f000h:0fff0h).
  • Сохраняет флаги.
  • Его легко настроить (настройка ISR относительно проста).

С модернизацией архитектуры у этого механизма появился большой недостаток: он медленный. До введения инструкции sysenter (обратите внимание, sysenter, а не syscall) не было более быстрой альтернативы (вентиль вызова был бы одинаково медленным).

С появлением Pentium Pro / II [1] была введена новая пара инструкций sysenter и sysexit для ускорения системных вызовов.
Linux начал использовать их начиная с версии 2.5 и, по-моему, до сих пор используется в 32-битных системах.
Я не буду объяснять весь механизм инструкции sysenter и сопутствующего VDSO , необходимого для его использования, нужно лишь сказать, что он был быстрее, чем механизм int (я не могу найдите статью от Энди Глеу, где он говорит, что sysenter оказался медленным на Pentium III, я не знаю, как он работает в настоящее время).

С появлением x86-64 ответ AMD на sysenter, то есть пару syscall / sysret, стал де-факто способом переключения из режима пользователя в режим ядра.
Это связано с тем, что sysenter на самом деле быстрый и очень простой (он копирует rip и rflags в rcx и r11 соответственно, маски rflags и переходит на адрес, установленный в IA32_LSTAR ).

64-битные версии Linux и Windows используют syscall.

Напомним, что ядро ​​может быть передано через три механизма:

  • Программные прерывания.
    Это было int 80h для 32-битной Linux (до 2.5) и int 2eh для 32-битной Windows.
  • Через sysenter.
    Используется 32-битными версиями Linux начиная с 2.5.
  • Через syscall.
    Используется в 64-битных версиях Linux и Windows.

Вот хорошая страница, чтобы привести ее в лучшую форму .

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

Инструкция syscall передает управление точке входа ядра (см. entry_64.s ) напрямую.
Это инструкция, которая просто делает это, она не реализована ОС, она используется ОС .

Термин исключение перегружен в CS, C ++ имеет исключения, также как и Java и C #.
ОС может иметь механизм захвата исключений, не зависящий от языка (в Windows он когда-то назывался SEH , теперь переписан).
Процессор также имеет исключения.
Я считаю, что мы говорим о последнем значении.

Исключения отправляются через прерывания, они являются своего рода прерыванием.
Само собой разумеется, что, хотя исключения являются синхронными (они происходят в определенных точках воспроизведения), они являются «нежелательными», они являются исключительными, в том смысле, что программисты склонны избегать их, а когда они происходят, это происходит из-за ошибки, необработанного угла случай или плохая ситуация.
Таким образом, они не используются для передачи управления ядру (они могли бы).

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

0 голосов
/ 25 апреля 2018

Не прямой ответ на вопрос, но это может вас заинтересовать (мне не хватает кармы, чтобы комментировать) - он подробно объясняет все выполнение пространства пользователя (включая glibc и как он выполняет системные вызовы):

http://www.maizure.org/projects/printf/index.html

Возможно, вам будет особенно интересен «Шаг 8 - финальная строка, записанная в стандартный вывод»:

А как выглядит __libc_write ...?

000000000040f9c0 <__libc_write>:
  40f9c0:  83 3d c5 bb 2a 00 00   cmpl   $0x0,0x2abbc5(%rip)  # 6bb58c <__libc_multiple_threads>
  40f9c7:  75 14                  jne    40f9dd <__write_nocancel+0x14>

000000000040f9c9 <__write_nocancel>:
  40f9c9: b8 01 00 00 00          mov    $0x1,%eax
  40f9ce: 0f 05                   syscall 
  ...cut...

Write просто проверяет состояние потоков и, если все в порядке, перемещает число записи системного вызова (1) в EAX и входит в ядро.

Некоторые заметки:

  • x86-64 Linux записывает системный вызов 1, старый x86 был 4
  • rdi относится к стандартному выводу
  • rsi указывает на строку
  • rdx - это количество строк

Обратите внимание, что это было для системы Linux x86-64 автора.

Для x86 это дает некоторую помощь:

http://www.tldp.org/LDP/khg/HyperNews/get/syscall/syscall86.html

В Linux выполнение системного вызова вызывается маскируемой передачей класса прерывания или исключения, вызванной инструкцией int 0x80. Мы используем вектор 0x80 для передачи управления ядру. Этот вектор прерывания инициализируется во время запуска системы вместе с другими важными векторами, такими как вектор системных часов.

Но как общий ответ для ядра Linux:

Верно ли мое понимание того, что системные вызовы языка C реализуются компилятором как системные вызовы с соответствующим кодом в сборке, которые, в свою очередь, реализуются ОС как механизм исключений (прерываний)?

Да

...