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

в 17:24, , рубрики: mips, ассемблер, низкоуровневое программирование, операционные системы, системное программирование, метки: , , , ,

Системные вызовы MIPSЭтим летом appplemac опубликовал статью, посвященную изучению ассемблера MIPS. В ней, в частности, была рассмотрена команда syscall, генерирующая системный вызов. Автор сосредоточился на объяснении ассемблера MIPS, и на мой взгляд, недостаточно подробно рассказал, что же это такое — системный вызов. Я в тот момент занимался переносом проекта под архитектуру MIPS, разбирался с прерываниями, исключениями и системными вызовами.

Сейчас, когда код уже написан и отлажен, я решил написать статью, которая бы более подробно раскрывала, как работает механизм системных вызовов в MIPS. Можно рассматривать ее как дополнение к той статье об ассемблере.

Введение

Прежде всего нужно разобраться, что же такое системные вызовы, и зачем они нужны.
Википедия дает такое определение:

Систе́мный вы́зов (англ. system call) в программировании и вычислительной технике — обращение прикладной программы к ядру операционной системы для выполнения какой-либо операции.
С точки зрения программиста системный вызов обычно выглядит как вызов подпрограммы или функции из системной библиотеки. Однако системный вызов как частный случай вызова такой функции или подпрограммы следует отличать от более общего обращения к системной библиотеке, поскольку последнее может и не требовать выполнения привилегированных операций.

Системные вызовы MIPS
Другими словами, системный вызов — это вызов функции с заранее известным адресом и одновременным переводом процессора с привилегированный режим (режим ядра). Перевод в режим ядра позволяет выполнять привилегированные команды, например, управление таблицами виртуальной памяти, запрещение/разрешение прерываний, а также обращаться к данным, хранящимся в ядре.
А заранее известный адрес означает, что все функции обработки можно представить массивом указателей, а данному обработчику будет соответствовать индекс в этом массиве. Отличие системного вызова от вызова функции заключается в том, что управление на базовый адрес + смещение передается аппаратно, самим процессором.

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

Архитектура MIPS

Перейдем к конкретной реализации данных подсистем в архитектуре MIPS.

В архитектуре MIPS версии 2 существует несколько режимов работы для прерываний. Они отличаются базовыми адресами и структурами самих таблиц прерываний.
Для базового адреса есть два режима:

  1. В первом процессор, столкнувшись с любым типом исключений, передает управление на адрес фиксированный адрес (0x80000180), размер обработчика составляет 128 байт.
  2. Во втором процессор передает управление на адрес, указанный в регистре CP0_EBASE, размер обработчика — 256 байт.

Для структуры таблицы прерываний также есть два режима работы: обычный, когда в ответ на все исключительные ситуации вызывается один обработчик, и векторный, при котором каждому номеру прерывания отводиться свое пространство для обработчика.

Задаются эти режимы в специальных регистрах процессора MIPS. Специальные регистры, в отличие от регистров общего назначения, используются программой для управления самим процессором.

В MIPS такие регистры вынесены в сопроцесор 0. И обращение к ним ведется специальными ассемблерными командами: mfc0 — для чтения регистров, и mtc0 — для записи в регистр.
Регистры адресуются индексом и селектором сопроцессора. Вот несколько важных для обработки системных вызовов регистров:

Название Индекс Селектор Описание
CP0_STATUS 12 0 управляющие флаги для процессора
CP0_CAUSE 13 0 информация о причине возникновения прерывания
CP0_EPC 14 0 адрес команды, которая исполнялась в момент прерывания
CP0_EBASE 15 1 базовый адрес процедуры обработки исключения

Возвращаясь к заданию режимов обработки исключений, они задаются в двух регистрах: CP0_STATUS и СP0_CAUSE, имеющих следующий формат.

СP0_STATUS

31-28 27 26 25 24 23 22 21 20 19 18-16 15-8 7 6 5 4-3 2 1 0
CU3..CU0 RP FR RE MX PX BEV TS SR NMI Impl IM7..IM0 KX SX UX KSU ERL EXL IE

CP0_CAUSE

31 30 29-28 27 26 25-24 23 22 21-16 15-10 9-8 7 6-2 1-0
BD TI CE DC PCI 0 IV WP 0 IP IP 0 exCode 0

Инициализация процессора

Я буду рассматривать только первый режим работы, как самый простой и совместимый со всеми MIPS процессорами. Все остальные режимы делаются похожим образом.

Для перевода процессора в этот режим нужно сбросить бит BEV в регистре статуса и бит IV в регистре причины.

Си-шный код из проекта

/* Setup a proper exception table and enable exceptions. */
static int mips_exception_init(void) {
	unsigned int reg;

	/* clear BEV bit */
	reg = mips_read_c0_status();
	reg &= ~(ST0_BEV);
	mips_write_c0_status(reg);

	/* clear CauseIV bit */
	reg = mips_read_c0_cause();
	reg &= ~(CAUSE_IV);
	mips_write_c0_cause(reg);

	/* copy the first exception handler */
	memcpy((void *)(EBASE + 0x180), &mips_first_exception_handler, 0x80);

	mips_setup_exc_table();

	/* clear EXL bit */
	reg = mips_read_c0_status();
	reg &= ~(ST0_ERL);
	mips_write_c0_status(reg);

	return 0;
}

После очистки этих бит при возникновении прерывания, исключения или системного вызова процессор прерывает последовательное выполнение инструкций и передает управление по адресу 0x80000180, где находится код первичной обработки, скопированный нами по данному адресу. Одновременно с этим процессор переходит в привилегированный режим, сохраняет адрес возврата в регистре CP0_EPC, и записывает причину (тип) исключения в регистр CP0_CAUSE (в поле exception code).

Про поле exception code стоит рассказать немного подробнее. Как было сказано выше, в MIPS, как, впрочем, и в других архитектурах, прерывания, системные вызовы и аппаратные исключения обычно реализуются похожим образом, в одной подсистеме. То есть первое, что должен сделать код обработчика, это сохранить информацию о том, что же произошло. Именно эта информация и заносится в поле exception code. В MIPS это поле может принимать следующие значения:

Код Обозначение Описание
0 INT Внешнее прерывание
1-3 Работа с виртульной памятью
4 ADDRL Чтение с невыравненного адреса
5 ADDRS Запись по невыравненному адресу
6 IBUS Ошибка при чтении инструкций
7 DBUS Ошибка на шине данных
8 SYSCALL Системный вызов
9 BKPT Брейкпоинт
10 RI Зарезервированная инструкция
11 Ошибка сопроцессора
12 OVF Арифметическое переполнение
13 и выше Операции с плавающей точкой

Обработка системных вызовов

Обработчик первого уровня

Первичный обработчик написан на ассемблере.

NESTED(mips_first_exception_handler, 0, $sp)
    .set  push             	/* save the current status of flags */

    mfc0  $k1, $CP0_CAUSE
    andi  $k1, $k1, 0x7c   	/* read exception number */

    j 	mips_second_exception_handler /* jump to real exception handler */
    nop

    .set  pop              	/* restore the previous status of flags */
END(mips_first_exception_handler)

Он всего лишь запоминает тип исключения в регистр $k1 и вызывает обработчик второго уровня, который уже не ограничен в размерах. Вызов выполняется командой «j», а не «jar», поскольку код обработчика помещается во время работы программы (мы его скопировали в функции инициализации), и нам необходимо иметь абсолютный, а не относительный адрес вызываемой процедуры.

Еще одна особенность, о которой стоит здесь упомянуть, это регистр k1.
В MIPS архитектуре существуют 32 регистра общего назначения r0 — r31. И по соглашению некоторые регистры используются специальным образом, например, регистр r31 используется как указатель на стек, и к нему можно обратиться по специальному имени sp. То же самое и с регистрами k0 (r26) и k1 (r27), компилятор их не использует, они зарезервированы для использования в ядре ОС, и обработка прерываний — это как раз такой случай специального использования.

Обработчик второго уровня

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

 LEAF(mips_second_exception_handler)
	SAVE_ALL                             /* save all needed registers */

	PTR_L   $k0, exception_handlers($k1) /* exception number is an offset in array */
	PTR_LA  $ra, restore_from_exception  /* return address for exit from exception */
	move    $a0, $sp                     /* Arg 0: saved regs. */
	jr      $k0                          /* Call C code. */
	nop

restore_from_exception:                  /* label for exception return address */
	RESTORE_ALL                          /* restore all registers and return from exception */
END(mips_second_exception_handler)

SAVE_ALL это ассемблерный макрос. Выглядит он следующим образом.

	.macro SAVE_ALL
		LONG_ADDI $sp, -PT_SIZE
		SAVE_SOME
		SAVE_AT
		SAVE_TEMP
		SAVE_STATIC
	.endm

Не буду приводить исходный текст всех вложенных макросов. Скажу лишь, что в первой строчке резервируется стек фрейм для прерывания, куда последовательно сохраняются все необходимые регистры.
SAVE_AT — регистр at (r1) зарезервирован для использования ассемблере и работа с ним должна отделяться директивами ".set noat" и ".set at" (чтобы не было предупреждений компилятора)
SAVE_TEMP — сохраняет временные регистры (r8-r15) и (r24-r25)
SAVE_STATIC — регистры s0-s7
SAVE_SOME — необходимые служебные регистры, например, указатель на стек и специальные регистры сопроцессора (например, регистр статуса), поэтому данный макрос должен стоять первым.

Затем происходит выбор правильного обработчика третьего уровня. Указатели на обработчики третьего уровня в нашем проекте хранятся в обычном массиве, тип исключения задает смещение. Именно смещение, а не индекс, поскольку создатели MIPS помещают в регистр CAUSE номер исключения со сдвигом на два бита влево, поэтому мы можем напрямую вызвать функцию из массива указателей, не проводя дополнительную арифметику.
Затем, перед вызовом функции мы хотим записать адрес возврата (ra). Ну и наконец, мы передаем в функцию обработчика информацию о состоянии, в котором мы вошли в прерывание, для этого мы передаем указатель на стек, а в сигнатуре Си-шной функции мы укажем описание (структуру) данного фрейма.

Вот описание этой структуры

typedef struct pt_regs {
	unsigned int reg[25];
	unsigned int gp; /* global pointer r28 */
	unsigned int sp; /* stack pointer r29 */
	unsigned int fp; /* frame pointer r30 */
	unsigned int ra; /* return address 31*/
	unsigned int lo;
	unsigned int hi;
	unsigned int cp0_status;
	unsigned int pc;
}pt_regs_t;

Обработчик третьего уровня (С-код)

Код си-шного обработчика следующий

void mips_c_syscall_handler(pt_regs_t *regs) {
	uint32_t result;

	/* v0 contains syscall number */
	uint32_t (*sys_func)(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) =
			 SYSCALL_TABLE[regs->reg[1]];

	/* a0, a1, a2, a3, s0 contain arguments */
	result = sys_func(regs->reg[3], regs->reg[4], regs->reg[5],
			    regs->reg[6], regs->reg[15]);

	/* v0 set equal to result */
	regs->reg[1] = result;

	regs->pc += 4;  /* skip comand generated syscall */
}

Надеюсь, что все понятно из кода:

  • Сначала получаем номер обработчика системного вызова с нужным номером.
  • Затем вызываем этот обработчик, передавая туда все параметры, которые могут быть при вызове системного вызова.
  • В регистр v0 укладываем результат вызова.
  • И, наконец, пропускаем команду сгенерировавшую системный вызов, иначе мы вернемся на тот же адрес, где он случился, и вызов произойдет снова.
Получение системного вызова

Теперь нужно рассказать о том, как же сделать системный вызов.
Системный вызов генерируется специальной ассемблерной командой: например, в x86 это int, в SPARC — ta, а в MIPS — syscall.

Как наверное стало понятно из предыдущего раздела, на момент системного вызова в регистре v0 должен храниться номер вызова, а в регистрах a0, a1, a2, a3 передаваемые параметры. Вот, например, код функции, которая положит один аргумент в регистр a0 и сделает системный вызов 0x11. Я предполагаю, что читатель знаком с gcc inline ассемблером

static inline int syscall_demo(int arg1) {                                           
long __res;                                 
__asm__ volatile (                      
          "move $a0, %2nt"           
         "li $v0, %1nt"  
          "syscallnt"      
          "move %0, $v0"
        : "=r" (__res)
        : "I"  (0x11),     
          "r"  ((long)(arg1)));
return __res;  
}

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

#define __SYSCALL1(NR,type,name,type1,arg1) 
static inline type name(type1 arg1)         
{                                           
long __res;                                 
__asm__ volatile (                          
          "move $a0, %2nt"                
		  "li $v0, %1nt"                  
          "syscallnt"                     
          "move %0, $v0"                    
                                            
        : "=r" (__res)                      
        : "I"  (NR),                        
          "r"  ((long)(arg1)));             
return __res;                               
}

Код для системных вызовов с другим количеством параметров аналогичен приведенному.

Собираем все вместе. У нас в проекте используется несколько тестов, среди которых тот, что расположен ниже.

SYSCALL1(1,int,syscall_1,int,arg1);

TEST_CASE("calling syscall with one argument") {
	test_assert_equal(syscall_1(1), 1);
}

Макрос SYSCALL раскрывается в приведенный выше код с inline ассемблером, в качестве номера у него подставляется 1 (первый аргумент макроса) в качестве имени вызова syscall_1 (третий параметр), тип возврата int (второй параметр макроса) и тип переменной тоже int (четвертый параметр макроса).
В самом тесте проверяется, что результат вызова syscall_1(1), будет равен единице.

Ссылки по теме

  1. Реализация прерываний для MIPS в загрузчике Das U-boot
  2. Реализация прерываний для MIPS в ядре Linux
  3. Код нашего проекта
Заключение

В завершение, тем, кому интересно более детально разобраться в данной теме, рекомендую взять код проекта и поиграться на qemu (на вики-страницах описано, как запустить). Понять, как все устроено гораздо проще, если походить по точкам останова со всеми удобствами Eclipse.

Спасибо всем, кто дочитал до конца! Буду рад услышать замечания, рекомендации и пожелания.

Автор: antonkozlov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js