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

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

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

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

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

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

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

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

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

Для каждого макроса есть 2+2×n параметров. Первый параметр соответствует типу возврата системного вызова. Второе - это имя системного вызова. Далее следует тип и имя для каждого параметра в порядке системного вызова. __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, Роберт Лав. PDF-файл на wordpress.com (см. Стр. 81); Результат Google Книг.

3 ответа

Решение

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

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

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

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

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

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

См. Также справочные страницы intro(2) и syscall(2) и syscalls(2), а также роль VDSO в системных вызовах.

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

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

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

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

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

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

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

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

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 макрос, когда ты не можешь.

Другие вопросы по тегам