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

Дата публикации 13 июн 2004

Определение конфигурации на аппаратном уровне — Архив WASM.RU

Введение.

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

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

Код (Text):
  1.  
  2. mov al,0FEh              ;маскируем все прерывания кроме таймера
  3. out 21h,al
  4. hlt
  5. rdtsc
  6. mov esi,eax
  7. hlt
  8. rdtsc
  9. sub eax,esi
  10. ;в EAX - количество тактов процессора за 1 тик таймера
  11. …….. ;преобразование в мегагерцы и вывод на экран
  12. mov al,0
  13. 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. Выход

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

Код (Text):
  1.  
  2. .586P
  3.  
  4. DESCRIPTOR STRUC              ;Структура дескриптора сегмента для
  5.                                                     ;защищенного режима
  6.  limit   dw 0
  7.  base_1  dw 0
  8.  base_2  db 0
  9.  attr    db 0
  10.  lim_atr db 0
  11.  base_3  db 0
  12. ENDS
  13.  
  14. GDT segment use16                     ;Таблица GDT
  15.  empty dq 0            
  16.  _code descriptor <0,0,0,0,0,0>   ;Дескриптор для сегмента кода программы
  17.  _temp descriptor <0,0,0,0,0,0>   ;"Нереальный" дескриптор
  18. GDT ends
  19.  
  20. data segment use16
  21.  gdtr df 0                       ;Поле для регистра GDTR
  22. string db "Memory available: ",20 dup (0)      
  23. data ends
  24.  
  25. stck segment stack use16             ;Стек
  26.  db 256 dup (0)
  27. stck ends
  28.  
  29. code segment use16
  30. assume cs:code,ss:stck,ds:gdt
  31.  
  32. start:              ;entry point
  33.  mov ax,gdt        
  34.  mov ds,ax
  35.  mov _code.limit,65535  ;Лимит сегмента кода 64Кб
  36.  mov eax,code           ;Получаем физический адрес и загружаем базу
  37.  shl eax,4         
  38.  mov _code.base_1,ax       
  39.  shr eax,8
  40.  mov _code.base_2,ah
  41.  mov _code.attr,09Ah        ;Атрибуты - сегмент кода
  42.                
  43. mov _temp.limit,65535   ;Устанавливаем лимит в максимальное значение
  44. mov _temp.attr,092h     ;Атрибуты - сегмент данных, доступ чтение\запись
  45. mov _temp.lim_atr,08Fh  ;Устанавливаем старшие биты лимита и бит G   
  46.  
  47.  assume ds:data     ;Получаем физический адрес таблицы GDT
  48.  mov ax,data           
  49.  mov ds,ax
  50.  mov eax,gdt           
  51.  shl eax,4
  52.  mov dword ptr [gdtr+2],eax  ;загружаем лимит и адрес таблицы GDT
  53.  mov word  ptr gdtr,23 
  54.  
  55.  cli                ;Запрет прерываний
  56.  mov al,80h         ;Запрет NMI
  57.  mov dx,70h
  58.  out dx,al
  59.  lgdt gdtr          ;Загружаем GDTR
  60.  
  61.  mov eax,cr0            ;Переключаемся в защищенный режим
  62.  inc al
  63.  mov cr0,eax
  64.  db 0EAh            ;Дальний JMP для загрузки CS селектором
  65.  dw offset protect
  66.  dw 08h            
  67.  
  68.  protect:  
  69.  mov ax,10h         ;Загружаем FS в защищенном режиме    
  70.  mov fs,ax
  71.  
  72.  mov eax,cr0            ;Идем назад в реальный режим
  73.  dec al
  74.  mov cr0,eax
  75.  db 0EAh           
  76.  dw offset real
  77.  dw code
  78.  
  79.  real:              ;Открываем вентиль GateA20
  80.  mov dx,92h        
  81.  in  al,dx
  82.  or  al,2
  83.  out dx,al
  84.  
  85.  mov ecx,1048576        ;Начальное значение счетчика - 1 Мегабайт
  86.  mov al,0AAh            ;Тестовое значение
  87.  
  88.  count:                
  89.  mov dl,byte ptr fs:[ecx]   ;Сохраняем старое значение по адресу
  90.  mov byte ptr fs:[ecx],al   ;пишем туда тестовое
  91.  mov al,byte ptr fs:[ecx]   ;читаем с того же адреса
  92.  mov byte ptr fs:[ecx],dl   ;востанавливаем старое значение
  93.  cmp al,0AAh            ;прочитали то что записали?
  94.  jnz exit           ;Нет - такого адреса физически не существует
  95.  inc ecx            ;Да - увеличиваем счетчик и повторяем все еще раз
  96.  jmp count
  97.  
  98.  exit:              ;Разрешить прерывания
  99.  mov al,0          
  100.  mov dx,70h
  101.  out dx,al
  102.  sti
  103.  
  104.  mov dx,92h         ;Закрыть вентиль A20
  105.  in  al,dx
  106.  and al,0FDh
  107.  out dx,al
  108.  
  109.  mov ax,cx          ;процеруда преобразования числа в строку требует
  110.  shr ecx,16         ;чтобы значение располагалось в DX:AX
  111.  mov dx,cx          ;Преобразуем DX:AX=ECX
  112.  push ds           
  113.  pop  es
  114.  lea di,string
  115.  add di,18          ;пропускаем строку  "Memory available"
  116.  call DwordToStr        ;преобразование в символьную форму
  117.  
  118.  mov ah,9          
  119.  mov dx,offset string       ;вывод
  120.  int 21h
  121.  
  122.  mov ax,4c00h           ;Завершение работы
  123.  int 21h
  124. code ends
  125. 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.

Код (Text):
  1.  
  2. ;BL - bus, BH - device, CL - function, CH - register
  3. RD_PCI PROC NEAR
  4.  mov dx,0CF8h
  5.  xor eax,eax
  6.  mov al,bl
  7.  or ah,80h ;Бит достоверности в 1
  8.  shl eax,16
  9.  mov ah,bh
  10.  shl ah,3
  11.  or ah,cl
  12.  mov al,ch
  13.  and al,0FCh ;Сбросить 2 младших бита
  14.  out dx,eax
  15.  mov dx,0CFCh
  16.  in eax,dx
  17.  ret
  18. RD_PCI ENDP

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

Код (Text):
  1.  
  2. mov bl,0;bus
  3. mov bh,0;device
  4. mov cl,0;function
  5. mov ch,0;register
  6.  
  7. label1:
  8. call rd_pci     ;Читаем регистр
  9. cmp eax,0ffffffffh  ;Если прочитались все единички - устройства нет
  10. jnz device_found        ;Если же не все единички - "что-то есть"
  11. label2:
  12.  
  13. ;inc cl         ;Если этот блок раскомментировать будут выводится не
  14. ;cmp cl,8       ;только устройства, но и  все их функции
  15. ;jnz label1
  16. ;mov cl,0
  17.  
  18. inc bh          ;Цикл устройств
  19. cmp bh,32
  20. jnz label1
  21. mov bh,0
  22.  
  23. inc bl          ;Цикл шин PCI
  24. cmp bl,255
  25. jz exit
  26. jmp label1
  27.  
  28. device_found:
  29. … ;Преобразование в символьную форму считанных значений и вывод на экран

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

К мануалу прилагается файл, в котором описаны коды 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

© Dark_Master

0 2.988
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532