Lekce 12 - Assembler - Signed a Unsigned čísla
V minulé lekci, Assembler - Kombinace skoků a příznaky, jsme se naučili kombinovat podmíněné a nepodmíněné skoky, zjistili, jak skokové instrukce získají výsledek z CMP a jak porovnávat signed čísla.
V ASM tutoriálu se podíváme na práci s celými čísly se znaménkem a bez něj a vysvětlíme si, jak jsou taková čísla vnitřně v paměti uložena. Jak asi tušíte, uložena budou v paměti jinak a Assembler nás na rozdíl od vyšších programovacích jazyků od tohoto faktu neodstíní. Bez dnešních znalostí bychom se tedy mnohdy divili, že jsme dostali jiné číslo, než jsme si mysleli.
Celá čísla v matematice
Všechna čísla se dají rozdělit do skupin (množin). My se stále budeme bavit o množině celých čísel.
Mezi celá čísla řadíme přirozená čísla, což jsou celá kladná
čísla, dále záporná čísla a nulu. V matematice se celá
čísla označují Z
. Jako příklad si můžeme uvést množinu
Z1 = { -3; -2; -1; 0; 1; 2; 3 }
. Součtem, rozdílem a součinem
celých čísel dostaneme opět celé číslo.
Zde asi není žádný problém, pojďme se tedy podívat, jak to vypadá v paměti.
Signed a unsigned čísla v Assembleru
Jak už jsme si říkali v lekci Assembler
- Datové typy, v Assembleru sice nerozlišujeme přímo datové typy, ale
udáváme, kolik má která proměnná zabírat paměti. Pro jednoduchost se
budeme dnes bavit o proměnných o velikosti 1 bajt (což je 8 nul a jedniček).
V tabulce v dané lekci byl uveden rozsah -128
až
255
. Ale to je přeci nesmysl, že? No, vlastně ano i ne.
Rozsah čísla je myšlený tak, že buď můžeme jako 8 nul a jedniček
uložit kladné číslo od 0
do 255
nebo záporné
číslo od -128
do 127
. V tomto druhém případě
bude např:
- hodnota
-128
uložena v paměti jako10000000
(nejnižší hodnota). - hodnota
127
jako01111111
(nejvyšší hodnota).
Prostě jsme se domluvili, že si rozsah posuneme o polovinu dolů a rázem můžeme ukládat i záporná čísla.
Jak ale poznáme, zda máme s danou adresou v paměti pracovat jako že má posun nebo ne? Samozřejmě nijak, musíme to prostě vědět. Proto používáme pro práci s kladnými a zápornými číslo jiné instrukce. Při deklaraci proměnné v Assembleru nerozlišujeme něco jako unsigned bajt a signed bajt, je to jednoduše bajt. Přesto ho můžeme používat na ukládání buď jen kladných čísel nebo kladných i záporných za cenu snížení rozsahu na polovinu z každé strany.
Teď použité termíny kladná a záporná čísla nejsou přesné, protože samozřejmě můžeme uložit kladné číslo s posunem tak, aby mohlo být třeba časem záporné, ale teď bylo kladné. Proto se v programování mluví o signed a unsigned číslech (čísla se znaménkem a bez znaménka, číslech, která mohou a nemohou být záporná).
Unsigned čísla
Pokud se budeme v Assembleru bavit o unsigned číslech, budeme tím myslet
čísla s rozsahem od 0
do maximální možné hodnoty daného
datového typu. My jsme si řekli, že budeme jako příklad uvádět bajt.
Konkrétně rozsah unsigned bajtu je tedy číslo 0
až
255
. Zde není nic co dále řešit.
Signed čísla
Oproti tomu signed čísla jsou jakési posunutí celého unsigned rozsahu o
polovinu pod 0
. Posunutím dostaneme pomyslnou osu, která bude
obsahovat kladná a záporná čísla. Rozsah signed bajtu je tedy číslo
-128
až 127
.
Zápis uvedený v tabulce datových typů, -128
až
255
, je tedy jen ilustrativním složení rozsahu unsigned a signed
bajtu.
Binární zápis unsigned a signed čísel
Mezi unsigned a signed proměnnou typu bajt není na první pohled
žádný rozdíl. Obě definujeme pomocí pseudoinstrukce
DB
a v binární formě také není co hledat. Něco ale vyčíst
přeci jen můžeme, viz dále.
Vezměme si binární číslo:
10000000
O tomto čísle můžeme tvrdit, že je kladné, ale i záporné. Proč?
To, jestli je číslo kladné, nebo záporné, určuje nejvyšší bit. Je-li nastaven, jedná se o číslo záporné. Pokud není nastaven, jedná se o číslo kladné. Avšak toto platí pro signed čísla. Pokud budeme mluvit o unsigned bajtu, bude se jednat o kladné číslo bez ohledu na nejvyšší bit.
Co tedy hraje klíčovou roli? Jak již bylo řečeno, jsou to instrukce. Vlastně je na nás, programátorech, abychom se rozhodli, jak s číslem budeme pracovat a samozřejmě musíme pak s jednou proměnnou pracovat stále stejným způsobem.
Tabulka signed rozsahů
Pro představu si můžeme uvést v tabulce, jak by vypadala reprezentace
signed čísel v paměti i za použití typu Word
(tedy 2
bajtů):
Typ | Kladná čísla | Záporná čísla |
---|---|---|
Byte | 00000000 (0) - 01111111 (127) | 10000000 (-128) - 11111111 (-1) |
Word | 0000000000000000 (0) - 0111111111111111 (32767) | 1000000000000000 (-32768) - 1111111111111111 (-1) |
Příklad - Skok na základě porovnání 2 čísel
Jako příklad si můžeme uvést skokové instrukce JG
a
JA
. Obě dělají to samé - pokud je první operand větší než
druhý, provede se skok. Jak ale asi tušíte, JG
uvažuje
znaménko, tedy pracuje se signed čísly.
Instrukce JG
Uveďme si jednoduchý zdrojový kód:
mov al, 01111111b ; Přeložíme jako 127. mov bl, 10000000b ; V tomto případě přeložíme jako -128. cmp al, bl jg .al_je_vetsi ret .al_je_vetsi: ret
Instrukce CMP
porovná dvě čísla. Výstupem je skok na
návěstí .al_je_vetsi
, protože v al
je kladné
číslo a v bl
číslo záporné.
Instrukce JA
Nyní si ukažme, co by se stalo, kdybychom pro záporné číslo použili
instrukci JA
, která znaménko neuvažuje:
mov al, 01111111b ; Přeložíme jako 127. mov bl, 10000000b ; V tomto případě přeložíme jako 128. cmp al, bl ja .al_je_vetsi ret .al_je_vetsi: ret
Na .al_je_vetsi
se teď nikdy nedostaneme.
Jde jen o to, že porovnání dvou čísel neproběhne v druhém případě korektně a skok se neprovede.
Instrukce pro práci se signed čísly
Nyní si uvedeme pár základních instrukcí pro práci se signed čísly, tedy s těmi, která mohou být i záporná. Všechny instrukce jsou si velice podobné a liší se pouze velikostí čísla, se kterým pracují.
Aritmetické a logické instrukce jako ADD
,
DEC
a AND
fungují pro unsigned a signed čísla
stejně, protože se stále jedná o stejně binárně zapsané číslo.
Znaménkové rozšiřování
Znaménkové rozšiřování se bude hodit tehdy, pokud budeme potřebovat pracovat s čísly v různě velkých registrech.
Instrukce, které si nyní uvedeme, fungují stejně, ale jsou pro různě velké registry.
Instrukce CBW
Instrukce CBW
slouží ke znaménkovému rozšiřování. Tato
instrukce znaménkově rozšiřuje do registru AX
na základě
registru AL
. Rozšiřování se provádí tak, že se vezme
nejvyšší bit z registru AL
a podle něj bude registr
AH
nabývat buď hodnoty 0x00
, pokud je nejvyšší
bit 0
, nebo 0xFF
, pokud má nejvyšší bit hodnotu
1
.
Instrukce se dá použít následovně:
mov al, 10000000b ; V tomto případě přeložíme jako -128. cbw ; Znaménkově rozšíříme do registru AX.
Na výstupu bychom dostali AX = 0xFF80
, což se rovná
-128
.
Instrukce CWD
Tato instrukce je podobná instrukci CBW
, ale rozšiřuje
registr AX
do registrového páru DX:AX
. Nemá
žádný operand.
Instrukci můžeme použít takto:
mov ax, 1000000000000000b ; V tomto případě přeložíme jako -32768. cwd ; Znaménkově rozšíříme do registrového páru DX:AX
Na výstupu dostaneme DX:AX = 0xFFFF8000
, což je
-32768
.
Instrukce CDQ
Instrukce CDQ
provádí znaménkové rozšíření registru
EAX
do registrového páru EDX:EAX
. Také nemá
žádný operand.
Používá se takto:
mov eax, 10000000000000000000000000000000b ; V tomto případě přeložíme jako -2147483648. cdq ; Znaménkově rozšíříme do registrového páru EDX:EAX.
Výstup bude EDX:EAX = 0xFFFFFFFF80000000
a to je
-2147483648
.
Instrukce CWDE
Tato instrukce provádí znaménkové rozšíření registru AX
do horní poloviny registru EAX
. Opět nemá žádný operand.
Použijeme ji následovně:
mov ax, 1000000000000000b ; V tomto případě přeložíme jako -32768. cwde ; Znaménkově rozšíříme do registru EAX.
Výstup bude EAX = 0xFFFF8000
, což je -32768
.
Skokové instrukce
Ostatní instrukce, které mohou pracovat se signed čísly, jsem si uvedli v několika předchozích článcích. Ještě si uvedeme pár skokových instrukcí, které uvažují znaménko.
Instrukce JE
(JZ
)
Provede skok, pokud je první operand roven druhému operandu, nebo je výsledek operace nula.
Instrukce JNE
(JNZ
)
Provede skok, pokud není první operand roven druhému operandu, nebo výsledek operace není nula.
Instrukce JG
(JNLE
)
Provede skok, pokud je první operand větší než druhý operand.
Instrukce JL
(JNGE
)
Provede skok, pokud je první operand menší než druhý operand.
Instrukce JNG
(JLE
)
Provede skok, pokud je první operand menší nebo roven druhému operandu.
Instrukce JNL
(JGE
)
Provede skok, pokud je první operand větší nebo roven druhému operandu.
Všechny tyto znalosti využijeme při tvorbě kalkulačky dále v kurzu. Tam bude nutné používat signed čísla, uživatel určitě očekává, že je bude moci zadat
V příští lekci, Assembler - Výpočty s reálnými čísly, se naučíme SSE instrukce pro práci s reálnými čísly.