Как получить доступ к системному вызову из пользовательского пространства?

Я прочитал несколько параграфов в LKD 1 и я просто не могу понять содержание ниже:

Доступ к системному вызову из пользовательского пространства

Как правило, библиотека C обеспечивает поддержку системных вызовов. Пользовательские приложения могут использовать прототипы функций из стандартных заголовков и связываться с библиотекой C, чтобы использовать ваш системный вызов (или библиотечную процедуру, которая, в свою очередь, использует ваш вызов syscall). Однако, если вы просто написали системный вызов, вряд ли glibc уже поддерживает его!

К счастью, Linux предоставляет набор макросов для переноса доступа к системным вызовам. Он устанавливает содержимое регистра и выдает инструкции об удержании. Эти макросы называются _syscalln(), где n находится между нулем и шестью. Число соответствует количеству параметров, переданных в syscall, потому что макрос должен знать, сколько параметров ожидать и, следовательно, нажимать на регистры. Например, рассмотрим системный вызов open(), определенный как

long open(const char *filename, int flags, int mode)

Макрос syscall для использования этого системного вызова без явной поддержки библиотеки будет

#define __NR_open 5
_syscall3(long, open, const char *, filename, int, flags, int, mode)

Затем приложение может просто вызвать open().

Для каждого макроса есть 2 + 2 & times; n параметров. Первый параметр соответствует типу возврата в syscall. Второй - это имя системного вызова. Далее следует тип и имя для каждого параметра в порядке системного вызова. Определение __NR_open находится в <asm/unistd.h>; это номер системного вызова. Макрос _syscall3 расширяется в C-функцию с встроенной сборкой; сборка выполняет шаги, описанные в предыдущем разделе, чтобы направить номер и параметры системного вызова в правильные регистры и выдать прерывание программного обеспечения для ловушки в ядро. Размещение этого макроса в приложении - это все, что требуется для использования системного вызова open().

Пусть напишите макрос, чтобы использовать наш великолепный новый системный вызов foo(), а затем напишите некоторый тестовый код, чтобы продемонстрировать наши усилия.

#define __NR_foo 283
__syscall0(long, foo)

int main ()
{
        long stack_size;

        stack_size = foo ();
        printf ("The kernel stack size is %ld\n", stack_size);
        return 0;
}

Что означает приложение может просто вызвать open()?

Кроме того, для последнего фрагмента кода, где есть объявление foo()? И как я могу сделать этот кусок кода компилируемым и исполняемым? Какие файлы заголовков мне нужно включить?

__________
1 Linux Kernel Development, Роберт Лав. PDF файл на wordpress.com (см. стр. 81); Результат Google Книги.

Ответ 1

Сначала вы должны понять, какова роль linux kernel и что приложения взаимодействуют с ядром только через системные вызовы.

Фактически приложение запускается на "виртуальной машине", предоставляемой ядром: оно работает в пользовательском пространстве и может выполните (на самом низком уровне машины) набор машинных инструкций, разрешенных в режиме пользовательского процессора, дополненном инструкцией (например, SYSENTER или INT 0x80...), используемый для выполнения системных вызовов. Таким образом, с точки зрения приложения пользовательского уровня, syscall является инструкцией атомарного псевдо-машинного устройства.

Linux Assembly Howto объясняет, как syscall можно выполнить на уровне сборки (то есть машинной инструкции).

GNU libc предоставляет функции C, соответствующие syscalls. Например, функция open - это крошечный клей (т.е. Обертка) над syscall числа NR__open (он делает syscall, а затем обновление errno). Обычно приложение обычно вызывает такие C-функции в libc вместо выполнения syscall.

Вы можете использовать некоторые другие libc. Например, MUSL libc настолько "проще", и его код, пожалуй, проще читать. Он также переносит исходные системные вызовы в соответствующие функции C.

Если вы добавите свой собственный системный вызов, лучше также реализовать аналогичную функцию C (в вашей собственной библиотеке). Поэтому у вас должен быть также заголовочный файл для вашей библиотеки.

См. также intro (2) и syscall (2) и syscalls (2) man pages, а также роль VDSO в syscalls.

Обратите внимание, что syscalls не являются функциями C. Они не используют стек вызовов (их можно даже вызвать без какого-либо стека). Syscall - это, в основном, число, например NR__open from <asm/unistd.h>, машинная инструкция SYSENTER с условными обозначениями, по которым регистры хранятся перед аргументами в syscall и какие из них сохраняются после результата [s] syscall (включая отказ результат, установить errno в библиотеке C, обертывающей syscall). Соглашения для системных вызовов не являются вызовами для функций C в спецификации ABI (например, x86-64 psABI). Поэтому вам нужна оболочка C.

Ответ 2

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

Системный вызов состоит из четырех этапов:

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

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

Например, среда Linux/i386 имеет следующее соглашение о системном вызове:

  • Передача управления из пользовательского режима в режим ядра может осуществляться либо программным прерыванием с номером 0x80 (инструкция по сборке INT 0x80), либо инструкцией SYSCALL (AMD), либо инструкцией SYSENTER (Intel)
  • Идентификатор запрашиваемой системной службы указывается целочисленным значением, хранящимся в регистре EAX, во время ввода в режиме ядра. Идентификатор службы ядра должен быть определен в форме _NR. Вы можете найти все идентификаторы системных служб в исходном дереве Linux на пути include\uapi\asm-generic\unistd.h.
  • До 6 параметров могут быть переданы через регистры EBX (1), ECX (2), EDX (3), ESI (4), EDI (5), EBP (6). Число в скобках - это последовательное число параметра.
  • Ядро возвращает статус службы, выполняемой в регистре EAX. Это значение обычно используется glibc для установки переменной errno.

В современных версиях Linux нет никакого макроса _syscall (насколько мне известно). Вместо этого библиотека glibc, являющаяся основной библиотекой интерфейса ядра Linux, предоставляет специальный макрос - INTERNAL_SYSCALL, который расширяется в небольшой фрагмент кода, заполненный встроенными инструкциями ассемблера. Этот фрагмент кода предназначен для конкретной аппаратной платформы и реализует все этапы системного вызова, и из-за этого этот макрос представляет собой системный вызов. Существует еще один макрос - INLINE_SYSCALL. Последний макрос обеспечивает обработку ошибок в виде glibc, в соответствии с которой при неудачном системном вызове -1 будет возвращен, а номер ошибки будет сохранен в переменной errno. Оба макроса определены в sysdep.h пакета glibc.

Вы можете вызвать системный вызов следующим образом:

#include <sysdep.h>

#define __NR_<name> <id>

int my_syscall(void)
{
    return INLINE_SYSCALL(<name>, <argc>, <argv>);
}

где <name> должно быть заменено строкой syscall name, <id> - по желаемому идентификатору системного номера службы <argc> - по фактическому числу параметров (от 0 до 6) и <argv> - по фактические параметры, разделенные запятыми (и начинаются с запятой, если присутствуют параметры).

Например:

#include <sysdep.h>

#define __NR_exit 1

int _exit(int status)
{
    return INLINE_SYSCALL(exit, 1, status); // takes 1 parameter "status"
}

или другой пример:

#include <sysdep.h>

#define __NR_fork 2 

int _fork(void)
{
    return INLINE_SYSCALL(fork, 0); // takes no parameters
}

Ответ 3

Пример минимальной сборки в сборе

hello_world.asm:

section .rodata
    hello_world db "hello world", 10
    hello_world_len equ $ - hello_world
section .text
    global _start
    _start:
        mov eax, 4               ; syscall number: write
        mov ebx, 1               ; stdout
        mov ecx, hello_world     ; buffer
        mov edx, hello_world_len
        int 0x80                 ; make the call
        mov eax, 1               ; syscall number: exit
        mov ebx, 0               ; exit status
        int 0x80

Скомпилировать и запустить:

nasm -w+all -f elf32 -o hello_world.o hello_world.asm
ld -m elf_i386 -o hello_world hello_world.o
./hello_world

Из кода легко вывести:

Конечно, сборка будет утомительно быстро, и вы скоро захотите использовать обертки C, предоставляемые glibc/POSIX, когда сможете, или макрос SYSCALL, когда вы не можете.