При работе на конкрентных операционных системах нередко многие ресурсы системы, например, системы ввода-вывода, для прикладных приложений не доступны. И чтобы получить доступ к этим ресурсам и прочим возможностям системы приложение должно выполнить определенную системную функцию или системный вызов.
Системный вызов приостанавливает выполнение программы и передает контроль ядру операционной системы. Операционная система проверяет валидность вызова и права приложения, выполняет соответствующую системную функцию и затем передает управление обратно процессу приложения.
Например, чтобы банально надлежащим образом завершить работу приложения, мы вынуждены выполнять на Linux системный вызов с номером 60:
.globl _start .text _start: movq $22, %rdi # в RDI код статуса результата movq $60, %rax # в RAX номер системной функции syscall # выполняем системную функцию
Для выполнения системной функции применяется инструкция syscall. Каждая системная функция имеет определенный номер, и перед выполнением нам надо положить в регистр RAX номер вызываемой системной функции. Например, у функции завершения приложения номер 60, который выше в примере помещается в регистр RAX. Собственно по номеру системной функции ОС и понимает, что именно надо сделать.
Кроме номера системной функции при вызове иногда необходимо передать чуть больше информации или параметры, которые помещаются последовательно в следующие регистры:
%rdi
%rsi
%rdx
%r10
%r8
%r9
То есть если функция принимает один дополнительный параметр, то значение для него передается в регистр RDI. Если функция принимает два дополнительных параметра, то следующий параметр помещается в регистр RSI и так далее.
Например, функция завершения приложения или системная функция exit принимает один дополнительный параметр - статусный код результата, который помещается в регистр RDI.
Также стоит отметить, что системный вызов изменяет регистры %rcx (хранит адрес следующей инструкции, которую будут выполнять приложение после завершения системного вызова) и регистр %r11 (хранит текущее состояние флагов из регистра %eflags).
Кроме того, системный вызов может возвращать некоторый результат, который помещается в регистр %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 секунд:
.globl _start .data curtime: .quad 0 # для хранения времени .text _start: # получаем начальное время movq $0xc9, %rax # номер системной функци movq $curtime, %rdi # адрес переменной для получения времени syscall # выполняем систеиный вызов movq curtime, %rdx # сохраняем полученное время в %rdx addq $5, %rdx # добавляем 5 секунд timeloop: movq $0xc9, %rax # проверяем время movq $curtime, %rdi syscall # если мы не достигли времени в %rdx, переходим обратно к timeloop cmpq %rdx, curtime jb timeloop exit: movq $0, %rdi movq $60, %rax 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 помещается количество символов строки
Например, выведем на консоль строку:
.globl _start .data message: .ascii "Hello METANIT.COM\n" len=.-message .text _start: movq $1, %rax # номер системной функци movq $1, %rdi # дескриптор стандартного (консольного) вывода movq $message, %rsi # адрес строки movq $len, %rdx # размер строки syscall # выполняем системный вызов movq %rax, %rdi # в RDI из RAX количество выведенных на консоль байтов movq $60, %rax syscall
Стоит обратить внимание, что этот системный вызов возвращает количество реально записанных в файл байтов, которые можно получить через регистр RAX. И в данном случае это количество помещаем в регистр RDI:
movq %rax, %rdi
Пример компиляции и выполнения программы:
eugene@Eugene:~/asm# as hello.s -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#