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

в 12:48, , рубрики: grub, 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, которая позволила создать раздел:

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

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 "quitr"
exit 0

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

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

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

Теперь займемся основной задачей – перечисление всех имеющихся на компьютере 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 устройства без операционной системы

Рассмотрим код, реализующий самый простой способ получения списка имеющихся на компьютере 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 этот код выводит следующий результат:

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

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

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

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

Автор: NWOcs

Источник

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


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