Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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 -128255. 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 jako 10000000 (nejnižší hodnota).
  • hodnota 127 jako 01111111 (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 0255. 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 -128127.

Zápis uvedený v tabulce datových typů, -128255, 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.


 

Předchozí článek
Assembler - Kombinace skoků a příznaky
Všechny články v sekci
Základy assembleru
Přeskočit článek
(nedoporučujeme)
Assembler - Výpočty s reálnými čísly
Článek pro vás napsal Jakub Verner
Avatar
Uživatelské hodnocení:
3 hlasů
Autor se věnuje programování v x86 Assembleru.
Aktivity