Как найти PCI устройства без операционной системы

от автора

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

В качестве минимальной базы для работы с PCI-устройствами будем использовать ядро, поддерживающее спецификацию Multiboot. Так удастся избежать необходимости писать собственный загрузочный сектор и загрузчик (loader). Кроме того, этот вопрос и так отлично освещен в интернете. В качестве загрузчика будет выступать GRUB. Грузиться мы будем с флэшки, так как с нее удобно загружать и виртуальную, и реальную машину. В качестве виртуальной машины будем использовать QEMU. В качестве реальной машины должна выступать машина с обычным BIOS-ом (не UEFI), поддерживающим загрузку с USB-HDD (обычно присутствует опция Legacy USB support). Для работы понадобятся Ubuntu Linux со следующими программами: expect, qemu, grub (их можно легко установить при помощи команды sudo apt-get install). Используемый gcc должен компилировать 32х битный код.

Рассмотрим первый шаг – создание ядра, поддерживающего спецификацию Multiboot. В случае использования GRUB-а в качестве загрузчика ядро будет создаваться из 3-х файлов:
Kernel.c – основной файл с кодом нашей программы и процедурой main();
Loader.s – содержит заголовок мультизагрузчика для GRUB;
Linker.ld – скрипт компоновщика ld, в котором в частности указывается, по какому адресу будет располагаться ядро.

Содержимое Linker.ld:

ENTRY (loader)  SECTIONS {     . = 0x00100000;      .text ALIGN (0x1000) :     {         *(.text)     }      .rodata ALIGN (0x1000) :     {         *(.rodata*)     }      .data ALIGN (0x1000) :     {         *(.data)     }      .bss :     {         sbss = .;         *(COMMON)         *(.bss)         ebss = .;     } } 

Скрипт компоновщика указывает, как слинковать уже скомпилированные объектные файлы. В первой строчке указано, что точкой входа в нашем ядре будет адрес с меткой «loader». Далее в скрипте указано, что начиная с адреса 0x00100000 (1Мб) будет располагаться секция text. Секции rodata, data и bss выровнены по 0x1000 (4Кб) и располагаются после секции text.

Содержимое Loader.s:

.global loader  .set FLAGS,    0x0 .set MAGIC,    0x1BADB002                .set CHECKSUM, -(MAGIC + FLAGS)   .align 4 .long MAGIC .long FLAGS .long CHECKSUM  # reserve initial kernel stack space .set STACKSIZE, 0x4000   .lcomm stack, STACKSIZE  .comm  mbd, 4   .comm  magic, 4    loader:     movl  $(stack + STACKSIZE), %esp     movl  %eax, magic     movl  %ebx, mbd            call  kmain        cli hang:     hlt      jmp   hang 

GRUB после загрузки образа ядра с диска ищет в первых 8Кб загруженного образа сигнатуру 0x1BADB002. Сигнатура является первым полем заголовка мультизагрузки. Сам заголовок выглядит следующим образом:

Offset

Type

Field Name

Note

0

u32

magic

required

4

u32

flags

required

8

u32

checksum

required

12

u32

header_addr

if flags[16] is set

16

u32

load_addr

if flags[16] is set

20

u32

load_end_addr

if flags[16] is set

24

u32

bss_end_addr

if flags[16] is set

28

u32

entry_addr

if flags[16] is set

32

u32

mode_type

if flags[2] is set

36

u32

width

if flags[2] is set

40

u32

height

if flags[2] is set

44

u32

depth

if flags[2] is set

Заголовок должен включать в себя минимум 3 поля – magic, flag, checksum. Поле magic является сигнатурой и, как уже было сказано выше, всегда равно 0x1BADB002. Поле flag содержит дополнительные требования к состоянию машины на момент передачи управления ОС. В зависимости от значения этого поля может меняться набор полей в структуре Multiboot Information. Указатель на структуру Multiboot Information содержит регистр EBX в момент передачи управления загружаемому ядру. В нашем случае поле flag имеет значение 0, и заголовок мультизагрузки состоит только из 3-ех полей.

На момент передачи управления ядру процессор работает в защищенном режиме с выключенной страничной адресацией. Обработка прерываний от устройств отключена. GRUB не формирует стек для загружаемого ядра, и это первое что должна сделать операционная система. В нашем случае под стек выделяется 16Кб. Последней выполненной ассемблерной инструкцией будет инструкция call kmain, которая передает управление коду на C, а именно функции void kmain(void).

Содержимое kernel.c:

#include "printf.h" #include "screen.h"  void kmain(void) {	 	clear_screen(); 	printf(" -- Kernel started! -- \n"); } 

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

Для сборки ядра будет использоваться следующий простой makefile:

CC	= gcc CFLAGS	= -Wall -nostdlib -fno-builtin -nostartfiles -nodefaultlibs LD	= ld   OBJFILES = \ 	loader.o  \ 	printf.o  \ 	screen.o  \ 	pci.o  \ 	kernel.o  start: all 	cp ./kernel.bin ./flash/boot/grub/ 	expect ./grub_install.exp 	qemu /dev/sdb  all: kernel.bin  .s.o: 	as -o $@ $<   .c.o: 	$(CC) $(CFLAGS) -o $@ -c $<   kernel.bin: $(OBJFILES) 	$(LD) -T linker.ld -o $@ $^   clean: 	rm $(OBJFILES) kernel.bin 

Теперь у нас есть ядро, которое можно загрузить. Пора проверить, что оно действительно загружается. Установим GRUB на флешку и скажем ему загружать наше ядро при старте. Для этого нужно выполнить следующие шаги:

1. Создать раздел на флешке, отформатировать его в файловую систему, поддерживаемую GRUB-ом (в нашем случае это файловая система FAT32). Мы воспользовались утилитой Disk Utility из комплекта Ubuntu, которая позволила создать раздел:

2. Примонтировать флешку и создать каталог /boot/grub/. Скопировать в него из /usr/lib файлы stage1, stage2, fat_stage1_5. Создать текстовый файл menu.lst в директории /boot/grub/ и записать в него

timeout   5 default   0  title  start_kernel root   (hd0,0) kernel /boot/grub/kernel.bin 

Для установки GRUB-а на флешку используется expect-скрипт в файле grub_install.exp. Его содержимое:

log_user 0 spawn grub expect "grub> " send "root (hd1,0)\r" expect "grub> " send "setup (hd1)\r" expect "grub> " send "quit\r" exit 0 

В конкретном случае возможны другие номера дисков и названия устройств. В конечном итоге компиляция и запуск виртуальной машины должны выполняться командой make start. Эта команда из makefile выполнит установку GRUB на флэшку с использованием скрипта grub_install.exp, а затем запустит виртуальную машину QEMU с нашей программой. Поскольку все загружается с реальной флэшки, то с нее можно загрузить не только виртуальную машину QEMU, но и реальный компьютер.

Запущенная виртуальная машина QEMU с нашей программой выглядит следующим образом:

Теперь займемся основной задачей – перечисление всех имеющихся на компьютере PCI-устройств. PCI – это основная шина с устройствами на компьютере. В нее помимо обычных устройств, которые вставляются во всем известные слоты на материнской плате, также подключены устройства, вшитые в саму материнскую плату (так называемые On-board devices), а так же ряд контроллеров (например, USB) и мостов на другие шины (например, PCI-ISA bridge). Таким образом, PCI – это основная шина на компьютере, с которой начинается опрос всех его устройств.

С каждым PCI-устройством связана структура из 256-ти байт (PCI Configuration Space), в которой располагаются его настройки. Конфигурация устройства в конечном итоге сводится к записи и чтению данных из этой структуры. Для всех PCI-устройств чтение и запись данных происходит через 2 порта ввода-вывода:
0xcf8 — конфигурационный порт, в который записывается PCI-адрес;
0xcfc — порт данных, через который происходит чтение и запись данных по указанному в конфигурационном порту PCI-адресу.

При чтении данных из PCI Configuration Space можно получить информацию об устройстве, а записывая туда данные устройство можно настроить.

PCI-адрес представляет собой следующую 32-х битную структуру:

Бит 31

Биты 30 – 24

Биты 23 – 16

Биты 15 – 11

Биты 10 – 8

Биты 7 – 2

Биты 1 – 0

Всегда 1

Зарезервировано

Номер шины

Номер устройства

Номер функции

Номер регистра

Всегда 0

Номер шины вместе с номером устройства идентифицируют физическое устройство на компьютере. Физическое устройство может включать в себя несколько логических, которые идентифицируются номером функции (например, плата видео захвата с контроллером Wi-Fi будет иметь, по крайней мере, две функции).

PCI Configuration Space условно разбита на регистры по 4 байта. Номер регистра, к которому происходит обращение, хранится с 2го по 7й биты в 32-х битном PCI-адресе. Поля структуры PCI Configuration Space, описывающей PCI-устройство, зависят от его типа. Но для всех типов устройств первые 4 регистра структуры содержат следующие поля:

Номер регистра

Биты 31 — 24

Биты 23 – 16

Биты 15 – 8

Биты 7 – 0

0

Device ID

Vendor ID

1

Status

Command

2

Class code

Subclass

Prog IF

Revision ID

3

BIST

Header type

Latency Timer

Cache Line Size

Class code – описывает тип (класс) устройства с точки зрения функций, которые устройство выполняет (сетевой адаптер, видео карта и т.д.);
Vendor ID – идентификатор производителя устройства (у каждого производителя устройств в мире есть один или несколько таких уникальных идентификаторов). Эти номера выдаются международной организацией PCI SIG;
Device ID – уникальный идентификатор устройства (уникален для заданного Vendor ID). Их нумерацию определяет сам производитель.

По полям DeviceID (сокращенно DEV) и VendorID (сокращенно VEN) определяется драйвер, соответствующий этому устройству. Иногда для этого используется еще дополнительный идентификатор RevisionID (сокращенно REV). Другими словами, Windows, обнаруживая новое устройство в компьютере, использует числа VEN, DEV и REV для поиска соответствующих им драйверов у себя на диске или в Интернете, используя сервера Microsoft. Также эти номера можно встретить в диспетчере устройств:

Рассмотрим код, реализующий самый простой способ получения списка имеющихся на компьютере PCI-устройств:

int ReadPCIDevHeader(u32 bus, u32 dev, u32 func, PCIDevHeader *p_pciDevice) { 	int i; 	 	if (p_pciDevice == 0) 		return 1; 	 	for (i = 0; i < sizeof(p_pciDevice->header)/sizeof(p_pciDevice->header[0]); i++) 		ReadConfig32(bus, dev, func, i, &p_pciDevice->header[i]); 		 	if (p_pciDevice->option.vendorID == 0x0000 ||  		p_pciDevice->option.vendorID == 0xffff || 		p_pciDevice->option.deviceID == 0xffff) 		return 1; 		 	return 0; }  void kmain(void) { 	int bus; 	int dev; 	 	clear_screen(); 	printf(" -- Kernel started! -- \n"); 	 	for (bus = 0; bus < PCI_MAX_BUSES; bus++) 		for (dev = 0; dev < PCI_MAX_DEVICES; dev++) 		{ 			u32 func = 0; 			PCIDevHeader pci_device; 			 			if (ReadPCIDevHeader(bus, dev, func, &pci_device)) 				continue; 			PrintPCIDevHeader(bus, dev, func, &pci_device); 			if (pci_device.option.headerType & PCI_HEADERTYPE_MULTIFUNC) 			{ 				for (func = 1; func < PCI_MAX_FUNCTIONS; func++) 				{ 					if (ReadPCIDevHeader(bus, dev, func, &pci_device)) 						continue; 					PrintPCIDevHeader(bus, dev, func, &pci_device); 				} 			} 		} } 

В данном коде происходит полный перебор номеров шин и номеров устройств в адресе, по которому происходит чтение. Если поле Header type содержит флаг PCI_HEADERTYPE_MULTIFUNC, то данное физическое устройство реализует несколько логических устройств, и при поиске PCI-устройств в адресе, записываемом в конфигурационный порт, нужно перебирать номер функции. Если VendorID имеет некорректное значение, то устройства с таким номером на этой шине нет. На Qemu этот код выводит следующий результат:

0x8086 – это VendorID оборудования компании Intel. DeviceID, равный 0x7000, соответствует устройству PIIX3 PCI-to-ISA Bridge. Загрузимся с получившейся флешки в VmWare Workstation 9.0. Список PCI-устройств оказался значительно длиннее и выглядит следующим образом:

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

ссылка на оригинал статьи http://habrahabr.ru/company/neobit/blog/162769/


Комментарии

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

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