Определение конфигурации на аппаратном уровне
Введение.
В данной статье мы рассмотрим вопросы нахождения и определения параметров различных устройств.
Когда у программиста возникает вопрос типа «Как определить сколько в компе оперативки?», в 90% случаев он решается тривиально – используется определенный сервис операционной системы который и отвечает на все вопросы вроде этого.
А что делать, если пользоваться сервисами нельзя, например в случае разработки собственной ОС? (звучит конечно малореально, но, тем не менее, энтузиасты всегда были, и если уж не писать свою ОС, то хотя бы разобраться как это делают уже написанные ОС, думаю, будет интересно.
На вопрос как определить установленное оборудование на полностью аппаратном уровне и призвана ответить данная статья.
Сразу определимся, что именно мы будем определять:
- Процессор (частота, производитель, возможности)
- Оперативная память (объем)
- HDD (Объем).
- Устройства PCI (производитель, модель)
1) ПРОЦЕССОР.
Определение любого существующего intel-совместимого процессора складывается из 3 основных этапов:
- Определение поддержки инструкции CPUID.
- Если она поддерживается — определение остальных параметров.
- Определение тактовой частоты.
Процессоры поддерживают инструкцию 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 происходит следующим образом:
- Маскируются все прерывания кроме таймерного.
- Делается HLT.
- Считывается и сохраняется значение счетчика.
- Снова HLT.
- Считывается значение счетчика.
- Разность значений считанных в пунктах 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) процентах случаев прочитаются нули). Сам алгоритм такой:
- Инициализировать счетчик.
- Сохранить в регистре значение из памяти по адресу [счетчик]
- Записать в память тестовое значение (в нашем случае это будет AAh)
- Прочитать из памяти.
- Восстановить старое значение по этому адресу.
- Сравнить записанное и прочитанное значение
- Если равны – счетчик=счетчик+1, если нет – выход из цикла.
- JMP пункт 2
На первый взгляд все очень просто, при практической же реализации приведенного алгоритма возникает множество проблем: во-первых сама программа считающая память расположена в этой самой памяти и рано или поздно она сама себя перезапишет тестовым значением. Этот нюанс обычно решается так:
программа выполняется в реальном режиме в пределах первого мегабайта, счет же начинается с адресов выше мегабайта.
Этот метод порождает другую проблему – в реальном режиме непосредственно доступен только этот самый один мегабайт. Эта проблема решается путем применения «нереального» режима, он же Big real-mode.
Кто в курсе что такое «нереальный» режим может пропустить этот абзац, те же кто в не в курсе приготовьтесь слушать %)
Как известно в процессоре каждый сегментный регистр имеет скрытые или теневые (shadow parts) части в которых в защищенном режиме кэшируется дескриптор сегмента, для программиста они невидимы. В защищенном режиме эти части обновляются всякий раз когда в сегментный регистр загружается новое значение, в реальном же режиме обновляются только поля базового адреса сегмента. Если в защищенном режиме создать сегмент с лимитом в 4Гб и загрузить в сегментный регистр такой селектор, после чего переключиться в реальный режим, и, не следуя рекомендациям интел, оставить предел равным 4Гб – значение лимита сегмента сохранится позволяя использовать 32-битные смещения. Алгоритм перехода в нереальный режим:
- Создать дескриптор с базой равной 0
- Установить предел сегмента в 4Гб
- Переключиться в защищенный режим
- Загрузить селектор сегмента в какой-либо сегментный регистр
- Переключиться в реальный режим
После этих действий можно в реальном режиме использовать конструкции типа:
мov ax,word ptr fs:[edx]
Где EDX может изменяться от нуля до 4Гб не вызывая никаких исключений защиты (в «настоящем» реальном режиме превышение 64Кб вызывает исключение GP#) Фактически EDX=целевой адрес, поскольку база сегмента в FS=0.
В защищенном режиме при включенной страничной адресации считать память этим методом бесполезно потому что кроме основной память будет считаться еще и файл подкачки на винчестере, и в перпективе можно всегда получать значение около 4Гб (зависит от ОС).
Здесь есть еще один тонкий момент: в книгах М.Гука и В.Юрова пишется что в качестве «нереального» сегментного регистра надо использовать FS или GS так как другие регистры часто перезагружаются и процессор якобы сбрасывает лимит в 64Кб после перезагрузки сегментного регистра в реальном режиме. На практике оказывается совсем не так. Процессор НЕ ТРОГАЕТ поля лимитов в реальном режиме.
Во избежание дополнительных проблем (возможных) я буду приводить пример с регистром FS.
Отвлеклись мы немного от главного, а именно от оперативной памяти.
Алгоритм:
- Установить «нереальный режим»
- Открыть старшие адресные линии (GateA20)
- Установить счетчик в 1048576 (1Mb)
- Цикл записи-чтения
- Вывести значение счетчика
- Закрыть вентиль A20
- Выход
Пример листинга:
.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
К мануалу прилагаются исходники:
- Вывод строки (CPUID(0)) и определение поддержки MMX,SSE, SSE2. (CPUID.asm)
- Определение частоты с помощью RDTSC (CLOCK.asm).
- Определение объема оперативной памяти (MEMORY.asm).
- Объем HDD в секторах по 512 байт (ATA_ID.asm)
- Нахождение и вывод VendorID и DeviceID всех PCI устройств (PCI.asm).
- TXT-файл по которому нужно расшифровывать VendorID и DeviceID (PCIDEVS.TXT).
Если возникнут какие-либо проблемы – пишите Dark_Master@tut.by
[C] Dark_Master
Источник: WASM.RU /13.06.2004/