Jak zapewne wiecie, wiele programów (systemy operacyjne, gry, ...) potrafi jakoś
dowiedzieć się
, na jakim procesorze zostały uruchomione. Rozpoznanie typu procesora
umożliwia na przykład uruchomienie dodatkowych optymalizacji w programie lub odmowę dalszego
działania, jeśli program musi korzystać z instrukcji niedostępnych na danym procesorze.
Wykrywanie rodzaju
CPU i
FPU nie jest trudne i
pokażę teraz, jak po kolei sprawdzać
typ procesora (nie można przecież zacząć sprawdzania od najwyższych).
Informacje, które tutaj podam, są oczywiście słuszne dla wszystkich procesorów rodziny x86 (AMD, Cyrix, ...), a nie tylko Intela.
Generalnie sposoby wykrywania są dwa: poprzez rejestr FLAG lub poprzez zakodowanie w kodzie instrukcji, które wykonają się tylko na danym modelu (i późniejszych).
Drugi sposób jest trochę trudniejszy: należy przejąć przerwanie INT6 (nieprawidłowa instrukcja) i sprawdzać, kiedy zostało wywołane.
Procesor 8088 od pozostałych odróżnia to, że zmniejsza on rejestr SP przed umieszczeniem go na stosie. Reszta robi to po umieszczeniu SP na stosie. Kod wyglądałby więc na przykład tak:
(przeskocz kod dla 8088)mov ax, sp push sp pop cx xor ax, cx ; lub cmp ax, cx jz nie_8088
Na tym procesorze w rejestrze flag bity 12-15 zawsze mają wartość 1.
(przeskocz kod dla 8086)pushf ; flagi na stos pop ax ; AX = flagi and ax, 0fffh ; czyścimy bity 12-15 push ax ; AX na stos popf ; flagi = AX pushf ; z powrotem na stos pop ax ; AX = flagi and ax, 0f000h ; zerujemy bity poza bitami 12-15 cmp ax, 0f000h ; jeśli ustawione, to 8086 jz jest_8086
Test polega na próbie wykonania instrukcji smsw dx
, nieprawidłowej
na procesorach wcześniejszych
niż 80286. Przerwanie nieprawidłowej instrukcji przejmujemy tak:
xor ax, ax mov es, ax les bx, [es:6 << 2] ; FASM: les bx, [es:(6 shl 2)] mov [_stare06+2], es mov [_stare06], bx mov es, ax mov word [es:(6 << 2)], moje06 ; FASM: mov word [es:(6 shl 2)], moje06 mov word [es:(6 << 2) + 2], seg moje06 ; FASM: mov word [es:(6 shl 2)], seg moje06
Sama procedura obsługi przerwania wyglądać będzie tak:
moje06: pop ax add ax, 3 push ax xor ax, ax iret
Proste: zwiększamy adres powrotny o 3 (długość instrukcji smsw dx
) i zerujemy AX
(potem w kodzie sprawdzimy jego wartość). Sam kod sprawdzający wygląda tak:
mov ax, 1 db 0fh, 1, 0e2h ; smsw dx or ax, ax jz jest_286
Przywrócenie oryginalnej procedury wygląda tak:
xor ax, ax les cx, [_stare06] mov ds, ax mov [ds:(6 << 2)], cx ; FASM: mov [ds:(6 shl 2)], cx mov [ds:(6 << 2) + 2], es ; FASM: mov [ds:(6 shl 2) + 2], es
Na tym procesorze bity 12-15 flag zawsze mają wartość 0. Przykładowy kod wygląda więc tak:
(przeskocz kod dla 80286)pushf ; flagi na stos pop ax ; AX = flagi or ax, 0f000h ; ustawiamy bity 12-15 push ax ; AX na stos popf ; flagi = AX pushf ; flagi na stos pop ax ; AX = flagi and ax, 0f000h ; jeśli wyczyszczone, to 286 jnz nie_286
Na tym procesorze nie można zmienić bitu numer 18 we flagach (wiemy, że rejestr flag ma 32 bity). Bit ten odpowiada za Alignment Check i spowoduje przerwanie m.in wtedy, gdy SP nie będzie podzielne przez 4. Dlatego, zanim będziemy testować ten bit, musimy zachować SP i wyzerować jego najmłodsze 2 bity.
(przeskocz kod dla 80386)mov dx, sp and sp, ~3 ; aby uniknąć AC fault. ; FASM: and sp, not 3 pushfd ; flagi na stos pop eax ; EAX = E-flagi mov ecx, eax ; zachowanie EAX xor eax, 40000h ; zmiana bitu 18 push eax ; EAX na stos popfd ; E-flagi = EAX pushfd ; flagi na stos pop eax ; EAX = flagi xor eax, ecx ; czy takie same? jeśli tak, to 386 mov sp, dx ; przywrócenie SP jz jest_386
Na tym procesorze nie można zmienić bitu 21 we flagach. Jeśli ten bit można zmienić, to
procesor obsługuje instrukcję CPUID
, której będziemy używać do
dalszego rozpoznania. Kod:
pushfd ; flagi na stos pop eax ; EAX = E-flagi mov ecx, eax ; zachowanie EAX xor eax, 200000h ; zmiana bitu 21 push eax ; EAX na stos popfd ; E-flagi = EAX pushfd ; flagi na stos pop eax ; EAX = flagi xor eax, ecx ; czy takie same? jeśli tak, to 486 jz jest_486 jmp jest_586
Zanim omówię sposób korzystania z instrukcji CPUID
,
zajmijmy się sposobem rozpoznania typu koprocesora.
Tutaj możliwości są tylko cztery: brak koprocesora, 8087, 80287, 80387. No to do roboty.
To sprawdzamy bardzo łatwo. Jeśli nie ma koprocesora, to w chwili wykonania instrukcji FPU może wystąpić przerwanie INT6 (nieprawidłowa instrukcja), ale nie o tym sposobie chciałem powiedzieć. Koprocesor można wykryć, jeśli słowo stanu zostanie zapisane prawidłowo. Oto kod:
(przeskocz test na istnienie FPU)fninit ; inicjalizacja zeruje rejestry ; wpisujemy jakąś niezerowa wartość: mov word [_fpu_status], 5a5ah ; zapisz słowo statusowe do pamięci: fnstsw [_fpu_status] mov ax, [_fpu_status] or al, al ; jeśli zapisało dobrze (zera oznaczają ; puste rejestry), to jest FPU jz jest_FPU
Sztuczka polega na wykorzystaniu instrukcji FDISI (wyłączenie przerwań), która rzeczywiście coś robi tylko na 8087. Po wyłączeniu przerwań w słowie kontrolnym zostaje włączony bit numer 7.
(przeskocz kod dla 8087); zachowaj słowo kontrolne do pamięci: fnstcw [_fpu_status] ; wyłączamy wszystkie ; przerwania (poprzez słowo kontrolne): and word [_fpu_status], 0ff7fh ; załaduj słowo kontrolne z pamięci: fldcw [_fpu_status] fdisi ; wyłączamy wszystkie przerwania ; (jako instrukcja) ; zachowaj słowo kontrolne do pamięci: fstcw [_fpu_status] test byte [_fpu_status], 80h ; bit 7 ustawiony? jz nie_8087 ; jeśli nie, to nie jest to 8087
Koprocesor ten nie odróżnia minus nieskończoności od plus nieskończoności. Kod na sprawdzenie tego wygląda tak:
(przeskocz kod dla 80287)finit fld1 ; st(0)=1 fldz ; st(0)=0,st(1)=1 fdivp st1 ; tworzymy nieskończoność, ; dzieląc przez 0 fld st0 ; st(1):=st(0)=niesk. fchs ; st(0)= -niesk. ; porównanie st0 z st1 i ; zdjęcie obu ze stosu fcompp ; 8087/287: -niesk. = +niesk., ; 387: -niesk. != +niesk. fstsw [_fpu_status] ; zapisz status do pamięci mov ax, [_fpu_status] ; AX = status sahf ; zapisz AH we flagach. tak sie składa, ; że tutaj również flaga ZF wskazuje na ; równość argumentów. jz jest_287 jmp jest_387
Od procesorów 586 (choć niektóre 486 też podobno ją obsługiwały), Intel i inni wprowadzili
instrukcję CPUID
. Pozwala ona odczytać wiele różnych
informacji o procesorze (konkretny typ,
rozmiary pamięci podręcznych, dodatkowe rozszerzenia, ...).
Korzystanie z tej instrukcji jest bardzo proste: do EAX wpisujemy numer (0-3)
i wywołujemy instrukcję, na przykład
mov eax, 1 cpuid
Teraz omówię, co można dostać przy różnych wartościach EAX.
EAX = maksymalny numer funkcji dla CPUID
.
EBX:EDX:ECX = marka procesora (12 znaków ASCII).
Intel - GenuineIntel
AMD - AuthenticAMD
NexGen - NexGenDriven
Cyrix, VIA - CyrixInstead
RISE - RiseRiseRise
,
Centaur Technology/IDT - CentaurHauls
(programowalne, może być inne)
United Microelectronics Corporation - UMC UMC UMC
Transmeta Corporation - GenuineTMx86
SiS - SiS SiS SiS
National Semiconductor - Geode by NSC
.
EAX = informacje o wersji:
sprawdź rozszerzone informacje o rodzinie)
EDX = cechy procesora (tutaj akurat z procesorów Intela; najpierw numery bitów):
CMPXCHG8B
SYSENTER
i SYSEXIT
CMOV*
CLFLUSH
FXSAVE
i FXRSTOR
EBX, ECX, EDX = informacje o pamięci podręcznej cache i TLB
Nawet te informacje, które tu przedstawiłem są już bardzo szczegółowe i z pewnością nie będą takie same na wszystkich procesorach. To jest tylko wstęp. Dalsze informacje można znaleźć na stronach producentów procesorów, na przykład AMD, Intel, ale także tutaj: Sandpile, Lista przerwań Ralfa Brown'a (plik opcodes.lst).