Как приручить ядро процессора*

от автора

image В данной статье рассказывается о этапах загрузки ядер процессоров серии QoriQ и участии в этом загрузчика u-boot, а также о выполнении отдельно взятой программы на отдельном процессорном ядре без участия ОС. Статья может заинтересовать системных программистов, стремящихся постичь все разнообразие процессорных архитектур. Также следует понимать, что некоторые определения и приемы актуальны и для других процессоров и систем.

* на примере процессоров freescale qoriq c ядрами e500mc и ppc booke isa.

К делу !

Жизнь начинается со старта ядра* номер 0, которое выполняет с фиксированного адреса код загрузчика. В нашем случае загрузчиком является u-boot, расположенный в flash памяти и доступный по физическим адресам 0x3ffff000. Сразу после старта этот адрес отображается на виртуальный 0xfffff000, о чем имеется запись в таблице отображения (см. документацию на e500).

*

здесь и далее под ядром понимается ядро процессора (core), если не указано иного.

Первой командой, исполняемой процессором является команда, расположенная по адресу 0xfffffffc. Как вы, наверно, уже догадались этой командой обязательно должна быть команда перехода в точку старта u-boot на этой странице. Как-то так:

/* */ .section .resetvec,"ax" 	b _start_e500  
для тех, кто в Интле

b ≈ jmp

для тех, кто в Java

это команда безусловного перехода на метку _start_e500

Далее в задачи u-boot входит включение и настройка кэшей, механизмов разграничения доступа, таблицы отображения адресов и, конечно, надо отобразить остальную часть себя в адресное пространство ядра процессора. Вообще u-boot — молодец: берет на себя всю грязную работу, в отличие от некоторых (не будем тыкать пальцем, это не его вина).

А как же остальные ядра? К остальным ядрам переходим, когда закончили с основным. Если их запустить, то они повторят последовательность действий нулевого ядра. Для предотвращения этого uboot изменят адрес трансляции загрузочной области на место расположения дополнительного загрузчика остальных ядер(см. ccsr boot space translation registerы).

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

В задачи дополнительного загрузчика также входит настройка кэш и добавление записи в таблицу отображения адресов, после чего ядра крутятся в такой веселой карусельки:

/* spin waiting for addr */ 2: 	lwz	r4,ENTRY_ADDR_LOWER(r10)  /* загрузить данные по адресу*/ 	andi.	r11,r4,1		/* если  младший бит = 1, то...*/ 	bne	2b				/* ...переход выше, к метке 2*/ 

Крутятся они до тех пор, пока в определенный u-boot адрес не запишут адрес точки старта с 0 младшим битом. Место, куда надо записать эту команду в u-boot называется spin-таблица и расположена она по фиксированному адресу (0xfffff000 + ENTRY_ADDR_LOWER). Кроме адреса старта, в эту таблицу можно записать значения регистров r6 и r3, которые будут загружены перед выполнением команды перехода в точку старта.

На точку старта накладывается ограничение в размер уже отображенной u-boot 64Мб страницы, это связано с внутренними тараканами uboot.

Для тех, кто учился на современных учебниках информатики

В PowerPC и некоторых других архитектурах страница является растяжимым понятием. В частности, на процессорах e500* она может растянуться в диапазоне от 4К до 4Гб (подробности и ограничения см. в документации).

Создание приложений для ядра.

Давайте разработаем программу для нашего ядра, которая традиционно будет печатать «hello world». Будем считать, что кросс компилятор и легковесную библиотеку libc мы уже собрали, а также мы умеем загружать ОС без поддержки многоядерности на одном из ядер (в качестве ОС можно, например, использовать скомпилированный соответствующим образом linux или lynxos-178). Поэтому приступим к самому тяжелому — программированию:

#include <stdio.h> int main (int argc, char *argv[]) { 	printf («hello world  \n»); 	return 0; } 

Готово. А куда и как будет выводить printf ?! Для этого придется написать некоторые заглушки для libc, которые можно подсмотреть в исходниках u-boot. Я использую упрощенный вариант:

int write (int fildes, char *buf, int nbyte)  { 	int wbyte = 0; 	while (nbyte > 0) { 		__putc(*buf); 		buf++; 		nbyte--; 		wbyte++; 	} 	return wbyte; }  int fstat(int fildes, struct stat *buf); { 	buf->st_mode = S_IFCHR; 	return 0; } 

И функция __putc должна обеспечить вывод одного символа в последовательный порт.

extern volatile unsigned char *uart_data; extern volatile unsigned char *uart_status;  static void __putc(unsigned char c) {         unsigned char v;          do {                 v = *uart_status;         } while (!(v & (1 << 5)));         *uart_data = c; } 

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

И обрезаем реальную точку старта исполняемого файла— функцию _start. Для инициализации можно оставить только обнуление bss сегмента:

int _start(int argv, char **argc) {         unsigned char * cp = (unsigned char *) &__sbss;         while (cp < (unsigned char *)&__ebss) {                 *cp++ = 0;         }  	return main(argv, argc); } 

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

$ powerpc-eabi-gcc -o hello hello.c start.c  

Будет работать? Нет! Процессор не понимает формат исполняемого файла elf. Надо отрезать лишнее. Обрезаем:

$ powerpc-eabi-objcopy -O binary  hello hello.bin  

Будет работать? Нет! Раньше точка старта задавалась в elf, а теперь она черт знает где. Более подробное местоположение можно посмотреть утилиткой powerpc-eabi-objdump. Конечно, можно в качестве точки старта указать u-boot и туда, но лучше написать указания линковщику о размещении точки старта в начале файла:

	OUTPUT_ARCH(powerpc:common) 	ENTRY(_start) 	STARTUP(start.o) 	... 

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

Собираем еще раз и думаем как удобно это делать с makefile:

$ powerpc-eabi-gcc -T hello.ld -o hello hello.c start.c  

Теперь, если мы все собрали правильно, у нас есть готовая для запуска на отдельном ядре, без ОС программа. Но лучше дополнительно все проверить с помощью objdump. Кстати, вы заметили адреса сегментов сбоку? Если не указано иного, то наша программа имеет позиционно зависимый код и запускать ее откуда попало нельзя. Но сейчас это нас не должно волновать, а исправить смещение можно с помощью сценария линковщика. Правильно собранная программа должна запускаться даже через u-boot.

Запуск ядра

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

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

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

На самом деле,

после записи адреса u-boot сделает прыжок по виртуальному адресу, но поскольку на данном этапе они равны, то можно считать это чудом, но понимать как оно работает.

А про ограничение в размер точки старта забыли?
Нельзя быть уверенным в каком диапазоне адресов у системы останется свободная память и может так случится, что выделенная память придется на неотображаемый u-boot диапазон адресов. Это приведет к выполнению исключения ошибки страницы, которое у нас не настроено, что в конечном счете приведет к останову ядра процессора.
Решений этого может быть несколько и мне больше всего понравилось сделать в ОС свой инициализатор ядра, расположенный в начальных адресах, который:

  • преодолеет ограничение точки старта;
  • ограничит наше приложение в используемой памяти так, чтобы был виден только участок памяти выделенный ОС;
  • настроит доступные для ядра устройства;
  • совершит прыжок в начало нашей программы;

После передачи управления программе в консоль должно вывестись «hello world
»
В дальнейшем, используя регистры управления, ядра процессора можно останавливать, перезапускать, менять частоту, выдавать им прерывания (это интересная, но очень специфичная тема) и многое другое.

Заключение

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

ссылка на оригинал статьи http://habrahabr.ru/post/237471/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *