Системные вызовы в Linux
В Linux, в отличие от Windows, прямые системные вызовы используются довольно часто. По меньшей мере, консольные приложения, написанные на ассемблере, порой содержат лишь системные вызовы, без обращений к функциям библиотек. Причём, этот механизм (как и номера функций(!)) различается для кода 32- и 64-битной разрядности (кстати, в Linux существует ещё и x32 ABI – это, попросту говоря, 64-битный код с 32-битными указателями).
Давайте разберёмся...
32-битный код (x86 ABI, архитектура i386)
Для приложений x86 имеется 2 варианта вызова системных функций:- Через прерывание 80h (
int 0x80 ).
- Через инструкцию
sysenter .
Системный вызов через прерывание 80h
Наиболее распространённый (хотя и более медленный), поскольку осуществляется проще и поддерживается любым процессором 386+.- В регистр EAX загружается номер функции.
- Параметры (зависящие от функции) загружаются в регистры EBX (первый параметр), ECX (второй), EDX (третий), ESI (четвёртый), EDI (пятый), EBP (шестой, хотя насколько я знаю, больше 5 параметров не используется).
- Осуществляется вызов прерывания:
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 и старше (а что, у кого-то более старый? )- В регистр EAX загружается номер функции.
- Параметры (зависящие от функции) загружаются в регистры EBX (первый параметр), ECX (второй), EDX (третий), ESI (четвёртый), EDI (пятый).
- В стек заносится адрес возврата и регистры ECX, EDX, EBP (именно в таком порядке).
- В регистр EBP загружается значение ESP.
- Выполняется инструкция
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 ; возврат в нашу программу |
|
А нашей программе нужно было бы совершать восстановление сохранённых регистров из стека, выполнив следующие инструкции:
Но тут создатели системы решили слегка упростить жизнь программистам и осуществлять выход из режима ядра ( 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)
Здесь всё несколько проще (за исключением странной очерёдности использования регистров для передачи параметров).- В регистр RAX загружается номер функции.
- Параметры (зависящие от функции) загружаются в регистры RDI (первый параметр), RSI (второй), RDX (третий), R10 (четвёртый), R8 (пятый), R9 (шестой).
- Выполняется инструкция
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 – константы с кодами ошибок, хендлами для стандартного (консольного) ввода-вывода и пр.
Как этим пользоваться?
Всё предельно просто - Подключаем в заголовке нашего исходника один из основных include-файлов: linux_syscall.inc (для кода любой разрядности) или linux_syscall_32.inc, linux_syscall_64.inc (для 32- и 64-битного кода соответственно):
Assembler | 1
| %include 'linux_syscall.inc' |
|
- В основном коде используем макросы 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_read (и lsys_con_read_r), lsys_con_write (и lsys_con_write_r), lsys_err_write (и lsys_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: |