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

Введение.

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

Когда у программиста возникает вопрос типа «Как определить сколько в компе оперативки?», в 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.

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
Добавил: javavirys ( 2017-02-16 02:35:57 )
Рейтинг: + 0 -
Просмотров: 2601

Специальные предложения