Ассемблер. Системные вызовы и режимы адресации
Обновл. 23 Фев 2020 |
Системные вызовы — это программные интерфейсы между пользователем и ядром. Мы уже использовали следующие системные вызовы:
sys_write — для вывода на экран;
sys_exit — для выхода из программы.
Системные вызовы Linux
В своих программах на ассемблере вы можете использовать системные вызовы Linux. Для этого нужно:
поместить номер системного вызова в регистр EAX;
сохранить аргументы системного вызова в регистрах EBX, ECX и т.д.;
вызвать соответствующее прерывание (80h);
результат обычно возвращается в регистр EAX.
Есть шесть регистров, в которых хранятся и используются аргументы необходимого системного вызова:
Эти регистры принимают последовательные аргументы. Если есть более шести аргументов, то ячейка памяти первого аргумента сохраняется в регистре EBX.
В следующем примере мы будем использовать системный вызов sys_exit :
А в следующем — sys_write :
Все системные вызовы перечислены в /usr/include/asm/unistd.h вместе с их номерами (значение, которое помещается в EAX перед вызовом int 80h ).
В следующей таблице приведены некоторые системные вызовы, которые мы будем использовать:
%eax | Название | %ebx | %ecx | %edx | %esx | %edi |
1 | sys_exit | int | — | — | — | — |
2 | sys_fork | struct pt_regs | — | — | — | — |
3 | sys_read | unsigned int | char * | size_t | — | — |
4 | sys_write | unsigned int | const char * | size_t | — | — |
5 | sys_open | const char * | int | int | — | — |
6 | sys_close | unsigned int | — | — | — | — |
В следующей программе мы запрашиваем число, а затем выводим его на экран:
Результат выполнения программы выше:
Please enter a number:
1234
You have entered:1234
Режимы адресации
Большинство инструкций на языке ассемблера требуют обработки операндов. Адрес операнда предоставляет место, где хранятся данные, подлежащие обработке. Некоторые инструкции не требуют операнда, в то время как другие могут требовать один, два или три операнда. В тех случаях, кода инструкции требуется два операнда, то первый операнд обычно является местом назначения, содержащий данные в регистре или в ячейке памяти, а второй — исходником. Исходник содержит либо данные для доставки (немедленная адресация), либо адрес (в регистре или в памяти) данных. Как правило, исходные данные остаются неизменными после операции.
Есть три основных режима адресации:
Регистровая адресация
Прямая (или ещё «непосредственная») адресация
Адресация памяти
Регистровая адресация
В режиме регистровой адресации регистр содержит операнд. В зависимости от инструкции регистр может быть первым операндом, вторым или обоими. Например:
Системные вызовы в 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 .
Системный вызов через инструкцию sysenter
Осуществляется быстрее, но немного сложнее и поддерживается только процессорами Pentium II и старше (а что, у кого-то более старый? )
- В регистр EAX загружается номер функции.
- Параметры (зависящие от функции) загружаются в регистры EBX (первый параметр), ECX (второй), EDX (третий), ESI (четвёртый), EDI (пятый).
- В стек заносится адрес возврата и регистры ECX, EDX, EBP (именно в таком порядке).
- В регистр EBP загружается значение ESP.
- Выполняется инструкция sysenter .
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.
Схема работы 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-битного кода соответственно):
В чём разница между всеми этими макросами?
- Макрос 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 (на русском, неполный набор функций, без номеров)
- Linux Syscall Reference (для x86, с номерами)
- LINUX SYSTEM CALL TABLE FOR X86 64 (для x64, с номерами, но без описания)
- LINUX System Call Quick Reference.pdf (неполная таблица номеров системных вызовов x86)
Буду рад, если пришлёте мне ссылки на хорошие справочники по системным вызовам!