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

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

Запись от Jin X размещена 18.07.2018 в 12:14
Показов 9652 Комментарии 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 Кб, 658 просмотров)
Тип файла: pdf syscalls.pdf (420.0 Кб, 2691 просмотров)
Тип файла: zip linux_sys_prog.zip (9.24 Мб, 3611 просмотров)
Тип файла: zip adv_linux_prog.zip (3.37 Мб, 23853 просмотров)
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 1
Комментарии
  1. Старый комментарий
    Аватар для Evg
    Если хочется писать программы на системных вызовах (я не знаю, зачем, но если вдруг), то логичнее и удобнее это делать при помощи компилятора и ассемблерных вставок. Пример тут: https://www.cyberforum.ru/asse... post219569
    Запись от Evg размещена 27.03.2019 в 10:17 Evg вне форума
 
Новые блоги и статьи
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр.
Jin X 06.09.2026
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр. Работая с форумом и нейросетями в браузере часто хочется что-то подкорректировать или добавить какого-то функционала. Ниже прикреплён. . .
Программа опроса у.з. расходомера SLS-720F
Argus19 02.09.2026
Программа опроса у. з. расходомера SLS-720F Программа опрашивает один раз в минуту три ультразвуковых расходомера SLS-720F через интерфейс RS-485 по протоколу Modbus RTU. Опрашиваются регистры. . .
Hyper-V: Компьютер должен поддерживать доверенный платформенный модуль 2.0.
Maks 31.08.2026
При установке Windows 11 на виртуальную машину Hyper-V 2-го поколения вылезла такая ошибка: Решение: в параметрах виртуальной машины, в разделе "Безопасность" (Security) активировать флаг. . .
Архитектура биовида Стива в Майнкрафте: Зачем бонобо кубический каннибализм
anaschu 30.08.2026
Кубический Вагинокапитализм в Minecraft: Математический инвариант ОДУ и рок Стивов-бонобо Главная задача разработанной «Модели Всего» — наглядно продемонстрировать наличие системной «судьбы». . .
Оттачиваю умение писать js программы.
russiannick 30.08.2026
Проектом выходного дня стало написание Книги шифров Виженера. Итогом стала версия 200, синий туман. Синий туман назван так, потому что замораживает текст под собой. Нажатие синих кнопок управляют. . .
мат медиц модель 30. презентация проекта
anaschu 27.08.2026
хоп хоп хоп хидахоп, а я кладую))
Как у меня протекала болезнь
zorxor 27.08.2026
Здравствуйте, друзья! Эта запись блога предназначена именно для вас - для моих дорогих друзей, которые знали меня лично. Чтобы ответить на вопрос - а что же со мной произошло на самом деле? Я учился. . .
Нашел вот забавное видео о измерениях. Лучшее что я видел на эту тему
kumehtar 26.08.2026
ILETXiw9bMQ Основная суть и тезисы по измерениям: 0D (Нулевое измерение): точка, не имеющая длины, ширины, высоты или объема. Объект не может перемещаться в 0D. 1D (Первое измерение):. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru