При работе на конкрентных операционных системах нередко многие ресурсы системы, например, системы ввода-вывода, для прикладных приложений не доступны. И чтобы получить доступ к этим ресурсам и прочим возможностям системы приложение должно выполнить определенную системную функцию или системный вызов.
Системный вызов приостанавливает выполнение программы и передает контроль ядру операционной системы. Операционная система проверяет валидность вызова и права приложения, выполняет соответствующую системную функцию и затем передает управление обратно процессу приложения.
Например, чтобы банально надлежащим образом завершить работу приложения, мы вынуждены выполнять на Linux системный вызов с номером 60:
global _start section .text _start: mov rdi, 22 ; в RDI код статуса результата mov rax, 60 ; в RAX номер системной функции syscall ; выполняем системную функцию
Для выполнения системной функции применяется инструкция syscall. Каждая системная функция имеет определенный номер, и перед выполнением нам надо положить в регистр RAX номер вызываемой системной функции. Например, у функции завершения приложения номер 60, который выше в примере помещается в регистр RAX. Собственно по номеру системной функции ОС и понимает, что именно надо сделать.
Кроме номера системной функции при вызове иногда необходимо передать чуть больше информации или параметры, которые помещаются последовательно в следующие регистры:
rdi
rsi
rdx
r10
r8
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#