Obsah
1. Překladače na platformě IBM PC: od assembleru k C (32bitový kód)
2. Vyplnění pole zadanou hodnotou
3. Výsledky vygenerované překladačem Borland C++
4. Výsledky vygenerované překladačem Watcom C/C++
5. Porovnání s moderními instrukčními sadami a moderními překladači
6. Překladače podporující 32bitovou instrukční sadu
7. Šestnáctibitové a 32bitovové instrukce v reálném a chráněném režimu
10. Překlad testovacích příkladů pomocí DJGPP
11. Funkce pro součet dvou celých čísel
12. Funkce pro vyplnění paměťové oblasti
13. Funkce pro nalezení největší hodnoty v poli
14. Funkce pro vyplnění pole zadanou hodnotou
15. Podpora operací s plovoucí řádovou čárkou v céčkových překladačích
16. Funkce pro součet dvou hodnot typu float
17. Výsledek překladu pomocí Borland C++
18. Výsledek překladu pomocí Watcom C/C++ a DJGPP
19. Repositář s demonstračními příklady
1. Překladače na platformě IBM PC: od assembleru k C (32bitový kód)
Na úvodní článek o vyšších programovacích jazycích používaných v době kralování platformy IBM PC a operačního systému DOSu dnes navážeme. Zatímco minule jsme se zaměřili na šestnáctibitový kód, jehož struktura musela odpovídat šestnáctibitovým mikroprocesorům původní řady 8086 (a také 80286), dnes (alespoň prozatím) přeskočíme jednu trnitou kapitolu vývoje (snahu o rozumné možnosti adresace celé dostupné paměti v reálném režimu) a zaměříme se na ty překladače jazyka C, které dokázaly generovat 32bitový kód kompatibilní s mikroprocesory 80386 a 80486. Ovšem nejenom to – tyto překladače obsahovaly podporu pro takzvané DOS extendery umožňující běh programů v chráněném režimu, ve kterém již neplatí omezení režimu reálného. Právě tyto překladače umožnili vývoj rozsáhlejších aplikací, samozřejmě včetně her.
2. Vyplnění pole zadanou hodnotou
Ještě se ovšem alespoň na chvíli zastavme u překladu zdrojových kódů z jazyka C do šestnáctibitové instrukční sady kompatibilní s původními mikroprocesory Intel 8086 (resp. Intel 8088). Poměrně dlouhou dobu se tradovalo, že překladačům jazyka C je v mnoha případech nutné „dopomoci“ v případě, že se přistupuje k prvkům polí. Namísto přístupu k prvkům polí přes index bylo doporučováno přímé využití ukazatelů, což překladačům mělo umožnit zjednodušené výpočty adres (ještě před programovou smyčkou atd.). Ostatně jeden takový příklad jsme si ukazovali minule. Jednalo se o realizaci funkce memset, jejíž zdrojový kód používá adresaci přes ukazatel:
void * memset(void *dest, register int val, register size_t len) { register unsigned char *ptr = (unsigned char*)dest; while (len-- > 0) *ptr++ = val; return dest; }
V dalších kapitolách se pokusíme zjistit, jak dobře či naopak špatně dokážou překladače optimalizovat funkci určenou pro vyplnění celého pole zadanou hodnotou. Tato činnost do značné míry odpovídá funkci memset zmíněné výše. Nyní ovšem budeme k prvkům polí přistupovat důsledně přes index, tj. bez využití ukazatelů:
void fill_array(int *array, int size, int value) { int i; for (i=0; i<size; i++) { array[i] = value; } }
3. Výsledky vygenerované překladačem Borland C++
Nejprve se podívejme na strojový kód vygenerovaný překladačem Borland C++ 2.0, s nímž jsme se již setkali minule. Bude se jednat o čistě šestnáctibitový kód, což znamená, že datový typ int bude mít šířku dvou bajtů, což mj. omezuje maximální velikost pole. To pochopitelně ovlivní způsob adresování prvků v poli, které je na šestnáctibitové architektuře z tohoto pohledu jednoduché:
_fill_array proc near push bp mov bp,sp push si ; ; int i; ; for (i=0; i<size; i++) { ; xor si,si jmp short @1@98 @1@50: ; ; array[i] = value; ; mov ax,si shl ax,1 mov bx,word ptr [bp+4] add bx,ax mov ax,word ptr [bp+8] mov word ptr [bx],ax inc si @1@98: cmp si,word ptr [bp+6] jl short @1@50 ; ; } ; } ; pop si pop bp ret _fill_array endp
Povšimněte si především toho, že překladač sice použil index registr SI pro úschovu adresy, ovšem jeho zvýšení o hodnotu 2 (16bitů) je řešeno přes akumulátor AX a vlastní přístup k prvku zajišťuje registr BX (vždy se do něj znovu načte bázová adresa pole). To vlastně znamená, že jedna operace je řešena s využitím tří registrů:
mov ax,si shl ax,1 mov bx,word ptr [bp+4] add bx,ax ... mov word ptr [bx],ax inc si
Dva přístupy do paměti jsou zbytečné. Zejména naplnění registru BX bázovou adresou pole by mohlo být vyřešeno mimo vlastní programovou smyčku – kód je mj. i v tomto ohledu dosti neoptimální. V tomto případě je tedy do určité míry pravda, že překladač přístup do pole neoptimalizuje (což jsme všem ani nevyžadovali).
Překladač Borland C++ dokáže provádět i malé optimalizace, což se projeví i na způsobu zápisu prvků do pole:
_fill_array proc near push bp mov bp,sp push si push di mov di,word ptr [bp+4] mov dx,word ptr [bp+6] mov cx,word ptr [bp+8] ; ; int i; ; for (i=0; i<size; i++) { ; xor si,si jmp short @1@98 @1@50: ; ; array[i] = value; ; mov bx,si shl bx,1 mov ax,cx mov word ptr [bx+di],ax inc si @1@98: cmp si,dx jl short @1@50 ; ; } ; } ; pop di pop si pop bp ret _fill_array endp
V tomto případě je zápis do pole kratší. Pro adresaci je opět použit registr BX společně s bází uloženou v registru DI (změna oproti předchozímu postupu):
mov bx,si shl bx,1 mov ax,cx mov word ptr [bx+di],ax inc si
4. Výsledky vygenerované překladačem Watcom C/C++
Druhým překladačem programovacího jazyka C, jehož možnosti otestujeme, je – podobně jako minule – Watcom C/C++. Ten by měl umět vygenerovat lepší (přesněji řečeno rychlejší) kód, než Borland C++. Podívejme se nejdříve na způsob překladu funkce fill_array při vypnutí všech optimalizací:
Module: C:\fill_a.c GROUP: 'DGROUP' CONST,CONST2,_DATA Segment: _TEXT BYTE USE16 00000022 bytes 0000 fill_array_: 0000 50 push ax 0001 B8 06 00 mov ax,0x0006 0004 E8 00 00 call __STK 0007 58 pop ax 0008 51 push cx 0009 56 push si 000A 89 C6 mov si,ax 000C 89 D9 mov cx,bx 000E 31 C0 xor ax,ax 0010 L$1: 0010 39 D0 cmp ax,dx 0012 7D 0B jge L$2 0014 89 C3 mov bx,ax 0016 D1 E3 shl bx,0x01 0018 01 F3 add bx,si 001A 89 0F mov word ptr [bx],cx 001C 40 inc ax 001D EB F1 jmp L$1 001F L$2: 001F 5E pop si 0020 59 pop cx 0021 C3 ret Routine Size: 34 bytes, Routine Base: _TEXT + 0000 No disassembly errors Segment: CONST WORD USE16 00000000 bytes Segment: CONST2 WORD USE16 00000000 bytes Segment: _DATA WORD USE16 00000000 bytes
Instrukce realizující interní počítanou smyčku sice nejsou naprogramovány ideálně (dokážeme napsat lepší kód), ovšem provádí se zde pouze jediný přístup do operační paměti, což je oproti překladači Borland C++ lepší řešení. Stále se však pro adresaci prvků pole používá zbytečně velké množství pracovních registrů, což je patrné ve chvíli, kdy z výsledného kódu vykopírujeme pouze vnitřní programovou smyčku:
L$1: cmp ax,dx jge L$2 mov bx,ax shl bx,0x01 add bx,si mov word ptr [bx],cx inc ax jmp L$1
Následuje výpis kódu vytvořeného týmž překladačem, ovšem po zapnutí vybraných optimalizací. Celková délka kódu zůstává shodná, ovšem jednotlivé instrukce jsou v tomto případě odlišné:
Module: C:\fill_a.c GROUP: 'DGROUP' CONST,CONST2,_DATA Segment: _TEXT BYTE USE16 00000022 bytes 0000 fill_array_: 0000 50 push ax 0001 B8 06 00 mov ax,0x0006 0004 E8 00 00 call __STK 0007 58 pop ax 0008 51 push cx 0009 56 push si 000A 89 C6 mov si,ax 000C 89 D9 mov cx,bx 000E 31 C0 xor ax,ax 0010 L$1: 0010 39 D0 cmp ax,dx 0012 7D 0B jge L$2 0014 89 C3 mov bx,ax 0016 01 C3 add bx,ax 0018 01 F3 add bx,si 001A 89 0F mov word ptr [bx],cx 001C 40 inc ax 001D EB F1 jmp L$1 001F L$2: 001F 5E pop si 0020 59 pop cx 0021 C3 ret Routine Size: 34 bytes, Routine Base: _TEXT + 0000 No disassembly errors Segment: CONST WORD USE16 00000000 bytes Segment: CONST2 WORD USE16 00000000 bytes Segment: _DATA WORD USE16 00000000 bytes
V tomto případě se liší způsob výpočtu adresy prvku pole – pro tento účel se nyní používá pracovní registr BX, zatímco akumulátor AX vystupuje v roli počitadla smyčky a současně i registru pro uložení indexu. Bázová adresa pole se nachází v index registru SI a kupodivu se báze+offset sčítá explicitně a nikoli s využitím adresovacího režimu:
L$1: cmp ax,dx jge L$2 mov bx,ax add bx,ax add bx,si mov word ptr [bx],cx inc ax jmp L$1
Zapnutí maximálních optimalizací vede k ještě zajímavějšímu kódu, jenž vypadá takto:
Module: C:\fill_a.c GROUP: 'DGROUP' CONST,CONST2,_DATA Segment: _TEXT BYTE USE16 0000001B bytes 0000 fill_array_: 0000 51 push cx 0001 56 push si 0002 89 C6 mov si,ax 0004 89 D1 mov cx,dx 0006 89 DA mov dx,bx 0008 89 F3 mov bx,si 000A 31 C0 xor ax,ax 000C L$1: 000C 39 C8 cmp ax,cx 000E 7D 08 jge L$2 0010 40 inc ax 0011 89 17 mov word ptr [bx],dx 0013 83 C3 02 add bx,0x0002 0016 EB F4 jmp L$1 0018 L$2: 0018 5E pop si 0019 59 pop cx 001A C3 ret Routine Size: 27 bytes, Routine Base: _TEXT + 0000 No disassembly errors Segment: CONST WORD USE16 00000000 bytes Segment: CONST2 WORD USE16 00000000 bytes Segment: _DATA WORD USE16 00000000 bytes
Vygenerovaný programový kód se o sedm bajtů zkrátil, ovšem důležité jsou optimalizace realizované ve vnitřní programové smyčce. Nyní je adresace skutečně krátká – využívá se zde pouze registr BX, jenž je původně nastaven na bázi pole (adresu prvního prvku). A i vlastní otestování, zda se dosáhlo konce pole, je realizováno porovnáním obsahu dvou registrů, což je velmi rychlá operace (první instrukce ve smyčce):
L$1: cmp ax,cx jge L$2 inc ax mov word ptr [bx],dx add bx,0x0002 jmp L$1
Pro zajímavost se ještě podívejme na způsob překladu též funkce, tentokrát ovšem do 32bitového kódu, ve kterém je šířka typu int rovna třiceti dvěma bitům, což ovlivňuje jak maximální délku pole, tak i rozsah hodnot jeho prvků:
Module: C:\fill_a.c GROUP: 'DGROUP' CONST,CONST2,_DATA Segment: _TEXT BYTE USE32 00000013 bytes 0000 fill_array_: 0000 51 push ecx 0001 89 D1 mov ecx,edx 0003 31 D2 xor edx,edx 0005 L$1: 0005 39 CA cmp edx,ecx 0007 7D 08 jge L$2 0009 42 inc edx 000A 89 18 mov dword ptr [eax],ebx 000C 83 C0 04 add eax,0x00000004 000F EB F4 jmp L$1 0011 L$2: 0011 59 pop ecx 0012 C3 ret Routine Size: 19 bytes, Routine Base: _TEXT + 0000 No disassembly errors Segment: CONST DWORD USE32 00000000 bytes git@github.com:tisnik/lightspeed-providers.git Segment: CONST2 DWORD USE32 00000000 bytes Segment: _DATA DWORD USE32 00000000 bytes
Šestnáctibitový a 32bitový kód se v tomto konkrétním případě liší vlastně jen nepatrně, což je ještě lépe patrné při porovnání stylem instrukce po instrukci:
L$1: L$1: cmp ax,cx cmp edx,ecx jge L$2 jge L$2 inc ax inc edx mov word ptr [bx],dx mov dword ptr [eax],ebx add bx,0x0002 add eax,0x00000004 jmp L$1 jmp L$1
5. Porovnání s moderními instrukčními sadami a moderními překladači
A jak bude stejný zdrojový kód přeložen moderním překladačem s využitím moderní (minimálně dobou vzniku) instrukční sady x86–64? Výsledný kód je ovlivněn snahou o lepší využití fronty instrukcí, což vede k rozbalení smyčky atd. Například překladač GCC při použití základních optimalizací (konkrétně při použití přepínače -O2) vygeneruje kód, v němž se v každé iteraci do pole zapíše dvojice hodnot. Možnost, že by pole obsahovalo lichý počet prvků (což pochopitelně nelze vyloučit), je řešena ještě před vstupem do vlastní programové smyčky. Celý kód je kvůli těmto optimalizacím sice delší, ale díky částečnému rozbalení smyčky (od návěští .L3 do návěští .L1) se lépe využijí možnosti moderních CPU:
fill_array: .LFB0: test esi, esi jle .L1 movsx rsi, esi lea rax, [rdi+rsi*4] and esi, 1 je .L3 mov DWORD PTR [rdi], edx add rdi, 4 cmp rdi, rax je .L11 .L3: mov DWORD PTR [rdi], edx add rdi, 8 mov DWORD PTR [rdi-4], edx cmp rdi, rax jne .L3 .L1: ret .L11: ret
Mnohem delší a interně složitější kód je vygenerován při povolení maximální úrovně optimalizací (přepínač -O9). Ústředním prvkem výsledného kódu je v tomto případě částečně rozbalená programová smyčka pracující s registry XMMx, tedy se 128bitovými vektory. V jedné iteraci se do pole zapíše celých 256 bitů, tj. 32 bajtů (16+16):
.L4: movups XMMWORD PTR [rax], xmm0 add rax, 32 movups XMMWORD PTR [rax-16], xmm0 cmp rax, rdi jne .L4
Celý kód je ovšem ještě mnohem složitější, protože řeší přípravu konstanty uložené do registru XMM0 a taktéž stavy, kdy pole obsahuje počet prvků, který není dělitelný osmi:
fill_array: .LFB0: mov r8, rdi mov ecx, esi test esi, esi jle .L1 lea eax, [rsi-1] cmp eax, 2 jbe .L6 shr esi, 2 movd xmm1, edx mov rax, rdi sal rsi, 4 pshufd xmm0, xmm1, 0 lea rdi, [rsi+rdi] and esi, 16 je .L4 lea rax, [r8+16] movups XMMWORD PTR [r8], xmm0 cmp rax, rdi je .L13 .L4: movups XMMWORD PTR [rax], xmm0 add rax, 32 movups XMMWORD PTR [rax-16], xmm0 cmp rax, rdi jne .L4 .L13: mov eax, ecx and eax, -4 test cl, 3 je .L15 .L3: movsx rsi, eax mov DWORD PTR [r8+rsi*4], edx lea rdi, [0+rsi*4] lea esi, [rax+1] cmp ecx, esi jle .L1 add eax, 2 mov DWORD PTR [r8+4+rdi], edx cmp ecx, eax jle .L1 mov DWORD PTR [r8+8+rdi], edx .L1: ret .L15: ret .L6: xor eax, eax jmp .L3
Naopak, pokud si vyžádáme vygenerování co nejkratšího kódu přepínačem -Os, získáme velmi jednoduchou smyčku s adresováním typu báze+index*konstanta:
fill_array: .LFB0: xor eax, eax .L2: cmp esi, eax jle .L5 mov DWORD PTR [rdi+rax*4], edx inc rax jmp .L2 .L5: ret
6. Překladače podporující 32bitovou instrukční sadu
Jednou z největších revolucí, která na platformě IBM PC proběhla, se stal postupný přechod od šestnáctibitových mikroprocesorů 8086 (resp. jejich varianty 8088) a 80286 k mikroprocesorům 80386. Interně se jednalo o zcela nové typy mikroprocesorů, které však nabízely plnou zpětnou kompatibilitu s předchozími čipy. Mikroprocesory 80386 nabízely nové užitečné instrukce, které jsme si již v tomto seriálu popsali (resp. alespoň většinu z nich).
To však není ani zdaleka vše, protože mikroprocesory 80386 a 80486 jsou nazývány 32bitovými čipy mj. i z toho důvodu, že se u nich rozšířila aritmeticko-logická jednotka takovým způsobem, aby bylo možné pracovat s 32bitovými operandy. A tomu se musela přizpůsobit i sada pracovních registrů. Společnost Intel se rozhodla, že namísto „párování“ dvou šestnáctibitových registrů do registrů 32bitových rozšíří původní registry ze šestnácti bitů na plných 32 bitů. Týká se to všech čtyř pracovních registrů AX, BX, CX, DX, indexových registrů SI, DI, bázových registrů SP, BP i ukazatele na instrukci IP. Tyto registry byly rozšířeny o horních šestnáct bitů, přičemž je stále možné adresovat spodních 16 bitů původními jmény registrů a/nebo (pouze u pracovních registrů) použít jejich horních a spodních osm bitů zvlášť (AX=AH+AL atd.). V assembleru se 32bitové registry poznají podle toho, že začínají písmenem E.
A konečně – zlepšily se možnosti adresování v chráněném režimu (což do jisté míry souvisí s možností pracovat s 32bitovými offsety) a navíc bylo možné využít i režim virtuální.
Všechny tyto změny museli tvůrci překladačů nějakým způsobem začít podporovat. V některých případech se jednalo „pouze“ o podporu nových instrukcí a rozšířených registrů, další překladače již dokázaly překládat plnohodnotný 32bitový kód pro chráněný režim. To je rozdíl, který se projevuje i na způsobu kódování instrukcí.
7. Šestnáctibitové a 32bitovové instrukce v reálném a chráněném režimu
V operačním systému DOS se ve výchozím stavu mikroprocesor nachází v takzvaném reálném režimu (real mode), ve kterém je sice možné používat jak šestnáctibitové, tak i 32bitové instrukce, ovšem v případě 32bitových instrukcí se před každou instrukci vloží prefix 0×66 nebo 0×67 (tj. instrukce jsou o jeden bajt delší). Ovšem naopak v 32bitovém chráněném režimu (protected mode) se prefixy vkládají před šestnáctibitové instrukce. Připomeňme si tyto rozdíly na dvojici krátkých příkladů psaných přímo v assembleru.
BITS 16 ; 16bitovy vystup pro DOS CPU 386 ; specifikace pouziteho instrukcniho souboru xor al, al inc al xor ax, ax inc ax xor eax, eax inc eax
Po překladu si povšimněte prefixů 0×66 u posledních dvou instrukcí:
27 00000000 30C0 xor al, al 28 00000002 FEC0 inc al 29 30 00000004 31C0 xor ax, ax 31 00000006 40 inc ax 32 33 00000007 6631C0 xor eax, eax 34 0000000A 6640 inc eax
Mohlo by se zdát, že třetí generace mikroprocesorů sice dokáže provádět 32bitové operace, ale platíme za to dosti vysokou cenu – každá instrukce je o bajt delší, což ovlivňuje využití cache atd. Ve skutečnosti tomu tak je, jak jsme si již naznačili, pouze v původním šestnáctibitovém režimu mikroprocesorů. V případě, že se mikroprocesor nachází v režimu 32bitovém (čehož ovšem není zcela snadné dosáhnout, pokud si vše programujeme sami), bude situace prakticky opačná. Jak by situace vypadala v 32bitovém režimu, si můžeme ověřit překladem následujícího příkladu, v němž jsme pozměnili specifikaci BITS 16 na BITS 32. Výsledný binární soubor sice nebude v DOSu přímo spustitelný, ovšem nás nyní zajímá především způsob zakódování 32bitových instrukcí:
BITS 32 ; 16bitovy vystup pro DOS CPU 386 ; specifikace pouziteho instrukcniho souboru xor al, al inc al xor ax, ax inc ax xor eax, eax inc eax
Nyní budou prefixy 0×66 použity naopak u šestnáctibitových operací. Operace nad bajty zůstanou v obou případech totožné:
27 00000000 30C0 xor al, al 28 00000002 FEC0 inc al 29 30 00000004 6631C0 xor ax, ax 31 00000007 6640 inc ax 32 33 00000009 31C0 xor eax, eax 34 0000000B 40 inc eax
Co to konkrétně znamená pro překladače jazyků C, Pascal atd.? Podívejme se nejprve na způsob překladu stejné funkce (memset), nejprve pro šestnáctibitový reálný režim. V prvním případě se používají jen šestnáctibitové instrukce, ovšem současně je velikost typu int pouze šestnáctibitová (což je mimochodem zcela v souladu s normami jazyka C!):
0000 memset_: 0000 51 push cx 0001 89 C1 mov cx,ax 0003 89 D8 mov ax,bx 0005 89 CB mov bx,cx 0007 L$1: 0007 48 dec ax 0008 3D FF FF cmp ax,0xffff 000B 74 05 je L$2 000D 88 17 mov byte ptr [bx],dl 000F 43 inc bx 0010 EB F5 jmp L$1 0012 L$2: 0012 89 C8 mov ax,cx 0014 59 pop cx 0015 C3 ret
Při překladu s využitím 32bitových instrukcí již bude velikost typu int taktéž 32bitová, ovšem současně překladač provedl překlad pro chráněný 32bitový režim. Za povšimnutí stojí prakticky stejná struktura výsledného kódu a taktéž adresace hodnotou uloženou v akumulátoru EAX (zatímco AX v režimu šestnáctibitovém pro tyto účely využít nelze):
0000 memset_: 0000 51 push ecx 0001 89 C1 mov ecx,eax 0003 L$1: 0003 4B dec ebx 0004 83 FB FF cmp ebx,0xffffffff 0007 74 05 je L$2 0009 88 10 mov byte ptr [eax],dl 000B 40 inc eax 000C EB F5 jmp L$1 000E L$2: 000E 89 C8 mov eax,ecx 0010 59 pop ecx 0011 C3 ret
Porovnání po jednotlivých instrukcích ukazuje, jak se vylepšené možnosti adresování v 32bitovém režimu projevily ve výsledném kódu:
memset_: memset_: push cx push ecx mov cx,ax mov ecx,eax mov ax,bx mov bx,cx L$1: L$1: dec ax dec ebx cmp ax,0xffff cmp ebx,0xffffffff je L$2 je L$2 mov byte ptr [bx], dl mov byte ptr [eax],dl inc bx inc eax jmp L$1 jmp L$1 L$2: L$2: mov ax,cx mov eax,ecx pop cx pop ecx ret ret
8. DJGPP
„DJGPP was born around 1989, when Richard Stallman spoke at … I asked if the FSF ever planned on porting gcc to MS-DOS, and he said it couldn't be done because gcc was too big and MS-DOS was a 16-bit operating system. Challenge in hand, I began.“
Naprostá většina překladačů vyšších programovacích jazyků, které byly pro platformu IBM PC a DOS dostupné v prvních deseti letech její existence, byla dostupná jako běžné komerční aplikace, které navíc nebyly vůbec levné. Dobrým příkladem je Lattice C, zpočátku ne moc dobrý překladač, který byl v roce 1982 (tedy v prvních letech existence platformy IBM PC) nabízen za 500 dolarů (tehdejších dolarů). Později se sice cena snížila, na což měla vliv společnost Borland, která nabízela relativně levné produkty, ovšem stále se nejednalo o zanedbatelnou položku. Později vznikly některé volně dostupné projekty popř. překladače šířené formou shareware (nízká cena) a freeware. Nicméně skutečný obrat nastal až s příchodem DJGPP, což bylo (a vlastně doposud je – projekt je totiž stále udržován) jméno pro balíček obsahující sadu GNU nástrojů, a to včetně GNU assembleru (gas), překladače jazyka C (gcc), GNU linkeru (ld), ale například i GNU debuggeru (gdb).

Obrázek 1: Instalace DJGPP v operačním systému DOS.
„The first gcc I built was 1.35, which I built on an ISC Unix system running on a 386/16. I wrote custom replacements for the system calls, linked with ISC's libc.a, write a custom program to turn the resulting binary into a 32-bit EXE that Phar Lap's extender could use, and had the first gcc that ran on MS-DOS.“
Navíc bylo nutné vyřešit způsob spouštění těchto nástrojů na operačním systému DOS s notoricky známými omezeními, které se týkají způsobu práce s operační pamětí. To bylo vyřešeno vyvinutím nového extenderu nazvaného CWSDPMI. Už jen fakt, že DJGPP podporoval vývoj aplikací, které tento extender podporovaly, stačil k rozšíření DJGPP a tím pádem i GCC mezi vývojáře. V DJGPP tak začalo vznikat poměrně velké množství aplikací pro operační systém DOS (nacházíme se v období, v němž se sice přecházelo na Windows, ale mnohé aplikace stále běžely v DOSu. Typickým příkladem jsou pochopitelně hry).

Obrázek 2: Překladač jazyka C GCC je volitelnou součástí DJGPP.
9. Malá odbočka: RHIDE
V souvislosti s projektem DJGPP je nutné se alespoň v krátkosti zmínit o integrovaném vývojovém prostředí RHIDE, které obsahovalo programátorský editor, správce projektů, rozhraní pro debugger atd. Celé prostředí bylo koncipováno takovým způsobem, aby připomínalo integrovaná vývojová prostředí nabízená společnosti Borland, takže přechod vývojářů na DJGPP+RHIDE byl (po prvotní konfiguraci, která určité znalosti vyžadovala) poměrně bezbolestný a rychlý (stejné či podobné klávesové zkratky, shodná okna, podobné dialogy). Vzhledem k tomu, že tento článek se zabývá překladači a nikoli IDE, ukážeme si pouze několik screenshotů RHIDE:

Obrázek 3: Z roku uvedeného v tomto dialogu je zřejmé, že RHIDE bylo vyvíjeno i poté, co už DOS prakticky přestal být (snad kromě embedded systémů) jen historickým termínem.

Obrázek 4: Programátorský editor se zvýrazněním syntaxe.

Obrázek 5: Průběh překladu projektu.
10. Překlad testovacích příkladů pomocí DJGPP
Testovací příklady, které jsme si ukázali v úvodním článku i v článku dnešním, se nyní pokusíme přeložit s využitím DJGPP v DOSu. Překlad bude vždy proveden několika způsoby – typicky bez povolených optimalizací, s povolenými optimalizacemi a většinou taktéž s optimalizacemi, které by měly vést ke kratšímu strojovému kódu. Výhodou projektu DJGPP je v této oblasti fakt, že se vlastně používá standardní překladač GCC, takže i jeho přepínače řídicí způsob překladu jsou stejné. A mimochodem – tyto stovky přepínačů lze změnit i v RHIDE v nastavení projektu:

Obrázek 6: Nastavení přepínačů překladače, které ovlivňují překlad.
11. Funkce pro součet dvou celých čísel
Začneme triviální funkcí, která provede součet dvou celých čísel a vrátí výsledek součtu. V DJGPP je typ int pochopitelně plně 32bitový:
int add(int a, int b) { return a+b; }
Výsledek překladu, pokud jsou optimalizace zakázány. Vidíme zde standardní práci se zásobníkovým rámcem (instrukce PUSH+MOV na začátku a POP na konci). Výsledek je vrácen přes registr EAX:
_add: LFB0: push ebp mov ebp, esp mov edx, DWORD PTR [ebp+8] mov eax, DWORD PTR [ebp+12] add eax, edx pop ebp ret
Výsledek překladu při povolení maximálních optimalizací. Nyní se již zásobníkový rámec neobsluhuje, nicméně se stále používají dva pracovní registry, i když by bylo možné kód ještě dále zjednodušit:
_add: LFB0: mov eax, DWORD PTR [esp+8] mov edx, DWORD PTR [esp+4] add eax, edx ret
Výsledek překladu při snaze o vytvoření nejkratšího kódu. Zajímavé je, že v tomto případě se sice skutečně celý výpočet provádí pouze s registrem EAX, ovšem současně se vytváří a ruší zásobníkový rámec.
_add: LFB0: push ebp mov ebp, esp mov eax, DWORD PTR [ebp+12] add eax, DWORD PTR [ebp+8] pop ebp ret
Nejkratší kód a volba -fomit-frame-pointer, která zabrání vytvoření standardního zásobníkového rámce:
_add: LFB0: mov eax, DWORD PTR [ebp+8] add eax, DWORD PTR [ebp+4] ret
12. Funkce pro vyplnění paměťové oblasti
Další funkcí, jejíž překlad pomocí DJGPP si ověříme, je funkce určená pro vyplnění paměťové oblasti určené ukazatelem. I tuto funkci jsme si ukázali minule, takže jen krátce:
#include <mem.h> void * memset(void *dest, register int val, register size_t len) { register unsigned char *ptr = (unsigned char*)dest; while (len-- > 0) *ptr++ = val; return dest; }
Překlad se zakázanými optimalizacemi, ve kterém kromě jiného můžeme vidět dvojí přístup do paměti, i to, že zápis je prováděn po bajtech:
_memset: LFB0: push ebp mov ebp, esp push ebx mov ecx, DWORD PTR [ebp+16] mov ebx, DWORD PTR [ebp+8] jmp L2 L3: mov eax, ebx lea ebx, [eax+1] mov dl, BYTE PTR [ebp+12] mov BYTE PTR [eax], dl L2: mov eax, ecx lea ecx, [eax-1] test eax, eax jne L3 mov eax, DWORD PTR [ebp+8] pop ebx pop ebp ret
Překlad s povolenými maximálními optimalizacemi vede k tomu, že zápis do pole je prováděn po čtyřech prvcích, ale pochopitelně se zajištěním funkcionality v případě, že délka pole není dělitelná čtyřmi:
_memset: LFB0: push edi push ebx mov edx, DWORD PTR [esp+20] test edx, edx je L12 xor ebx, ebx mov edi, DWORD PTR [esp+12] mov bl, BYTE PTR [esp+16] cmp edx, 4 jnb L22 L3: and edx, 3 je L12 xor eax, eax L6: mov BYTE PTR [edi+eax], bl inc eax cmp eax, edx jb L6 L12: mov eax, DWORD PTR [esp+12] pop ebx pop edi ret L22: xor eax, eax mov al, bl mov ah, al mov ecx, eax sal ecx, 16 or eax, ecx test edi, 1 jne L23 L4: test edi, 2 jne L24 L5: mov ecx, edx shr ecx, 2 rep stosd jmp L3 L23: mov BYTE PTR [edi], al dec edx mov ecx, DWORD PTR [esp+12] lea edi, [ecx+1] jmp L4 L24: mov WORD PTR [edi], ax sub edx, 2 add edi, 2 jmp L5
A konečně výsledek snahy o krátký výsledný strojový kód. Zápis je prováděn pomalu – po bajtech a dokonce se v každé iteraci provádí jedno čtení a jeden zápis do paměti:
_memset: LFB0: push ebp mov ebp, esp push ebx mov edx, DWORD PTR [ebp+8] mov ecx, DWORD PTR [ebp+16] add ecx, edx mov eax, edx L2: cmp eax, ecx je L6 inc eax mov bl, BYTE PTR [ebp+12] mov BYTE PTR [eax-1], bl jmp L2 L6: pop ebx mov eax, edx pop ebp ret
13. Funkce pro nalezení největší hodnoty v poli
Zajímavější a interně složitější je funkce, která má nalézt největší prvek v poli s prvky typu unsigned int. Tato funkce (i s jejím ověřením) vypadá následovně:
#include <stdio.h> typedef unsigned int uint; uint find_max(uint *array, uint length) { uint max = 0; uint i; uint *item = array; for (i=0; i<length; i++) { if (max < *item) { max = *item; } item++; } return max; } int main(void) { #define LENGTH 10 uint array[LENGTH] = {5, 6, 7, 8, 9, 0, 1, 2, 3, 4}; uint max = find_max(array, LENGTH); printf("%d\n", max); return 0; }
Výsledek překladu po zákazu optimalizací. Povšimněte si, že nyní se používají „lokální proměnné“ uložené na zásobníkovém rámci (veškeré přístupy do paměti s EBP-hodnota):
_find_max: LFB0: push ebp mov ebp, esp sub esp, 16 mov DWORD PTR [ebp-4], 0 mov eax, DWORD PTR [ebp+8] mov DWORD PTR [ebp-12], eax mov DWORD PTR [ebp-8], 0 jmp L2 L4: mov eax, DWORD PTR [ebp-12] mov eax, DWORD PTR [eax] cmp DWORD PTR [ebp-4], eax jnb L3 mov eax, DWORD PTR [ebp-12] mov eax, DWORD PTR [eax] mov DWORD PTR [ebp-4], eax L3: add DWORD PTR [ebp-12], 4 inc DWORD PTR [ebp-8] L2: mov eax, DWORD PTR [ebp-8] cmp eax, DWORD PTR [ebp+12] jb L4 mov eax, DWORD PTR [ebp-4] leave ret
Po povolení optimalizací se již žádné lokální proměnné nejsou zapotřebí a výpočty se provádí s hodnotami uloženými v registrech nebo přímo v poli. Povšimněte si, že ze subrutiny se vyskakuje na dvou místech:
_find_max: LFB0: push esi push ebx mov ebx, DWORD PTR [esp+16] mov esi, DWORD PTR [esp+12] test ebx, ebx je L5 xor eax, eax xor ecx, ecx L4: mov edx, DWORD PTR [esi+eax*4] cmp ecx, edx jnb L3 mov ecx, edx L3: inc eax cmp ebx, eax jne L4 pop ebx mov eax, ecx pop esi ret L5: xor ecx, ecx pop ebx mov eax, ecx pop esi ret
Nejčitelnější obecně bývá výsledek překladu při snaze o vytvoření nejkratšího kódu. Je tomu tak i v tomto případě, ovšem nyní se uvnitř smyčky provádí hned tři přístupy do operační paměti, což pochopitelně nevede k nejrychlejšímu kódu:
_find_max: LFB0: push ebp xor eax, eax mov ebp, esp xor edx, edx L2: cmp eax, DWORD PTR [ebp+12] je L7 mov ecx, DWORD PTR [ebp+8] mov ecx, DWORD PTR [ecx+eax*4] cmp edx, ecx jnb L3 mov edx, ecx L3: inc eax jmp L2 L7: mov eax, edx pop ebp ret
14. Funkce pro vyplnění pole zadanou hodnotou
Třetí příklad, jehož překlad pomocí DJGPP si otestujeme, obsahuje funkci pro vyplnění pole zadanou hodnotou. Tedy jedná se o příklad ze druhé kapitoly:
void fill_array(int *array, int size, int value) { int i; for (i=0; i<size; i++) { array[i] = value; } }
Opět se nejdříve podíváme na výsledek překladu se zákazem optimalizací. Ten vypadá následovně. Je zde patrné hned několik přístupů do paměti (zásobníku atd.) uvnitř smyčky:
_fill_array: LFB0: push ebp mov ebp, esp sub esp, 16 mov DWORD PTR [ebp-4], 0 jmp L2 L3: mov eax, DWORD PTR [ebp-4] lea edx, [0+eax*4] mov eax, DWORD PTR [ebp+8] add edx, eax mov eax, DWORD PTR [ebp+16] mov DWORD PTR [edx], eax inc DWORD PTR [ebp-4] L2: mov eax, DWORD PTR [ebp-4] cmp eax, DWORD PTR [ebp+12] jl L3 nop nop leave ret
Mnohem efektivnější je kód získaný po povolení všech optimalizací na rychlost. Nyní je programová smyčka zkrácena na pouhé čtyři instrukce s jediným přístupem do paměti:
_fill_array: LFB0: mov ecx, DWORD PTR [esp+8] mov edx, DWORD PTR [esp+12] test ecx, ecx jle L1 mov eax, DWORD PTR [esp+4] lea ecx, [eax+ecx*4] L3: mov DWORD PTR [eax], edx add eax, 4 cmp ecx, eax jne L3 L1: ret
Výsledek překladu při snaze o co nejmenší kód, která ovšem tvoří standardní zásobníkové rámce:
_fill_array: LFB0: push ebp xor eax, eax mov ebp, esp mov edx, DWORD PTR [ebp+8] mov ecx, DWORD PTR [ebp+16] L2: cmp eax, DWORD PTR [ebp+12] jge L6 mov DWORD PTR [edx+eax*4], ecx inc eax jmp L2 L6: pop ebp ret
Snaha o co nejkratší kód se zákazem vytváření standardních zásobníkových rámců:
_fill_array: LFB0: mov edx, DWORD PTR [esp+4] mov ecx, DWORD PTR [esp+12] xor eax, eax L2: cmp eax, DWORD PTR [esp+8] jge L5 mov DWORD PTR [edx+eax*4], ecx inc eax jmp L2 L5: ret
15. Podpora operací s plovoucí řádovou čárkou v céčkových překladačích
Překladače programovacího jazyka C se na platformě IBM PC (a DOS) musely vypořádat ještě s jedním problémem – jakým způsobem zajistit výpočty s hodnotami s plovoucí řádovou čárkou, tj. v céčku s hodnotami typu float a double. Vzhledem k tomu, že již víme, že matematický koprocesor byl volitelnou součástí počítače (což se změnilo až v dobách Pentia), mohlo nastat několik variant:
- Program byl přeložen takovým způsobem, že matematický koprocesor striktně vyžadoval
- Program byl naopak přeložen tak, že všechny výpočty vždy prováděl softwarově (a tudíž velmi pomalu)
- Na začátku byla provedena detekce matematického koprocesoru a výpočet se realizoval dvakrát (byl proveden rozeskok)
- Využívalo se systému přerušení, kdy knihovna pro softwarové výpočty reagovala na situaci, kdy došlo k přerušení při pokusu o vyvolání FPU operace na počítači, který koprocesor neobsahoval

Obrázek 7: Volby v IDE překladače Borland C++, které ovlivňují, jakým způsobem se budou provádět výpočty s hodnotami s plovoucí řádovou čárkou.
16. Funkce pro součet dvou hodnot typu float
Abychom si otestovali, jakým způsobem překladače dokážou provádět výpočty s FPU hodnotami, necháme si přeložit tuto jednoduchou funkci, která má sečíst své dva parametry typu float a vrátit výsledek součtu:
float add(float x, float y) { return x+y; }
17. Výsledek překladu pomocí Borland C++
Překladač Borland C++ dokáže generovat instrukce matematického koprocesoru a v případě potřeby je dokáže i emulovat. Výsledek překladu výše uvedené funkce je poměrně přímočarý. Připomeňme si jen, že parametry jsou předány přes zásobník a po vytvoření zásobníkového rámce (BP) jsou umístěny na adresách BP+4 a BP+8:
; ; float add(float x, float y) { ; assume cs:_TEXT _add proc near push bp mov bp,sp ; ; return x+y; ; fld dword ptr [bp+4] fadd dword ptr [bp+8] jmp short @1@50 @1@50: ; ; } ; pop bp ret _add endp
Jediná dostupná optimalizace odstraní krátký skok na další adresu, takže strojový kód výsledné subrutiny bude vypadat následovně:
_TEXT segment byte public 'CODE' ; ; float add(float x, float y) { ; assume cs:_TEXT _add proc near push bp mov bp,sp ; ; return x+y; ; fld dword ptr [bp+4] fadd dword ptr [bp+8] ; ; } ; pop bp ret _add endp
18. Výsledek překladu pomocí Watcom C/C++ a DJGPP
Překladač Watcom C/C++ pochopitelně taktéž podporuje práci s matematickým koprocesorem. Překlad bez optimalizací vypadá prakticky stejně, jak u překladače Borland C++, ovšem povšimněte si explicitního volání instrukce FWAIT (lze vypnout):
Module: c:\add_f.c GROUP: 'DGROUP' CONST,CONST2,_DATA Segment: _TEXT BYTE USE16 00000017 bytes 0000 add_: 0000 B8 04 00 mov ax,0x0004 0003 E8 00 00 call __STK 0006 55 push bp 0007 89 E5 mov bp,sp 0009 9B D9 46 04 fld dword ptr 0x4[bp] 000D 9B D8 46 08 fadd dword ptr 0x8[bp] 0011 90 nop 0012 9B fwait 0013 5D pop bp 0014 C2 08 00 ret 0x0008 Routine Size: 23 bytes, Routine Base: _TEXT + 0000 No disassembly errors Segment: CONST WORD USE16 00000000 bytes Segment: CONST2 WORD USE16 00000000 bytes Segment: _DATA WORD USE16 00000000 bytes
Po provedení optimalizací se subrutina zmenší na sedmnáct bajtů, ovšem základ je stále zachován:
Module: c:\add_f.c GROUP: 'DGROUP' CONST,CONST2,_DATA Segment: _TEXT BYTE USE16 00000011 bytes 0000 add_: 0000 55 push bp 0001 89 E5 mov bp,sp 0003 9B D9 46 04 fld dword ptr 0x4[bp] 0007 9B D8 46 08 fadd dword ptr 0x8[bp] 000B 90 nop 000C 9B fwait 000D 5D pop bp 000E C2 08 00 ret 0x0008 Routine Size: 17 bytes, Routine Base: _TEXT + 0000 No disassembly errors Segment: CONST WORD USE16 00000000 bytes Segment: CONST2 WORD USE16 00000000 bytes Segment: _DATA WORD USE16 00000000 bytes
A konečně můžeme povolit využití FPU knihovny. Poté je překlad triviální – provede se jen skok na subrutiny __FSA, což pravděpodobně znamená floating single add:
Module: c:\add_f.c GROUP: 'DGROUP' CONST,CONST2,_DATA Segment: _TEXT BYTE USE16 00000003 bytes 0000 add_: 0000 E9 00 00 jmp __FSA Routine Size: 3 bytes, Routine Base: _TEXT + 0000 No disassembly errors Segment: CONST WORD USE16 00000000 bytes Segment: CONST2 WORD USE16 00000000 bytes Segment: _DATA WORD USE16 00000000 bytes
Poslední je DJGPP. Překlad se zákazem optimalizací dopadne takto:
_add: push ebp mov ebp, esp fld DWORD PTR [ebp+8] fadd DWORD PTR [ebp+12] pop ebp ret
Po povolení optimalizací se pouze prohodí operandy součtu (kdoví proč):
_add: push ebp mov ebp, esp fld DWORD PTR [ebp+12] fadd DWORD PTR [ebp+8] pop ebp ret
Výsledek překladu při povolení optimalizací a zákazu použití standardních zásobníkových rámců:
_add: fld DWORD PTR [esp+8] fadd DWORD PTR [esp+4] ret
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