Время на прочтение
13 мин
Количество просмотров 40K
Про системные вызовы уже много было сказано, например здесь или здесь. Наверняка вам уже известно, что системный вызов — это способ вызова функции ядра ОС. Мне же захотелось копнуть глубже и узнать, что особенного в этом системном вызове, какие существуют реализации и какова их производительность на примере архитектуры x86-64. Если вам также интересны ответы на данные вопросы, добро пожаловать под кат.
System call
Каждый раз, когда мы хотим что-то отобразить на мониторе, записать в устройство, считать с файла, нам приходится обращаться к ядру ОС. Именно ядро ОС отвечает за любое общение с железом, именно там происходит работа с прерываниями, режимами процессора, переключениями задач… Чтобы пользователь программой не смог завалить работу всей операционной системы, было решено разделить пространство памяти на пространство пользователя (область памяти, предназначенная для выполнения пользовательских программ) и пространство ядра, а также запретить пользователю доступ к памяти ядра ОС. Реализовано это разделение в x86-семействе аппаратно при помощи сегментной защиты памяти. Но пользовательской программе нужно каким-то образом общаться с ядром, для этого и была придумана концепция системных вызовов.
Системный вызов — способ обращения программы пользовательского пространства к пространству ядра. Со стороны это может выглядеть как вызов обычной функции со своим собственным calling convention, но на самом деле процессором выполняется чуть больше действий, чем при вызове функции инструкцией call. Например, в архитектуре x86 во время системного вызова как минимум происходит увеличение уровня привилегий, замена пользовательских сегментов на сегменты ядра и установка регистра IP на обработчик системного вызова.
Программист обычно не работает с системными вызовами напрямую, так как системные вызовы обернуты в функции и скрыты в различных библиотеках, например libc.so в Linux или же ntdll.dll в Windows, с которыми и взаимодействует прикладной разработчик.
Теоретически, реализовать системный вызов можно при помощи любого исключения, хоть при помощи деления на 0. Главное — это передача управления ядру. Рассмотрим реальные примеры реализаций исключений.
Способы реализации системных вызовов
Выполнение неверной инструкции.
Ранее, ещё на 80386 это был самый быстрый способ сделать системный вызов. Для этого обычно применялась бессмысленная и неверная инструкция LOCK NOP, после исполнения которой процессором вызывался обработчик неверной инструкции. Это было больше 20 лет назад и, говорят, этим приёмом обрабатывались системные вызовы в корпорации Microsoft. Обработчик неверной инструкции в наши дни используется по назначению.
Call gates
Для того, чтобы иметь доступ к сегментам кода с различным уровнем привилегий, в Intel был разработан специальный набор дескрипторов, называемый gate descriptors. Существует 4 вида таких дескрипторов:
- Call gates
- Trap gates (для исключений, вроде int 3, требующих выполнения участка кода)
- Interrupt gates (аналогичен trap gates, но с некоторыми отличиями)
- Task gates (полагалось, что будут использоваться для переключения задач)
Нам интересны только call gates, так как именно через них планировалось реализовывать системные вызовы в x86.
Call gate реализован при помощи инструкции call far или jmp far и принимает в качестве параметра call gate-дескриптор, который настраивается ядром ОС. Является достаточно гибким механизмом, так как возможен переход и на любой уровень защитного кольца, и на 16-битный код. Считается, что call gates производительней прерываний. Этот способ использовался в OS/2 и Windows 95. Из-за неудобства использования в Linux механизм так и не был реализован. Со временем совсем перестал использоваться, так как появились более производительные и простые в обращении реализации системных вызовов (sysenter/sysexit).
Системные вызовы, реализованные в Linux
В архитектуре x86-64 ОС Linux существует несколько различных способов системных вызовов:
- int 80h
- sysenter/sysexit
- syscall/sysret
- vsyscall
- vDSO
В реализации каждого системного вызова есть свои особенности, но в общем, обработчик в Linux имеет примерно одинаковую структуру:
- Включается защита от чтения/записи/исполнения кода пользовательского пространства.
- Заменяется пользовательский стек на стек ядра, сохраняются callee-saved регистры.
- Выполняется обработка системного вызова
- Восстановление стека, регистров
- Отключение защиты
- Выход из системного вызова
Рассмотрим немного подробнее каждый системный вызов.
int 80h
Изначально, в архитектуре x86, Linux использовал программное прерывание 128 для совершения системного вызова. Для указания номера системного вызова, пользователь задаёт в eax номер системного вызова, а его параметры располагает по порядку в регистрах ebx, ecx, edx, esi, edi, ebp. Далее вызывается инструкция int 80h, которая программно вызывает прерывание. Процессором вызывается обработчик прерывания, установленный ядром Linux ещё во время инициализации ядра. В x86-64 вызов прерывания используется только во время эмуляции режима x32 для обратной совместимости.
В принципе, никто не запрещает пользоваться инструкцией в расширенном режиме. Но вы должны понимать, что используется 32-битная таблица вызовов и все используемые адреса должны помещаться в 32-битное адресное пространство. Согласно SYSTEM V ABI [4] §3.5.1, для программ, виртуальный адрес которых известен на этапе линковки и помещается в 2гб, по умолчанию используется малая модель памяти и все известные символы находятся в 32-битном адресном пространстве. Под это определение подходят статически скомпилированные программы, где и возможно использовать int 80h. Пошаговая работа прерывания подробно описана на stackoverflow.
В ядре обработчиком этого прерывания является функция entry_INT80_compat и находится в arch/x86/entry/entry_64_compat.S
Пример вызова int 80h
section .text
global _start
_start:
mov edx,len
mov ecx,msg
mov ebx,1 ; file descriptor (stdout)
mov eax,4 ; system call number (sys_write)
int 0x80 ; call kernel
mov eax,1 ; system call number (sys_exit)
int 0x80 ; call kernel
section .data
msg db 'Hello, world!',0xa
len equ $ - msg
Компиляция:
nasm -f elf main.s -o main32.o
ld -melf_i386 main32.o -o a32.out
Или в расширенном режиме (программа работает так как компилируется статически)
nasm -f elf64 main.s -o main.o
ld main.o -o a.out
sysenter/sysexit
Спустя некоторое время, ещё когда не было x86-64, в Intel поняли, что можно ускорить системные вызовы, если создать специальную инструкцию системного вызова, тем самым минуя некоторые издержки прерывания. Так появилась пара инструкций sysenter/sysexit. Ускорение достигается за счёт того, что на аппаратном уровне при выполнении инструкции sysenter опускается множество проверок на валидность дескрипторов, а так же проверок, зависящих от уровня привилегий [3] §6.1. Также инструкция опирается на то, что вызывающая её программа использует плоскую модель памяти. В архитектуре Intel, инструкция валидна как для режима совместимости, так и для расширенного режима, но у AMD данная инструкция в расширенном режиме приводит к исключению неизвестного опкода [3]. Поэтому в настоящее время пара sysenter/sysexit используется только в режиме совместимости.
В ядре обработчиком этой инструкции является функция entry_SYSENTER_compat и находится в arch/x86/entry/entry_64_compat.S
Пример вызова sysenter
section .text
global _start
_start:
mov edx,len ;message length
mov ecx,msg ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
push continue_l
push ecx
push edx
push ebp
mov ebp,esp
sysenter
hlt ; dumb instructions that is going to be skipped
continue_l:
mov eax,1 ;system call number (sys_exit)
mov ebx,0
push ecx
push edx
push ebp
mov ebp,esp
sysenter
section .data
msg db 'Hello, world!',0xa
len equ $ - msg
Компилирование:
nasm -f elf main.s -o main.o
ld main.o -melf_i386 -o a.out
Несмотря на то, что в реализации архитектуры от Intel инструкция валидна, в расширенном режиме скорее всего такой системный вызов никак не получится использовать. Это из-за того, что в регистре ebp сохраняется текущее значение стека, а адрес верхушки независимо от модели памяти находится вне 32-битного адресного пространства. Это всё потому, что Linux отображает стек на конец нижней половины каноничного адреса пространства.
Разработчики ядра Linux предостерегают пользователей от жесткого программирования sysenter из-за того, что ABI системного вызова может измениться. Из-за того, что Android не последовал этому совету, Linux пришлось откатить свой патч для сохранения обратной совместимости. Правильно реализовывать системный вызов нужно используя vDSO, речь о которой будет идти далее.
syscall/sysret
Так как именно AMD разработали x86-64 архитектуру, которая и называется AMD64, то они решили создать свой собственный системный вызов. Инструкция разрабатывалась AMD, как аналог sysenter/sysexit для архитектуры IA-32. В AMD позаботились о том, чтобы инструкция была реализована как в расширенном режиме, так и в режиме совместимости, но в Intel решили не поддерживать данную инструкцию в режиме совместимости. Несмотря на всё это, Linux имеет 2 обработчика для каждого из режимов: для x32 и x64. Обработчиками этой инструкции является функции entry_SYSCALL_64 для x64 и entry_SYSCALL_compat для x32 и находится в arch/x86/entry/entry_64.S и arch/x86/entry/entry_64_compat.S соответственно.
Кому интересно более подробно ознакомиться с инструкциями системных вызовов, в мануале Intel [0] (§4.3) приведён их псевдокод.
Пример вызова syscall
section .text
global _start
_start:
mov rdx,len ;message length
mov rsi,msg ;message to write
mov rdi,1 ;file descriptor (stdout)
mov rax,1 ;system call number (sys_write)
syscall
mov rax,60 ;system call number (sys_exit)
syscall
section .data
msg db 'Hello, world!',0xa
len equ $ - msg
Компилирование
nasm -f elf64 main.s -o main.o
ld main.o -o a.out
Пример вызова 32-битного syscall
Для тестирования следующего примера потребуется ядро с конфигурацией CONFIG_IA32_EMULATION=y и компьютер AMD. Если же у вас компьютер фирмы Intel, то можно запустить пример на виртуалке. Linux может без предупреждения изменить ABI и этого системного вызова, поэтому в очередной раз напомню: системные вызовы в режиме совместимости правильнее исполнять через vDSO.
section .text
global _start
_start:
mov edx,len ;message length
mov ebp,msg ;message to write
mov ebx,1 ;file descriptor (stdout)
mov eax,4 ;system call number (sys_write)
push continue_l
push ecx
push edx
push ebp
syscall
hlt
continue_l:
mov eax,1 ;system call number (sys_exit)
mov ebx,0
push ecx
push edx
push ebp
syscall
section .data
msg db 'Hello, world!',0xa
len equ $ - msg
Компиляция:
nasm -f elf main.s -o main.o
ld main.o -melf_i386 -o a.out
Непонятна причина, по которой AMD решили разработать свою инструкцию вместо того, чтобы расширить инструкцию Intel sysenter на архитектуру x86-64.
vsyscall
При переходе из пространства пользователя в пространство ядра происходит переключение контекста, что является не самой дешёвой операцией. Поэтому, для улучшения производительности системных вызовов, было решено их обрабатывать в пространстве пользователя. Для этого было зарезервировано 8 мб памяти для отображения пространства ядра в пространство пользователя. В эту память для архитектуры x86 поместили 3 реализации часто используемых read-only вызова: gettimeofday, time, getcpu.
Со временем стало понятно, что vsyscall имеет существенные недостатки. Фиксированное размещение в адресном пространстве является уязвимым местом с точки зрения безопасности, а отсутствие гибкости в размере выделяемой памяти может негативно сказаться на расширении отображаемой области ядра.
Для того, чтобы пример работал, необходимо, чтобы в ядре была включена поддержка vsyscall: CONFIG_X86_VSYSCALL_EMULATION=y
Пример вызова vsyscall
#include <sys/time.h>
#include <stdio.h>
#define VSYSCALL_ADDR 0xffffffffff600000UL
int main()
{
// Offsets in x86-64
// 0: gettimeofday
// 1024: time
// 2048: getcpu
int (*f)(struct timeval *, struct timezone *);
struct timeval tm;
unsigned long addrOffset = 0;
f = (void*)VSYSCALL_ADDR + addrOffset;
f(&tm, NULL);
printf("%d:%d\n", tm.tv_sec, tm.tv_usec);
}
Компиляция:
gcc main.c
Linux не отображает vsyscall в режиме совместимости.
На данный момент, для сохранения обратной совместимости, ядро Linux предоставляет эмуляцию vsyscall. Эмуляция сделана для того, чтобы залатать дыры безопасности в ущерб производительности.
Эмуляция может быть реализована двумя способами.
Первый способ — при помощи замены адреса функции на системный вызов syscall. В таком случае виртуальный системный вызов функции gettimeofday на x86-64 выглядит следующим образом:
movq $0x60, %rax
syscall
ret
Где 0x60 — код системного вызова функции gettimeofday.
Второй же способ немного интереснее. При вызове функции vsyscall генерируется исключение Page fault, которое обрабатывается Linux. ОС видит, что ошибка произошла из-за исполнения инструкции по адресу vsyscall и передаёт управление обработчику виртуальных системных вызовов emulate_vsyscall (arch/x86/entry/vsyscall/vsyscall_64.c).
Реализацией vsyscall можно управлять при помощи параметра ядра vsyscall. Можно как отключить виртуальный системный вызов при помощи параметра vsyscall=none, задать реализацию как при помощи инструкции syscall syscall=native, так и через Page fault vsyscall=emulate.
vDSO (Virtual Dynamic Shared Object)
Чтобы исправить основной недостаток vsyscall, было предложено реализовать системные вызовы в виде отображения динамически подключаемой библиотеки, к которой применяется технология ASLR. В «длинном» режиме библиотека называется linux-vdso.so.1, а в режиме совместимости — linux-gate.so.1. Библиотека автоматически подгружается для каждого процесса, даже статически скомпилированного. Увидеть зависимости приложения от неё можно при помощи утилиты ldd в случае динамической компоновки библиотеки libc.
Также vDSO используется в качестве выбора наиболее производительного способа системного вызова, например в режиме совместимости.
Список разделяемых функций можно посмотреть в руководстве.
Пример вызова vDSO
#include <sys/time.h>
#include <dlfcn.h>
#include <stdio.h>
#include <assert.h>
#if defined __x86_64__
#define VDSO_NAME "linux-vdso.so.1"
#else
#define VDSO_NAME "linux-gate.so.1"
#endif
int main()
{
int (*f)(struct timeval *, struct timezone *);
struct timeval tm = {0};
void *vdso = dlopen(VDSO_NAME,
RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD);
assert(vdso && "vdso not found");
f = dlsym(vdso, "__vdso_gettimeofday");
assert(f);
f(&tm, NULL);
printf("%d:%d\n", tm.tv_sec, tm.tv_usec);
}
Компиляция:
gcc -ldl main.c
Для режима совместимости:
gcc -ldl -m32 main.c -o a32.elf
Правильнее всего искать функции vDSO при помощи извлечения адреса библиотеки из вспомогательного вектора AT_SYSINFO_EHDR и последующего парсинга разделяемого объекта. Пример парсинга vDSO из вспомогательного вектора можно найти в исходном коде ядра: tools/testing/selftests/vDSO/parse_vdso.c
Или если интересно, то можно покопаться и посмотреть, как парсится vDSO в glibc:
- Парсинг вспомогательных векторов: elf/dl-sysdep.c
- Парсинг разделяемой библиотеки: elf/setup-vdso.h
- Установка значений функций: sysdeps/unix/sysv/linux/x86_64/init-first.c, sysdeps/unix/sysv/linux/x86/gettimeofday.c, sysdeps/unix/sysv/linux/x86/time.c
Согласно System V ABI AMD64 [4] вызовы должны происходить при помощи инструкции syscall. На практике же к этой инструкции добавляются вызовы через vDSO. Поддержка системных вызовов в виде int 80h и vsyscall остались для обратной совместимости.
Сравнение производительности системных вызовов
С тестированием скорости системных вызовов всё неоднозначно. В архитектуре x86 на выполнение одной инструкции влияет множество факторов таких как наличие инструкции в кэше, загруженность конвейера, даже существует таблица задержек для данной архитектуры [2]. Поэтому достаточно сложно определить скорость выполнения участка кода. У Intel есть даже специальный гайд по замеру времени для участка кода [1]. Но проблема в том, что мы не можем замерить время согласно документу из-за того, что нам нужно вызывать объекты ядра из пользовательского пространства.
Поэтому было решено замерить время при помощи clock_gettime и тестировать производительность вызова gettimeofday, так как он есть во всех реализациях системных вызовов. На разных процессорах время может отличаться, но в целом, относительные результаты должны быть схожи.
Программа запускалась несколько раз и в итоге бралось минимальное время исполнения.
Тестирование int 80h, sysenter и vDSO-32 производилось в режиме совместимости.
Программа тестирования
#include <sys/time.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <syscall.h>
#include <dlfcn.h>
#include <limits.h>
#define min(a,b) ((a) < (b)) ? (a) : (b)
#define GIGA 1000000000
#define difftime(start, end) (end.tv_sec - start.tv_sec) * GIGA + end.tv_nsec - start.tv_nsec
static struct timeval g_timespec;
#if defined __x86_64__
static inline int test_syscall() {
register long int result asm ("rax");
asm volatile (
"lea %[p0], %%rdi \n\t"
"mov $0, %%rsi \n\t"
"mov %[sysnum], %%rax \n\t"
"syscall \n\t"
: "=r"(result)
: [sysnum] "i" (SYS_gettimeofday), [p0] "m" (g_timespec)
: "rcx", "rsi");
return result;
}
#endif
static inline int test_int80h() {
register int result asm ("eax");
asm volatile (
"lea %[p0], %%ebx \n\t"
"mov $0, %%ecx \n\t"
"mov %[sysnum], %%eax \n\t"
"int $0x80 \n\t"
: "=r"(result)
: [sysnum] "i" (SYS_gettimeofday), [p0] "m" (g_timespec)
: "ebx", "ecx");
return result;
}
int (*g_f)(struct timeval *, struct timezone *);
static void prepare_vdso() {
void *vdso = dlopen("linux-vdso.so.1",
RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD);
if (!vdso) {
vdso = dlopen("linux-gate.so.1",
RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD);
}
assert(vdso && "vdso not found");
g_f = dlsym(vdso, "__vdso_gettimeofday");
}
static int test_g_f() {
return g_f(&g_timespec, 0);
}
#define VSYSCALL_ADDR 0xffffffffff600000UL
static void prepare_vsyscall() {
g_f = (void*)VSYSCALL_ADDR;
}
static inline int test_sysenter() {
register int result asm ("eax");
asm volatile (
"lea %[p0], %%ebx \n\t"
"mov $0, %%ecx \n\t"
"mov %[sysnum], %%eax \n\t"
"push $cont_label%=\n\t"
"push %%ecx \n\t"
"push %%edx \n\t"
"push %%ebp \n\t"
"mov %%esp, %%ebp \n\t"
"sysenter \n\t"
"cont_label%=: \n\t"
: "=r"(result)
: [sysnum] "i" (SYS_gettimeofday), [p0] "m" (g_timespec)
: "ebx", "esp");
return result;
}
#ifdef TEST_SYSCALL
#define TEST_PREPARE()
#define TEST_PROC_CALL() test_syscall()
#elif defined TEST_VDSO
#define TEST_PREPARE() prepare_vdso()
#define TEST_PROC_CALL() test_g_f()
#elif defined TEST_VSYSCALL
#define TEST_PREPARE() prepare_vsyscall()
#define TEST_PROC_CALL() test_g_f()
#elif defined TEST_INT80H
#define TEST_PREPARE()
#define TEST_PROC_CALL() test_int80h()
#elif defined TEST_SYSENTER
#define TEST_PREPARE()
#define TEST_PROC_CALL() test_sysenter()
#else
#error Choose test
#endif
static inline unsigned long test() {
unsigned long result = ULONG_MAX;
struct timespec start = {0}, end = {0};
int rt, rt2, rt3;
for (int i = 0; i < 1000; ++i) {
rt = clock_gettime(CLOCK_MONOTONIC, &start);
rt3 = TEST_PROC_CALL();
rt2 = clock_gettime(CLOCK_MONOTONIC, &end);
assert(rt == 0);
assert(rt2 == 0);
assert(rt3 == 0);
result = min(difftime(start, end), result);
}
return result;
}
int main() {
TEST_PREPARE();
// prepare calls
int a = TEST_PROC_CALL();
assert(a == 0);
a = TEST_PROC_CALL();
assert(a == 0);
a = TEST_PROC_CALL();
assert(a == 0);
unsigned long result = test();
printf("%lu\n", result);
}
Компиляция:
gcc -O2 -DTEST_SYSCALL time_test.c -o test_syscall
gcc -O2 -DTEST_VDSO -ldl time_test.c -o test_vdso
gcc -O2 -DTEST_VSYSCALL time_test.c -o test_vsyscall
#m32
gcc -O2 -DTEST_VDSO -ldl -m32 time_test.c -o test_vdso_32
gcc -O2 -DTEST_INT80H -m32 time_test.c -o test_int80
gcc -O2 -DTEST_SYSENTER -m32 time_test.c -o test_sysenter
О системе
cat /proc/cpuinfo | grep "model name" -m 1 — Intel® Core(TM) i7-5500U CPU @ 2.40GHz
uname -r — 4.14.13-1-ARCH
Таблица Результатов
| Реализация | время (нс) |
|---|---|
| int 80h | 498 |
| sysenter | 338 |
| syscall | 278 |
| vsyscall emulate | 692 |
| vsyscall native | 278 |
| vDSO | 37 |
| vDSO-32 | 51 |
Как можно увидеть, каждая новая реализация системного вызова является производительней предыдущей, не считая vsysvall, так как это эмуляция. Как вы наверное уже догадались, если бы vsyscall был таким, каким его задумывали, время вызова было бы аналогично vDSO.
Все текущие сравнения производительности были произведены с патчем KPTI, исправляющим уязвимость meltdown.
Бонус: Производительность системных вызовов без KPTI
Патч KPTI был разработан специально для исправления уязвимости meltdown. Как известно, данный патч замедляет производительность ОС. Проверим производительность с выключенным KPTI (pti=off).
Таблица результатов с выключенным патчем
| Реализация | Время (нс) | Увеличение времени исполнения после патча (нс) | Ухудшение производительности после патча (t1 - t0) / t0 * 100% |
|---|---|---|---|
| int 80h | 317 | 181 | 57% |
| sysenter | 150 | 188 | 125% |
| syscall | 103 | 175 | 170% |
| vsyscall emulate | 496 | 196 | 40% |
| vsyscall native | 103 | 175 | 170% |
| vDSO | 37 | 0 | 0% |
| vDSO-32 | 51 | 0 | 0% |
Переход в режим ядра и обратно в среднем после патча стал занимать примерно на 180 нс. больше времени, видимо это и есть цена сброса TLB-кэша.
Производительность системного вызова через vDSO не ухудшилась по причине того, то в данном типе вызова нет перехода в режим ядра, и, следовательно, нет причин сбрасывать TLB-кэш.
Для дальнейшего чтения
Реализация виртуальных системных вызовов в ядре Linux (очень хорошая книга, советую): https://0xax.gitbooks.io/linux-insides/content/SysCall/syscall-3.html
История развития Linux: https://www.win.tue.nl/~aeb/linux/lk/lk-4.html
Анатомия системных вызовов, часть 1: https://lwn.net/Articles/604287/
Анатомия системных вызовов, часть 2: https://lwn.net/Articles/604515/
Ссылки
[0] Intel 64 and IA-32 Architectures Developer’s Manual: Vol. 2B
[1] How to benchmark code execution times …
[2] Instruction latencies and throughput for AMD and Intel x86 processors
[3] AMD64 Architecture Programmer’s Manual Volume 2: System Programming
[4] System V ABI AMD64
First some background knowledge, this is from the book: Linux System Programming: Talking Directly to the Kernel and C Library
Signals are a mechanism for one-way asynchronous notifications. A
signal may be sent from the kernel to a process, from a process to
another process, or from a process to itself.The Linux kernel implements about 30 signals.
Signals interrupt an executing process, causing it to stop whatever it
is doing and immediately perform a predetermined action.
Ok moving further, from here I will quote this part:
On the Intel family of microprocessors, such as the Pentium, int 80h
is the assembly language op code for interrupt 80h. This is the
syscall interrupt on a typical Intel-based Unix system, such as
FreeBSD. It allows application programmers to obtain system services
from the Unix kernel.
I can not quite make the connection in my head really. So when I for example use the
write
method defined in Posix, and when it is compiled into assembly, and then further assembled into an object code and linked to an executable in a given architecture that runs Linux…. a system call is made right?
I am assuming the compiled code would look something like this:
mov eax,4 ;code for system_write
mov ebx,1 ;standard output
mov ecx, memlocation; memlocation is just some location where a number of ascii is present
mov edx, 20; 20 bytes will be written
int 80h;
Ok my question is exactly at this point. Will int 80h send a signal to kernel / interrupt the kernel? Is kernel just one process? (Is it the init process?) When the cpu executes the int 80h , what exactly happens? The registers are full of information already, (eax, ebx, ecx and edx in my example..), but how is this information used?
I can not quite make the connection between the CPU — the kernel and what exactly the CPU does as it executes the int 80h.
I can imagine some code resides somewhere in the memory that actually sends the required information to the device driver but to which process does this code belong to? (I am assuming kernel but is kernel just one process?) And how does int 80h instruction jump to that code? Is it something that Linux has to implement somehow?
The int 80h instruction is used in x86 assembly language to cause a software interrupt and invoke GNU / Linux services . In Linux there are system calls that provide basic functions to access the hardware : disks, keyboard , video , ports , etc. These basic functions are called program services or APIs (Application Programming Interface).
Introduction
There are approximately 250 services provided by this outage. The service number is put in EAX and then the other parameters are put in the remaining microprocessor registers: EBX, ECX, EDX, ESI, EDI and EBP.
Examples
Service 1 : exit the current process and return to the system that invoked it. In EBX the output mode is set, generally we put 0 to indicate that the output occurred normally (that is, it was not caused by an error).
In NASM
mov eax,1 mov ebx,0 int 80h
In Gas
movl $0, %ebx movl $1, %eax intl $0x80
Service 3 : reading ( read ). The required parameters are:
- EBX: input unit (0: standard input).
- ECX: Pointer to a memory area where the obtained characters will be left.
- EDX: Maximum number of characters to read.
In NASM
mov eax , 3 mov ebx , 0 mov ecx , sentence mov edx , 100 int 80 h
In Gas
movl $ 100 , % edx movl $ sentence , % ecx movl $ 0 , % ebx movl $ 3 , % eax intl $ 0x80
Service 4 : write ( write ). The required parameters are:
- EBX: output unit (1: standard output).
- ECX: Pointer to a memory area where the characters to be displayed are located.
- EDX: Maximum number of characters to display.
In NASM
mov eax , 4 mov ebx , 1 mov ecx , sentence mov edx , 100 int 80 h
In Gas
movl $ 100 , % edx movl $ sentence , % ecx movl $ 1 , % ebx movl $ 4 , % eax intl $ 0x80
Services for working with files
Service 5 : opening ( open ). The required parameters are:
- EBX: the address of a null-terminated character string .
- ECX: Access mode.
- EDX: permissions to the file, if it is opened by creating it.
Access modes:
- O-RDONLY 0: The file is opened only for reading.
- O-WRONLY 1: The file is only opened for writing.
- O-RDWR 2: The file is opened for reading and writing.
- O-CREAT 256: Create the file if it does not exist.
- O-APPEND 2000h: The file is opened for writing only at the end, adding information.
Permissions:
- S-IRUSR 400H: The file can be read by the owner.
- S-IWUSR 200h: The file can be written by the owner.
- S-IROTH: The file can be read by other users.
- S-IWOTH: The file can be written by other users.
In NASM
mov eax , 5 mov ebx , ” pepito.txt ” , 0 mov ecx , 1 mov edx , 0 int 80 h
In Gas
movl $ 0 , % edx movl $ 1 , % ecx movl $ ” pepito.txt ” , % ebx movl $ 5 , % eax intl $ 0x80
As an alternative to create files you can use the 8 ( create ) service.
For reading and writing the file, services 3 and 4 mentioned above are used, but the value of EBX is changed by the address of the file to read / write or its descriptor.
The 13h ( lseek ) service is used to manipulate the pointer in the file . The required parameters are:
- EBX: Descriptor of the file whose pointer is to be manipulated
- ECX: Number of bytes to move from the point that is indicated as a reference.
- EDX: Reference point for displacement. 0 start, 1 current position, 2 end
Ex: position at the beginning of the file
In NASM
mov eax,13h mov ebx, descriptor mov ecx,0 mov edx,0 int 80h
In Gas
movl $ 0 , % edx movl $ 0 , % ecx movl $ descriptor , % ebx movl $ 0x13 , % eax intl $ 0x80
After working with the file, it should be closed. For that there is the service 6 ( close ), for which it is enough to put in EBX the descriptor of the file to close.
There are many more services, for example to work with the PC clock , to work with directories , to kill processes, etc.
References
Unix Assembly Language Programming (En inglés)
See also
Linux System Call Table
Linux Syscall Reference (In English)
Сегмент
машинных команд в программе на ассемблере
состоит из строк, задающих отдельные
команды. Наиболее важными по частоте
применения являются команды перемещения
данных, называемые также командами
пересылки
(данных). В рассматриваемых ассемблерах
мнемокодом таких команд служит слово
MOV
(сокращение от английского слова move).
Команды эти имеют два операнда, один
задает исходные данные или место их
хранения, другой операнд задает место
хранения результата. Общую форму записи
команд пересылки можно представить в
виде
MOV
куда,
откуда_или_что
Для
задания места операнда в компьютере
используется множество способов, которые
будут детально рассмотрены позже. В
простейших случаях место расположения
данных — это просто обозначение регистра.
Соответственно, при выполнении команды
исходные данные будут извлекаться из
указанного в команде регистра и/или
результат будет записываться в указывающий
результат регистр. Например, первая из
команд
MOV
cl, bh
MOV
edx, esi
переписывает
данные из регистра BH
в регистр CL,
а вторая из приведенных команд пересылает
значение из регистра ESI
в регистр EDX.
В рассматриваемых ассемблерах принято
систематическое правило: местонахождение
результата (если он только указывается
явно в записи команды) всегда задается
в самом левом операнде команды. (Для
других ассемблеров может применяться
другое правило, в частности ассемблер
AT&T
требует, чтобы место размещения результата
команды задавалось в правом ее операнде.)
В
качестве источника данных для многих
команд могут задаваться непосредственно
константы, например, команды
MOV
EAX, 1237
MOV
DL, ‘W’
при
выполнении процессором помещают,
соответственно, в регистр EAX
десятичное число 1237 и в регистр DL
код символа W.
Если
метка metka
(имя области данных) обозначает начало
области данных, то команда пересылки
MOV
edx,
metka
помещает
в регистр edx
не все данные этой области (если их
много, они просто бы не вошли) и даже не
четыре байта из начала этой области, а
помещает в регистр edx
адрес
начала этой области. Такое решение на
самом деле наиболее дальновидно, так
как, имея такой адрес, можно из этой
области в другой части программы извлечь
столько данных, сколько необходимо в
конкретной ситуации. Причем в эту другую
часть программы достаточно передать в
качестве связующей информации всего
лишь содержимое регистра edx,
но не нужно передавать отдельные байты
или другие единицы этой области.
Заметим,
что только что рассмотренное решение
относится только к ассемблеру NASM.
В ассемблерах же MASM
и TASM
для аналогичной цели указания адрес
области данных в команде используется
запись вида OFFSET
имя_области_данных,
так что приведенная выше для NASM
команда запишется для них в виде
MOV
edx, OFFSET metka
При
выполнении практически любых прикладных
программ невозможно обойтись без участия
операционной системы. Это обусловлено
той причиной, что без вывода результатов
для пользователя программа для последнего
теряет какой-либо смысл. Вывод же, равно
как и ввод, в современных компьютерах
невозможен без участия ОС. Поэтому
простейшее, что следует научиться
программисту, начинающему пользоваться
языком ассемблера, — это приемы вывода
данных. Эти приемы в большинстве
модификаций существенно зависят от
особенностей конкретного API
ОС, но в современных ОС есть универсальная
возможность, которой мы и воспользуемся.
В
стандартных библиотеках языка Си для
любой операционной системы имеется
функция вывода нижнего уровня, которая
имеет прототип
unsigned
int write(int handle, char *buffer, unsigned int size)
и
которая выводит символы текста из буфера
buffer,
заданного вторым его аргументом в файл,
заданный хэндлом handle,
причем выводит size
байтов этого текста, если только это
возможно.
Упомянутая
выше универсальная возможность есть
средство вывода в стандартный файл.
Если при запуске любой программы не
делать никаких дополнительных указаний,
то операционная система предоставляет
открытый ею файл, связанный с экраном,
в качестве моделируемого файла вывода.
В
большинстве хорошо спроектированных
ОС для доступа к стандартным файлам
служат стандартные значения хэндлов,
равные 0, 1 и 2. Именно, через хэндл, равный
нулю, в дальнейшем обеспечивается доступ
к стандартному вводу (по умолчанию, если
при запуске программы не сделано
соответствующего переназначения,
стандартный ввод идет с клавиатуры).
Через хэндл, равный единицы, обеспечивается
доступ к стандартному выводу (т.е. по
умолчанию — вывод на экран). Хэндл со
значением два предназначен для
оперативного вывода сообщений об
ошибках, и также по умолчанию его
использование приводит к выводу на
экран. Обозначение стандартных ввода
и выводов хэндлами со значениями 0, 1, 2
присуще операционным системам Unix,
OS/2
и даже старенькой однозадачной MS-DOS,
но в ОС типа Windows
используется более сложный вариант,
который будет рассматриваться значительно
позже.
Для
наших целей простейшего вывода данных
с помощью языка ассемблера (в тех ОС,
где это возможно) достаточно записать
простую последовательность команд,
которая по своим действиям равносильна
вызову указанной выше функции write
со значением первого аргумента, равного
единице.
Дальше
мы рассмотрим особенности обращения к
программным функциям операционных
систем.
Наиболее
эффективным путем доступа к внутренним
подпрограммам (функциям по терминологии
Си) операционной системы оказалась
очень специфическая команда, называемая
программным
прерыванием.
Эта команда в рассматриваемых ассемблерах
записывается в виде
INT
номер_прерывания
где
номер_прерывания
есть небольшое целое неотрицательное
число, не превосходящее 255. Вместо вызова
подпрограммы по имени этой подпрограммы,
команда прерывания позволяет вызвать
подпрограмму по условному небольшому
номеру. Изнанка такого обращения состоит
в том, что практически только операционная
система может предварительно соотнести
такому номеру начало подпрограммы.
Команды программных прерываний используют
для более частных целей очень мощный и
сложный механизм прерываний процессора.
Это аппаратно-программный механизм,
предназначенный в первую очередь для
вызова подпрограмм, обеспечивающих
действия по аппаратно возникающим
сигналам, причем с автоматическим
возвратом в ту программу, которая
временно приостанавливается таким
вызовом и именно в том место, на котором
такая программа была приостановлена
им.
Оказалась,
что модификация аппаратно-программного
механизма прерываний позволяет очень
быстро обращаться к внутренним
подпрограммам операционной системы,
причем при таком обращении сложившиеся
аппаратные средства предоставляют
возможности автоматической смены
привилегий и прав доступа к информации.
Дело в том, что многие действия,
осуществляемые ОС, и внутренние данные
ОС сделаны аппаратно недоступными
пользовательским программам, а
переключение на права и привилегии при
прерываниях могут автоматически
изменяться.
Для
начального изучения возможностей и
использования ассемблера сейчас важно
лишь то, что к внутренним действиям ряда
операционных систем можно обратиться
командами INT
номер.
При этом нужно знать, который номер
прерывания задать и как передать
аргументы подпрограмме ОС, которая
будет выполнять наш запрос. Сразу же
обратим внимание, что доступ к внутренним
функциям ОС через программные прерывания
возможет в ОС Linux
и MS-DOS,
но скрыт в качестве промежуточного
механизма для таких ОС, как Windows
и OS/2.
Программный интерфейс последних
использует доступ к функциям ОС через
именованные функции, доступные в свою
очередь с помощью специальных библиотек
подпрограмм, постоянно находящихся в
оперативной памяти и одновременно
обслуживающих множество запросов. (Они
называются библиотеками динамической
компоновки.) Последний вариант доступа
будет как более сложный рассмотрен
значительно позже — после изучения
средств ассемблера, позволяющих описать
такие обращения.
Наиболее
систематичное использование программных
прерываний для доступа к базовым функциям
ОС содержит Linux.
Здесь все такие функции вызываются
через команду
INT
80H
а
какая именно функция нужна — задается
числом в регистре eax
(это число должно быть занесено в регистр
eax
перед выполнением команды INT
80H).
Все
аргументы, заданные прототипом системной
функции на языке Си, должны быть
предварительно помещены в регистры
ebx,
ecx,
edx,
esi
и edi.
Причем первый аргумент прототипа функции
должен быть помещен в регистр ebx,
второй аргумент ее — в регистр ecx,
третий — в регистр edx,
четвертый — в регистр esi
и пятый — в регистр edi.
Практически для пользователя такой
системы нужно знать только номер,
соответствующий базовой системной
функции. Эти номера содержатся в файле
unistd.h,
находящемся в большинстве модификаций
Linux
в каталоге /usr/include/asm.
Для
функции write
текущие версии Linux
дают число 4, для функции read
— число 3, для функции exit
— число 1. Этих функций, как ни странно,
оказывается достаточно для всех
рассматриваемых далее программ, которые
предназначены для ознакомления с
программированием на ассемблере.
Изученного
нами материала уже достаточно для
написания простейшей программы,
результаты работы которой можно
наблюдать. Эта программа, приведенная
в листинге 2.2.1, предназначена для
вывода на экран текста Privet!
GLOBAL
_start
SEGMENT
.text
_start:
;—
write(1, txt, 7) == <4>(ebx, ecx, edx)
mov
eax,4 ; N function=write
mov
ebx,1 ; N handle=1 (stdout)
mov
ecx, txt ; address of txt
mov
edx,7 ; number of byte
int
80h
mov
eax,1 ; N function=exit
int
80h SEGMENT .data
txt
db ‘Privet!’
Листинг
2.2.1. Простейшая программа вывода текста
на ассемблере NASM
В
этой программе описано два сегмента со
стандартными именами .text
и .data,
причем в сегменте данных описана
именованная область данных с именем
txt,
которая содержит текст, заданный
директивой DB.
Сегмент
машинных команд по существу содержит
записанные на ассемблере последовательные
вызовы системных функций write(1,
txt,
7) и exit(0).
Каждый такой вызов состоит из занесения
в регистры аргументов и обращения к
команде INT
80H.
Занесение аргументов производится
командами пересылки MOV.
Первый аргумент этих функций, задаваемый
во всех трех данных вызовах константой,
заносится в регистр ebx.
Второй аргумент для функций write
в нашем примере задает адрес области
текста, который должен быть выведен,
поэтому для его занесения использованы
команды вида
MOV
ecx,
имя_области_текста
Последний
аргумент этих функций в данной программе
также является константой, поэтому
заносится командами вида
MOV
edx,
число
Такой
же по виду командой в регистр eax
(перед вызовом программного прерывания)
заносится системный номер базовой
системной функции.
Для
наглядности перед командами реализации
вызова системной функции приведен ее
текст на языке Си, но записанный в виде
комментариев. Причем для удобства
усвоения запись вызова функции на языке
Си сопровождается условной записью
вида <номер_функции>(ebx,
ecx,
edx)
или подобной, но с другим числом аргументов
в скобках. Последняя форма записи
напоминает об используемых в Linux
правилах вызова базовых системных
функций.
Соседние файлы в предмете Системное программное обеспечение
- #
- #
15.06.20141.32 Кб99l3.asm
- #
15.06.20141.32 Кб76l5.asm
- #
15.06.2014215 б58l5a.asm
- #
15.06.20141.11 Кб158Lab1.asm
- #
15.06.2014456 б77Lab1a.asm
Содержание
- Общая часть
- Программное прерывание
- SYSENTER
- Системный вызов
- Специфичный для Linux
- Пример использования
- Короче говоря
Общая часть
РЕДАКТИРОВАТЬ: 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.
Есть три вещи, которые должны произойти при вызове ядра (системный вызов):
- Система переходит из “пользовательского режима” в “режим ядра” (кольцо 0).
- Стек переключается из “пользовательского режима” в “режим ядра”.
- Прыжок сделан в подходящую часть ядра.
Очевидно, что, оказавшись внутри ядра, код ядра должен будет знать, что вы на самом деле хотите, чтобы ядро делало, следовательно, помещая что-то в EAX, и часто больше вещей в другие регистры, так как есть такие вещи, как “имя файла, который вы хотите открыть” “или” буфер для чтения данных из файла в “и т.д. и т.д.
Различные процессоры имеют разные способы для достижения вышеупомянутых трех шагов. В x86 есть несколько вариантов, но два наиболее популярных для рукописного asm: int 0xnn (32-битный режим) или syscall (64-битный режим). (Существует также 32-битный режим sysenter, представленный Intel по той же причине, по которой AMD представила 32-битную версию syscall: в качестве более быстрой альтернативы медленному int 0x80. 32-битный glibc использует любой эффективный механизм системных вызовов., используя медленный int 0x80 если нет ничего лучшего.)
64-разрядная версия инструкции syscall была представлена в архитектуре x86-64 как более быстрый способ ввода системного вызова. Он имеет набор регистров (использующих механизмы MSR x86), которые содержат адрес RIP, к которому мы хотим перейти, какие значения селектора загружать в CS и SS, а также для перехода с Ring3 на Ring0. Он также сохраняет адрес возврата в ECX/RCX. [Пожалуйста, прочитайте руководство по набору инструкций для всех деталей этой инструкции – это не совсем тривиально!]. Поскольку процессор знает, что это переключится на Ring0, он может делать правильные действия.
Одним из ключевых моментов является то, что syscall только манипулирует регистрами; это не делает никаких грузов или магазинов. (Вот почему он перезаписывает RCX с сохраненным RIP и R11 с сохраненным RFLAGS). Доступ к памяти зависит от таблиц страниц, и записи таблицы страниц имеют бит, который может сделать их действительными только для ядра, но не для пространства пользователя, поэтому при доступе к памяти при изменении уровня привилегий может потребоваться ожидание, а не только запись регистров. swapgs в режиме ядра, ядро обычно использует swapgs или другой способ поиска стека ядра. (syscall не изменяет RSP; он все еще указывает на стек пользователя при входе в ядро.)
При возврате с использованием инструкции SYSRET значения восстанавливаются из предопределенных значений в регистрах, поэтому, опять же, это быстро, потому что процессору просто нужно настроить несколько регистров. Процессор знает, что он изменится с Ring0 на Ring3, поэтому может быстро сделать правильные вещи.
(Процессоры AMD поддерживают инструкцию syscall из 32-разрядного пользовательского пространства; процессоры Intel – нет. X86-64 изначально был AMD64; поэтому мы использовали syscall в 64-разрядном режиме. AMD переработала сторону ядра syscall для 64-разрядного режим, поэтому точка входа 64-битного ядра syscall значительно отличается от точки входа 32-битного syscall в 64-битных ядрах.)
Вариант int 0x80 используемый в 32-битном режиме, решает, что делать, основываясь на значении в таблице дескрипторов прерываний, что означает чтение из памяти. Там он находит новые значения CS и EIP/RIP. Новый регистр CS определяет новый уровень “кольца” – в этом случае Ring0. Затем он будет использовать новое значение CS для просмотра сегмента состояния задачи (на основе регистра TR), чтобы выяснить, какой указатель стека (ESP/RSP и SS), и затем, наконец, перейдет на новый адрес. Поскольку это менее прямое и более общее решение, оно также медленнее. Старые EIP/RIP и CS хранятся в новом стеке вместе со старыми значениями SS и ESP/RSP.
При возврате, используя инструкцию IRET, процессор считывает адрес возврата и значения указателя стека из стека, а также загружает новые значения сегмента стека и сегмента кода из стека. Опять же, процесс является общим и требует довольно много операций чтения из памяти. Так как он является общим, процессор также должен будет проверить “меняем ли мы режим с Ring0 на Ring3, если это так, изменим эти вещи”.
Итак, в итоге, это быстрее, потому что это должно было работать именно так.
Для 32-битного кода, да, вы можете использовать медленный и совместимый int 0x80 если хотите.
Для 64-битного кода int 0x80 медленнее, чем syscall и syscall ваши указатели до 32-битного, поэтому не используйте его. См. Что произойдет, если вы используете 32-битный int 0x80 Linux ABI в 64-битном коде? Кроме того, int 0x80 недоступен в 64-битном режиме во всех ядрах, поэтому он небезопасен даже для sys_exit который не принимает аргументов указателя: CONFIG_IA32_EMULATION может быть отключен, а особенно отключен в подсистеме Windows для Linux.
Пример работы с системным вызовом
Вот пример очень простой программы, которая ничего не делает и просто завершается с кодом выхода 2. Для выхода используется вызов системной функции (syscall) через прерывание int 80H.
Примечание. Следует учитывать, что в Linux есть и другие механизмы вызова системной функции — через ассемблерные инструкции sysenter/sysexit, через инструкции syscall/sysret которые разработаны в AMD для x32 и x64 архитектуры, но не приняты в Intel, через подсистему vsyscall (устарело), и через механизм vDSO (Virtual Dynamic Shared Object). Каждый более новый механизм вызова системной функции работает быстре чем устаревший. Например, скорость вызова через int 80H на i7-5500U (2.5GHz) будет примерно 500нс, а через механизм vDSO около 50нс. Разница в 10 раз, и этот факт может существенно влиять на производительность, если системный вызов происходит в цикле.
Эта простая программа показывает базовую структуру текста программы на ассемблере NASM:
; Начало текстового сегмента
section .text
global _start
; Точка входа в программу
_start:
; Передача кода системного вызова (для sys_exit это код 1)
mov eax, 1
; Значение, которое будет возвращать системный вызов sys_exit при выходе
mov ebx, 2
; Вызов ОС
int 80h
Сборка примера
Трансляция NASM в объектный файл:
nasm -f elf -o program.o program.asm
Компоновка:
ld -o program program.o
Компоновка при использовании внешней библиотеки C (линковка с динамической библиотекой):
ld -dynamic-linker /lib/ld-linux.so.2 -lc -o program program.o
Пример вывода строки на экран (Hello Word)
Вот пример классического Hello Word на Ассемблере NASM:
section .text
global _start
_start:
mov edx,len ; В EDX помещается длина строки
mov ecx,msg ; В ECX записывается адрес начала строки
mov ebx,1 ; В EBX помещается дескриптор файла (для stdout он всегда равен 1)
mov eax,4 ; В EAX помещается system call number 4 (sys_write)
int 0x80 ; Вызов функции ядра
mov eax,1 ; В EAX помещается system call number 1 (sys_exit)
int 0x80 ; Вызов функции ядра
section .data
msg db ‘Hello, world!’,0xa
len equ $ — msg
Компиляция:
nasm -f elf main.s -o main32.o
ld -melf_i386 main32.o -o a32.out
Или в расширенном режиме (программа будет работать, так как компилируется статически)
nasm -f elf64 main.s -o main.o
ld main.o -o a.out
