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

Системные вызовы в Linux и инструкция syscall

Последнее обновление: 21.01.2024

При работе на конкрентных операционных системах нередко многие ресурсы системы, например, системы ввода-вывода, для прикладных приложений не доступны. И чтобы получить доступ к этим ресурсам и прочим возможностям системы приложение должно выполнить определенную системную функцию или системный вызов.

Системный вызов приостанавливает выполнение программы и передает контроль ядру операционной системы. Операционная система проверяет валидность вызова и права приложения, выполняет соответствующую системную функцию и затем передает управление обратно процессу приложения.

Например, чтобы банально надлежащим образом завершить работу приложения, мы вынуждены выполнять на Linux системный вызов с номером 60:

global _start

section .text
_start:
    mov rdi, 22     ; в RDI код статуса результата
    mov rax, 60     ; в RAX номер системной функции
    syscall         ; выполняем системную функцию

Для выполнения системной функции применяется инструкция syscall. Каждая системная функция имеет определенный номер, и перед выполнением нам надо положить в регистр RAX номер вызываемой системной функции. Например, у функции завершения приложения номер 60, который выше в примере помещается в регистр RAX. Собственно по номеру системной функции ОС и понимает, что именно надо сделать.

Кроме номера системной функции при вызове иногда необходимо передать чуть больше информации или параметры, которые помещаются последовательно в следующие регистры:

  1. rdi

  2. rsi

  3. rdx

  4. r10

  5. r8

  6. r9

То есть если функция принимает один дополнительный параметр, то значение для него передается в регистр RDI. Если функция принимает два дополнительных параметра, то следующий параметр помещается в регистр RSI и так далее.

Например, функция завершения приложения или системная функция exit принимает один дополнительный параметр - статусный код результата, который помещается в регистр RDI.

Также стоит отметить, что инструкция syscall изменяет регистры rcx и r11. В регистр RCX сохраняется предыдущее значение регистра RIP - адрес следующей инструкции, которую будут выполнять приложение после завершения системного вызова, а в RIP помещается адрес обработчика системного вызова. Также syscall изменяет регистр флагов RFLAGS в соответствии с системным вызовом, а старое значение RFLAGS сохраняется в регистр r11. Поэтому, если программа использует регистры rcx и r11, то перед выполнением системного вызова эти регистры следует сохранить, например, в стек, чтобы не потерять их содержимое.

Кроме того, системный вызов может возвращать некоторый результат, который помещается в регистр rax.

Полный и постоянно обновляемый список системных вызовов для Linux x86-64 можно найти по ссылке Linux kernel syscall tables

Получение времени

Системы Unix измеряют время в секундах, которые прошли прошедших с начала эпохи Unix - с 00:00 1 января 1970 года. Для получения этого времени применяется системный вызов с номером 201 (0xc9). Он требует один параметр — указатель на 64-битное значение для хранения времени. При успешном выполнении регистр rax будет содержать значение указателя (то же значение, которое передано через rdi):

SYSCALL_DEFINE1(time, __kernel_old_time_t __user *, tloc)
{
	__kernel_old_time_t i = (__kernel_old_time_t)ktime_get_real_seconds();

	if (tloc) {
		if (put_user(i,tloc))
			return -EFAULT;
	}
	force_successful_syscall_return();
	return i;
}

Например, возьмем приложение, которое будет ждать примерно 5 секунд:

global _start

section .data
curtime dq 0    ; для хранения времени

section .text
_start:
    ; получаем начальное время
    mov rax, 0xc9      ; номер системной функци
    mov rdi, curtime     ; адрес переменной для получения времени
    syscall             ; выполняем систеиный вызов

    mov rdx, [curtime]      ; сохраняем полученное время в %rdx
    
    add rdx, 5           ; добавляем 5 секунд

timeloop:
    mov rax, 0xc9         ; проверяем время
    mov rdi, curtime
    syscall
    ; если мы не достигли времени в rdx, переходим обратно к timeloop
    cmp qword [curtime], rdx
    jb timeloop

exit:
    mov rdi, 22
    mov rax, 60
    syscall 

Здесь программа получает время Unix (начальное время). Затем зацикливается, постоянно повторно запрашивая текущее время, пока программа не получит время как минимум через 5 секунд после начального времени.

Запись в файл

Одна из наиболее часто используемых системных функций - это функция записи в файл, которая пишет в файл некоторую информацию. Само понятие "файл" охватывает в Linux очень многие вещи. Когда файл открывается, в операционной системе ему присваивается номер, который применяется для ссылки на этот файл. Это число еще называется дескриптор файла и обычно имеет небольшое значение. Когда процесс запускается, программе обычно доступны три файловых дескриптора. Дескриптор файла 0 представляет файл "стандартного ввода", который обычно представляет собой ввод с клавиатуры в консоли. Дескриптор файла 1 относится к стандартного выводу, который обычно представляет вывод на консоль. Дескриптор файла 2 относится к файлу стандартных ошибок и предназначен для вывода сообщений об ошибках.

На Linux для записи в файл предназначена системная функция write, которая имеет номер 1 и которая принимает 3 параметра:

ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
	struct fd f = fdget_pos(fd);
	ssize_t ret = -EBADF;

	if (f.file) {
		loff_t pos, *ppos = file_ppos(f.file);
		if (ppos) {
			pos = *ppos;
			ppos = &pos;
		}
		ret = vfs_write(f.file, buf, count, ppos);
		if (ret >= 0 && ppos)
			f.file->f_pos = pos;
		fdput_pos(f);
	}

	return ret;
}
  • В регистр RDI помещается дескриптор вывода - то есть куда выводить строку. К примеру, это может быть файл на диске, консоль и т.д.

  • В регистр RSI помещается адрес строки

  • В регистр RDX помещается количество символов строки

Например, выведем на консоль строку:

global _start

section .data
message db "Hello METANIT.COM", 10
len equ $-message   ; размер строки

section .text
_start:
    mov rax, 1           ; номер системной функци
    mov rdi, 1           ; дескриптор стандартного (консольного) вывода
    mov rsi, message     ; адрес строки
    mov rdx, len         ; размер строки
    syscall              ; выполняем системный вызов

    mov rdi, rax         ; в RDI из RAX количество выведенных на консоль байтов 
    mov rax, 60
    syscall 

Стоит обратить внимание, что этот системный вызов возвращает количество реально записанных в файл байтов, которые можно получить через регистр RAX. И в данном случае это количество помещаем в регистр RDI:

mov rdi, rdi

Пример компиляции и выполнения программы:

eugene@Eugene:~/asm# nasm -f elf64 hello.asm -o hello.o
eugene@Eugene:~/asm# ld hello.o -o hello
eugene@Eugene:~/asm# ./hello
Hello METANIT.COM
eugene@Eugene:~/asm# echo $?
18
eugene@Eugene:~/asm#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850