Форум программистов, компьютерный форум, киберфорум
Jin X
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

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

Запись от Jin X размещена 18.07.2018 в 12:14
Показов 9598 Комментарии 1

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

В Linux, в отличие от Windows, прямые системные вызовы используются довольно часто. По меньшей мере, консольные приложения, написанные на ассемблере, порой содержат лишь системные вызовы, без обращений к функциям библиотек. Причём, этот механизм (как и номера функций(!)) различается для кода 32- и 64-битной разрядности (кстати, в Linux существует ещё и x32 ABI – это, попросту говоря, 64-битный код с 32-битными указателями).

Давайте разберёмся...



32-битный код (x86 ABI, архитектура i386)

Для приложений x86 имеется 2 варианта вызова системных функций:
  • Через прерывание 80h (int 0x80).
  • Через инструкцию sysenter.
Системный вызов через прерывание 80h

Наиболее распространённый (хотя и более медленный), поскольку осуществляется проще и поддерживается любым процессором 386+.
  1. В регистр EAX загружается номер функции.
  2. Параметры (зависящие от функции) загружаются в регистры EBX (первый параметр), ECX (второй), EDX (третий), ESI (четвёртый), EDI (пятый), EBP (шестой, хотя насколько я знаю, больше 5 параметров не используется).
  3. Осуществляется вызов прерывания: int 0x80.
Assembler
1
2
3
4
5
6
7
8
  mov eax,function_number
  mov ebx,param_1  ; если есть
  mov ecx,param_2  ; если есть
  mov edx,param_3  ; если есть
  mov esi,param_4  ; если есть
  mov edi,param_5  ; если есть
  mov ebp,param_6  ; если есть
  int 0x80  ; вызов!
Системный вызов через инструкцию sysenter

Осуществляется быстрее, но немного сложнее и поддерживается только процессорами Pentium II и старше (а что, у кого-то более старый? )
  1. В регистр EAX загружается номер функции.
  2. Параметры (зависящие от функции) загружаются в регистры EBX (первый параметр), ECX (второй), EDX (третий), ESI (четвёртый), EDI (пятый).
  3. В стек заносится адрес возврата и регистры ECX, EDX, EBP (именно в таком порядке).
  4. В регистр EBP загружается значение ESP.
  5. Выполняется инструкция sysenter.
Зачем нужны такие сложности?
Дело в том, что инструкция sysenter, осуществляющая вход (переключение) в режим ядра, заносит в регистры EIP и ESP определённые значения (предварительно загруженные системой в специальные MSR-регистры), не сохраняя нигде адреса возврата и старого значения ESP. Поэтому вызывающему коду необходимо делать это самостоятельно (чтобы продолжить своё выполнение), и вполне логично сохранять их именно в стеке. Инструкция выхода из режима ядра (переключения в режим пользователя) sysexit загружает в ESP значение регистра ECX, а в EIP – значение регистра EDX, поэтому эти регистры, во избежание их порчи, также необходимо сохранять (в стеке). Ну и для того, чтобы передать ядру указатель стека пользовательской программы (оно же изменяется инструкцией sysenter, см. выше), значение ESP записывается в EBP, предварительно сохранив последний.
Однако, если системные функции заканчивались бы просто инструкцией sysexit, было бы логичнее сохранять адрес возврата последним, чтобы ядро выполняло:
Assembler
1
2
3
4
mov ecx,ebp
pop ebp
pop edx
sysexit  ; возврат в нашу программу
А нашей программе нужно было бы совершать восстановление сохранённых регистров из стека, выполнив следующие инструкции:
Assembler
1
2
pop edx
pop ecx
Но тут создатели системы решили слегка упростить жизнь программистам и осуществлять выход из режима ядра (sysexit) не сразу обратно в пользовательскую программу, а в промежуточную процедуру, которая делает следующее:
Assembler
1
2
3
4
pop ebp
pop edx
pop ecx
ret  ; возврат в нашу программу

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
  mov eax,function_number
  mov ebx,param_1  ; если есть
  mov ecx,param_2  ; если есть
  mov edx,param_3  ; если есть
  mov esi,param_4  ; если есть
  mov edi,param_5  ; если есть
  push .ret  ; адрес возврата
  push ecx   ; сохранение этих регистров требуется системой
  push edx
  push ebp
  mov ebp,esp
  sysenter  ; вызов!
.ret:
64-битный код (x64 и x32 ABI, архитектура x86-64)

Здесь всё несколько проще (за исключением странной очерёдности использования регистров для передачи параметров).
  1. В регистр RAX загружается номер функции.
  2. Параметры (зависящие от функции) загружаются в регистры RDI (первый параметр), RSI (второй), RDX (третий), R10 (четвёртый), R8 (пятый), R9 (шестой).
  3. Выполняется инструкция syscall (не путайте с sysenter – это две разные инструкции!)
Внимание! Значения регистров RCX и R11 при выполнении системного вызова уничтожаются!
Почему это происходит?
Дело в том, что инструкция входа в режим ядра syscall сохраняет в регистре RCX значение RIP, а в R11 – значение регистра флагов (указатель стека RSP при этом не меняется) и переходит к выполнению функции ядра (адрес которой хранится в специальном MSR-регистре). Инструкция sysret же выполняет всё наоборот: восстанавливает RIP из регистра RCX и регистр флагов (почти весь) из регистра R11.
Поскольку в x64 кол-во регистров довольно много, а RCX и R11 не участвуют для передачи параметров, создатели системы решили не заморачиваться с сохранением этих регистров (возможно, заодно и для ускорения системного вызова и возврата).
Почему используются именно эти 2 регистра? Спросите об этом у специалистов Intel (и заодно про sysexit) – потом расскажете

Что же касается странной очерёдности использования регистров для передачи параметров, то здесь ситуация такова. В соответствии с соглашением о вызовах в 64-битной Linux параметры функций заносятся в регистры в следующем порядке: RDI, RSI, RDX, RCX, R8, R9. Однако поскольку регистр ECX имеет специальное назначение при системном вызове (сохраняет RIP), его заменили на другой свободный регистр – R10.

Assembler
1
2
3
4
5
6
7
8
  mov rax,function_number
  mov rdi,param_1  ; если есть
  mov rsi,param_2  ; если есть
  mov rdx,param_3  ; если есть
  mov r10,param_4  ; если есть
  mov r8,param_5   ; если есть
  mov r9,param_6   ; если есть
  syscall  ; вызов! (регистры RCX и R11 будут уничтожены)
Схема работы x64 и x32 ABI (напомню, что x32 – это 64-битный код с 32-битными указателями) одинаковая, разве что для x32 есть несколько дополнительных функций.


Возврат результата системных вызовов

Системные вызовы, как и [почти] все библиотечные функции возвращают результат в регистре EAX (RAX).
Все остальные регистры сохраняются (разумеется, кроме RCX и R11 в x64 и x32).


Реализация механизма системных вызовов

В MASM и fasm имеется директива (макрос) invoke, cinvoke для вызова функций WinAPI, а как насчёт Linux? Для Linux чаще всего используют ассемблеры NASM и GAS.
Для NASM есть проект NASMX, включающий в себя 3 include-файла, в которых описан 1 макрос syscall, номера функций для x86 и x64 (про доп.функции x32 забыли) и константы с кодами ошибок и т.п. Для GAS, как я понимаю, можно использовать стандартные Linux'овские include'ы, а про макросы я ничего не знаю (знаете – напишите мне).



Мои макросы системных вызовов для NASM



Предлагаю вам несколько написанных мной include-файлов для NASM с различными макросами системных вызовов для 32- и 64-битного кода, а также примеры их использования.

Основные файлы:
  • linux_syscall.inc – универсальный include, автоматически определяющий разрядность кода и включающий соответствующие .inc и .mac файлы.
  • linux_syscall_32.inc – include, включающий все необходимые .inc и .mac файлы для 32-битного кода.
  • linux_syscall_64.inc – include, включающий все необходимые .inc и .mac файлы для 64-битного кода.
Для файла linux_syscall.inc наличие linux_syscall_32.inc и linux_syscall_64.inc не требуется, зато для всех этих 3-х include'ов требуется наличие следующих файлов (которые можно подключать и по-отдельности, без указанных выше файлов):
  • linux_sysfunc_32.inc – номера функций системных вызовов для 32-битного кода (x86).
  • linux_syscall_32.mac – макросы системных вызовов для 32-битного кода (x86).
  • linux_sysfunc_64.inc – номера функций системных вызовов для 64-битного кода (x64 и x32).
  • linux_syscall_64.mac – макросы системных вызовов для 64-битного кода (x64 и x32).
  • linux_syscall_fn.mac – именованные макросы для некоторых функций системных вызовов (см. ниже).
  • linux_const.inc – константы с кодами ошибок, хендлами для стандартного (консольного) ввода-вывода и пр.

Как этим пользоваться?

Всё предельно просто
  1. Подключаем в заголовке нашего исходника один из основных include-файлов: linux_syscall.inc (для кода любой разрядности) или linux_syscall_32.inc, linux_syscall_64.inc (для 32- и 64-битного кода соответственно):
    Assembler
    1
    
    %include 'linux_syscall.inc'
  2. В основном коде используем макросы lsyscall, lsyscallr, $lsyscall и/или $lsyscallr, указывая по порядку номер функции и соответствующие ей параметры через запятую:
    Assembler
    1
    2
    3
    4
    5
    
    lsyscall sys_read, STDIN_FILENO, buffer, bufsize  ; читаем данные из консоли в буфер buffer
    lsyscall sys_write, STDOUT_FILENO, , eax  ; выводим прочитанные данные на консоль
    lsyscall sys_exit, 0  ; выходим из программы
    ; более подробные комментарии, включая причину пропуска одного из параметров в sys_write, см. чуть ниже,
    ; в следующем примере
    Либо используем именованные макросы (описанные в файле linux_syscall_fn.mac, который включается автоматически при включении linux_syscall.inc, linux_syscall_32.inc или linux_syscall_64.inc) для работы с отдельными функциями системных вызовов Linux. На данный момент там описаны только макросы для работы с консолью и для выхода из программы: lsys_con_readlsys_con_read_r), lsys_con_writelsys_con_write_r), lsys_err_writelsys_err_write_r) и lsys_exit:
    Assembler
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    lsys_con_read buffer, bufsize  ; читаем до bufsize байт из консоли (по умолчанию с клавиатуры) в buffer,
      ; результат (кол-во прочитанных байт) возвращается в EAX (-1 при ошибке)
    lsys_con_write , eax  ; выводим EAX байт на консоль (по умолчанию на экран) из буфера buffer
      ; здесь запятая в начале – это не отделение имени макроса от параметров, а пропуск первого параметра
      ; (buffer), который отсутствует из-за того, что его значение такое же, как и в предыдущей функции,
      ; а системные вызовы не меняют никакие регистры, кроме EAX.
      ; Как вариант, можно было бы написать так: lsys_con_write ecx, eax ...или... lsys_con_write buffer, eax .
      ; Причина пропуска параметра в предыдущем примере (lsyscall sys_write...) та же, там тоже можно было бы
      ; использовать регистр ECX или адрес буфера (buffer).
    lsys_exit  ; выходим из программы (если код завершения не задан, используется 0)
В чём разница между всеми этими макросами?
  • Макрос lsyscall загружает параметры в регистры в прямом порядке (EBX, ECX, EDX, ESI, EDI, EBP для 32-битного кода или RDI, RSI, RDX, R10, R8, R9 для 64-битного).
  • Макрос lsyscallr загружает параметры в регистры в обратном порядке (EBP, EDI, ESI, EDX, ECX, EBX для 32-битного кода или R9, R8, R10, RDX, RSI, RDI для 64-битного).
  • Макросы $lsyscall и $lsyscallr работают так же, но перед записью параметров сохраняют в стеке (а после системного вызова восстанавливают) регистры EBX, ESI, EDI, EBP (которые в соответствии с соглашением о вызовах необходимо сохранять при изменении внутри функций), если конечно, эти регистры используются. Эти макросы определены только для 32-битного кода, а также при использовании linux_syscall.inc (для 64-битного кода это только псевдонимы макросов lsyscall и lsyscallr, поскольку в 64-х битах сохраняемыми регистрами являются RBX, RBP, R12-R15, которые не используются в качестве параметров системных вызовов – не путайте соглашения для Windows и Linux).
    Важно: используйте последовательности идущих друг за другом макросов $lsyscall и $lsyscallr с осторожностью, понимая работу этого механизма, поскольку пропуск параметров или использование регистров в качестве параметров может повлечь за собой передачу неверных значений, восстановленных предыдущей функцией.
  • Макросы lsys_con_read и lsys_con_read_r, lsys_con_write и lsys_con_write_r, а также lsys_err_write и lsys_err_write_r отличаются порядком загрузки параметров в регистры аналогично макросам lsyscall и lsyscallr.
  • Макросы lsys_err_write и lsys_err_write_r отличаются от lsys_con_write и lsys_con_write_r тем, что осуществляют вывод на устройство вывода ошибки, которым почти всегда является экран. В отличие от обычного устройства вывода, вывод ошибки не перенаправляется в файл с помощью символа ">" в командной строке.
  • Значение регистра EAX/RAX (номер функции) загружается всегда последним, вне зависимости от наличия суффикса 'r' в имени макроса!

Если у вас пока нет желания более глубоко разбираться в моих макросах, либо вы хотите сперва опробовать описанное выше, можете пропустить нижеследующие списки

Дополнительные макросы 32-битного режима (linux_syscall_32.mac):
  • Макрос lsyscall_use_int80 – использовать int 0x80 для системных вызовов во всех нижеследующих макросах [$]lsyscall[r] (действует по умолчанию).
  • Макрос lsyscall_use_sysenter – использовать sysenter для системных вызовов во всех нижеследующих макросах [$]lsyscall[r].
  • Макросы lsyscalli, lsyscallir, $lsyscalli, $lsyscallir – аналогичны макросам [$]lsyscall[r], но используют именно int 0x80 вне зависимости от выбранного режима (lsyscall_use_XXX).
  • Макросы lsyscallse, lsyscallser, $lsyscallse, $lsyscallser – аналогичны макросам [$]lsyscall[r], но используют именно sysenter вне зависимости от выбранного режима (lsyscall_use_XXX).

Дополнительные макросы именованных вызовов (linux_syscall_fn.mac):
  • Макрос lsyscall_fn_noregsaving – использовать макросы lsyscall и lsyscallr (не сохраняющие регистры) для всех нижеследующих системных вызовов в именованных макросах вроде lsys_read_con, lsys_write_con и пр. (действует по умолчанию).
  • Макрос lsyscall_fn_regsaving – использовать макросы $lsyscall и $lsyscallr (сохраняющие регистры) для всех нижеследующих системных вызовов в именованных макросах.
p.s. Макрос lsys_exit всегда использует lsyscall (т.е. режим lsyscall_fn_noregsaving), т.к. сохранение регистров при завершении программы бессмысленно.

Макросы для внутреннего применения, которые тем не менее можно использовать и в программах (любой разрядности):
  • Макрос movx reg,value – оптимизированный вариант инструкции mov: использует xor reg,reg при записи нулевого значения в регистр и or reg,-1 при записи значения -1. NASM автоматически заменяет 64-битный регистр 32-битным при записи в первый числового значения, не превышающего 32-х бит, поэтому здесь подобная оптимизация не требуется. Обнуление 64-битного регистра через xor NASM не оптимизирует, но макрос делает это сам (например, movx rax,0 преобразуется в xor eax,eax). Если указаны 2 одинаковых регистра, макрос не генерирует инструкций (за исключением 32-битных регистров в 64-битном режиме, т.к. приём вида mov eax,eax используется для очистки старших 32-х бит 64-битного регистра вместо недопустимого movzx rax,eax).
    Важно: макрос не предназначен для записи в память, т.к. вызов вида movx [eax],0 сгенерирует недопустимый код xor [eax],[eax], а mov [eax],-1 более медленный or [eax],-1.
  • Макрос find_in_list exp, list выполняет поиск выражения exp в списке list (например, find_in_list REG, eax,ebx,ecx,edx). Устанавливает в качестве результата значение ?found_in_list = -1, если выражение найдено, ?found_in_list = 0 в противном случае.

При подключении файла linux_syscall.inc становятся доступны следующие идентификаторы (которые позволяют создавать программы разной разрядности без изменения кода):
  • ?ax, ?bx, ?cx, ?dx, ?si, ?di, ?bp, ?sp – псевдонимы регистров EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP или RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP в зависимости от разрядности приложения.
  • ?rfn – регистр, используемый для записи номера функции (EAX или RAX, то же, что и ?ax, по сути), ?rp1..?rp6 – регистры, используемые для параметров №1..6 в зависимости от разрядности кода.
  • ?size = 4 для 32-битного кода, ?size = 8 для 64-битного кода.
  • ?dd – псевдоним dd или dq, ?resd – псевдоним resd или resq в зависимости от разрядности кода.

Пара слов про чувствительность к регистру букв.
  • Имена всех макросов (включая именованные макросы и макросы для внутреннего использования) НЕчувствительны к регистру.
  • Псевдонимы регистров (?ax, ?rfn, ?rp1 и пр), а также ?size, ?dd, ?resd НЕчувствительны к регистру.
  • Номера функций (sys_exit, sys_read, sys_write...) и константы из файла linux_const.inc (STDIN_FILENO, STDOUT_FILENO...) чувствительны к регистру.

Вот, собственно, и всё.
Все исходники прикреплены к данной статье! (см. ниже файл Linux_syscall.NASM.zip)
Примеры использования находятся в папке examples (все они выполняют одно и то же, но разными способами, при этом генерируется код x86, x64 и x32). Там же расположены cmd/sh-файлы для компиляции и готовые программы

p.s. Есть планы по доработке и созданию новых макросов, по пока обещать ничего не буду.



Где найти описание системных функций, их номера и параметры?



Приведу несколько ссылок:Буду рад, если пришлёте мне ссылки на хорошие справочники по системным вызовам!

О программировании в Linux:
Вложения
Тип файла: zip Linux_syscall.NASM.zip (39.7 Кб, 639 просмотров)
Тип файла: pdf syscalls.pdf (420.0 Кб, 2334 просмотров)
Тип файла: zip linux_sys_prog.zip (9.24 Мб, 3596 просмотров)
Тип файла: zip adv_linux_prog.zip (3.37 Мб, 23841 просмотров)
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 1
Комментарии
  1. Старый комментарий
    Аватар для Evg
    Если хочется писать программы на системных вызовах (я не знаю, зачем, но если вдруг), то логичнее и удобнее это делать при помощи компилятора и ассемблерных вставок. Пример тут: https://www.cyberforum.ru/asse... post219569
    Запись от Evg размещена 27.03.2019 в 10:17 Evg вне форума
 
Новые блоги и статьи
Часы электронные
Uhbif79 12.08.2026
Выкладываю программу часов. Программа позволяет: 1. Использовать системное время и дату, 2. Есть возможность вводить время и дату вручную. 3. Реализованы 2 будильника: начало и конец рабочего дня. . . .
Часы с будильником на основе класса QLCDNumber
Uhbif79 12.08.2026
Всем добрый день, выкладываю программу часов с будильником на основе класса QLCDNumber. Здесь я пробовал самостоятельно создавал классы, впервые столкнулся с видимостью переменной одного класса из. . .
Установка MinGW GCC 16.2 и CMake
8Observer8 10.08.2026
VK Видео: https:/ / vkvideo. ru/ video-240781534_456239017 YouTube: eY5-5PyI9NM Текстовая версия
Неделя из жизни имитационной модели склада: мои кривые руки растут, откуда надо
anaschu 10.08.2026
Неделя из жизни имитационной модели склада: как я почти написал неправильную логику и что с этим делать Работаю сейчас над учебно-рабочим проектом: строю в AnyLogic имитационную модель процессов. . .
Калькулятор для расчета родства
russiannick 07.08.2026
1. Задача: Создать калькулятор для расчета родства. Родственных связей существует 8 ступеней, такие как: p - отец P - мать q - муж Q - жена b - брат B - сестра s - сын S - дочь
Мир по моей воле
kumehtar 07.08.2026
Когда-то кажется, что всё просто. Ты весь такой светлый. Причиняешь добро. Борешься за справедливость в этом тёмном мире. Потом начинаешь замечать одну неприятную вещь. Почти каждый хороший. . .
Кредитный калькулятор
Maks 05.08.2026
Решение задачи по прикладной информатике средствами 1С. Задача: Напишите приложение-калькулятор, которое помогает рассчитывать параметры кредита для аннуитетного и дифференцированного видов. . .
У нас сейчас поговорку "Опять 25" нужно переделать на "Опять +35".
kumehtar 04.08.2026
С ностальгией вспоминаю времена моего детства, когда у нас и правда +25 - была максимальная температура летом. Раньше +25 °C реально казались вершиной жары, когда можно было весь день пропадать на. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru