Obsah
1. Problematika ukazatelů v překladačích jazyka C pro systém DOS
2. Adresování stylem segment:offset
3. Rozsah paměti IBM PC s mikroprocesorem 8086 či 8088
4. Rozsah adresovatelné paměti mikroprocesoru 80286
5. Může offset přesáhnout hranici jednoho segmentu?
6. Paměťové modely překladačů jazyka C v reálném režimu
7. Praktická část: paměťové modely překladače Borland C++ 3.0
8. Velikosti ukazatelů a základní aritmetika s ukazateli v praxi
9. Funkce vracející větší z hodnot předaných přes ukazatel
10. Překlad pro paměťové modely s krátkými ukazateli pro data
11. Překlad pro paměťové modely s dlouhými ukazateli pro data
12. Alokace a přístup k prvkům pole přesahujícího velikost jednoho segmentu
13. Alokace rozsáhlého pole s jeho inicializací
15. Pomocná makra pro práci s ukazateli se segmenty a offsety
16. Demonstrační příklad: nastavení grafického režimu 13h a vyplnění obrazovky barevným vzorkem
17. Výsledek překladu do strojového kódu
18. Překlad programové smyčky vyplňující obrazovou paměť
19. Repositář s demonstračními příklady
1. Problematika ukazatelů v překladačích jazyka C pro systém DOS
Platforma IBM PC a operační systém DOS byly v mnoha ohledech problematické, a to jak z pohledu uživatelů, tak i (a to možná ještě více) vývojářů. Samotný tandem DOS+BIOS například nabízel pouze základní služby pro práci se soubory, procesy a do jisté míry i s HW zařízeními. Ovšem například způsob mapování mezi IO porty procesoru a fyzickými porty, mapování mezi paměťovou oblastí zařízení a adresovým prostorem z pohledu procesoru nebo výběr HW přerušení, která bude zařízení používat, se řešil dalšími prostředky, které ležely mimo možnosti DOSu a BIOSu. Typicky se tyto hodnoty nastavovaly přepínači umístěnými přímo na zařízení a pokud bylo nastavení nekorektní, samotný systém většinou ani nedokázal tuto situaci detekovat (a už vůbec ne ji opravit).
Ovšem problematický byl i samotný vývoj aplikací pro IBM PC s DOSem. Jádro problému spočívalo v tom, jakým způsobem mikroprocesory řady Intel 8088/8086 pracovaly s adresami (tedy z pohledu jazyka C s ukazateli). Nejednalo se o jedinou hodnotu, ale o dvojici hodnot segment+offset, které se většinou zapisovaly stylem segment:offset. Tento koncept do značné míry komplikoval ukazatelovou aritmetiku. Kvůli tomu, že samotná nutnost explicitně pracovat se segmenty komplikovala programový kód, nabízeli výrobci překladačů různé paměťové modely, které umožňovaly (pro menší aplikace nebo pro aplikace s malými nároky na operační paměť) existenci segmentů do jisté míry ignorovat. Problémy ovšem nastaly s přenositelností nebo s přechodem na „větší“ paměťový model.
A právě touto problematikou se budeme zabývat v dnešním článku.
2. Adresování stylem segment:offset
Jak jsme si již naznačili v úvodní kapitole, používaly mikroprocesory Intel 8088/8086 adresy reprezentované dvojicí hodnot nazývaných segment a offset. Obě tyto hodnoty byly šestnáctibitové, tj. jak segment, tak i offset byly v rozsahu 0×0000 až 0×ffff. Logická adresa se přitom přímo v mikroprocesoru počítala následujícím způsobem:
adresa = segment × 16 + offset
Samozřejmě se ovšem nemuselo provádět pomalé násobení, protože postačovalo hodnotu segmentu posunout doleva o čtyři bity, takže výpočet ve skutečnosti probíhal zhruba následovně:
adresa = (segment << 4) + offset
Z pohledu programátora byl segment uložen v jednom ze segmentových registrů nazvaných CS, DS, ES a SS. V typickém programu obsahoval CS segment s programovým kódem, DS segment s daty a SS segment se zásobníkem (a nikde není řečeno, že tyto segmenty nemohou být totožné). Registr ES se většinou mohl libovolně měnit a použít například pro přístup do obrazové paměti karty CGA:
mov ax, 0xb800 mov es, ax mov di, 0 ; nyni ES:DI obsahuje adresu prvniho pixelu ve video RAM
Tento koncept má z pohledu programátora v céčku minimálně dvě nevýhody:
- Problematická aritmetika s ukazateli (už jen zvýšení ukazatele o jedničku může či nemusí fungovat)
- Problematické porovnání ukazatelů, protože i dvě rozdílné dvojice segment:offset mohou ve skutečnosti reprezentovat tutéž adresu
3. Rozsah paměti IBM PC s mikroprocesorem 8086 či 8088
Vraťme se na chvíli k prapůvodním mikroprocesorům Intel 8086/8088, se kterými byla další řada mikroprocesorů do značné míry kompatibilní. Připomeňme si, že tyto čipy měly fyzicky vyvedených pouze dvacet adresových vodičů a dokázaly tak adresovat přesně 1MB paměti (ani o bajt více – nedokázaly tedy přistoupit k HMA!). Výpočet adresy probíhal na základě kombinace segmentu a offsetu přesně takovým způsobem, jak bylo popsáno v předchozí kapitole.
Teoreticky tedy může adresa (říkejme jí logická adresa) přesáhnout přes 1MB v případě, že použijeme vysoké číslo segmentu (0×f000 a výše) i offsetu. Ovšem jak bude vypadat fyzická adresa (přenesená na adresovou sběrnici) na mikroprocesorech 8086/8088? Napoví nám obrázky jejich pinů, v nichž nalezneme i adresové vodiče:
Obrázek 1: Piny mikroprocesoru Intel 8086.
Obrázek 2: Piny mikroprocesoru Intel 8088.
U mikroprocesoru Intel 8086 nalezneme mj. i piny AD0 až AD15 a taktéž piny A16 až A19. Naproti tomu u čipu Intel 8088 se jedná o piny AD0 až AD7 následované piny A8 až A19. Žádné jiné rozdíly mezi oběma čipy přitom z externího HW pohledu neexistují. Piny začínající znaky AD se přitom používají pro adresovou i datovou sběrnici (data jsou multiplexována, což byl velmi často používaný způsob – nejprve se jedním směrem přenesla adresa a poté se provedlo čtení či zápis dat), zatímco piny začínající jen znakem A značí vodiče pouze adresové sběrnice. Co to znamená? Intel 8086 má externí datovou sběrnici se šestnácti vodiči a adresovou sběrnici s dvaceti vodiči. Naproti tomu Intel 8088 má pouze osmibitovou externí datovou sběrnici (zlevnění celého systému na úkor rychlosti), ovšem adresová sběrnice má pořád dvacet vodičů.
To ovšem v důsledku znamená, že fyzická adresa nepřesáhne rozsah jednoho megabajtu, ale „přeteče“ do nultého segmentu.
4. Rozsah adresovatelné paměti mikroprocesoru 80286
Mikroprocesor Intel 80286, který představuje druhou generaci čipů z rodiny 80×86, má oproti mikroprocesorům Intel 8086/8088 odlišně koncipované piny, což je dobře patrné z následujícího obrázku:
Obrázek 3: Piny mikroprocesoru Intel 80286.
Vidíme zde dva důležité rozdíly. Prvním rozdílem je oddělení datové sběrnice (která je mimochodem stále šestnáctibitová) od sběrnice adresové. To znamená, že jsou vyvedeny samostatné vodiče D0 až D15 pro obousměrné přenosy dat a není zapotřebí řešit multiplexing dat a adres. Pro nás je ovšem dnes důležitější fakt, že adresových vodičů je nyní více; konkrétně se jedná o piny A0 až A23. To tedy znamená, že lze adresovat 224 bajtů, protože lze opět pracovat s jednotlivými bajty a nikoli jen s celými šestnáctibitovými slovy. Fyzický paměťový rozsah mikroprocesorů Intel 80286 je tedy 16MB – o mnoho více, než tomu bylo v případě původní první generace čipů z rodiny 80×86.
Aby bylo možné využít celý paměťový rozsah, podporují mikroprocesory 80286 takzvaný chráněný režim, v němž lze navíc (jak již jeho název napovídá) na HW úrovni zajistit například to, že se nepřekročí nastavená velikost zásobníku, že se nebude (například virem) modifikovat programový kód atd. Ke konceptu chráněného režimu se ještě vrátíme. Jednalo se o důležitou technologii, která však v případě 80286 nebyla plně využívána (a to hned několika důvodů).
Co se však stane na počítači vybaveném mikroprocesorem 80286 (s adresovým rozsahem 224 bajtů) v případě, že výpočet přesáhne přes hranici jednoho megabajtu? To je totiž docela dobře možné, což naznačuje i další tabulka:
| Segment (16b) | Offset (16b) | Fyzická adresa (24b) | Poznámka |
|---|---|---|---|
| ffff | 0000 | ffff0 | jsme stále v rozsahu 1MB |
| ffff | 000f | fffff | jsme stále v rozsahu 1MB |
| f000 | ffff | fffff | jsme stále v rozsahu 1MB |
| ffff | 0010 | 100000 | první adresa, která se již nevejde do rozsahu 1MB |
| f001 | ffff | 100000 | odlišná reprezentace stejné adresy |
| ffff | ffff | 10ffef | přesáhli jsme rozsah 1MB – nejvyšší možná adresa |
Z této tabulky je patrné, že fyzická adresa skutečně může přesáhnout rozsah 1MB, což na původních čipech 8086/8088 nebylo možné, už jen z toho důvodu, že neexistovat adresový vodič A20 (tedy dvacátý první vodič, protože je indexujeme od nuly).
O kolik bajtů (či adres) přesáhneme onu mýtickou hranici jednoho megabajtu si můžeme snadno vypočítat:
0xffff0 + 0xffff - 0xfffff = 65520 bajtů/adres
K dispozici tedy máme prakticky celých 64kB navíc (kromě šestnácti bajtů na konci). Dnes se to sice může zdát jako zanedbatelná hodnota, ale právě kvůli oněm šedesáti čtyřem kilobajtů se komplikoval návrh PC i BIOSu až do cca roku 2009.
5. Může offset přesáhnout hranici jednoho segmentu?
Víme již, že výpočet adresy probíhá na základě kombinace segmentu a offsetu. Jak segment, tak i offset jsou šestnáctibitové hodnoty, které se kombinují způsobem naznačeným výše:
adresa = segment × 16 + offset
Jenže to není přesná (resp. ucelená) informace. Je nutno dodat, jakým způsobem byl vypočítán offset. Mikroprocesory řady Intel 8086/8088 totiž podporovaly tyto adresovací režimy:
| Použitý zápis v assembleru | Adresovací režim |
|---|---|
| přímá adresa (16bit) | displacement/direct |
| [BX] | register indirect |
| [SI] | register indirect |
| [DI] | register indirect |
| [BX+SI] | based indexed mode |
| [BX+DI] | based indexed mode |
| [BP+SI] | based indexed mode |
| [BP+DI] | based indexed mode |
| [BP+offset8_bit] | based mode |
| [BX+offset8_bit] | based mode |
| [SI+offset8_bit] | indexed mode |
| [DI+offset8_bit] | indexed mode |
| [BP+offset16_bit] | based mode |
| [BX+offset16_bit] | based mode |
| [SI+offset16_bit] | indexed mode |
| [DI+offset16_bit] | indexed mode |
| [BX+SI+offset8_bit] | based indexed displacement |
| [BX+DI+offset8_bit] | based indexed displacement |
| [BP+SI+offset8_bit] | based indexed displacement |
| [BP+DI+offset8_bit] | based indexed displacement |
| [BX+SI+offset16_bit] | based indexed displacement |
| [BX+DI+offset16_bit] | based indexed displacement |
| [BP+SI+offset16_bit] | based indexed displacement |
| [BP+DI+offset16_bit] | based indexed displacement |
Co to znamená v praxi? Řekněme, že v segmentovém registru DS (data segment) bude uložena nula a v registru BX bude uložena hodnota 0×8000. Následně použijeme instrukci:
mov DS:[BX+0x8001], AL
Otázka zní, do jaké fyzické buňky RAM se uloží obsah registru AL? Máme přitom k dispozici dvě varianty výpočtu. Triviální dosazení do vzorce pro výpočet adresy by vedlo k výpočtu:
0x0000 × 16 + 0x8000 + 0x8000 = 0x10000
Ovšem můžeme si také uvědomit, že sčítačka pro offsety je realizována v šestnáctibitové aritmeticko-logické jednotce a ve skutečnosti tedy proběhne spíše něco takového:
0x0000 × 16 + (0x8000 + 0x8000) & 0xffff = 0x00000
Ve skutečnosti je správný druhý výpočet, protože offset je na čipech Intel 8086/8088 vždy šestnáctibitový. To znamená, že veškeré výpočty offsetu uvedené v předchozí tabulce za všech předpokladů vedou k hodnotám v rozsahu 0×0000 až 0×ffff a nikdy tedy nepřekročíme velikost segmentu.
6. Paměťové modely překladačů jazyka C v reálném režimu
Z výše uvedených informací je zřejmé, že práce s adresami ve formátu segment:offset je poměrně problematická. A navíc existují aplikace (krátké utilitky, rezidentní programy, krátká dema – intra atd.), které se spokojí s maximálně 64kB pro programový kód a/nebo 64kB pro data. Tvůrci překladačů jazyka C se snažili podporovat i tyto méně náročné aplikace tím, že umožnili používání „krátkých ukazatelů“ (near pointers), což jsou ukazatele platné pouze v rámci jednoho segmentu. Díky tomu, že segment nebude měněn, bude veškerá ukazatelová aritmetika či porovnání ukazatelů opět korektní.
Takto vznikl koncept paměťových modelů. Původně existovaly čtyři modely:
| Model | Ukazatel na data | ||
|---|---|---|---|
| krátký | dlouhý | ||
| Ukazatel na kód |
krátký | Small | Compact |
| dlouhý | Medium | Large | |
Záleželo tedy jen na programátorovi, který z modelů si vybere. Typicky pro rezidentní programy (typu ovladač myši) postačoval model s maximálně 64kB kódu a 64kB dat. Mnohé i krátké programy potřebovaly větší prostor pro data (až do limitu 1MB) a rozsáhlé aplikace pak větší prostor pro data i pro programový kód (oba limity jsou 1MB).
Obrázek 4: Podporu pro paměťové modely je nutné explicitně nainstalovat (Borland C++ 2.0).
7. Praktická část: paměťové modely překladače Borland C++ 3.0
Všechny důležité rysy ukazatelů reprezentovaných stylem segment:offset i paměťových modelů si ukážeme na typickém dobovém představiteli céčkových překladačů. Překladač programovacího jazyka C a C++ nazvaný Borland C++ 3.0 podporoval v operačním systému DOS celkem šest různých paměťových modelů. Tyto modely jsou vypsány v následující tabulce:
| # | Paměťový model | Ukazatele | Stručný popis modelu |
|---|---|---|---|
| 1 | Tiny | krátké | 64kB pro program i data, všechny čtyři segmentové registry jsou totožné (a tedy limit 64kB se vztahuje na celek) |
| 2 | Small | krátké | 64kB programový kód, 64kB pro data |
| 3 | Medium | dlouhé pro program, krátké pro data | 1MB programový kód, 64kB pro data |
| 4 | Compact | krátké pro program, dlouhé pro data | 64kB programový kód, 1MB pro data |
| 5 | Large | dlouhé | 1MB programový kód, 1MB pro data |
| 6 | Huge | dlouhé | 1MB pro programový kód, 1MB pro data |
Paměťový model Huge jako jediný neomezuje délku paměťových struktur na 64kB. Týká se to většinou polí, které tuto délku mohou překročit – ovšem situace není zdaleka tak jednoduchá, jak by to mohlo z tohoto popisu vypadat (viz další text).
Obrázek 5: Podporu pro paměťové modely je nutné explicitně nainstalovat (Borland C++ 3.0).
8. Velikosti ukazatelů a základní aritmetika s ukazateli v praxi
Volba paměťového modelu určuje i velikost ukazatelů. Ovšem navíc měl programátor možnost velikost ukazatelů specifikovat přímo takzvaným modifikátorem. Ukažme si tuto vlastnost na jednoduchém příkladu, který si uvedeme ve třech modifikacích. Vytvoříme ukazatel obsahující hodnotu 0×ffff (tedy vlastně segment je buď nespecifikován nebo je nulový), vypíšeme jeho hodnotu a následně ji zvýšíme o jedničku:
#include <stdio.h>
int main(void) {
unsigned char *ptr = (unsigned char*)0xffff;
printf("%p\n", ptr);
ptr++;
printf("%p\n", ptr);
return 0;
}
V paměťovém modelu Tiny se pracuje jen s offsety, protože všechny segmentové registry se nemění. Proto bude ukazatel šestnáctibitový a vypíše se:
ffff 0000
Zkusme se nyní přepnout do paměťového režimu s limitem 1MB pro data. Zde je již možné používat dlouhé ukazatele, které používají modifikátor far:
#include <stdio.h>
int main(void) {
unsigned char far *ptr = (unsigned char*)0xffff;
printf("%p\n", ptr);
ptr++;
printf("%p\n", ptr);
return 0;
}
Mohlo by se zdát, že nyní zvýšení hodnoty ukazatele povede k tomu, že se změní jeho segmentová část. Ovšem není tomu tak, protože to by pro překladač znamenalo vygenerování dalších pomocných instrukcí (neexistuje instrukce typu „zvyš dvojici ES:DI“). Z hodnoty 0000:ffff se ve skutečnosti stane hodnota 0000:0000! Zkusme si to přímo v IDE:
Obrázek 6: Poněkud neslavný pokus o zvýšení hodnoty ukazatele.
Pokud je nutné pracovat s ukazateli přesahujícími hranici segmentů a současně podporujícími ukazatelovou aritmetiku, je nutné použít modifikátor huge:
#include <stdio.h>
int main(void) {
unsigned char huge *ptr = (unsigned char*)0xffff;
printf("%p\n", ptr);
ptr++;
printf("%p\n", ptr);
return 0;
}
Nyní je již vše korektní, ovšem za cenu delšího a pomalejšího programu:
Obrázek 7: Zvýšení „huge“ ukazatele o jedničku.
9. Funkce vracející větší z hodnot předaných přes ukazatel
Vliv různých paměťových modelů je nejvíce patrný u funkcí, které jako svůj argument (nebo argumenty) akceptují ukazatel a popřípadě taktéž ukazatel vrací. Proto si pro prozkoumání vlastností jednotlivých modelů naprogramujeme funkci pro nalezení větší hodnoty z dvojice, přičemž tato dvojice hodnot je předána nepřímo – přes ukazatele. A i návratovou hodnotou nebude přímo větší prvek, ale pouze ukazatel na něj. V jazyku C by implementace takové funkce mohla vypadat následovně:
#include <stdio.h>
typedef unsigned int uint;
uint* larger_value(uint *x, uint *y) {
if (*x > *y) {
return x;
} else {
return y;
}
}
Otestujeme základní funkcionalitu:
int main(void) {
uint a = 1;
uint b = 2;
printf("%d\n", *larger_value(&a, &b));
printf("%d\n", *larger_value(&b, &a));
return 0;
}
Obrázek 8: Výběr paměťového modelu pro celý projekt (Borland C++ 3.0).
10. Překlad pro paměťové modely s krátkými ukazateli pro data
Nyní se pokusme výše uvedenou funkci larger_value přeložit v paměťových modelech s krátkými ukazateli na data. Jedná se o paměťové modely Tiny, Small a Medium.
Začneme s nejjednodušším modelem Tiny. Oba ukazatele se předávají jako dvoubajtové hodnoty (přes zásobník) a návratová hodnota funkce má taktéž dva bajty. Interně se pro adresování používá segmentový registr DS, který není zapotřebí explicitně udávat. Výsledek (ukazatel) se vrací v registru AX:
;
; uint* larger_value(uint *x, uint *y) {
;
assume cs:_TEXT
_larger_value proc near
push bp
mov bp,sp
push si
push di
mov si,word ptr [bp+4]
mov di,word ptr [bp+6]
;
; if (*x > *y) {
;
mov ax,word ptr [si]
cmp ax,word ptr [di]
jbe short @1@86
;
; return x;
;
mov ax,si
pop di
pop si
pop bp
ret
@1@86:
;
; } else {
; return y;
;
mov ax,di
;
; }
; }
;
pop di
pop si
pop bp
ret
_larger_value endp
V paměťovém modelu Small se vygeneruje naprosto totožný kód. Sice platí, že CS!=DS, to ovšem nijak nezmění způsob překladu funkce, protože CS se použije jen instrukcí RET a DS naopak pro všechny přístupy do paměti:
;
; uint* larger_value(uint *x, uint *y) {
;
assume cs:_TEXT
_larger_value proc near
push bp
mov bp,sp
push si
push di
mov si,word ptr [bp+4]
mov di,word ptr [bp+6]
;
; if (*x > *y) {
;
mov ax,word ptr [si]
cmp ax,word ptr [di]
jbe short @1@86
;
; return x;
;
mov ax,si
pop di
pop si
pop bp
ret
@1@86:
;
; } else {
; return y;
;
mov ax,di
;
; }
; }
;
pop di
pop si
pop bp
ret
_larger_value endp
Paměťový model Medium vyžaduje, aby adresa subrutiny byla uložena ve dvojici segment:offset. To znamená, že se na zásobníku posunou adresy obou argumentů (což je patrné, nyní se používá BP+6 a BP+8 a nikoli BP+4 a BP+6), ovšem to je jediná změna. Zbytek kódu zůstane stejný, i když jeho volání bude nepatrně pomalejší:
;
; uint* larger_value(uint *x, uint *y) {
;
assume cs:LARGER_TEXT
_larger_value proc far
push bp
mov bp,sp
push si
push di
mov si,word ptr [bp+6]
mov di,word ptr [bp+8]
;
; if (*x > *y) {
;
mov ax,word ptr [si]
cmp ax,word ptr [di]
jbe short @1@86
;
; return x;
;
mov ax,si
pop di
pop si
pop bp
ret
@1@86:
;
; } else {
; return y;
;
mov ax,di
;
; }
; }
;
pop di
pop si
pop bp
ret
_larger_value endp
Všechny čtyři varianty si můžeme porovnat instrukci po instrukci. Kód v prvních dvou sloupcích je totožný:
; TINY ; SMALL ; MEDIUM
_larger_value proc near _larger_value proc near _larger_value proc far
push bp push bp push bp
mov bp,sp mov bp,sp mov bp,sp
push si push si push si
push di push di push di
mov si,word ptr [bp+4] mov si,word ptr [bp+4] mov si,word ptr [bp+6]
mov di,word ptr [bp+6] mov di,word ptr [bp+6] mov di,word ptr [bp+8]
mov ax,word ptr [si] mov ax,word ptr [si] mov ax,word ptr [si]
cmp ax,word ptr [di] cmp ax,word ptr [di] cmp ax,word ptr [di]
jbe short @1@86 jbe short @1@86 jbe short @1@86
mov ax,si mov ax,si mov ax,si
pop di pop di pop di
pop si pop si pop si
pop bp pop bp pop bp
ret ret ret
@1@86: @1@86: @1@86:
mov ax,di mov ax,di mov ax,di
pop di pop di pop di
pop si pop si pop si
pop bp pop bp pop bp
ret ret ret
_larger_value endp _larger_value endp _larger_value endp
11. Překlad pro paměťové modely s dlouhými ukazateli pro data
Poněkud odlišným způsobem dopadne překlad stejné funkce pro paměťové modely, ve kterých se data mohou nacházet v libovolném segmentu (tedy v rozsahu jednoho megabajtu). Jedná se o modely Compact, Large a Huge.
Nejdříve se podívejme na překlad pro model Compact, ve kterém je velikost programového kódu omezena na 64kB. Povšimněte si, že nyní jsou ukazatele předány každý ve čtyřech bajtech. Segment ukazatelů se přenese do registru ES instrukcí LES a následně se porovnávané hodnoty přečtou přes „plnou“ adresu ES:BX. Výsledná adresa (4 bajty) je vrácena ve dvojici registrů DX a AX:
;
; uint* larger_value(uint *x, uint *y) {
;
assume cs:_TEXT
_larger_value proc near
push bp
mov bp,sp
;
; if (*x > *y) {
;
les bx,dword ptr [bp+4]
mov ax,word ptr es:[bx]
les bx,dword ptr [bp+8]
cmp ax,word ptr es:[bx]
jbe short @1@86
;
; return x;
;
mov dx,word ptr [bp+6]
mov ax,word ptr [bp+4]
pop bp
ret
@1@86:
;
; } else {
; return y;
;
mov dx,word ptr [bp+10]
mov ax,word ptr [bp+8]
;
; }
; }
;
pop bp
ret
_larger_value endp
V paměťovém modelu Large je provedena jedna změna – návratová adresa subrutiny je čtyřbajtová (segment:offset), takže jsou posunuty relativní adresy argumentů. Ostatní kód ovšem zůstává beze změny:
;
; uint* larger_value(uint *x, uint *y) {
;
assume cs:LARGER_TEXT
_larger_value proc far
push bp
mov bp,sp
;
; if (*x > *y) {
;
les bx,dword ptr [bp+6]
mov ax,word ptr es:[bx]
les bx,dword ptr [bp+10]
cmp ax,word ptr es:[bx]
jbe short @1@86
;
; return x;
;
mov dx,word ptr [bp+8]
mov ax,word ptr [bp+6]
pop bp
ret
@1@86:
;
; } else {
; return y;
;
mov dx,word ptr [bp+12]
mov ax,word ptr [bp+10]
;
; }
; }
;
pop bp
ret
_larger_value endp
V paměťovém modelu Huge se navíc na zásobníkový rámec ukládá i obsah registru DS, protože ten se může uvnitř subrutiny změnit (a taktéž mění). Opět platí, že zbytek kódu zůstává prakticky beze změny:
;
; uint* larger_value(uint *x, uint *y) {
;
assume cs:LARGER_TEXT
_larger_value proc far
push bp
mov bp,sp
push ds
mov ax,LARGER_DATA
mov ds,ax
;
; if (*x > *y) {
;
les bx,dword ptr [bp+6]
mov ax,word ptr es:[bx]
les bx,dword ptr [bp+10]
cmp ax,word ptr es:[bx]
jbe short @1@86
;
; return x;
;
mov dx,word ptr [bp+8]
mov ax,word ptr [bp+6]
pop ds
pop bp
ret
@1@86:
;
; } else {
; return y;
;
mov dx,word ptr [bp+12]
mov ax,word ptr [bp+10]
;
; }
; }
;
pop ds
pop bp
ret
_larger_value endp
Opět si nyní porovnejme výsledky získané překladem pro různé paměťové modely. Základ zůstává stále stejný – přístup k hodnotám přes dlouhé ukazatele segment:offset, ovšem odlišuje se práce s návratovou adresou ze subrutiny a v případě paměťového modelu Huge i úschova a obnova registru DS:
; COMPACT ; LARGE ; HUGE
_larger_value proc near _larger_value proc far _larger_value proc far
push bp push bp push bp
mov bp,sp mov bp,sp mov bp,sp
push ds
mov ax,LARGER_DATA
mov ds,ax
les bx,dword ptr [bp+4] les bx,dword ptr [bp+6] les bx,dword ptr [bp+6]
mov ax,word ptr es:[bx] mov ax,word ptr es:[bx] mov ax,word ptr es:[bx]
les bx,dword ptr [bp+8] les bx,dword ptr [bp+10] les bx,dword ptr [bp+10]
cmp ax,word ptr es:[bx] cmp ax,word ptr es:[bx] cmp ax,word ptr es:[bx]
jbe short @1@86 jbe short @1@86 jbe short @1@86
mov dx,word ptr [bp+6] mov dx,word ptr [bp+8] mov dx,word ptr [bp+8]
mov ax,word ptr [bp+4] mov ax,word ptr [bp+6] mov ax,word ptr [bp+6]
pop ds
pop bp pop bp pop bp
ret ret ret
@1@86: @1@86: @1@86:
mov dx,word ptr [bp+10] mov dx,word ptr [bp+12] mov dx,word ptr [bp+12]
mov ax,word ptr [bp+8] mov ax,word ptr [bp+10] mov ax,word ptr [bp+10]
pop ds
pop bp pop bp pop bp
ret ret ret
_larger_value endp _larger_value endp _larger_value endp
12. Alokace a přístup k prvkům pole přesahujícího velikost jednoho segmentu
Z předchozího textu vyplývá, že je obecně poměrně problematické vytvářet pole (nebo další paměťové struktury), jejichž velikost přesahuje hranici jednoho segmentu. Překladač Borland C/C++ ovšem dokáže provádět alokaci větších struktur, a to ve všech paměťových modelech kromě modelu Tiny. Pro tento účel slouží funkce nazvaná farmalloc, která se podobá funkci malloc, ovšem s tím rozdílem, že akceptuje velikost zapsanou hodnotou typu unsigned long a nikoli pouze unsigned int (povšimněte si, že v ANSI/ISO C je to již odlišné – tam máme ke stejnému účelu předepsán specializovaný typ size_t – jakékoli přenosy takových programů na jiné systémy jsou tedy minimálně problematické). V každém případě vrací funkce farmalloc ukazatel typu void far *, tedy „dlouhý“ ukazatel typu segment:offset. A právě ten využijeme v dalším demonstračním příkladu.
13. Alokace rozsáhlého pole s jeho inicializací
Alokace pole, jehož velikost přesahuje hranici jednoho segmentu, může vypadat následovně. Zavoláme funkci farmalloc a následně pole v programové smyčce vyplníme nulami:
#include <stdio.h>
int main(void) {
#define LENGTH 100000
long i;
unsigned char far *array = (unsigned char far*)farmalloc(LENGTH);
printf("%p\n", array);
for (i=0; i<LENGTH; i++) {
array[i] = 0;
}
return 0;
}
14. Výsledek překladu
Opět se podívejme na způsob překladu výše uvedeného demonstračního příkladu do strojového kódu. V první části je zavolána funkce nazvaná farmalloc pro alokaci paměti a získání „dlouhého“ ukazatele na nový paměťový blok. V registrech AX:DX je funkci farmalloc předána vyžadovaná kapacita (65536+34464=100000):
_TEXT segment byte public 'CODE'
;
; int main(void) {
;
assume cs:_TEXT
_main proc near
push bp
mov bp,sp
sub sp,8
;
; #define LENGTH 100000
; long i;
; unsigned char *array = (unsigned char*)farmalloc(LENGTH);
;
mov ax,1
mov dx,34464
push ax
push dx
call near ptr _farmalloc
pop cx
pop cx
Dále se získaný ukazatel uloží na zásobník do místa pro lokální proměnnou.
cwd
mov word ptr [bp-6],dx
mov word ptr [bp-8],ax
Následuje inicializace pole, což je programová smyčka, která vlastně vůbec není optimalizována a při každé iteraci se hned několikrát musí přistoupit do operační paměti. Povšimněte si způsobu zvýšení ukazatele přes dvojici instrukcí ADD+ADC (nižší slovo, vyšší slovo):
;
; for (i=0; i<LENGTH; i++) {
;
mov word ptr [bp-2],0
mov word ptr [bp-4],0
jmp short @1@114
@1@58:
;
; array[i] = 0;
;
les bx,dword ptr [bp-8]
add bx,word ptr [bp-4]
mov byte ptr es:[bx],0
add word ptr [bp-4],1
adc word ptr [bp-2],0
@1@114:
cmp word ptr [bp-2],1
jl short @1@58
jne short @1@198
cmp word ptr [bp-4],34464
jb short @1@58
@1@198:
;
; }
; return 0;
;
xor ax,ax
jmp short @1@226
@1@226:
;
; }
;
mov sp,bp
pop bp
ret
15. Pomocná makra pro práci s ukazateli se segmenty a offsety
Překladače Borland C/C++ mj. obsahují i hlavičkový soubor dos.h, ve kterém kromě funkcí, které jsou specifické pro operační systém DOS, nalezneme i trojici maker určených pro zpracování „dlouhých“ ukazatelů. Jedno z maker lze použít pro konstrukci takového ukazatele z předaného segmentu a offsetu, další dvě makra naopak slouží pro získání segmentové či offsetové části ukazatele:
| # | Makro | Stručný popis makra |
|---|---|---|
| 1 | MK_FP | sestaví ukazatel z předaného segmentu a offsetu |
| 2 | FP_SEG | získá z ukazatele pouze segmentovou část |
| 3 | FP_OFF | získá z ukazatele pouze offsetovou část |
Zajímavé je, že jsou tato makra interně založena na konverzi ukazatelů s využitím modifikátorů _seg a near. Jejich definice totiž vypadají následovně:
#define MK_FP( seg,ofs )( (void _seg * )( seg ) +( void near * )( ofs )) #define FP_SEG( fp )( (unsigned )( void _seg * )( void far * )( fp )) #define FP_OFF( fp )( (unsigned )( fp ))
16. Demonstrační příklad: nastavení grafického režimu 13h a vyplnění obrazovky barevným vzorkem
V dnešním posledním demonstračním příkladu spojíme všechny znalosti získané v předchozích kapitolách. Vytvoříme jednoduché (nutno říci, že velmi triviální) demo, které nejdříve provede přepnutí do grafického režimu standardní grafické karty VGA s rozlišením 320×200 pixelů a s 256 barvami (tento režim ostatně velmi dobře známe). Následně vyplníme obrazovku barevným vzorkem, přičemž pro jednoduchost nebudeme modifikovat barvovou paletu. Demo by mělo vytvořit tento obrázek:
Obrázek 9: Takto by měl vypadat výsledek po spuštění dema v DOSu nebo jeho emulátoru.
Obrazová paměť v grafických režimech karty VGA začíná na adrese A000:0000, takže si připravíme „dlouhý“ ukazatel s touto adresou. Použijeme přitom makro MK_FP:
unsigned char far *ptr = (unsigned char*)MK_FP(0xa000, 0000);
printf("%p\n", ptr);
Přepnutí do grafického režimu bude ve skutečnosti triviální. Postačuje nám totiž zavolat službu BIOSu číslo 10h, v registru AH předat číslo služby (což je v tomto případě služba číslo 0) a v registru AL předat číslo grafického režimu. Překladače Borland C/C++ podporují vložení kódu v assembleru, takže se přepnutí režimu realizuje následovně:
asm {
mov ah, 00h
mov al, 13h
int 10h
}
Poslední částí dema je vyplnění obrazové paměti, tj. 320×200=64000 pixelů, barvovým vzorkem:
for (i=0; i<(unsigned)(320*200); i++) {
*ptr++ = i;
}
Celý zdrojový kód dema bude vypadat následovně:
#include <dos.h>
#include <stdio.h>
int main(void) {
unsigned char far *ptr = (unsigned char*)MK_FP(0xa000, 0000);
unsigned int i;
printf("%p\n", ptr);
getch();
asm {
mov ah, 00h
mov al, 13h
int 10h
}
for (i=0; i<(unsigned)(320*200); i++) {
*ptr++ = i;
}
getch();
return 0;
}
17. Výsledek překladu do strojového kódu
Zajímavé bude zjistit, jak vlastně dopadl překlad dema do strojového kódu. Na začátku se nastavují lokální proměnné, zejména pak ukazatel uložený na adresách [BP-2] a [BP-4]:
;
; int main(void) {
;
assume cs:MODE13H_TEXT
_main proc far
push bp
mov bp,sp
sub sp,6
;
; unsigned char far *ptr = (unsigned char*)MK_FP(0xa000, 0000);
;
mov word ptr [bp-2],40960
mov word ptr [bp-4],0
Následuje výpis ukazatele (nezajímavé) i volání funkce getch() čekající na stisk klávesy:
;
; unsigned int i;
; printf("%p\n", ptr);
;
push word ptr [bp-2]
push word ptr [bp-4]
push ds
mov ax,offset DGROUP:s@
push ax
call far ptr _printf
add sp,8
;
; getch();
;
call far ptr _getch
Část zapsaná v assembleru se přeložila přesně tak, jak jsme ji zapsali (žádné optimalizace se zde ani neočekávaly):
mov ah, 00h
mov al, 13h
int 10h
18. Překlad programové smyčky vyplňující obrazovou paměť
A konečně se podívejme na způsob překladu programové smyčky, která vyplní celou obrazovou paměť barvovým vzorkem. V jazyku C byla tato smyčka napsána triviálním způsobem, takže by se mohlo zdát, že její překlad bude proveden optimálně. Ovšem i při zapnutí všech dostupných optimalizací bude výsledek velmi pomalý a neoptimální:
; for (i=0; i<(unsigned)(320*200); i++) {
;
mov word ptr [bp-6],0
@1@170:
;
; *ptr++ = i;
;
les bx,dword ptr [bp-4]
mov al,byte ptr [bp-6]
mov byte ptr es:[bx],al
inc word ptr [bp-4]
inc word ptr [bp-6]
cmp word ptr [bp-6],64000
jb short @1@170
; }
19. Repositář s demonstračními příklady
Demonstrační příklady napsané v jazyce C, které jsou primárně určené pro překlad s využitím překladačů Turbo C a (Open)Watcom C), byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/8bit-fame. Jednotlivé demonstrační příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:
Borland C:
(Open)Watcom pro platformu IBM PC:
GCC pro platformu x86–64:
DGJPP pro platformu IBM PC + DOS:
20. Odkazy na Internetu
- DJGPP (Wikipedia)
https://cs.wikipedia.org/wiki/DJGPP - DJGPP home page
http://www.delorie.com/djgpp/ - DJGPP Zip File Picker
http://www.delorie.com/djgpp/zip-picker.html - The Intel 8088 Architecture and Instruction Set
https://people.ece.ubc.ca/~edc/464/lectures/lec4.pdf - x86 Opcode Structure and Instruction Overview
https://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf - x86 instruction listings (Wikipedia)
https://en.wikipedia.org/wiki/X86_instruction_listings - x86 assembly language (Wikipedia)
https://en.wikipedia.org/wiki/X86_assembly_language - Intel Assembler (Cheat sheet)
http://www.jegerlehner.ch/intel/IntelCodeTable.pdf - 25 Microchips That Shook the World
https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world - Chip Hall of Fame: MOS Technology 6502 Microprocessor
https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor - Chip Hall of Fame: Intel 8088 Microprocessor
https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-intel-8088-microprocessor - Jak se zrodil procesor?
https://www.root.cz/clanky/jak-se-zrodil-procesor/ - Apple II History Home
http://apple2history.org/ - The 8086/8088 Primer
https://www.stevemorse.org/8086/index.html - flat assembler: Assembly language resources
https://flatassembler.net/ - FASM na Wikipedii
https://en.wikipedia.org/wiki/FASM - Fresh IDE FASM inside
https://fresh.flatassembler.net/ - MS-DOS Version 4.0 Programmer's Reference
https://www.pcjs.org/documents/books/mspl13/msdos/dosref40/ - DOS API (Wikipedia)
https://en.wikipedia.org/wiki/DOS_API - Bit banging
https://en.wikipedia.org/wiki/Bit_banging - IBM Basic assembly language and successors (Wikipedia)
https://en.wikipedia.org/wiki/IBM_Basic_assembly_language_and_successors - X86 Assembly/Bootloaders
https://en.wikibooks.org/wiki/X86_Assembly/Bootloaders - Počátky grafiky na PC: grafické karty CGA a Hercules
https://www.root.cz/clanky/pocatky-grafiky-na-pc-graficke-karty-cga-a-hercules/ - Co mají společného Commodore PET/4000, BBC Micro, Amstrad CPC i grafické karty MDA, CGA a Hercules?
https://www.root.cz/clanky/co-maji-spolecneho-commodore-pet-4000-bbc-micro-amstrad-cpc-i-graficke-karty-mda-cga-a-hercules/ - Karta EGA: první použitelná barevná grafika na PC
https://www.root.cz/clanky/karta-ega-prvni-pouzitelna-barevna-grafika-na-pc/ - RGB Classic Games
https://www.classicdosgames.com/ - Turbo Assembler (Wikipedia)
https://en.wikipedia.org/wiki/Turbo_Assembler - Microsoft Macro Assembler
https://en.wikipedia.org/wiki/Microsoft_Macro_Assembler - IBM Personal Computer (Wikipedia)
https://en.wikipedia.org/wiki/IBM_Personal_Computer - Intel 8251
https://en.wikipedia.org/wiki/Intel_8251 - Intel 8253
https://en.wikipedia.org/wiki/Intel_8253 - Intel 8255
https://en.wikipedia.org/wiki/Intel_8255 - Intel 8257
https://en.wikipedia.org/wiki/Intel_8257 - Intel 8259
https://en.wikipedia.org/wiki/Intel_8259 - Support/peripheral/other chips – 6800 family
http://www.cpu-world.com/Support/6800.html - Motorola 6845
http://en.wikipedia.org/wiki/Motorola_6845 - The 6845 Cathode Ray Tube Controller (CRTC)
http://www.tinyvga.com/6845 - CRTC operation
http://www.6502.org/users/andre/hwinfo/crtc/crtc.html - The 6845 Cathode Ray Tube Controller (CRTC)
http://www.tinyvga.com/6845 - Motorola 6845 and bitwise graphics
https://retrocomputing.stackexchange.com/questions/10996/motorola-6845-and-bitwise-graphics - IBM Monochrome Display Adapter
http://en.wikipedia.org/wiki/Monochrome_Display_Adapter - Color Graphics Adapter
http://en.wikipedia.org/wiki/Color_Graphics_Adapter - Color Graphics Adapter and the Brown color in IBM 5153 Color Display
https://www.aceinnova.com/en/electronics/cga-and-the-brown-color-in-ibm-5153-color-display/ - The Modern Retrocomputer: An Arduino Driven 6845 CRT Controller
https://hackaday.com/2017/05/14/the-modern-retrocomputer-an-arduino-driven-6845-crt-controller/ - flat assembler: Assembly language resources
https://flatassembler.net/ - FASM na Wikipedii
https://en.wikipedia.org/wiki/FASM - Fresh IDE FASM inside
https://fresh.flatassembler.net/ - MS-DOS Version 4.0 Programmer's Reference
https://www.pcjs.org/documents/books/mspl13/msdos/dosref40/ - DOS API (Wikipedia)
https://en.wikipedia.org/wiki/DOS_API - IBM Basic assembly language and successors (Wikipedia)
https://en.wikipedia.org/wiki/IBM_Basic_assembly_language_and_successors - X86 Assembly/Arithmetic
https://en.wikibooks.org/wiki/X86_Assembly/Arithmetic - Art of Assembly – Arithmetic Instructions
http://oopweb.com/Assembly/Documents/ArtOfAssembly/Volume/Chapter6/CH06–2.html - ASM Flags
http://www.cavestory.org/guides/csasm/guide/asm_flags.html - Status Register
https://en.wikipedia.org/wiki/Status_register - Is it worthwhile to learn x86 assembly language today?
https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1 - Why Learn Assembly Language?
http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language - Is Assembly still relevant?
http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant - Why Learning Assembly Language Is Still a Good Idea
http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html - Assembly language today
http://beust.com/weblog/2004/06/23/assembly-language-today/ - Assembler: Význam assembleru dnes
http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz - Programming from the Ground Up Book – Summary
http://savannah.nongnu.org/projects/pgubook/ - DOSBox
https://www.dosbox.com/ - The C Programming Language
https://en.wikipedia.org/wiki/The_C_Programming_Language - Hercules Graphics Card (HCG)
https://en.wikipedia.org/wiki/Hercules_Graphics_Card - Complete 8086 instruction set
https://content.ctcd.edu/courses/cosc2325/m22/docs/emu8086ins.pdf - Complete 8086 instruction set
https://yassinebridi.github.io/asm-docs/8086_instruction_set.html - 8088 MPH by Hornet + CRTC + DESiRE (final version)
https://www.youtube.com/watch?v=hNRO7lno_DM - Area 5150 by CRTC & Hornet (Party Version) / IBM PC+CGA Demo, Hardware Capture
https://www.youtube.com/watch?v=fWDxdoRTZPc - 80×86 Integer Instruction Set Timings (8088 – Pentium)
http://aturing.umcs.maine.edu/~meadow/courses/cos335/80×86-Integer-Instruction-Set-Clocks.pdf - Colour Graphics Adapter: Notes
https://www.seasip.info/VintagePC/cga.html - Restoring A Vintage CGA Card With Homebrew HASL
https://hackaday.com/2024/06/12/restoring-a-vintage-cga-card-with-homebrew-hasl/ - Demoing An 8088
https://hackaday.com/2015/04/10/demoing-an-8088/ - Warnings Are Your Friend – A Code Quality Primer
https://hackaday.com/2018/11/06/warnings-are-your-friend-a-code-quality-primer/ - Defending Against Compiler-Based Backdoors
https://blog.regehr.org/archives/1241 - Reflections on Trusting Trust
https://www.win.tue.nl/~aeb/linux/hh/thompson/trust.html - Coding Machines (povídka)
https://www.teamten.com/lawrence/writings/coding-machines/ - Stage0
https://bootstrapping.miraheze.org/wiki/Stage0 - Projekt stage0 na GitHubu
https://github.com/oriansj/stage0 - Bootstraping wiki
https://bootstrapping.miraheze.org/wiki/Main_Page - Bootstrapped 6502 Assembler
https://github.com/robinluckey/bootstrap-6502 - IBM Basic assembly language and successors (Wikipedia)
https://en.wikipedia.org/wiki/IBM_Basic_assembly_language_and_successors - X86 Assembly/Bootloaders
https://en.wikibooks.org/wiki/X86_Assembly/Bootloaders - What is a coder's worst nightmare?
https://www.quora.com/What-is-a-coders-worst-nightmare/answer/Mick-Stute - Tiny C Compiler
https://bellard.org/tcc/ - Welcome to C–
https://www.cs.tufts.edu/~nr/c--/index.html - c4 – C in four functions
https://github.com/rswier/c4 - Tiobe index
https://www.tiobe.com/tiobe-index/ - Lattice C (Wikipedia)
https://en.wikipedia.org/wiki/Lattice_C - Aztec C (Wikipedia)
https://en.wikipedia.org/wiki/Aztec_C - Digital Mars (Wikipedia)
https://en.wikipedia.org/wiki/Digital_Mars - Stránky projektu Open Watcom
https://openwatcom.org/ - Repositář Open Watcom
https://github.com/open-watcom/open-watcom-v2 - Watcom C/C++ (Wikipedia)
https://en.wikipedia.org/wiki/Watcom_C/C%2B%2B - Turbo C (Wikipedia)
https://en.wikipedia.org/wiki/Turbo_C - Borland C++ (Wikipedia)
https://en.wikipedia.org/wiki/Borland_C%2B%2B