Определение конфигурации на аппаратном уровне


Введение.

В данной статье мы рассмотрим вопросы нахождения и определения параметров различных устройств.

Когда у программиста возникает вопрос типа «Как определить сколько в компе оперативки?», в 90% случаев он решается тривиально – используется определенный сервис операционной системы который и отвечает на все вопросы вроде этого.

А что делать, если пользоваться сервисами нельзя, например в случае разработки собственной ОС? (звучит конечно малореально, но, тем не менее, энтузиасты всегда были, и если уж не писать свою ОС, то хотя бы разобраться как это делают уже написанные ОС, думаю, будет интересно.

На вопрос как определить установленное оборудование на полностью аппаратном уровне и призвана ответить данная статья.

Сразу определимся, что именно мы будем определять:

  1. Процессор (частота, производитель, возможности)
  2. Оперативная память (объем)
  3. HDD (Объем).
  4. Устройства PCI (производитель, модель)

1)      ПРОЦЕССОР.

Определение любого существующего intel-совместимого процессора складывается из 3 основных этапов:

  1. Определение поддержки инструкции CPUID.
  2. Если она поддерживается — определение остальных параметров.
  3. Определение тактовой частоты.

Процессоры поддерживают  инструкцию  CPUID (как intel, так и AMD), начиная с пятого поколения (Pentium) и поздних моделей 486 (чтобы TASM вас «правильно понял» при использовании CPUID, он должен быть версии 5.0 и выше).

Если она не поддерживается – определить производителя и другие параметры процессора возможно только какими-либо недокументированными путями.

Посмотрим, чем отличаются процессоры не поддерживающие CPUID (80386, 80486, более старые процессоры вроде 80286 и ниже, я думаю, рассматривать нет смысла).

Все просто – если бит 18 в EFLAGS доступен, значит процессор 486 или круче, если его невозможно изменить инструкцией POPF – 386.

В том же EFLAGS нужно попробовать изменить бит ID (21) если его можно программно изменить – процессор поддерживает инструкцию CPUID.

CPUID имеет параметр, который задается в регистре EAX.

Обычно в ответ на вызов CPUID с EAX=0 процессор возвращает в EBX:ECX:EDX некоторую строку-идентификатор производителя.

У intel это «GenuineIntel», у AMD – «AuthenticAMD», у Cyrix – «CyrixInstead».

(Обратите внимание, что размеры всех строк – 12 символов – три 4-байтных регистра).

При вызове CPUID с EAX=1 в регистре EAX возвращается информация о типе, модели и степпинге (изменения в рамках одной модели) процессора.

Эти значения расшифровываются по специальным таблицам.

EAX[00:03] – степпинг (stepping)
EAX[07:04] – модель (model)
EAX[11:08] – семейство (family)
EAX[13:12] – тип (type)
EAX[15:14] – резерв (reserved)
EAX[19:16] – расширенная модель (extended model) (только Pentium 4)
EAX[23:20] – расширенное семейство (extended family) (только Pentium 4)
EAX[31:24] – резерв (reserved)
EBX[07:00] – брэнд-индекс (brand-index)
EBX[15:08] – длина строки, очищаемой инструкцией CLFLUSH (Pentium 4)
EBX[23:16] — резерв
EBX[31:24] – идентификатор APIC процессора.
ECX – 0

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

Бит Описание
0 Наличие сопроцессора
1 Расширение для режима V86, наличие флагов VIP и VIF в EFLAGS
2 Расширения отладки (останов по обращению к портам)
3 Возможности расширения размера страниц до 4Мб
4 Наличие счетчика меток реального времени (и инструкции RDTSC)
5 Поддержка модельно-специфических регистров в стиле Pentium
6 Расширение физического адреса до 36 бит
7 Поддержка Machine Check Exception (исключение машинного контроля)
8 Инструкция CMPXCHG8B
9 Наличие APIC
10 RESERVED
11 Поддержка инструкций SYSENTER и SYSEXIT (для AMD – SYSCALL и SYSRET)
12 Регистры управления кэшированием (MTRR)
13 Поддержка бита глобальности в элементах каталога страниц
14 Поддержка архитектуры машинного контроля
15 Поддержка инструкций условной пересылки CMOVxx
16 Поддержка атрибутов страниц
17 Возможность использования режима PSE-36 для страничной адресации
18 Поддержка серийного номера процессора
19 Поддержка инструкции CLFLUSH
20 RESERVED
21 Поддержка отладочной записи истории переходов
22 Наличие управления частотой синхронизации(ACPI), для AMD – “фирменное” MMX
23 Поддежка MMX
24 Поддержка инструкций сохранения\восстановления контекста FPU
25 SSE
26 SSE2
27 Самослежение (Self Snoop)
28 RESERVED
29 Автоматическое снижение производительности при перегреве
30 Наличие расширенных инструкций AMD 3Dnow!
31 Наличие AMD 3Dnow!

При вызове CPUID с EAX=2 (функция появилась начиная с Pentium II, в процессорах AMD она недоступна)  в регистрах EAX, EBX, ECX, EDX возвращаются так называемые «дескрипторы», которые описывают возможности кэшей и TLB буферов. Причем AL содержит число, указывающее сколько раз необходимо последовательно выполнить CPUID (с EAX=2) для получения полной информации. Дескрипторы постоены по такому принципу: никаких битов тестировать не нужно, если определенный байт просто присутствует в регистре – значит его нужно интерпретировать. На практике обычно делают так, к примеру EDX, сначала смотрят что в DL, интерпретируют его содержимое, потом делают SHR EDX,8 и смотрят опять DL и т.д. Признаком достоверности информации в регистре является бит 31, если он равен 1 – содержимое регистра достоверно. Прежде чем выполнять команду CPUID с EAX=2 сначала нужно удостовериться что текущий процессор ее подерживает.

Счастливые обладатели процессоров Pentium III (только их) могут определить серийный номер своего процессора (предварительно разрешив в BIOS его сообщение процессором, которое по умолчанию отключено) при помощи CPUID с EAX=3.

В регистрах EDX:ECX возвращаются младшие 64 бита номера, вместе с тем, что возвращается в EAX при CPUID (EAX=1), они составляют уникальный 96-битный идентификатор процессора (о котором, в свое время, было столько разговоров).

Кроме того, процессоры AMD имеют возможности вызова функций EAX=80000005h и 80000006h по ним сообщается такая информация как ассоциативность TLB и элементов кэша, но в такие дебри мы сейчас углубляться не будем.

В процессорах AMD (начиная с K5) и Pentium4 имеются возможности сообщения некоторой 48-символьной строки (не той что по CPUID(0)) эти возможности также задействуются с помощью номеров функций более 80000000h.

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

К мануалу прилагается исходник посвященный использованию инструкции CPUID, программа определяет поддержку MMX, SSE, SSE2. Там используются только случаи с EAX=0 и EAX=1, причина этого проста —  начиная с EAX=2 начинаются очень большие разночтения между intel и AMD, а я не хочу делать мануал заточенный под intel (так же как и под AMD). Предусмотреть оба случая — значит усложнить программу и найти себе проблемы с тестированием на разных процессорах.

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

Начиная с Pentium в архитектуру был введен счетчик тактов (вообще говоря интел его так не называет, и утвеждает что в будущем он может считать не такты, гарантируется лишь, что счетчик будет монотонно возрастать) мы будем определять частоту процессора используя именно этот счетчик. Для начала немного о нем самом: Счетчик тактов имеет разрядность 64 бита и увеличивается на 1 с каждым тактом процессора начиная с сигнала RESET#, он продолжает счет при выполнении инструкции HLT (собственно при выполнении этой инструкции процессор вовсе не останавливается, а всего-навсего непрерывно выполняет инструкцию NOP, которая ,в свою очередь , является закамуфлированной инструкцией XCHG AX,AX (опкод NOP – 10010000b, опкод XCHG AX,reg – 10010reg, что при использовании регистра AX (000) дает 10010000b, интересно, что фактически существует 32-разрядный аналог NOP-а – XCHG EAX,EAX, на кодовую последовательность 66h,90h процессор реагирует нормально). Считывание счетчика тактов можно запретить для прикладных программ (CPL=3) уставнокой в 1 бита TSD в CR4 (в win считываение запрещено). После выполнения инструкции RDTSC (у кого на нее ругается компилятор – db 0fh,031h) регистры EDX:EAX содержат текущее значение счетчика. Измерение частоты при помощи RDTSC происходит следующим образом:

  1. Маскируются все прерывания кроме таймерного.
  2. Делается HLT.
  3. Считывается и сохраняется значение счетчика.
  4. Снова HLT.
  5. Считывается значение счетчика.
  6. Разность значений считанных в пунктах 3 и 5 есть количество тактов за 1 тик таймера (частота прерываний таймера примерно 18,2Гц).

На первый взгляд ничего непонятно. Посмотрим на временную диаграмму.

Момент запуска программы обозначен как t0, штрихи на оси – моменты, когда происходит прерывание от таймера. Первый HLT в листинге нужен для того чтобы преодолеть время t1, которое неизвестно заранее, так как программа может быть запущена в произвольное время. Затем, в момент между t1 и t2 считывается значение счетчика, оно сохраняется и снова делается HLT, процессор будет простаивать до первого прерывания, то есть практически ровно период t2, который и равен периоду прерываний от таймера. Таким образом, при известном значении периода таймера 18,2 Гц, а также количества тактов за этот период можно узнать точную тактовую частоту.

mov al,0FEh              ;маскируем все прерывания кроме таймера
out 21h,al
hlt 
rdtsc
mov esi,eax
hlt
rdtsc
sub eax,esi
;в EAX - количество тактов процессора за 1 тик таймера
…….. ;преобразование в мегагерцы и вывод на экран
mov al,0
out 21h,al

2)      ОПЕРАТИВНАЯ ПАМЯТЬ

Теперь поговорим о оперативной памяти.

Ставший уже классическим метод определения ее объема заключается в следующем принципе:

Если что-то записать по несуществующему физически адресу, а потом прочитать что-то с этого же адреса — записанное и прочитанное значения естественно не совпадут (в 99,(9) процентах случаев прочитаются нули). Сам алгоритм такой:

  1. Инициализировать счетчик.
  2. Сохранить в регистре значение из памяти по адресу [счетчик]
  3. Записать в память тестовое значение (в нашем случае это будет AAh)
  4. Прочитать из памяти.
  5. Восстановить старое значение по этому адресу.
  6. Сравнить записанное и прочитанное значение
  7. Если равны – счетчик=счетчик+1, если нет – выход из цикла.
  8. JMP пункт 2

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

программа выполняется в реальном режиме в пределах первого мегабайта, счет же начинается с адресов выше мегабайта.

Этот метод порождает другую проблему – в реальном режиме непосредственно доступен только этот самый один мегабайт. Эта проблема решается путем применения «нереального» режима, он же Big real-mode.

Кто в курсе что такое «нереальный» режим может пропустить этот абзац, те же кто в не в курсе приготовьтесь слушать %)

Как известно в процессоре каждый сегментный регистр имеет скрытые или теневые (shadow parts) части в которых в защищенном режиме кэшируется дескриптор сегмента, для программиста они невидимы. В защищенном режиме эти части обновляются всякий раз когда в сегментный регистр загружается новое значение, в реальном же режиме обновляются только поля базового адреса сегмента. Если в защищенном режиме создать сегмент с лимитом в 4Гб и загрузить в сегментный регистр такой селектор, после чего переключиться в реальный режим, и, не следуя рекомендациям интел, оставить предел равным 4Гб  – значение лимита сегмента сохранится позволяя использовать 32-битные смещения. Алгоритм перехода в нереальный режим:

  1. Создать дескриптор с базой равной 0
  2. Установить предел сегмента в 4Гб
  3. Переключиться в защищенный режим
  4. Загрузить селектор сегмента в какой-либо сегментный регистр
  5. Переключиться в реальный режим

После этих действий можно в реальном режиме использовать конструкции типа:

мov ax,word ptr fs:[edx]

Где EDX может изменяться от нуля до 4Гб не вызывая никаких исключений защиты (в «настоящем» реальном режиме превышение 64Кб вызывает исключение GP#) Фактически EDX=целевой адрес, поскольку база сегмента в FS=0.

В защищенном режиме при включенной страничной адресации считать память этим методом бесполезно потому что кроме основной память будет считаться еще и файл подкачки на винчестере, и в перпективе можно всегда получать значение около 4Гб (зависит от ОС).

Здесь есть еще один тонкий момент: в книгах М.Гука и В.Юрова пишется что в качестве «нереального» сегментного регистра надо использовать FS или GS так как другие регистры часто перезагружаются и процессор якобы сбрасывает лимит в 64Кб после перезагрузки сегментного регистра в реальном режиме. На практике оказывается совсем не так. Процессор НЕ ТРОГАЕТ поля лимитов в реальном режиме.

Во избежание дополнительных проблем (возможных) я буду приводить пример с регистром FS.

Отвлеклись мы немного от главного, а именно от оперативной памяти.

Алгоритм:

  1. Установить «нереальный режим»
  2. Открыть старшие адресные линии (GateA20)
  3. Установить счетчик в 1048576 (1Mb)
  4. Цикл записи-чтения
  5. Вывести значение счетчика
  6. Закрыть вентиль A20
  7. Выход

Пример листинга:

.586P

DESCRIPTOR STRUC              ;Структура дескриптора сегмента для 
                                                    ;защищенного режима
 limit   dw 0
 base_1  dw 0
 base_2  db 0
 attr    db 0
 lim_atr db 0
 base_3  db 0
ENDS

GDT segment use16                     ;Таблица GDT
 empty dq 0 			
 _code descriptor <0,0,0,0,0,0>   ;Дескриптор для сегмента кода программы
 _temp descriptor <0,0,0,0,0,0>   ;"Нереальный" дескриптор
GDT ends

data segment use16
 gdtr df 0		                 ;Поле для регистра GDTR
string db "Memory available: ",20 dup (0)		
data ends

stck segment stack use16             ;Стек
 db 256 dup (0)
stck ends

code segment use16
assume cs:code,ss:stck,ds:gdt

start:				;entry point
 mov ax,gdt			
 mov ds,ax
 mov _code.limit,65535	;Лимит сегмента кода 64Кб
 mov eax,code			;Получаем физический адрес и загружаем базу
 shl eax,4			
 mov _code.base_1,ax		
 shr eax,8
 mov _code.base_2,ah
 mov _code.attr,09Ah		;Атрибуты - сегмент кода
				
mov _temp.limit,65535	;Устанавливаем лимит в максимальное значение
mov _temp.attr,092h	 	;Атрибуты - сегмент данных, доступ чтение\запись
mov _temp.lim_atr,08Fh	;Устанавливаем старшие биты лимита и бит G	

 assume ds:data		;Получаем физический адрес таблицы GDT 
 mov ax,data			
 mov ds,ax
 mov eax,gdt			
 shl eax,4
 mov dword ptr [gdtr+2],eax  ;загружаем лимит и адрес таблицы GDT
 mov word  ptr gdtr,23	

 cli				;Запрет прерываний 
 mov al,80h			;Запрет NMI
 mov dx,70h
 out dx,al
 lgdt gdtr			;Загружаем GDTR

 mov eax,cr0			;Переключаемся в защищенный режим
 inc al
 mov cr0,eax
 db 0EAh			;Дальний JMP для загрузки CS селектором 
 dw offset protect
 dw 08h				

 protect:	
 mov ax,10h			;Загружаем FS в защищенном режиме		
 mov fs,ax

 mov eax,cr0			;Идем назад в реальный режим
 dec al
 mov cr0,eax
 db 0EAh			
 dw offset real
 dw code

 real:				;Открываем вентиль GateA20
 mov dx,92h			
 in  al,dx
 or  al,2
 out dx,al 

 mov ecx,1048576		;Начальное значение счетчика - 1 Мегабайт
 mov al,0AAh			;Тестовое значение

 count: 				
 mov dl,byte ptr fs:[ecx]	;Сохраняем старое значение по адресу
 mov byte ptr fs:[ecx],al	;пишем туда тестовое
 mov al,byte ptr fs:[ecx]	;читаем с того же адреса
 mov byte ptr fs:[ecx],dl	;востанавливаем старое значение
 cmp al,0AAh			;прочитали то что записали?
 jnz exit			;Нет - такого адреса физически не существует
 inc ecx			;Да - увеличиваем счетчик и повторяем все еще раз
 jmp count

 exit: 				;Разрешить прерывания
 mov al,0			
 mov dx,70h
 out dx,al
 sti

 mov dx,92h			;Закрыть вентиль A20
 in  al,dx
 and al,0FDh
 out dx,al 

 mov ax,cx			;процеруда преобразования числа в строку требует
 shr ecx,16			;чтобы значение располагалось в DX:AX
 mov dx,cx			;Преобразуем DX:AX=ECX
 push ds			
 pop  es
 lea di,string
 add di,18			;пропускаем строку  "Memory available"
 call DwordToStr		;преобразование в символьную форму 

 mov ah,9			
 mov dx,offset string		;вывод
 int 21h

 mov ax,4c00h			;Завершение работы
 int 21h
code ends
end start

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

Есть один способ многократного увеличения скорости программы. Дело в том, что этот исходник считает память с точностью до байта, такая точность вообще говоря не нужна, т.к. размер современной планки памяти не может быть некратным мегабайту, поэтому можно наращивать счетчик сразу прибавляя к нему значение 1048576, чего можно достичь заменив в цикле записи-чтения команду inc ecx на add ecx,1048576.

 3) ОБЪЕМ HDD

Детект объема винчестера производится с помощью ATA команды IDENTIFY DEVICE.

Что там к чему, смотрите мою статью «ATA для дZенствующих. Часть 1»

Там же лежит исходник ATA_ID.asm который определяет объем винта..

4) Устройства PCI.

Теперь пришло время препарировать шину PCI.

Сначала введем фундаментальное понятие – конфигурационное пространство PCI (PCI configuration space).

Так называется массив регистров, который имеется у каждого PCI-устройства, через них задаются различные параметры (номера прерываний для устройства и т.д.). Общение с PCI-устройствами происходит в основном через 2 32-битных порта 0CF8h и 0CFCh. Через них можно читать и писать в это самое конфигурационное пространство определенного устройства.

Происходит этот процесс следующим образом:

В регистре 0CF8h задается адрес устройства на шине, после чего из 0CFCh считываются (записываются) данные.

Координаты устройства на шине (формат 0CF8h) выглядят так:

31-й бит показывает достоверность информации в регистре, там должен быть 1.

Bus Number – номер шины PCI. (их вполне может быть несколько, например порт AGP использует не ту шину, к которой подключены слоты PCI).

Device Number – номер устройства на шине

Function Number – номер функции устройства (здесь надо немного определится с терминологией, дело в том что под функцией и подразумевается собственно устройство, тогда так под устройством (device) подразумевается абонент шины, то есть, если, например, есть карта в которой совмещены 2 каких-либо устройства, то она будет восприниматься как одно устройство с двумя функциями, причем даже такое «однофункциональное» устройство как видеокарта может иметь множество функций). Это деление на устройства и функции в большинстве случаев чисто логическое, «основное» устройство соответствует функции 0.

Register Number – номер регистра конфигурационного пространства который следует прочитать (записать). (Вообще используется все поле до 0-го бита, но поскольку обмен производится двойными словами (4 байта) то получается что младшие 2 бита всегда нулевые).

Нас сейчас интересует, как можно узнать тип и производителя устройства. Посмотрим на карту конфигурационного пространства:

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

VendorID – код производителя.

DeviceID – код устройства.

Пришло время ответить на очень важный вопрос «А что, если прочитать что-то из конфигурационного пространства реально несуществующего устройства?»

Ответ: прочитается специально зарезервированное для этой цели значение 0FFFFFFFFh (хотя если это делать под win то ОС может подставить туда все что угодно).

Из этого всего можно сделать такой вывод: чтобы найти все устройства нужно в цикле (изменяя Bus от нуля до 255, dev от 0 до 31, func от нуля до 7) читать их конфигурационные простраства, если прочиталось 0FFFFFFFFh значит устройства нет, если же прочиталось что-то другое – устройство присутствует.

Вот пример процедуры читающей из конфигурационного пространства PCI.

Номер функции задается в BL, номер устройства в BH, функция в CL, и смещение (номер регистра) в CH.

;BL - bus, BH - device, CL - function, CH - register
RD_PCI PROC NEAR
 mov dx,0CF8h
 xor eax,eax
 mov al,bl
 or ah,80h ;Бит достоверности в 1
 shl eax,16
 mov ah,bh
 shl ah,3
 or ah,cl
 mov al,ch
 and al,0FCh ;Сбросить 2 младших бита
 out dx,eax
 mov dx,0CFCh
 in eax,dx
 ret
RD_PCI ENDP

А вот как может выглядеть код для нахождения всех устройств:

mov bl,0;bus
mov bh,0;device
mov cl,0;function
mov ch,0;register

label1:
call rd_pci		;Читаем регистр
cmp eax,0ffffffffh	;Если прочитались все единички - устройства нет
jnz device_found        ;Если же не все единички - "что-то есть"
label2:

;inc cl			;Если этот блок раскомментировать будут выводится не 
;cmp cl,8		;только устройства, но и  все их функции
;jnz label1
;mov cl,0

inc bh			;Цикл устройств
cmp bh,32
jnz label1
mov bh,0

inc bl			;Цикл шин PCI
cmp bl,255
jz exit
jmp label1

device_found:
… ;Преобразование в символьную форму считанных значений и вывод на экран

… ;Преобразование в символьную форму считанных значений и вывод на экран

 

К мануалу прилагается файл, в котором описаны коды VendorID и DeviceID.

Первый символ в строке показывает, что описывает строка:

D – device code

V – vendor code

Потом идет сам код, потом название устройства. Пример:

«V        1106    VIA Technologies Inc»

Строка описывает код производителя (V).

Код VIA Technologies Inc – 1106h

К мануалу прилагаются исходники:

  1. Вывод строки (CPUID(0)) и определение поддержки MMX,SSE, SSE2. (CPUID.asm)
  2. Определение частоты с помощью RDTSC (CLOCK.asm).
  3. Определение объема оперативной памяти (MEMORY.asm).
  4. Объем HDD в секторах по 512 байт (ATA_ID.asm)
  5. Нахождение и вывод VendorID и DeviceID всех PCI устройств (PCI.asm).
  6. TXT-файл по которому нужно расшифровывать VendorID и DeviceID (PCIDEVS.TXT).

Если возникнут какие-либо проблемы – пишите Dark_Master@tut.by

[C] Dark_Master

 

Источник: WASM.RU /13.06.2004/

Поделиться в соц сетях

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

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

Есть идеи, замечания, предложения? Воспользуйтесь формой Обратная связь или отправьте сообщение по адресу replay@sciencestory.ru
© 2017 Истории науки. Информация на сайте опубликована в ознакомительных целях может иметь ограничение 18+