int21h

Záludnosti a pasti

Aneb různé drobnosti a postřehy z praxe. Nic převratného, ale hodí se to znát.

Hardware: reprezentace záporných čísel uvnitř počítače

Jak vlastně procesor pozná, jestli je daný řetězec bitů kladné nebo záporné číslo? Docela dlouho jsem žil v mylné představě, že znaménko se ukládá do nejvyššího bitu čísla a zbytek zůstává ve stejném tvaru jako kladné číslo. Tak to ale není.

Záporná čísla se v PC ukládají tak, aby byla při sčítání a odčítání plně kompatibilní s kladnými: využívá se "přetékání" a "podtékání". Co to je? Podívejme se na příklad s osmibitovým číslem, které budeme zvětšovat o jedničku:

binárně:     desítkově:
00000000         0
00000001         1
00000010         2
00000011         3
00000100         4
  ...
11111110       254
11111111       255    Teď to...
00000000         0    ...přeteklo.
00000001         1
00000010         2

Úplně stejně to funguje při "podtečení", kdy se ze samých nul stávají samé jedničky. A to je jádro pudla: pokud číslo považujeme za číslo se znaménkem, samé jedničky znamenají hodnotu -1. Při odčítání to hezky vynikne:

binárně:    bez znaménka:    se znaménkem:
00000010           2               2
00000001           1               1
00000000           0               0
11111111         255              -1
11111110         254              -2
11111101         253              -3
  ...            ...             ...

Pokud si možný rozsah čísla představíme jako kruh a ne jako úsečku, vidíme, že jediný rozdíl je v posunutí bodu "nepovoleného přechodu", který se u bezznaménkových čísel (byte, word, dword) nachází mezi nulou a maximem a u čísel se znaménkem (shortint, integer, longint) mezi maximem do mínusu a maximem do plusu (10000000b | 01111111b).

Jednička v nejvyšším bitu tedy skutečně znamená zápornost, ale jejím vynulováním rozhodně nevznikne absolutní hodnota! Postup při násobení a dělení z toho důvodu musí být rozdílný, proto procesor rozlišuje instrukce mul a div pro čísla bez znaménka a imul a idiv pro čísla se znaménkem.

Kdybychom potřebovali otočit znaménko čísla ručně, dělá se to tak, že se znegují všechny jeho bity (instrukce NOT) a pak se k němu přičte jednička (INC nebo ADD). Na první pohled to vypadá dost nelogicky, ale kupodivu to opravdu funguje :-).

Při převodu jakéhokoli 16bitového čísla na 8bitové vždycky stačí ponechat nižší byte a vyšší zahodit. Pokud se hodnota nachází v mezích výsledného 8bitového čísla, vyjde správný výsledek, ať jde o word nebo integer. Pro opačný převod čísel se znaménkem má procesor speciální instrukci cbw, která do vyššího bytu rozkopíruje nejvyšší bit nižšího bytu.

TP: 32bitové instrukce a problémy s nimi

Ano, i v šestnáctibitovém Turbo Pascalu se setkáme s dvaatřicetibitovými instrukcemi. Tím teď nemyslím instrukce ve vkládaném Assembleru ručně doplněné o prefix 66h, ale běžné pascalovské operace s longinty. Překladač pro tyto výpočty generuje dvě varianty kódu, 16- a 32bitovou, a to, která z nich se použije, se rozhoduje až za chodu programu podle hodnoty globání proměnné Test8086. Tu automaticky nastavuje inicializační část jednotky System podle typu procesoru, na kterém program zrovna běží: 0 pro 8086 (16b), 1 pro 286 (16b) nebo 2 pro 386 a vyšší (32b). Chytrý fígl, i když dnes už prakticky zbytečný (já osobně jsem se s živou dvaosmšestkou nikdy nesetkal).

Problém nastává při použití vlastních asynchronních přerušení, tj. procedur s klíčovým slovem interrupt za hlavičkou. Toto slovo překladači říká, že jde o proceduru pro obsluhu přerušení, že se má ukončovat instrukcí iret místo běžného ret, a hlavně že má po svém skončení vrátit všechny registry do původního stavu. Jasně, žádný problém, od toho máme zásobník, a tak překladač automaticky doplní příslušná push a pop. Jenomže schovává pouze 16bitové registry. Z toho vyplývá, že jestli někde uvnitř přerušení pracujeme s longinty, horní wordy aritmetických registrů se přepíší a neobnoví. A jestli se přerušení vyvolalo zrovna uprostřed nějakého longintového výpočtu v hlavním programu... máme problém, Houstone. Co s tím? Řešení jsou dvě:

  1. Pokud chcete zachovat univerzálnost a pouštět program i na 086 a 286, přepište na začátku obsluhy přerušení proměnnou Test8086 na něco menšího než 2, aby se longintové výpočty v ní prováděly 16bitovou oklikou. Na konci ji pak nezapomeňte vrátit na původní hodnotu!
  2. Druhé řešení je vykašlat se na 16bitové procesory a pomocí vkládaného Assembleru si všechny 32bitové registry uložit ručně:
    db 66h; push AX
    db 66h; push BX
    db 66h; push CX
    db 66h; push DX
    a na konci je zase ručně obnovit:
    db 66h; pop DX
    db 66h; pop CX
    db 66h; pop BX
    db 66h; pop AX
    Kdyby překladač znal 32bitové názvy instrukcí, stačilo by psát push EAX atd., ale bavíme se o TP, kterému se 32bitovost musí vnucovat strojovým kódem.

Pokud si všechny použité registry ukládáte ručně, je už celkem zbytečné používat klíčové slovo interrupt - akorát by zdržovalo 16bitovým opakováním téhož. Stačí proceduru deklarovat jako far a na její konec dát instrukci iret, tím z ní uděláte plnohodnotnou obsluhu přerušení.

Záludnosti TP: automatické přetypování čísel

V jednom výrazu můžeme kombinovat čísla různých typů, překladač si to automaticky převede na jeden společný. Jaký? Počet bitů výsledku v závislosti na počtu bitů jednotlivých operandů (bez ohledu na jejich pořadí) udává následující tabulka:

1. operand2. operandvýsledek
8816
816
1616
163232
3232

Na typu proměnné na levé straně přiřazovacího operátoru vůbec nezáleží, takže jestli vám ve výrazu přetékají wordy, longintem nalevo to nezachráníte.

Výrazy s více než dvěma operandy se rozloží na dvojoperandové podvýrazy a vyhodnocují se postupně, přičemž výše uvedená tabulka platí pro každý podvýraz zvlášť - na to pozor! Například výraz typu longint*integer*integer se vyhodnotí v pořadí (longint*integer)*integer, takže v každém okamžiku bude aspoň jeden operand 32bitový. Ovšem integer*integer*longint se bere jako (integer*integer)*longint, podvýraz v závorce je jenom 16bitový a jestli přeteče, je v háji celý výsledek!

Pokud není možné operandy poskládat tak, aby překladač správně vyhodnotil výsledek jako 32bitový, je nutné ruční přetypování na longint. Potom samozřejmě pozor: longint(integer*integer) je na nic, protože přetypovává už přeteklý 16bitový výraz v závorce. Místo toho je potřeba psát longint(integer)*integer.

Bug TP: nedostupnost některých lokálních proměnných z Asm

Představme si následující situaci:

procedure Vnejsi;
var X:...;
  procedure Vnitrni;
  Begin
  ...
  End;
Begin
...
End;

Proměnnou X můžeme používat jak v proceduře Vnejsi, tak i ve vnořené proceduře Vnitrni (jediné omezení je, že ve Vnitrni nejde použít jako řídicí proměnná pro cyklus for). Kupodivu úplně jiná situace nastane, když je vnořená procedura napsaná v Asm:

procedure Vnejsi;
var X:...;
  procedure Vnitrni; assembler;
  Asm
  ...
  End;
Begin
...
End;

nebo ani nemusí být v Asm celá:

procedure Vnejsi;
var X:...;
  procedure Vnitrni;
  Begin
  ...
  asm
  ...
  end;
  ...
  End;
Begin
...
End;

Z assemblerovských úseků v proceduře Vnitrni se k proměnné X nedostaneme!

Překladač sice žádnou chybu nehlásí (syntakticky je všechno v pořádku), ale nějak to špatně zkompiluje či co a místo na tu proměnnou nás nasměruje někam do háje. Při čtení z X potom dostaneme nesmysl a při zápisu do ní někdy i Nepovolenou Operaci a pád programu. Takže bacha na to. Nejjistější je všechno předávat přes parametry.

Záludnosti TP: optimalizace

Ještě nedávno jsem žil v domnění, že Turbo Pascal doslova přeloží každou kravinu, kterou mu naservírujeme. Ale je to jinak - alespoň v některých případech. Jednou jsem si napsal takovouhle prasárnu:

function ...;
label xxx;
Begin
getmem, new, assign, reset...
...
if něco je špatně then goto xxx;
...
if něco jiného je špatně then goto xxx;
...
if nějaká další chyba then goto xxx;
...
if jiná chyba then goto xxx;
...
if nedopadlo to dobře then goto xxx;
...
if false then xxx:writeln('Nastala chyba!');
freemem, dispose, close... {závěrečný úklid, který se musí provést, i když dojde k chybě}
End;

Mělo to fungovat tak, že když se do toho místa dojde normálně, hláška se nezobrazí, protože je pod podmínkou, která není nikdy splněna. Jenom kdyby se skočilo přímo na to návěští, zpráva by se zobrazila.

Teorie hezká, jenže překladači došlo, že "if false" nebude splněno nikdy, pročež je zbytečné příkazy za ním překládat a celý řádek zahodil. Ovšem nedošlo mu, že si zahodil návěstí, ale už nic neprovedl s gotem, které na něj skáče. Nestěžoval si, cílovou adresu nějakým záhadným způsobem plácnul o pár příkazů výše a ejhle, nekonečná smyčka byla na světě.

Takže pozor. Občas překlad není úplně doslovný. A blbuvzdornost u gota nečekejte, protože tvůrci TP nepředpokládali, že by ho někdo chtěl prakticky používat.

Záludnosti TP: příkaz with

Představme si následující situaci:

type zaznam = record
              ...
              end;
     ukazatel = ^zaznam;

var u:ukazatel;

procedure proc1;
begin
...udělej něco s u^...
end;

...
with u^ do begin
           ...
           proc1;
           ...
           end;

Co se stane? Spadne nám to. Příkaz with interně nastavuje adresní registry, aby se daly jednotlivé položky recordu adresovat jednodušeji a rychleji. Jenže to dříve definovaná procedura nemůže tušit, ta v adresních registrech očekává obvyklou adresu data segmentu globálních proměnných. Takže sáhne vedle a problém je na světě.

Syntakticky je všechno v pořádku, takže si překladač nestěžuje a chyba se nám projeví až nesmyslnými výsledky nebo Nepovolenou operací a pádem programu.

Řešení je jednoduché: co nejvíce omezit používání globálních proměnných v procedurách a funkcích. Nejspolehlivější je dokonale je zapouzdřit a veškeré vstupy a výstupy předávat přes parametry a návratovou hodnotu.

Assembler: podmíněné skoky

Tohle samozřejmě najdete v každé příručce o Asm, ale většinou tam bude něco na způsob "skočí při CF=1, SF=0 a AF=0", což mi nic moc neříká. Tak jsem si sepsal přehlednější tabulku, aby se mi to líp pamatovalo: jak to dopadne po instrukci cmp a,b?

při:skáče:
a=bje, jz
a<bjl, jc, jb, jnae, js
a>=bjge, jnc, jae, jnb, jns
a>bjg, ja, jnbe
a<=bjle, jbe, jna

J jako Jump, ostatní písmena znamenají Less than, Carry, Below, Above, Equal, Sign, Greater than. Při L a G se porovnávané hodnoty berou jako shortinty nebo integery se znaménkem, při A a B jako byty nebo wordy bez znaménka. Takže např. když AX=-1 (FFFFh) a BX=+1 (0001h), bude po cmp AX,BX skákat jl, ale ne jb.

Záludnosti TP: parametry assemblerovských procedur

Pascal předává procedurám a funkcím parametry dvěma způsoby: hodnotou nebo odkazem. První způsob (procedure blabla(parametr:typ);) znamená, že se na zásobníku vytvoří kopie předané proměnné a ať se s ní uvnitř procedury dělá cokoli, tu původní to neovlivní. Druhý způsob (procedure blabla(var parametr:typ);) předá do procedury přímo adresu původní proměnné a žádnou kopii nevytváří, takže pokud ji procedura změní, změna se projeví i navenek.

Výše uvedené ovšem nemusí vždy platit pro funkce psané kompletně v assembleru a s direktivou assembler za hlavičkou. Tam se kopie parametrů vytvářejí pouze v případě, že nejsou větší než 4 byty. Cokoli většího (stringy, recordy, pole atd.) se vždy předává odkazem, i když v definici parametru není uvedeno slovo var! Parametry s var se odkazem předávají vždy, bez ohledu na velikost - tady už žádná past není.

Procedury a funkce bez direktivy assembler, i když obsahují vložené bloky asm, se chovají standardně - tedy všechny parametry bez var se předávají jako kopie bez ohledu na velikost.

Bug TP: kopírování vícerozměrných polí

Příklad:

type Pole = array[1..5,1..7] of char;

var JednoPole:Pole;
    DesetPoli:array[1..10] of Pole;

...

JednoPole[3,6]:='A';   {OK}
DesetPoli[2,3,6]:='B'; {OK}
DesetPoli[4,2,1]:=JednoPole[1,5]; {OK}
JednoPole:=DesetPoli[5]; {pozor, někdy nefunguje!}
move(DesetPoli[5],JednoPole,sizeof(JednoPole)); {taky někdy nefunguje!}
move(DesetPoli[5,1,1],JednoPole[1,1],sizeof(JednoPole)); {OK}

Kopírování bloků dat mezi vícerozměrnými poli někdy trochu hapruje. Syntakticky je všechno v pořádku a mělo by to fungovat, leč někdy nefunguje: data se nezkopírují vůbec nebo jenom částečně nebo možná někam jinam. Co přesně znamená to "někdy"? Na žádnou zákonitost jsem nepřišel. Možná záleží na velikosti těch polí: u array[1..30,1..25,1..80] se mi chyba projevila, u array[1..5,1..2,1..4] ne. Nebo možná na tom, kde v paměti se nacházejí. Přetékáním indexovací proměnné to určitě nebylo, v obou případech jsem použil word a pole neměla víc než 64 KB.

Spolehlivé je buď kopírovat prvek po prvku, nebo použít Move a dát jí adresu začátků polí až do posledního indexu.

2013-??-?? | Mircosoft
Datum: 20.12.2009 18:14
Od: Tin
Titulek: podmíněné skoky
e ... equal (rovnost)
a ... above (nad) -- bezznaménková
g ... greater (větší) -- se znaménkem
b ... below (pod)
l ... less (menší)

JNGE ... jump if not greater or equal -> když není vetší rovno -> je menší JL (pro čísla se znaménkem)
Reklamy: