Remember, an int is not always 16 bits. I'm not sure, but if the 80386 is one step closer to Intel's slugfest with the CPU curve that is aymptotically approaching a real machine, perhaps an int has been implemented as 32 bits by some Unix vendors...?
-- Derek Terveer
![]() |
![]() |
![]() |
|
![]() |
Tento článek by měl poradit těm, kteří se rozhodli DJGPP otestovat, ale narazili na nejrůznější potíže při pokusech zkompilovat své starší programy pomocí GNU C. Popisuje většinu problémů, které nastanou při přechodu z jiných překladačů, zejména Borland C. Je tedy zaměřen na DOSové uživatele, ale snad pomůže i ostatním. | ![]() |
|
![]() |
![]() |
![]() |
Toto je jedna z formulek, která je velmi často omílána v různých časopisech. Dozvíme se, že Windows 95 jsou 32 bitový operační systém, že používá 32 bitovou FAT a dokonce má 32 bitový přístup k disku. Kupodivu málokdo ví, co to přesně znamená a k čemu to vlastně je dobré. Reklamy nám pouze vsugerovaly, že co je 32 bitové, to je moderní, rychlé, stabilní a dává pocit jistoty a bezpečí. To je samozřejmě nesmysl.
Kolik bitů má procesor se určuje většinou podle šířky datových cest. Tedy kolik bitů se po takové cestě v procesoru přetáhne naráz. Z tohoto pohledu opravdu 32 bitové procesory toho zvládnou více, než 16ti bitové. Procesory Intelu jsou 32 bitové od 386DX.
Situace ale není tak jednoduchá. Pro 32 bitový procesor je dobré psát kód, který těch 32 bitů opravdu využije. Všechny procesory z řady 80x86 starší, než 386 byly 16ti bitové (alespoň navenek) a proto i assemblerové instrukce jsou takové, že 32 bitů nevyužijou a pokaždé zpracovávají pouze 16ti bitová data. Procesory 386 přirozeně rozšířily tuto instrukční sadu a přidaly 32 bitové registry. Pokud použijete 32 bitový registr na místě 16ti bytového, počítá se interně v 32 bitech a teprve potom se vlastně využije celý procesor. Je li nějaký kód 32 bitový, znamená to vlastně jenom to, že namísto 16ti bytových registrů používá i tyto 32 bitové.
Takový kód by měl být schopen využít všechny schopnosti procesoru. Je ale otázkou, jestli sčítáni 16 plus 16 provedené 32 bitově je rychlejší, nežli stejné sčítání 16-ti bitově. Logická odpověď je, že není. 32 bitové sčítání sice využije celý procesor, ale zase sčítá navíc jenom pár zbytečních cifer, které jsou stejně nulové. Procesor toho musí udělat o něco víc, a tak by to mělo být o něco pomalejší. Dněšní procesory ale provedou sčítání jak 16-ti bitově, tak 32 bitově v jednom taktu a tak by to mělo trvat stejně. Z této úvahy je vidět, že zrychlení způsobené 32 bitovým procesorem nepříjde samo, ale je nutné přepsat programy tak, aby 32 bitů opravdu využily.
Intel pro přidání 32 bitových instrukcí použil takzvaný prefix. To je věc, která se napíše před instrukci a ta se rázem změní z 16-ti bitové na 32 bitovou. Provedení tohoto prefixu ale trvá celý takt. Tím pádem 32 bitové sčítání je dvakrát pomalejší. Na pentiu navíc tyto instrukce nemohou do druhé pipeliny a proto je provádění kódu s prefixy až několikanásobně pomalejší. Situace je ještě horší u novějších procesorů.
Aby toho nebylo dost, vyvojáři Intelu se rozhodli tento problém nějak obejít. Přidali ještě možnost nastavit části kódu (segmentu) příznak, který určuje, jestli kód je 32 bitový. Pokud je tento příznak nastaven, instrukce jsou automaticky 32 bitové (nepotřebují tedy prefix). Naopak 16-ti bitové instrukce ale prefix potřebují. Tím pádem naopak sčítání v 16 ti bitech je zcela nelogicky dvakrát pomalejší, než 32 bitové.
To má první praktický dopad pro programování. Narozdíl od
například Borland C, GCC generuje 32 bitový kód. Proto je i velikost typu
int
4 bajty. Provádění kódu obsahující int
by tedy melo být
přibližně stejně rychlé v Borland C i GCC pouze s tím rozdílem, že v případě
GCC budou všechny výpočty 32 bitové a budou tedy brát větší čísla.
Není to úplně pravda. 32 bitové programy většinou dnes běží o něco rychelji, protože novější procesory jsou výrazně rychlejší v případě 32 bitového kódu. GCC navíc lépe optimalizuje. Na druhou stranu ale 386 (zejména SX) chroustá 32 bitů pomaleji.
Typy short
a long
ale zůstávají na obou překladačích
stejné (16 a 32 bitů) a proto kód používající typ short
je na BC
ryhlejší a naopak kód používající long
je rychlejší na GCC.
Z toho lze vyvodit, že by se člověk měl snažit používat všude 32 bitové
hodnoty, pokud možno nepoužívat typ short
a nahradit je typem int
.
Dokonce i proměné typu char
(8 bitů) potřebují prefix.
Proto je často lepší počítat s použitím typu int
.
Není to ale úplně pravidlem, protože 8-mi bitových regitrů je více,
než 16-ti bitových a 32 bitových a proto se někdy ušetří práce se zásobníkem.
Situace se ještě více komplikuje v případě použití polí.
Někdy se pole typu short
, či char
ještě vejde do cache
(která je na pentiu 4KB) zatímco int
už ne. Přístup do takové
paměti je potom pomalejší. Zde nezbýva nic jiného, než experimentovat.
Protože jsou typy jinak dlouhé, má to logicky dopad na velikost struktur, které se často ukládají na disk. Soubory uložené kódem z jednoho překladače pak nenačtete kódem z druhého. Navíc ale GCC struktury zarovnává. Od 486 se pro 32 bitové hodnoty uložené na adresách dělitelných čtyřmi použije 32 bitový přístup. Pokud adresy dělitelné nejsou, použije se pomalejší přístup. Proto se GCC snaží všechny 32 bitové hodnoty držet na adresách dělitelných číslem 4, a 16ti bitové hodnoty na adresách dělitelných číslem 2.
Protože ale norma nedovoluje přeházet položky ve strukturách, GCC prostě před každou položku přidá volné místo tak, aby adresa byla správně dělitelná v případě, že začátek struktury leží na adrese dělitelné čtyřmi. Aby toto bylo zaručeno, zvětší ještě strukturu tak, aby její celková velikost byla násobek čtyř, aby i pole struktur bylo správně zarovnané. Aby i začátky pole byly správně zarovnané, i funkce pro alokaci paměti (malloc atd.) vrací adresy dělitelné číslem 4. Díky tomu například struktura:
![]() |
![]() |
![]() |
|
![]() |
struct { char a; char b; int c; }; |
![]() |
|
![]() |
![]() |
![]() |
má velikost 8 bajtů. A
se uloží na začátek, b
hned za něj.
Potom se dva bajty vynechají, aby byla adresa c
dělitelná čtyřmi
a pak se uloží čtyř bajtová hodnota C. Struktura je tedy stejná, jako:
![]() |
![]() |
![]() |
|
![]() |
struct { char a; char b; short unused; int c; }; |
![]() |
|
![]() |
![]() |
![]() |
Naopak ale struktura:
![]() |
![]() |
![]() |
|
![]() |
struct { char a; int c; char b; }; |
![]() |
|
![]() |
![]() |
![]() |
zabírá celých 12 bajtů, protože za proměnou a
se vynechají
3 bajty pro zarovnání a za posledním další 3 bajty pro zarovnání
velikosti struktury. Proto je ve strukturách dobré řadit položky
podle velikosti. Ušetří se tím nějaké místo v paměti.
Pokud tuto funkci potřebujete vypnout --- potřebujete strukturu
načítat z disku tak, aby byla stejná jako ta stará, nebo komunikovat s vnejším
světem (vesa driver apod), je nutné nastavit atribut packed
:
![]() |
![]() |
![]() |
|
![]() |
struct { char a; int c; char b; } __attribute__ ((packed)); |
![]() |
|
![]() |
![]() |
![]() |
Tato struktura má potom velikost 6 bajtů. Navíc lze tento atribut použít uprostřed struktury, pokud je třeba pouze nějaký prvek přilepit hned za předchozí:
![]() |
![]() |
![]() |
|
![]() |
struct { char a; int c __attribute__ ((packed)); char b; } |
![]() |
|
![]() |
![]() |
![]() |
Toto zarovnávání lze také určovat explicitně pomocí atributu
aligned
:
![]() |
![]() |
![]() |
|
![]() |
struct { char a; int c __attribute__ ((aligned (16))); char b; } |
![]() |
|
![]() |
![]() |
![]() |
Tady se int
zarovná na 16 bajtů (stejně jako velikost celé
struktury, aby to platilo i v poli). Velikost struktury tedy bude 32
bajtů.
Tyto atributy lze používat i na normální proměné, pokud to je třeba.
Běžně se v programech čísla větší, než 65536 příliš nevyskytují a proto 32 bitů zas tak důležitých není. Jedno z míst, kde opravdu pomůže je adresace. 64KB paměti adresovatelných 16-ti bitovým číslem je prostě málo. Procesory Intel v 16-ti bitovém módu toto obcházejí pomocí segmentů:
Celá paměť je rozdělena na 64KB dlouhé bloky. Adresace potom probíhá tak, že napřed zvolíte segment, který určuje blok a teprve potom offset, který určuje vlastní pozici v segmentu. Je tak tedy možné adresovat mnohem více paměti. Určování všech adres nadvakrát by ale bylo pomalé a proto se používají segmentové registry. Každý program má nastavený například registr CS (code segment). Pozice v programu se potom určuje pouze pomocí offsetu a použitým segmentem je implicitně CS. Podobně funguje registr DS (data segment) pro data a další registry. Všechno funguje dobře, pokud program není delší, než 64KB a nepoužívá více dat. V případě, že program je delší, je nutné používat jinou instrukci pro skoky, volání apod, které používají plné adresy (segment i offset) a podobně u dat. Tyto instrukce jsou ale zase pomalejší.
BC se s tímto vypořadává zavedením nekolika pamětových modelů.
Jeden extrém je small, kde jsou všechny adresy krátké a není
tedy možné mít data či kód delší, než 64Kb. V druhém extrému
jsou zase všechny adresy dlouhé a kód je potom delší a pomalejší.
Máte ale k dispozici celou paměť. Většinou se používají různé
hybridní modely, kde některá data jsou blízko a jiná daleko.
K tomu se používají dva typy ukazatelů. Do C je potom nutné přidat
slovíčka far
a near
, pomocí kterých se rozlišuje mezi těmito typy.
Je také třeba víceméně zdvojit memorymanagement a přidat farmalloc
pro alokaci vzdálených dat. I funkce přebírající jako parametr
ukazatel by měly být ve dvou verzích --- pro far
i near
ukazatele.
Také se tím rozbije pointerová aritmetika --- není možné míchat far
a
near
ukazatele. Prostě začne tím opravdové peklo. Právě tato adresace
je důvodem, proč GCC 16-ti bitový mód Intelovkých procesorů vůbec nepodporuje.
Proradný, vývojářům firmy Intel ale ani to nestačilo a situaci ještě zdokonalili. U XT použili pouze pár prvních bitů v segmentu jako adresu a proto XT umí adresovat pouze 640KB paměti. To ale brzo začalo být málo a proto se do XT začaly přidávat hardwarové paměti EMS, které ale nebyly běžně adresovatelné. Bylo z nich vidět pouze okénko 64KB a pomocí speciálního ovladače se určovalo, kterou část paměti okénko zobrazuje. Paměť tak byla pomalejší a práce s ní byla komplikovaná, ale alespoň nějaká byla.
Vše ještě více zkomplikovaly procesory řady 286. Zde je možné použít celé segmentové adreasy a adresovat tak o něco více paměti. Protože ale XT vyšší bity adresy ignorovaly (tedy aresa 1 bylo to samé co adresa 640KB+1), přestaly by fungovat nějaké hypotetické obskurdní aplikace přisupující na adresu 1 pomocí té delší. Vznikla tedy šílenost jménem A20 gate (ten zmíněný ignorovaný bit je právě 21.). Program používající více, než 640KB RAM tedy napřed A20 gate zapne a potom může adresovat dál. Běžně je ale A20 gate vypnutá a proto naše hypotetická aplikace by mohla dál fungovat myslíc si, že je na XT.
To by ale nebylo tak strašné jako to, že existuje několik způsobů přepínaní této brány. Dobrý ovladač je musí všechny znát. Bežně se o ně stará HIMEM.SYS. Proto po jeho zavedení máte k dispozici část paměti jménem HMA (high memory area). To je právě místo nad 640KB, kde lze normálně provádět kód, pokud je A20 zaplá.
286 ale už umí adresovat 16MB paměti dokonce pomocí několika berliček (himemu) ji i používat. Pracuje se s ní samozřejmě úplně jinak, než z přímo adresovatelnou dolní pamětí, nebo EMS. Proto programy musí znát ještě další způsob jak přistupovat do paměti - XMS.
Snad Vás potěším, že právě 32 bitů toto řeší. 32 bitová adresa
je dost dlouhá na to, aby adresovala celou paměť. Proto není třeba
dělat žádné segmenty, offsety, XMS, EMS, UMB, HMA, far
, near
,
paměťové modely a další strašáky. Pokud je jednou paměť
řádně zinicalizována (to není tak jednoduché, už třeba díky A20 gate),
je možné pomocí jednoduché 32bitové adresy určit libovolné
místo v paměti stejně rychle, jako v případě small modelu v reálném módu.
Díky tomu můžete na vše co se týká těchto věcí zapomenout.
Ve zdrojových kódech smazat všechna slova far
a near
a radovat se z toho,
že máte lineární paměť. Navíc tím padá omezení na velikost polí,
protože už není řádný důvod, aby pole byla omezena na 64KB.
Další věc, která odpadá jsou overlaye. Protože velikost programu už není omezena volnou dolní pamětí, můžete klidně udělat 2MB dlouhý program, aniž by bylo nutné jej nějak porcovat.
Protected mód je to, co se většinou skrýva pod reklamou na 32 bitový operační systém. Správnému operačnímu systému by mělo být v principu jedno, na kolikabitovém procesoru běží (vyjma limitů na některé věci - jako soubory. Protected mód ale dává systému nové a netušené možnosti.
Protected mód umožňuje, aby jeden řídící programu získal kontrolu nad ostatními. Může jim povolovat a zakazovat nejrůznější věci a různě je tahat za nos. Může jim například tvrdit, že mají sound blaster, všechny jejich přístupy na sound blaster chytit a provádět na jiné zvukové kartě. Také umožňuje multitásking - operační systém může procesu tvrdit, že běží na počítači sám, ale přesto jej kykoliv přerušit a předat řízení jinému.
Jednou z nejkrásnějších věcí protected módu je virtuální paměť. Paměť, kterou program vidí vůbec nemusí být pamětí počítače. Paměť se rozdělí na stránky (4KB) a při přístupu k ní, se nejprve zjistí o jakou stránku se jedná a potom se v tabulce vyhledá, kde daná stránka ve fyzické paměti je.
Je tím tedy možné vytvořit specielní rozložení paměti pro každý proces. Od adresy 0 může začínat jeho kód a od nějaké další adresy že začíná neomezená volná paměť. Ostatní věci v paměti - systém, bios, další procesy, videoram se před programem schová. Nemůže tedy tyto věci nijak ohrozit.
Navíc je možné stránkám přiřadit různá přístupová práva - nastavit nějakou část pouze pro čtení (například kód sdílené knihovny), nebo pouze pro provádění apod.
Protože se operační systém dozví v případě, že tyto přístupová práva byla porušena, může emulovat virtuální paměť. Pokud není dost fyzické paměti, uloží se nějaké části programu na disk. Zablokuje potom přístup k této části paměti. V případě, že se program pokusí tuto paměť použít, dojde k vyjímce a operační systém zase data načte z disku. Program tak nic nepozná.
Volná paměť potom může být stejně velká, jako fyzická plus volné místo na disku dohromady.
Pokud proces chce přistupovat například k videoram, může to sdělit operačnímu systému a ten mu potom vidoram může přidat do jeho paměti přidat tam, kam si proces řekl.
Protože DOS o protected módu neví, nejpřirozenější způsob jak pod ním provádět protectedmódový program je k němu přidat nějaký zavaděč, který se napřed postará o nastolení protected módu. To samo o sobě znamená vyplnění několika tabulek, vypnutí A20 gate a přepnutí procesoru. Není to nic strašného.
Problém ale je, že DOS i BIOS je pouze 16-ti bitový a v 32 bitovém
protected módovém prostředí prostě nepoběží. Proto by takový program
už nemohl ani na disk. Proto vznikl režim jménem V86. To je režim
procesoru, kdy se procesor tváří, jako kdyby byl v normálním (reálném)
režimu, ale přesto je v protected módu. Tedy real modový program (DOS a BIOS)
běží, ale program zprostředkující protected mód (v tomto případě to tedy
není operační systém) nad nimi má stejnou kontrolu, jako nad normálním
procesem v protected módu dokonce včetně virtuální paměti. Tento režim
například bežně používají i programy jako EMM386, nebo QEMM pro emulaci
okénka do EMS. Nevýhodou tohoto režimu je, že ne všechny instrukce procesor
zvládne sám a některé se musí emulovat (podobně jako koprocesor
na 286). Tyto instrukce pak běží samozřejmě pomalu. Mezi ně patří
většina instrukcí, které jsou normálně privilegované a protected
módovy program si je bez dovolení nemůže dovolit - například
cli
a sti
.
Proto 16ti bitové programy pod V86 jsou pomalejší a navíc v emulaci mohou
být chyby. Pro většinu věcí to ale stačí.
Protože emulovat i interrupty a proto jsou systémová volání pracující hlavně s interrupty docela pomalá. Je také nutné data pro ně připravit do nějakého transfer bufferu, aby náhodou nebyly mimo dolních 640 KB. Proto přístup k disku pod protected módem hlavně v případě, že přistupujete po malých kouscích je výrazně pomalejší. 32 bitový přístup k disku pod Windows dělá přesně to, že nahradí DOSová volání pro práci s diskem 32 bitovým kódem a tak se ušetří několik přechodů mezi protected a V86 módem při přístupu k disku (volá se jenom BIOS). Nejedná se tedy o opravdový 32 bitový přístup (že by se IDE řadič dostával 32 bitová čísla - což je také možné).
V případě, že toto všechno vyřešíte, pořád není vyhráno. Privilegovaný program řídící protected mód musí být totiž pouze jeden. EMM386 a Qemm tento mód využívají a proto Váš program se pod nimi proste nespustí. Při pokusu o přechod do protected modu zkolabuje. K tomu vzniklo rozšíření VCPI. V případě, že je VCPI server v paměti, nepoužijete standardní kód pro inicializaci protected módu, ale požádáte VCPI server, aby vám předal řízení. Ten to udělá a vy tak získáte kontrolu. Nakonec ji zase předáte zpátky.
Aby toho ale nebylo málo, například Windows DOS prompt toto nepodporuje. Windows by potom ztratily by totiž možnost provádět multitásking v případě, že předají řízení. Proto byl vytvořen nový standard jménem DPMI. DPMI znamená DOS protected mode interface. DOS je v názvu je poněkud zavádějící, protože DPMI s DOSem nemá nic společného. Jinak je ale název vpořádku.
Jedná se o jakýsi ovladač, který na daném interruptu (podobně jako třeba ovladač myši) spřístupňuje nejběžnější věci, které protected módový program dělá (přepínaní do proteted módu, zavolání real modového interruptu, práce s virtuální pamětí atd...)
32 bitový program tedy pak nepotřebuje svůj vlastní kód pro inicializaci protected módu, V86 apod. Prostě a jednoduše volá DPMI.
Aby to ale nebylo tak snadné, DPMI bylo navrženo pro 286, která už měla jakýsi protected mód, ale tak nešikovny, že ho stejně nikdo nepoužíval. Díky tomu má DPMI mnoho omezení. Navíc DPMI neběží na všech konfiguracích a proto program stejně musí mít i jiné cesty k inicializaci. Existuje několik verzí DPMI a díky velké komplikovanosti standardu mají skoro všechny implementace nějaké nedostatky.
Pokud ale máte štěstí, DPMI je dostupné a funguje, máte lineární paměť, možnost, volat realmodové interrupty a další nezbytné věci. Jedna z největších komplikací je pouze naprosto zbytečné omezení na velikost zásobníku (zásobník se musí určit na začátku).
Abych shrnul poznatky z předchozích odtavců. Pokud chcete mít program v protected módu, musíte zvládnout jeho inicializaci, V86 režim, VCPI a DPMI.
To je důvod, proč bežně vznikají extendery, což jsou externí programy, které se postarají o nastartování protected módu a spuštění 32 bitové aplikace. Ve Watcom c to je známý Dos4GW, v EMX/GCC to je EMX a RSX, ve starém DJGPP to bylo go32 apod.
V DJGPP v2 se rozhodli to udělat jinak. Programy kompilované pod DJGPP jsou přímo DPMI clienty a používají DPMI jako ,,extender'' . V případě, že používáte Windows, nebo Qemm s DPMI, není tedy třeba žádný externí program. V případě, že DPMI server neběží, program se pokusí použít externí program (CWSDPMI). Teprve v případě, že ani to nejde, napíše hlášku o tom, že potřebuje DPMI a skončí. Protože běžný extender musí běžet pod DPMI a může tedy obsahovat pouze podmnožinu jeho služeb, zdá se mi to jako dobré řešení.
CWSDPMI je velmi kvalitní implementace DPMI - běží téměř všude, je relativně krátký (20KB) a umí velké množství DPMI volání. Neemuluje vůbec 16-ti bitové DPMI, to ale v případě DJGPP není třeba.
Důležitá věc je, že nesmíte zapomenout přibalit CWSDPMI ke všem programům.
Je možné ještě použít mezi uživateli Watcom C velmi rozšířený DPMI server
pmode
. Ten je ještě kratší a o něco málo rychlejší. Existuje také
utilita, co jej přidá přímo do EXE souboru, což někteří považují za
profesionálnější. Ja osobně to považuju za zbytečné plýtvání místem na disku.
Pmode ale neemuluje celé DPMI, je poměrně nesnášenlivý a neumí
swapovat na disk. Proto jej nedoporučuju.
Vlastní distribuce CWSDPMI obsahuje ještě dva programy. Prvním je CWSDPR0. Pokud použijete tento DPMI server, program běží v privilegovaném módu, proto je možné například použít časovací instrukce na pentiu. Ale pro běžné použití to je kničemu. Nemůže swapovat na disk, neodpovídá přesně DPMI specifikaci apod.
Druhým programem je CWSPARAM. Ten umožňuje nastavit mnoho
užitečných věcí. Jako první je cesta k swapovacímu souboru. Běžně
se swapuje na c:
, ale například u diskless stanic je cestu třeba přenastavit.
Další důležítá věc je položka Minimum application memory desired before
640K paging
. Běžně totiž DPMI programy vůbec nepoužívají paměť pod
640KB. Někdy se stává, že téměř cela XMS je použita pro cache apod.
Tato položka určuje minimální velikost XMS, kdy se ještě dolní paměť
nepoužije. Pokud Váš program nepoužívá žádné speciální DOSové tryky, které
potřebují paměť (jako třeba zavádění residentů či DOS prompt),
je dobré tuto hodnotu nastavit na maximální velikost paměti, kterou Váš programpotřebuje.
Další položka určuje kolik paměti se má ponechat pro DOS v případě,
že se začne dolní paměť používat. Pokud nepoužíváte DMA, soubory atd.,
je možné tuto hodnotu nastavit dokonce na 0. Jinak je standardní nastavení
přiměřené.
Tento program spolu s dokumentací k CWSDPMI je take dobré přibalit k Vašemu programu. Uživatelé pak mají možnost nastavit si parametry dle libosti.
Výsledkem linkování není DOSový EXE sobor, jako v Borland C,
ale COFF objekt. K jeho suštění jde použít zavaděč GO32V2, nebo
k němu přidat tzv. stub. Stub je krátký 16ti bitový kód, který se postará
o správné zavedení 32 bitového objektu, načtení CWSPMI (pokud
to je třeba), přepnutí do protected modu a odstartování vlastního
programu. Tento stub se k programu přidává programem stubify
. Navíc
má ale několik standardních voleb, které lze přenastavit programem
stubedit
. Z Makefile to lze dělat pomocí parametrů z příkazové
řádky. Pokud žádné parametry nezadáte, program o ně požádá interaktivně.
Lze nastavit velikost zásobníku (mimochodem u překladačů C a C++
Md cc1
je někdy nutné tuto velikost zvětšit, pokud odmíta přeložit
nějaký složitější program).
Další parametr je velikost transfer bufferu. To je buffer používaný pro volání DOSových služeb (třeba když potřebujete uložit něco do souboru, data se napřed zkopírují do tohoto bufferu a teprve potom se volá DOS). U programů, které často vyměňují data s DOSem je dobré tuto velikost zvětšit (pomůže to třeba u preprocesoru a linkeru na síti)
Pomocí parametru Base name of file to actualy run
lze
vytvářet napodobeninu symbolických linků. Pokud není nastaven na prázdný
řetězec, nezavede se program z toho samého souboru, ale z jiného. Pod UNIXem
je běžné, aby jeden program měl dvě jména. Pokud to potřebujete pod
DJGPP, jde program uložit do jednoho souboru a potom vytvořit druhý
soubor, který obsahuje pouze stub s nastaveným tímto parametrym.
Value to pass as argv[0]
- co se programu má předat jako jméno.
Pokud je prázdný řetězec, použije se jméno souboru.
Program to provide DPMI server
- jaký program se má volat,
v případě, že DPMI server neni k dispozici. Standardně to je
CWSDPMI.
Programy pod GCC jsou 32 bitové, proto nelze použít běžné
emulátory koprocesoru. Pokud používáte nějaký floating point kód
a chcete, aby program fungoval i na 386, je nutné přidat emulátor.
Ten lze zalinkovat přímo do kódu pomocí knihovny emu (přepínač
-lemu
) pro GCC, nebo zavádět z externího modulu emu387.dxe
.
Pokud přidáte emu387.dxe
do stejného adresáře, jako je EXE
soubor, mělo by všechno chodit. Pokud je jinde, je nutné nastavit
proměnnou:
![]() |
![]() |
![]() |
|
![]() |
set emu387=c:/djgpp/bin/emu387.dxe |
![]() |
|
![]() |
![]() |
![]() |
Někdy stává (alespoň si na to na mailing listu kdysi dávno někdo stěžoval), že program špatně zdetekuje, že chybí koprocesor. Potom pomůže nastavení:
![]() |
![]() |
![]() |
|
![]() |
set emu387=c:/djgpp/bin/emu387.dxe |
![]() |
|
![]() |
![]() |
![]() |
Po provedení stubu se dostane ke slovu zaváděci kód. Ten má
za úkol všechno připravit a zavolat vlastní main
. Dělá doho o něco
víc, než je zvykem. DJGPP se tím snaží docílit větší kompatibility s
UNIXem. (V UNIXu například expanze příkazové řádky (*
a další znaky)
nejsou věcí programu, ale věcí shellu, který program volá.) DJGPP
tedy expanduje tyto znaky automaticky při startu, aby programy už
dostaly parametry stejně jako v UNIXu. Tato automatická
konverze je občas nežádoucí, protože pak nemůžete předat programu
parametry obsahující znaky jako *
.
Díky tomu jsou také výsledné EXE soubory o něco větší. Většina funkcí jde ale vypnout v případě, že nejsou třeba. Proto lze velikost výsledného EXE souboru změnšit na 6KB, což není tak strašné vzhledem k tomu, co vše se při inicializaci děje. DJGPP je tedy i docela vhodné pro psaní krátkých utilit.
Všechny prototoypy pro tento kód sídlí v headeru crt0.h
.
Rúzné parametry pro start programu lze nastavit pomocí proměné
_crt0_startup_flags
. Nastavíte jí tak, že do svého programu napíšete
int _crt0_startup_flags=něco
. Funkce musí být globální (tedy mimo
funkci a bez static
). Jde nastavit následující flagy:
_CRT0_FLAG_PRESERVE_UPPER_CASE
Pokud tento flag je nastaven,
nepřevede se argv[0]
(jméno programu) na malá písmena.
_CRT0_FLAG_USE_DOS_SLASHES
Nepřevádí se v argv[0]
opačná
lomítka na normální
_CRT0_FLAG_DROP_EXE_SUFFIX
Pokud je nastaven, vynechá se přípona .exe
v argv[0]
_CRT0_FLAG_DROP_DRIVE_SPECIFIER
pokud je nastaven, vynechá se jméno disku v argv[0]
(pokud je uvedeno)
_CRT0_FLAG_DISALLOW_RESPONSE_FILES
Standardně zavaděcí kód projde
parametry a pokud najde parametr ve formátu @jmeno
, přečte si
parametry se souboru jmeno
a dosadí je. Pokud je tento flag
nastaven, nic takového se nestane.
_CRT0_FLAG_FILL_SBRK_MEMORY
Pokud je nastaven, každá pamět přidávaná do programu se napřed snuluje. To potřebují některé chybné programy z UNIXU, protože tam se to děje automaticky (kvůli bezpečnosti)
_CRT0_FLAG_FILL_DEADBEEF
Paměť nenuluje, ale nastavuje na hodnotu
0xdeadbeef
, což umožňuje vychytat chyby.
_CRT0_FLAG_NEARPTR
Pokud je nastaven, od adresy __djgpp_nearptr_base
se spřístupní fyzická paměť. Vypne to ochranu paměti a proto se
to nedoporučuje.
_CRT0_FLAG_NULLOK
Vypne odchytávání přístupu na ukazatel NULL
.
_CRT0_FLAG_NMI_SIGNAL
Pokud se nastaví, nejsou MNI signály propouštěny do 16ti bitového kódu. To občas způsobuje potíže s green BIOSy
_CRT0_FLAG_NO_LFN
Vypne podporu pro dlouhé názvy pod W95
_CRT0_FLAG_UNIX_SBRK
Zařídí, aby se sbrk chovalo jako v UNIXu. Tedy aby všechna paměť byla zasebou. To ale občas vyžaduje přesouvání bloků paměti a také některé DPMI servery neumožňují tak naalokovat celou paměť.
_CRT0_FLAG_LOCK_MEMORY
Uzamkne celý program v paměti a zakáže swapování. To je občas třeba, když chcete psát ovladače interruptů a nechcete je zamykat ručně. Vypne to ale virtuální paměť a tak to není dobrý nápad. Podle mých zkušeností tento flag nějak nefunguje.
_CRT0_FLAG_PRESERVE_FILENAME_CASE
Vypne převod názvů z velkých písmen na malá.
Nastavením této proměné sice můžete vypnout různé funkce, ale pořád se zalinkují do programu. Pokud chcete určité kusy kódu vynechat, je nutné místo nich napsat prázdné funkce, které potom linker použije místo těch originálních.
Startovací kód mimo zavádění programu ještě dělá:
*
, @
apod)Vypnutí zavedení souboru s environmentem (to jde udělat skoro ve všech programech, které nemají zapadnout do originálního vývojového prostředí DJGPP) se provede pomocí:
![]() |
![]() |
![]() |
|
![]() |
void __crt0_load_environment_file(char *_app_name) { return; } |
![]() |
|
![]() |
![]() |
![]() |
Expanze speciálních znaků se provádí pomocí:
![]() |
![]() |
![]() |
|
![]() |
char **__crt0_glob_function(char *_arg) { return 0; } |
![]() |
|
![]() |
![]() |
![]() |
Pomocí této funkce můžete nastavit i svůj vlastní expander.
Přípravu argv
a argc
provádí funkce:
![]() |
![]() |
![]() |
|
![]() |
void __crt0_setup_arguments(void); |
![]() |
|
![]() |
![]() |
![]() |
Pokud váš program nepoužívá parametry, je možné ji nahradit za prázdnou funkci.
Dále lze z programu vyloučit exception handling (program potom v případě že například vydělí nulou nevypíše registry, ale zatuhne)
![]() |
![]() |
![]() |
|
![]() |
void _npxsetup(void) { return; } int __emu387_load_hook; short __djgpp_ds_alias; void __djgpp_exception_setup(void) { return; } int __djgpp_set_ctrl_c(int enable) { return 0; } |
![]() |
|
![]() |
![]() |
![]() |
Poslední šikovná proměná je _stklen
, potomcí které můžete
nastavit velikost zásobníku bez použítí programu stubedit
.
Mimochodem jedna z metod jak zmenšit velikost výsledného souboru
je nezalinkovat debugovací informace pomocí přepínače -s
, nebo je
zrušit pomocí programu strip
.
Pokud potřebujete přístup k nějaké fyzické paměti (třeba videoram), není nic jednoduššího, než požádat DPMI, aby ji dalo do adresovatelného prostoru Vašeho programu. Naneštěstí toto je funkce DPMI 1.0 a vyšší a tak ji většina DPMI serverů nepodporuje. Proto musí začít magie.
I v protected módu existují segmenty a offsety. Segmenty jsou tak velké (celá adresovatelná paměť), že se používá jenom jeden segment. V DPMI ale program běží uvnitř jednoho segmentu, zatímco fyzická paměť je v jiném segmentu. Proto lze použít segmentace pro přístup do vnější paměti.
Segment dosu je uložen v proměnné _dos_ds
a existují funkce
_farpoke*
a _farpeek*
které umožňují přistupovat na tyto vzdálené
ukazatele (_farpokeb
přistupuje k bajtu, _farpokew
k wordu atd.) Funkce se při zapnuté optimalizaci inlinují a jsou implementovány takto:
![]() |
![]() |
![]() |
|
![]() |
extern __inline__ void _farpokeb(unsigned short selector, unsigned long offset, unsigned char value) { __asm__ __volatile__ ("movw %w0,%%fs\n" " .byte 0x64 \n" " movb %b1,(%k2)" : : "rm" (selector), "qi" (value), "r" (offset)); } extern __inline__ void _farpokew(unsigned short selector, unsigned long offset, unsigned short value) { __asm__ __volatile__ ("movw %w0,%%fs \n" " .byte 0x64 \n" " movw %w1,(%k2)" : : "rm" (selector), "ri" (value), "r" (offset)); } |
![]() |
|
![]() |
![]() |
![]() |
Těch lze použít pro adresaci videoram asi takto:
![]() |
![]() |
![]() |
|
![]() |
#define putpixel(x,y,c) _farpokeb(_dos_ds, 0xA0000 + (y)*320 + (x), (c)) |
![]() |
|
![]() |
![]() |
![]() |
To sice funguje, ale zbytečně se pokaždé nastavuje fs
.
Jednodušší je ho nastavit jednou na začátku. K tomu slouží funkce
_farsetsel
a _farnspoke*
.
Na začátek vykresolvací smyčky přidáte: _farsetsel(_dos_ds)
a potom
kreslíte pomocí _farnspokeb(0xA0000 + y*320 + x, color);
Toto je asi nejčastěji používaná metoda pro přístup k paměti. Funguje pod každým DPMI a je docela rychlá. Existují také far verze pro kopírování paměti, takže když něco nakreslíte do framebufferu, není probém to potom zkopírovat do VRAM. To lze také dělat hned dvěma způsoby:
![]() |
![]() |
![]() |
|
![]() |
dosmemput(doublebuffer, 320*200, videoptr); movedata(_my_ds(), doublebuffer, _dos_ds, videoptr, sizeof(*doublebuffer)); |
![]() |
|
![]() |
![]() |
![]() |
Další metoda je zpřístupnění celé dosové paměti. To naprosto vypne veškerou ochranu paměti a proto se to nedoporučuje. Pokud ale potřebujete opravdu rychle no RAM, je to jedna z možností. Vypadá to asi takto:
![]() |
![]() |
![]() |
|
![]() |
#include <sys/nearptr.h> unsigned char *videoptr = (unsigned char *)0xA0000; __djgpp_nearptr_enable(); videoptr[y*320 + x + __djgpp_conventional_base] = color; __djgpp_nearptr_disable(); |
![]() |
|
![]() |
![]() |
![]() |
Je ještě nutné si pamatovat, že __djgpp_conventional_base
je
proměnná a může se měnit po každém zavolání alokace paměti.
Obojí je velmi snadné. O přístup na porty se starají inlinované
funkce outport*(port,hodnota)
a inport*(port)
(*
je b
pro byte,
w
pro word a l
pro long). Funkce jsou definované v pc.h
.
![]() |
![]() |
![]() |
|
![]() |
void setpalette(char color, struct rgbstruct rgb) { outportb(0x3c8, color); outportb(0x3c9, rgb.red); outportb(0x3c9, rgb.green); outportb(0x3c9, rgb.blue); } |
![]() |
|
![]() |
![]() |
![]() |
Protože se funkce inlinují, není nutné kvůli tomu psát assembler - výsledek je úplně stejný.
Pro volání realmode interruptů existuje DPMI služba. Funkce pro
ní je v dpmi.h
a jmenuje se __dpmi_int
. Její použití je následující:
![]() |
![]() |
![]() |
|
![]() |
void setmode(short mode) { __dpmi_regs r; r.x.ax = mode; __dpmi_int(0x10,&r); } |
![]() |
|
![]() |
![]() |
![]() |
O nastavování interruptu se stará DPMI server. Pokud chcete pověsit nějakou funkci na interrupt, je nutné udělat několik věcí. Nejdůležitější je zabránit swapování. Intelové totiž virtualní paměť v tomto případě dost zkomplikovali. Pokud je vyvolán hardwarový interrupt (ten co odchytáváte) a z něho se vyvolá další (výpadek stránkování), procesor spanikaří a vyvolá se automaticky interrupt 8 - dvojitá chyba. Z tohoto interruptu už není návratu a většinou způsobí, že se počítač zničeho nic vyrezetuje.
Nevidím žádný logický důvod pro toto chování. Následek ale je,
že není možné, aby se ovladač preřušení odswapoval na disk. Proto
je nutné DPMI serveru říct, že tento kus paměti se nesmí swapovat
(uzamknout). Nejjdedušší řešení je zablokovat celé swapování,
pomocí flagu _CRT0_FLAG_LOCK_MEMORY
. Takové zamykání naveliko ale
rozhodně není dobrý nápad a hlavně multitáskovému OS může pořádně
zkomplikovat život.
Je možné také zamykat jednodlivé stránky paměti pomocí funkcí
_go32_dpmi_lock_data
a _go32_dpmi_lock_code
. Allegro
definuje následující makro pro zamykání proměných:
![]() |
![]() |
![]() |
|
![]() |
#define LOCK_VARIABLE(x) _go32_dpmi_lock_data((void *)&x, sizeof(x)) |
![]() |
|
![]() |
![]() |
![]() |
Toto makro je nutné zavolat na všechny proměné, které handler
používá. Navíc je nutné zamknout vlastní funkci handleru (a všechny
funkce, co volá). Nejde ale zjistit velikost funkce pomocí sizeof
.
Allegro to řeší tak, že za koncem funkce se napíše další prázdná
funkce pomocí makra:
![]() |
![]() |
![]() |
|
![]() |
#define END_OF_FUNCTION(x) void x##_end() { } |
![]() |
|
![]() |
![]() |
![]() |
A makro pro zamykání funkcí potom uzamkne oblast mezi funkcí
jmeno
a funkcí jmeno_end
:
![]() |
![]() |
![]() |
|
![]() |
#define LOCK_FUNCTION(x) _go32_dpmi_lock_code(x, (long)x##_end - (long)x) |
![]() |
|
![]() |
![]() |
![]() |
Vlastní handler přerušení vypadá naprosto stejně jako normální funkce:
![]() |
![]() |
![]() |
|
![]() |
void int8(void) { unsigned offset = ScreenPrimary+(2*79)+1; _farsetsel(_dos_ds); _farnspokeb(offset,1+_farnspeekb(offset)); } END_OF_FUNCTION(int8) |
![]() |
|
![]() |
![]() |
![]() |
Metod, jak nainstalovat handler na prerušení je několik.
Normálně by se člověk ještě musel postarat o ukládání registrů atd. To
jde obejít pomocí funkce _go32_dpmi_allocate_iret_wrapper
. Ta vyrobí
krátkou assemblerovou funkci, která se o to postará. Použití vypadá
asi takto:
![]() |
![]() |
![]() |
|
![]() |
_go32_dpmi_seginfo info; info.pm_offset = int8; _go32_dpmi_allocate_iret_wrapper(&info); _go32_dpmi_set_protected_mode_interrupt_handler(8, &info); ... _go32_dpmi_free_iret_wrapper(&info); /*uvolní paměť použítou wrapperem*/ |
![]() |
|
![]() |
![]() |
![]() |
Ještě by ale bylo nutné se starat o volání původního handleru.
Pokud chcete aby i o to se postarala knihovna, stačí použít
funkci _go32_dpmi_chain_protected_mode_interrupt_vector
ta se sama
postará o wrapery a volání:
![]() |
![]() |
![]() |
|
![]() |
_go32_dpmi_seginfo pmint; pmint.pm_selector = _my_cs(); pmint.pm_offset = (unsigned)&int8; _go32_dpmi_chain_protected_mode_interrupt_vector(8, &pmint); |
![]() |
|
![]() |
![]() |
![]() |
už všechno zařídí a obrazovka začne pěkně blikat. Ale jenom až do chvíle, dokud se DPMI nepokusí o swapování. Není totiž pamět zamknutá. To se udělá takto:
![]() |
![]() |
![]() |
|
![]() |
LOCK_FUNCTION(int8); LOCK_VARIABLE(ScreenPrimary); |
![]() |
|
![]() |
![]() |
![]() |
Také je nutné se ujistit, že optimalizace jsou zapnuté a že se funkce pro přístup do VRAM inlinují.
Z handelrů nelze skákat pomocí longjmp
a není dobrý nápad volat
knihovní funkce jako printf
.
Samozřejmě, že assemlerové wrappery trošku zdržují. Pokud vám jde o čas, je možné si všechno napsat ručně. Tak to dělá například allegro a tak není nic jednoduššího, než se podívat do zdrojových kódů. Protože se o interrupty ale stará DPMI, nelze se spolehnout na to, že budou rychlé. Pokud použijete CSDPMI, je možné takto napsat i ovladač od pc-speakeru (40000 interruptů do sekundy). Například ale pod Qemm DPMI to už nestihne.
Použití inline assembleru Je popsáno v článku o rozšíreních GCC. Proto
se nyní budu zabývat linkováním assemblerových objetů. Pro psaní
assembleru jde použít buď standardní program gas
(GNU assembler).
Ten používá AT&T syntax, kterou jsem také popsal ve zvláštním článku,
nebo program NASM
, který má syntax (až na drobné vyjimky)
shodnou s programem TASM a proto asi bude lepší v případě, že máte nějaký
assemblerový kód a chcete jej použít. Díky rozdílům mezi 16 a 32
bitovým kódem stejně ale bude nutné udělat nějaké změny.
Existuje také převaděč ta2as
, který se stará o překlad z Intelí
syntaxe do AT&T. Není ale příliš spolehlivý.
Dále budu psát hlavně o programu gas
, protože NASM nepoužívám
Asi jeden z nejjedodušších způsobů, jak se naučit gas
je
použít jednoduchý program v C jej do assembleru (pomocí -S
).
Základní struktura souboru je asi následující:
![]() |
![]() |
![]() |
|
![]() |
.file "jméno" .data mojedato: .word 0 .text .globl __myasmfunc __myasmfunc: ... ret |
![]() |
|
![]() |
![]() |
![]() |
Vlastní GAS je velmi jednoduchý proram. Nemá žádnou podporu
pro makra. Pokud ale použijete jako příponu .S
(ne .s
), prožene se
nejprve soubor preprocesorem. Proto lze použít makra a konstanty
preprocesoru. DJGPP má standardně poměrně šikovnou sadu maker
v souboru libc/asmdefs.h
.
![]() |
![]() |
![]() |
|
![]() |
#include <libc/asmdefs.h> .file "jmeno.S" .data .align 2 mojedata: .word 0 ... .text .align 4 FUNC(__mojefunkce) ENTER movl ARG1, %eax ... jmp label ... label: ... LEAVE |
![]() |
|
![]() |
![]() |
![]() |
Takto napsané funkce se potom chovájí jako standardní funkce v C. Prototyp vypadá asi takto:
![]() |
![]() |
![]() |
|
![]() |
void mojefunkce(int p); |
![]() |
|
![]() |
![]() |
![]() |
Volací konvence jsou standardní jako v C - parametry jsou na zásobníku obráceně (první pop vyzvedne první parametr). O vrácení zásobníku se stará volající funkce. Pokud máte kód napsaný pro pascal, tedy funkce po sobě uklízí samy, je nutné k prototypu napsat:
![]() |
![]() |
![]() |
|
![]() |
void mojefunkce(int p) __attribute((stdcall)); |
![]() |
|
![]() |
![]() |
![]() |
GCC také umí registrové volací konvence. Potom je prvních n
parametrů (maximálně 3) uloženo v registrech. To se deklaruje:
![]() |
![]() |
![]() |
|
![]() |
void mojefunkce(int a, int b, int c) __attribute((regparm(3))); |
![]() |
|
![]() |
![]() |
![]() |
Použijí se registry EAX
, EDX
, ECX
. Registrové volací konvence
jsou často rychlejší, než zásobníkové, proto takové prototypy se
občas vyplatí použít i u často volaných céčkových funkci.
Nejdůležitější pseudoinstrukce jsou:
.allign n
zarovná adresu následující instrukce tak, aby byla
dělitelná číslem n
.
.data
datový segment
.text
kódový segment
.globl symbol
symbol je globální
.comm symbol, délka
následující symbol je common (takových symbolů může být v objektech více se stejným jménem a linker je potom spojí za sebe.
.ascii
Ascii řetězec
.asciz
jako .ascii
ale přidá nakonec nulu
.byte
8 bitů
.short
16 bitů
.int
32 bitů
.quad
64 bitů
.double
floating point hodnota
.float
floating point hodnota
.fill kolikrát, velikost, hodnota
vyplnění oblasti hodnotou
Novější verze gas
u podoporují i věci jako .ifdef
, .macro
atd. Zdá se
mi ale lepší použít preprocesor. Více je v dokumentaci (info -f gas
)