Ответ 1
Общая часть
РЕДАКТИРОВАТЬ: Linux несущественные части удалены
Хотя это не совсем неправильно, сужение до int 0x80
и syscall
упрощает вопрос, так как с sysenter
есть по крайней мере третий вариант.
Использование 0x80 и eax для номера системного вызова, ebx, ecx, edx, esi, edi и ebp для передачи параметров является лишь одним из многих других возможных вариантов реализации системного вызова, но эти регистры - те, которые 32-битный Linux ABI выбрал из этих регистров.,
Прежде чем более внимательно взглянуть на применяемые методы, следует отметить, что все они связаны с проблемой выхода из тюрьмы привилегий, в которой участвует каждый процесс.
Другим выбором из представленных здесь, предлагаемых архитектурой x86, было бы использование шлюза вызовов (см.: http://en.wikipedia.org/wiki/Call_gate).
Единственная другая возможность, присутствующая на всех машинах i386, - это использование программного прерывания, которое позволяет ISR (программе обработки прерываний или просто обработчику прерываний) работать с уровнем привилегий, отличным от предыдущего.
(Забавный факт: некоторые операционные системы i386 использовали исключение недопустимой инструкции для входа в ядро для системных вызовов, потому что это было на самом деле быстрее, чем инструкция int
на 386 ЦП. См. Инструкции OsDev syscall/sysret и sysenter/sysexit, включающие сводку возможные механизмы системного вызова.)
Программное прерывание
Что именно происходит после срабатывания прерывания, зависит от того, требует ли переключение на ISR изменение привилегии:
(Руководство для разработчиков программного обеспечения Intel® 64 и IA-32)
6.4.1 Операции вызова и возврата для процедур обработки прерываний или исключений
...
Если сегмент кода для процедуры-обработчика имеет тот же уровень привилегий, что и текущая исполняемая программа или задача, процедура-обработчик использует текущий стек; если обработчик выполняется на более привилегированном уровне, процессор переключается в стек для уровня привилегий обработчиков.
....
Если происходит переключение стека, процессор выполняет следующие действия:
Временно сохраняет (внутренне) текущее содержимое регистров SS, ESP, EFLAGS, CS и> EIP.
Загружает селектор сегмента и указатель стека для нового стека (то есть стека для вызываемого уровня привилегий) из TSS в регистры SS и ESP и переключается в новый стек.
Выдвигает временно сохраненные значения SS, ESP, EFLAGS, CS и EIP для стека прерванных процедур в новый стек.
Добавляет код ошибки в новый стек (при необходимости).
Загружает селектор сегмента для нового сегмента кода и новый указатель команд (из шлюза прерывания или шлюза прерывания) в регистры CS и EIP соответственно.
Если вызов осуществляется через шлюз прерывания, очищает флаг IF в регистре EFLAGS.
Начинает выполнение процедуры-обработчика на новом уровне привилегий.
... вздох, кажется, нужно много сделать, и даже когда мы закончим, это не станет намного лучше:
(отрывок взят из того же источника, что и упомянутый выше: Руководство для разработчиков программного обеспечения для архитектуры Intel® 64 и IA-32)
При выполнении возврата из обработчика прерываний или исключений с уровнем привилегий, отличным от прерванной процедуры, процессор выполняет следующие действия:
Выполняет проверку привилегий.
Восстанавливает регистры CS и EIP до их значений до прерывания или исключения.
Восстанавливает регистр EFLAGS.
Восстанавливает регистры SS и ESP до их значений до прерывания или исключения, что приводит к переключению стека обратно в стек прерванной процедуры.
Возобновляет выполнение прерванной процедуры.
SYSENTER
Еще одна опция на 32-битной платформе, которая вообще не упоминается в вашем вопросе, но тем не менее используется ядром Linux, - это инструкция sysenter
.
(Руководство для разработчиков программного обеспечения Intel® 64 и IA-32, том 2 (2A, 2B и 2C): справочник по набору инструкций, AZ)
Описание Выполняет быстрый вызов системной процедуры или процедуры уровня 0. SYSENTER - это сопутствующая инструкция к SYSEXIT. Инструкция оптимизирована для обеспечения максимальной производительности системных вызовов от кода пользователя, выполняющегося на уровне привилегий 3, до операционной системы или исполнительных процедур, выполняющихся на уровне привилегий 0.
Одним из недостатков использования этого решения является то, что оно присутствует не на всех 32-битных машинах, поэтому метод int 0x80
все еще должен быть предоставлен на тот случай, если процессор не знает об этом.
Инструкции SYSENTER и SYSEXIT были введены в архитектуру IA-32 в процессоре Pentium II. Доступность этих инструкций на процессоре указывается с помощью флага функции SYSENTER/SYSEXIT present (SEP), возвращаемого в регистр EDX инструкцией CPUID. Операционная система, которая квалифицирует флаг SEP, должна также квалифицировать семейство и модель процессора, чтобы гарантировать, что инструкции SYSENTER/SYSEXIT действительно присутствуют
Системный вызов
Последняя возможность, инструкция syscall
, в значительной степени допускает ту же функциональность, что и инструкция sysenter
. Наличие обоих связано с тем, что один (systenter
) был представлен Intel, а другой (syscall
) - AMD.
Специфичный для Linux
В ядре Linux для реализации системного вызова может быть выбрана любая из трех упомянутых выше возможностей.
Смотрите также Полное руководство по системным вызовам Linux.
Как уже говорилось выше, метод int 0x80
является единственной из 3 выбранных реализаций, которая может работать на любом процессоре i386, так что это единственная возможность, которая всегда доступна для 32-разрядного пользовательского пространства.
(syscall
- единственный, который всегда доступен для 64-битного пространства пользователя, и единственный, который вы должны когда-либо использовать в 64-битном коде; ядра x86-64 могут быть CONFIG_IA32_EMULATION
без CONFIG_IA32_EMULATION
, а int 0x80
прежнему вызывает 32-битный ABI, который усекает указатели до 32-разрядных.)
Чтобы разрешить переключение между всеми тремя вариантами, каждому запускаемому процессу предоставляется доступ к специальному общему объекту, который дает доступ к реализации системного вызова, выбранной для работающей системы. Это странно выглядящий linux-gate.so.1
вы уже могли столкнуться как неразрешенная библиотека при использовании ldd
или чего-то подобного.
(Арка /x86/vdso/vdso32-setup.c)
if (vdso32_syscall()) {
vsyscall = &vdso32_syscall_start;
vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start;
} else if (vdso32_sysenter()){
vsyscall = &vdso32_sysenter_start;
vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start;
} else {
vsyscall = &vdso32_int80_start;
vsyscall_len = &vdso32_int80_end - &vdso32_int80_start;
}
Чтобы использовать его, все, что вам нужно сделать, это загрузить все номера системных вызовов регистров в eax, параметры в ebx, ecx, edx, esi, edi, как в случае реализации системного вызова int 0x80
, и call
основную подпрограмму.
К сожалению, не все так просто; чтобы минимизировать риск безопасности фиксированного предопределенного адреса, местоположение, в котором vdso
(виртуальный динамический общий объект) будет виден в процессе, рандомизировано, поэтому сначала вам нужно будет определить правильное местоположение.
Этот адрес индивидуален для каждого процесса и передается процессу после его запуска.
Если вы не знали, что при запуске в Linux каждый процесс получает указатели на параметры, переданные после его запуска, и указатели на описание переменных среды, в которых он запущен, и передаются в его стеке - каждый из них завершается NULL.
В дополнение к этому третий блок так называемых эльфийских вспомогательных векторов передается в соответствии с упомянутыми ранее. Правильное местоположение закодировано в одном из них, содержащем идентификатор типа AT_SYSINFO
.
Таким образом, макет стека выглядит следующим образом (адреса растут вниз):
- Параметр-0
- ...
- Параметр-м
- НОЛЬ
- среда-0
- ...
- среда-н
- НОЛЬ
- ...
- вектор вспомогательного эльфа:
AT_SYSINFO
- ...
- вектор вспомогательного эльфа:
AT_NULL
Пример использования
Чтобы найти правильный адрес, вам нужно сначала пропустить все аргументы и все указатели среды, а затем начать сканирование на AT_SYSINFO
как показано в примере ниже:
#include <stdio.h>
#include <elf.h>
void putc_1 (char c) {
__asm__ ("movl $0x04, %%eax\n"
"movl $0x01, %%ebx\n"
"movl $0x01, %%edx\n"
"int $0x80"
:: "c" (&c)
: "eax", "ebx", "edx");
}
void putc_2 (char c, void *addr) {
__asm__ ("movl $0x04, %%eax\n"
"movl $0x01, %%ebx\n"
"movl $0x01, %%edx\n"
"call *%%esi"
:: "c" (&c), "S" (addr)
: "eax", "ebx", "edx");
}
int main (int argc, char *argv[]) {
/* using int 0x80 */
putc_1 ('1');
/* rather nasty search for jump address */
argv += argc + 1; /* skip args */
while (*argv != NULL) /* skip env */
++argv;
Elf32_auxv_t *aux = (Elf32_auxv_t*) ++argv; /* aux vector start */
while (aux->a_type != AT_SYSINFO) {
if (aux->a_type == AT_NULL)
return 1;
++aux;
}
putc_2 ('2', (void*) aux->a_un.a_val);
return 0;
}
Как вы увидите, взглянув на следующий фрагмент /usr/include/asm/unistd_32.h
в моей системе:
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
Системный вызов, который я использовал, - это номер 4 (запись), переданный в регистре eax. Принимая в качестве аргументов filedescriptor (ebx = 1), указатель данных (ecx = & c) и размер (edx = 1), каждый из которых передается в соответствующем регистре.
Короче говоря
Сравнение предположительно медленного выполнения системного вызова int 0x80
на любом процессоре Intel с (надеюсь) гораздо более быстрой реализацией с использованием (действительно изобретенной AMD) инструкции syscall
сравнивает яблоки с апельсинами.
ИМХО: Скорее всего, здесь будет sysenter
инструкция sysenter
вместо int 0x80
.