Obsah
1. Překladače programovacího jazyka C pro historické osmibitové mikroprocesory
2. Překladače a cross překladače pro mikroprocesory MOS 6502
7. Překlad funkce bez parametrů a bez návratových hodnot
8. Překlad do assembleru s odkazy na původní zdrojový kód
11. Funkce pro vyplnění bloku paměti specifikovanou hodnotou
12. Assembler vygenerovaný pro základní verzi funkce memset
13. Osmibitová lokální proměnná (počitadlo smyčky) s modifikátorem register
14. Assembler vygenerovaný pro upravenou verzi funkce memset
15. Konstantní parametry funkce
16. Přímý přístup do paměti přes ukazatel, využití smyčky while
17. Otočení smyčky – využití stejné sémantiky, jako při práci v assembleru
19. Repositář s demonstračními příklady
1. Překladače programovacího jazyka C pro historické osmibitové mikroprocesory
O programovacím jazyku C se někdy s nadsázkou říká, že je to „přenositelný assembler“ [1]. Programy napsané v jazyce C skutečně mohou být velmi efektivní, ovšem do značné míry záleží na kvalitě céčkového překladače. Dnes si ve stručnosti řekneme, jak je tomu v případě slavného osmibitového mikroprocesoru MOS 6502, jehož instrukční kód jsme si do jisté míry přiblížili v seriálu o vývoji her a grafických dem pro herní konzoli NES.
Obrázek 1: Přebal druhého vydání slavné knihy „The C Programming Language“ (Kerningan, Ritchie)
Překladače jazyka C existují pro prakticky každou počítačovou platformu i pro prakticky každý typ mikroprocesoru. Výjimkou není ani MOS 6502, ovšem na tomto místě je nutné poznamenat, že právě u podobných typů mikroprocesorů, jako je MOS 6502, stojí tvůrci překladačů před nelehkým úkolem, protože programátorský model MOS 6502 se v mnoha ohledech odlišuje od „ideálního céčkového mikroprocesoru“ A jak by vlastně měl takový ideální procesor vypadat? Měl by mít velký počet pracovních registrů, obsahovat instrukce pro snadnou manipulaci s obsahem zásobníku, pracovní registry by mohly být použity při adresování, měl by podporovat adresování s offsetem (násobeným ×1, ×2, ×4 apod.) a ideálně by se při adresování mohla provádět inkrementace či dekrementace offsetu, opět o hodnotu 1, 2, 4 apod. Navíc by paměť měla být viditelná jako lineární prostor s buňkami s postupně rostoucími adresami(„flat model“). Tomuto ideálu se kdysi přiblížil čip Motorola 68000 a dále RISCové procesory, z nichž mnohé byly navrženy právě s ohledem na sémantické možnosti jazyka C.
Obrázek 2: Logo překladačů Aztec C.
Z programátorského hlediska se mikroprocesor MOS 6502 dosti podstatným způsobem odlišuje jak od konkurenčního Intelu 8080 (i od později vydaného Zilogu Z80), tak i od čipu RCA-1802. Zatímco procesor Intel 8080 obsahoval poměrně rozsáhlou sadu obecně použitelných osmibitových registrů (A, B, C, D, E, H a L), které se u některých instrukcí kombinovaly do 16bitových registrových párů, měl 6502 pouze jeden osmibitový akumulátor (registr A) a dva taktéž osmibitové index-registry pojmenované X a Y. Oba zmíněné typy procesorů samozřejmě obsahovaly další speciální registry, jako ukazatel na vrchol zásobníku (SP), programový čítač (PC) a příznakový registr (F).
Na první pohled by se mohlo zdát, že počet registrů mikroprocesoru MOS 6502 je zcela nedostatečný pro provádění většiny aritmetických či logických operací. Ve skutečnosti tomu tak není, protože tento procesor podporuje načtení druhého operandu z operační paměti (rychlost RAM nebyla tak limitujícím faktorem, jako je tomu dnes – ve skutečnosti byl přístup do RAM dvojnásobně rychlý v porovnání s mikroprocesorem). U mnoha instrukcí je podporován větší počet adresovacích režimů, celkově je možné operandy strojových instrukcí adresovat třinácti navzájem odlišnými způsoby. Při adresování se často používají oba index-registry, které je možné inkrementovat a dekrementovat – tím je umožněno provádění blokových přenosů dat, mazání souvislé oblasti paměti atd.
2. Překladače a cross překladače pro mikroprocesory MOS 6502
Některé překladače programovacího jazyka C pro MOS 6502 byly navrženy přímo pro běh na strojích osazených tímto čipem. To je dnes zcela normální situace (aplikace pro PC se překládají na PC), ovšem v případě MOS 6502 se museli tvůrci překladačů vypořádat s pomalým čipem, velmi malou kapacitou paměti a navíc i relativně pomalým externím paměťovým médiem (typicky disketa, protože kazetové verze C by byly ještě problematičtější). V důsledku těchto omezení se jednalo spíše o projekty určené pro amatérské použití, zatímco profesionální software stále vznikal v assembleru. Jednou z prvních implementací překladače C pro MOS 6502 je C/65 od slavné firmy Optimized Systems Software (OSS).
Obrázek 3: C/65 od společnosti Optimized Systems Software (OSS).
Taktéž se na tomto místě musíme zmínit o známém překladači Aztec C, jenž byl portovaný na velké množství různých typů mikropočítačů, zapomenout nesmíme ani na Deep Blue C (viz též https://en.wikipedia.org/wiki/Deep_Blue_C) pro osmibitové počítače Atari. Zde se autoři museli vyrovnat s faktem, že znaková sada neobsahovala složené závorky, takže zápis vypadal například takto:
main() $( printf("Hello World!"); $)
Obrázek 4: Dobová reklama na nástroje společnosti OSS.
Zajímavější jsou z dnešního pohledu cross compilery a cross assemblery (viz poznámka o českém překladu tohoto názvu). Tyto typy nástrojů jsou velmi často používané i dnes, zejména v oblasti mikrořadičů, digitálních signálových procesorů nebo mobilních telefonů (viz například Scratchbox). Ovšem tato technologie se používala již na začátku osmibitové éry. Například vývoj her pro herní konzoli Atari 2600 (Atari Video Computer System neboli Atari VCS) byl prováděn na minipočítači. Ovšem i později některé firmy vyvíjely profesionální software pro Atari, C64 i další osmibitové mikropočítače na výkonnějších strojích, kde se prováděl i překlad.
Obrázek 5: Jeden z konkurenčních překladačů k Aztec C byl Lattice C (ovšem až v pozdější době).
Dobrým a možná i typickým příkladem jsou právě cross překladače programovacího jazyka C. Tvorbou těchto cross překladačů se zabývala například společnost Manx Software Systems, jejíž překladače céčka (Aztec C) určené pro IBM PC s DOSem i pro osobní mikropočítače Macintosh dokázaly provádět cross překlad na osmibitové mikropočítače Commodore C64 a Apple II. Na chvíli se u Aztec C zastavme, i když přímo nesouvisí s osmibitovými Atari.
Aztec C totiž byl ve své době velmi úspěšný překladač, jenž existoval jak ve verzi pro osmibitové mikroprocesory (MOS 6502, Zilog Z-80), tak i pro mikroprocesory 16bitové a 32bitové. Tento překladač byl velmi úspěšný právě na Amize, kde byl používán, společně s Lattice C, prakticky až do faktického zániku této platformy. Ovšem na IBM PC jeho sláva netrvala dlouho, především z toho důvodu, že firma Microsoft považovala segment překladačů za poměrně důležitý a snažila se vytlačit jakoukoli konkurenci z trhu (i když ve skutečnosti v té době ještě neměla vlastní céčkový překladač). Společnosti Manx Software Systems se postupně zmenšoval počet platforem, na něž bylo možné překladač prodávat a přechod na podporu vestavěných systémů již přišel dosti pozdě. A právě pro cross překlad se Aztec C může používat dodnes (běží v DOSu, takže dnes vlastně taktéž v emulovaném prostředí).
Podobným stylem byl řešen i Microsoft C původně vytvořený společností, která stála za slavným Lattice C. Ostatně Lattice C byl s velkou pravděpodobností vůbec prvním překladačem céčka pro IBM PC (pochází z roku 1982). Ten byl později převeden i na Amigu, dále se rozšířil i na minipočítače a mainframy společnosti IBM. Firma Microsoft překladač Lattice C nabízela pod svým názvem MSC (Microsoft C) a teprve verze MSC 4.0 byla skutečně vytvořena přímo programátory z Microsoftu. Lattice C byl používán i při portaci aplikací z operačního systému CP/M na DOS (dnes je však možné pouze odhadnout, kolik kódu bylo skutečně napsáno v céčku a kolik kódu vzniklo transformací assembleru).
3. cc65
Prvním cross překladačem, s nímž se v dnešním článku seznámíme (a současně i překladačem nejznámějším), je překladač programovacího jazyka C, který se jmenuje cc65. Ve skutečnosti se však nejedná pouze o čistý překladač céčka, ale o sadu dalších vývojářských nástrojů, mezi něž patří i výše zmíněný cross assembler ca65, dále linker ld65, disassembler da65, simulátor procesorů 6502 sim65 atd. Tento překladač je možné použít nejenom pro tvorbu aplikací pro osmibitové mikropočítače Atari, ale i pro osmibitové stroje firmy Commodore (VIC20, C64, C128 atd.), osmibitové mikropočítače řady Apple II, herní konzoli NES, konzoli TurboGrafx-16 atd.
Jazyk akceptovaný překladačem cc65 do určité míry odpovídá ISO standardu jazyka C (a tím pádem i původnímu ANSI standardu, dokonce je podporováno několik vlastností z C99); standard je vnucen přepínačem –standard. Existuje však několik dosti podstatných rozdílů a nedostatků cc65, mezi něž patří neexistence datových typů float a double (a tím pádem i celá část céčka, která předepisuje konverze s těmito typy atd.). Dále existuje omezení při definicích funkcí – funkce nemohou vracet struktury ani unie; struktury dokonce není možné předávat hodnotou (což ale většinou nevadí, právě naopak). Kromě toho je sice možné použít modifikátor volatile, ovšem ten nemá žádný podstatný význam (což je v případě mikroprocesorů MOS 6502 a jejich možností pochopitelné). Naproti tomu má či může mít velký vliv modifikátor register, který naopak mnoho moderních překladačů již nepotřebuje či dokonce zcela ignoruje.
Naopak mezi rozšíření cc65 oproti standardu jazyka C (jak ANSI, tak i ISO normy) patří podpora bloků psaných v assembleru, podpora pseudoproměnných A a AX (což je primární registr mikroprocesoru, tedy akumulátor, v případě AX rozšířený na šestnáct bitů přes registr X). Podporovány jsou konstanty zapsané ve dvojkové soustavě (0b101) a použít lze i počítaná goto (což je částečně převzato z GCC).
4. vbcc
Dalším transpřekladačem programovacího jazyka C určeného mj. i pro překlad zdrojových kódů do strojového kódu osmibitových mikroprocesorů MOS 6502, je překladač nazvaný vbcc – portable ISO C compiler. Tento překladač, jehož stránky lze nalézt na adrese http://www.compilers.de/vbcc.html, podporuje generování kódu pro různé architektury mikroprocesorů a mikrořadičů, od mikroprocesorů osmibitových přes šestnáctibitové čipy až po moderní mikroprocesory 32bitové a 64bitové. vbcc taktéž umožňuje provádět různé optimalizace, a to ve většině případů mnohem kvalitněji, než výše zmíněný cc65 (jenž je však známější a používanější). Tímto zajímavým překladačem se budeme dále zabývat v samostatném článku.
Podporované mikroprocesory, včetně na nich postavených počítačů:
Čip | Počítače/konzole/systém |
---|---|
Motorola 6800/6801/6803/6808/68hc11 | (jen vasm, bare systémy) |
Motorola 6809/6309/68hc12 | OS-9/NitrOS-9 (tbc), Vectrex (tbc) |
Motorola 68000 | AmigaOS, Atari TOS, MINT, Atari Jaguar, Linux |
Coldfire | MINT, Arnewsh 5206 |
PowerPC | AmigaOS, PowerUp, WarpOS, MorphOS, Linux, NetBSD, OpenFirmware |
6502/65C02 | C64, C128, PET, Atari 8bit, BBC Micro/Master, NES, MEGA65, Commander X16 |
i386 | PC, Linux, NetBSD, DOS |
VideoCore IV (RasperryPi GPU) | Linux, RPi |
C16X/ST10 | MM167 |
DEC Alpha | Linux |
5. 6502-gcc
Třetím transpřekladačem jazyka C do strojového kódu mikroprocesorů MOS 6502, o kterém se v dnešním článku alespoň ve stručnosti zmíníme, je překladač nazvaný 6502-gcc. Název tohoto překladače naznačuje, že je založen na známém projektu GCC (GNU Compiler Collection), do kterého byl přidán nový back-end. Díky standardnímu front-endu GCC je zajištěna prakticky stoprocentní kompatibilita s programovacím jazykem C (na rozdíl od některých nekompatibilních rysů cc65, o nichž jsme se ostatně již krátce zmínili). Nevýhodou je, že je tento projekt prakticky neudržovaný a taktéž je méně známý (takže existuje jen velmi malá podpora od ostatních vývojářů).
6. Instalace ca65 a cc65
Instalace assembleru ca65 i céčkového překladače cc65 je na většině distribucí Linuxu velmi snadná, neboť se jedná o balíčky (typicky) umístěné přímo v repositářích dané distribuce. Dobrým příkladem může být Linux Mint, v níž instalace vypadá následovně:
$ sudo apt-get install cc65 Reading package lists... Done Building dependency tree Reading state information... Done Suggested packages: cc65-doc The following NEW packages will be installed: cc65 0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded. Need to get 2 162 kB of archives. After this operation, 31,8 MB of additional disk space will be used. Get:1 http://archive.ubuntu.com/ubuntu focal/universe amd64 cc65 amd64 2.18-1 [2 162 kB] Fetched 2 162 kB in 5s (423 kB/s) Selecting previously unselected package cc65. (Reading database ... 291820 files and directories currently installed.) Preparing to unpack .../archives/cc65_2.18-1_amd64.deb ... Unpacking cc65 (2.18-1) ... Setting up cc65 (2.18-1) ...
Po dokončení instalace budou k dispozici tři nové nástroje (a několik podpůrných nástrojů).
V první řadě se jedná o assembler:
$ cc65 --version cc65 V2.18 - Ubuntu 2.18-1
Dále o překladač céčka:
$ ca65 --version ca65 V2.18 - Ubuntu 2.18-1
A využijeme i samostatný linker:
$ ld65 --version ld65 V2.18 - Ubuntu 2.18-1
Instalace céčkového překladače cc65 na Fedoře 36 je taktéž velmi snadná:
# dnf install cc65 Last metadata expiration check: 0:21:57 ago on Sat 10 Sep 2022 04:10:12 AM EDT. Dependencies resolved. ================================================================================ Package Architecture Version Repository Size ================================================================================ Installing: cc65 x86_64 2.19-3.fc36 beaker-Fedora 421 k Installing dependencies: cc65-devel noarch 2.19-3.fc36 beaker-Fedora 1.8 M Installing weak dependencies: cc65-doc noarch 2.19-3.fc36 beaker-Fedora 623 k cc65-utils x86_64 2.19-3.fc36 beaker-Fedora 29 k Transaction Summary ================================================================================ Install 4 Packages Total download size: 2.8 M Installed size: 33 M Is this ok [y/N]:
Kontrola nainstalované verze:
$ cc65 --version cc65 V2.18 - Fedora 2.19-3.fc36
7. Překlad funkce bez parametrů a bez návratových hodnot
Struktura kódu generovaného překladačem ca65 do značné míry závisí na tom, jaké přepínače jsou při překladu použity. Ukážeme si to na příkladu jednoduché funkce, která je bez parametrů a taktéž nemá žádné návratové hodnoty. Ve funkci se pouze používá trojice lokálních proměnných a tři přiřazovací příkazy (takže teoreticky lze funkci přeložit do jediné instrukce jsr – návrat z podprogramu, či ji zcela odstranit):
void main(void) { register unsigned char a; register unsigned char b; register unsigned char c; a = 10; b = 20; c = a + b; }
Vyzkoušíme si překlad pouze zadáním příkazu:
$ cc65 local_add.c
Výsledkem nebude objektový kód, jak je to běžné na platformě PC (i jinde), ale kód v assembleru určený pro další zpracování assemblerem ca65. Tento kód vypadá následovně:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _main ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" jsr decsp3 ldx #$00 lda #$0A ldy #$02 sta (sp),y ldx #$00 lda #$14 ldy #$01 sta (sp),y ldy #$02 ldx #$00 lda (sp),y jsr pushax ldy #$03 ldx #$00 lda (sp),y jsr tosaddax ldx #$00 ldy #$00 sta (sp),y jsr incsp3 rts .endproc
8. Překlad do assembleru s odkazy na původní zdrojový kód
Zejména při zkoumání delšího přeloženého kódu může být výhodné, pokud jsou v assembleru uvedeny odkazy na původní céčkový zdrojový kód. V praxi to vypadá tak, že je nejdříve v poznámce zapsán (typicky) jeden programový řádek v céčku, za nímž následuje sekvence instrukcí, do kterých je tento řádek přeložen.
Odkazy na původní zdrojový kód jsou do assembleru přidány po použití přepínače -T:
$ cc65 -T local_add.c
Nyní bude výsledek vypadat takto:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _main ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; a = 10; ; jsr decsp3 ldx #$00 lda #$0A ldy #$02 sta (sp),y ; ; b = 20; ; ldx #$00 lda #$14 ldy #$01 sta (sp),y ; ; c = a + b; ; ldy #$02 ldx #$00 lda (sp),y jsr pushax ldy #$03 ldx #$00 lda (sp),y jsr tosaddax ldx #$00 ldy #$00 sta (sp),y ; ; } ; jsr incsp3 rts .endproc
Nyní již máme určitou představu o způsobu překladu. Zejména je patrné, jak jsou lokální proměnné uloženy na zásobníku. Pro adresování se používá adresa uložená na adrese sp (to není registr, ale skutečně adresa operační paměti). Jako offset k tomuto uměle vytvořenému zásobníku slouží registr y.
Uložení hodnoty 10 do zásobníku:
lda #$0A ; ukládaná konstanta ldy #$02 ; offset od vrcholu zásobníku sta (sp),y ; vlastní uložení hodnoty
Součet je proveden nepřímo – zavoláním subrutin (viz zvýrazněný kód):
ldy #$02 ; načtení hodnoty proměnné a do akumulátoru lda (sp),y jsr pushax ; volaná subrutina ldy #$03 lda (sp),y ; načtení hodnoty proměnné b do akumulátoru jsr tosaddax ; volaná subrutina ldy #$00 sta (sp),y ; uložení výsledku na zásobník
9. Statické lokální proměnné
Prakticky všechny moderní mikroprocesorové architektury ukládají parametry funkcí i lokální proměnné do zásobníkových rámců (stack frame). Pokud se o podobný přístup pokusíme na architektuře MOS 6502, je nutné zásobníkový rámec simulovat přes adresu (uloženou v operační paměti), která simuluje registr SP, jenž na MOS 6502 neexistuje (zásobník zde má jen 256 adres a navíc není umožněno relativní adresování přes offset – což je možná největší nedostatek MOS 6502).
Alternativně je možné používat statické lokální proměnné, což jsou vlastně globální proměnné, které jsou ovšem z pohledu programátora dostupné pouze v jedné funkci. Generovaný kód by měl být nepatrně menší i rychlejší. Přepínačem -Cl lze použití statických lokálních proměnných vynutit, a to i tehdy, pokud není použito klíčové slovo static:
$ cc65 -T -Cl local_add.c
Nyní bude výsledný vygenerovaný assembler opět odlišný:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _main ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "BSS" L0002: .res 1,$00 L0003: .res 1,$00 L0004: .res 1,$00 .segment "CODE" ; ; a = 10; ; ldx #$00 lda #$0A sta L0002 ; ; b = 20; ; ldx #$00 lda #$14 sta L0003 ; ; c = a + b; ; ldx #$00 lda L0002 jsr pushax ldx #$00 lda L0003 jsr tosaddax ldx #$00 sta L0004 ; ; } ; rts .endproc
Nyní jsou lokální proměnné uloženy v paměti rezervované v segmentu BSS (není součástí objektového kódu). Příkladem je uložení hodnoty 10 do lokální (statické) proměnné a:
ldx #$00 lda #$0A sta L0002
10. Zapnutí optimalizací společně s využitím statických lokálních proměnných, proměnných v registrech atd.
V předchozích dvou kapitolách jsme mohli vidět, že generovaný kód není v žádném případě dokonalý. Překladač cc65 nabízí přepínač -O pro zapnutí základních optimalizací (ovšem nečekejme žádné zázraky, nejedná se o clang/LLVM ani o gcc):
$ cc65 -T -Cl -O local_add.c
Opět se podívejme na vygenerovaný kód v assembleru:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _main ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "BSS" L0002: .res 1,$00 L0003: .res 1,$00 L0004: .res 1,$00 .segment "CODE" ; ; a = 10; ; lda #$0A sta L0002 ; ; b = 20; ; lda #$14 sta L0003 ; ; c = a + b; ; lda L0002 clc adc L0003 sta L0004 ; ; } ; rts .endproc
Stále se používají statické proměnné (což je v pořádku), ovšem navíc se odstranilo neustálé nulování index registru X a namísto volání podprogramů pro součet je nyní vlastní výpočet zcela přímočarý:
; c = a + b; ; lda L0002 clc adc L0003 sta L0004
Přepínačem -Or navíc můžeme vynutit další optimalizaci – uložení hodnot na adresy umístěné v nulté stránce paměti (zero page), což umožňuje použití kratších a rychlejších instrukcí:
$ cc65 -T -Cl -Or local_add.c
Výsledek překladu:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _main ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; register unsigned char a; ; lda regbank+5 jsr pusha ; ; register unsigned char b; ; lda regbank+4 jsr pusha ; ; register unsigned char c; ; lda regbank+3 jsr pusha ; ; a = 10; ; lda #$0A sta regbank+5 ; ; b = 20; ; lda #$14 sta regbank+4 ; ; c = a + b; ; lda regbank+5 clc adc regbank+4 sta regbank+3 ; ; } ; ldy #$00 L0008: lda (sp),y sta regbank+3,y iny cpy #$03 bne L0008 jmp incsp3 .endproc
Další optimalizace lze zapnout přepínačem -Oi, který vynutí inlining kódu (mnohé funkce se tedy nebudou volat, ale jejich kód se přímo vloží na místo jejich volání):
$ cc65 -T -Cl -Oi local_add.c
Výsledek bude vypadat podobně, jako v předminulém příkladu:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _main ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "BSS" L0002: .res 1,$00 L0003: .res 1,$00 L0004: .res 1,$00 .segment "CODE" ; ; a = 10; ; lda #$0A sta L0002 ; ; b = 20; ; lda #$14 sta L0003 ; ; c = a + b; ; lda L0002 clc adc L0003 sta L0004 ; ; } ; rts .endproc
Nakonec povolíme všechny optimalizace, což ostatně dělá většina autorů:
$ cc65 -T -Cl -Osir local_add.c
S výsledkem:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _main ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; register unsigned char a; ; lda regbank+5 jsr pusha ; ; register unsigned char b; ; lda regbank+4 jsr pusha ; ; register unsigned char c; ; lda regbank+3 jsr pusha ; ; a = 10; ; lda #$0A sta regbank+5 ; ; b = 20; ; lda #$14 sta regbank+4 ; ; c = a + b; ; lda regbank+5 clc adc regbank+4 sta regbank+3 ; ; } ; ldy #$00 lda (sp),y sta regbank+3 iny lda (sp),y sta regbank+4 iny lda (sp),y sta regbank+5 jmp incsp3 .endproc
V tomto případě je výsledek již poměrně dobrý: používají se proměnné uložené do nulté stránky paměti a i samotný součet je triviální:
; c = a + b; ; lda regbank+5 clc adc regbank+4 sta regbank+3
11. Funkce pro vyplnění bloku paměti specifikovanou hodnotou
V navazujících kapitolách se budeme zabývat způsobem překladu funkce memset, která má vyplnit zadaný blok paměti specifikovanou hodnotou (omezíme se však na blok o velikosti maximálně 256 bajtů). Obecný zápis zdrojového kódu této funkce, který nebere do úvahy specifika překladače ani mikroprocesoru, může vypadat takto:
#include <stdint.h> void memset8(uint8_t * dest, uint8_t c, uint8_t n) { int i; for (i = 0; i < n; i++) { dest[i] = c; } } int main(void) { uint8_t *dest = (uint8_t *) 0x0600; uint8_t bytes = 0xff; uint8_t fill = 0x00; memset8(dest, fill, bytes); return 0; }
Ze zápisu je patrné, že se nijak nesnažíme o nějaké optimalizace, explicitní použití ukazatelů atd.
12. Assembler vygenerovaný pro základní verzi funkce memset
Překladači cc65 předáme přepínače -Cl (lokální proměnné budou statické) a -O (optimalizace). Vygenerovaný kód v assembleru bude vypadat následovně. Do kódu byly na vhodná místa doplněny komentáře:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _memset8 .export _main ; --------------------------------------------------------------- ; void __near__ memset8 (__near__ unsigned char *, unsigned char, unsigned char) ; --------------------------------------------------------------- .segment "CODE" .proc _memset8: near .segment "BSS" L0002: .res 2,$00 ; místo pro uložení 16bitového počitadla .segment "CODE" ; ; { ; jsr pusha ; ; for (i = 0; i < n; i++) { ; lda #$00 sta L0002 ; nastavení počáteční hodnoty 16bitového počitadla sta L0002+1 L0003: lda L0002 ldy #$00 cmp (sp),y lda L0002+1 ; kontrola, zda počitadlo nedosáhlo konečné hodnoty sbc #$00 bcs L0004 ; podmíněný výskok ze smyčky ; ; dest[i] = c; ; ldy #$03 jsr ldaxysp clc adc L0002 ; adresa prvku do pomocné buňky ptr1 sta ptr1 txa adc L0002+1 sta ptr1+1 ; dtto pro vyšší bajt adresy ldy #$01 lda (sp),y dey sta (ptr1),y ; modifikace prvku v poli přes pomocnou buňku ptr1 s adresou ; ; for (i = 0; i < n; i++) { ; lda L0002 ldx L0002+1 jsr incax1 ; zvýšení hodnoty počitadla na konci smyčky sta L0002 stx L0002+1 jmp L0003 ; skok na začátek smyčky ; ; } ; L0004: jmp incsp4 .endproc ; --------------------------------------------------------------- ; int __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "BSS" L000F: .res 2,$00 L0011: .res 1,$00 L0013: .res 1,$00 .segment "CODE" ; ; uint8_t *dest = (uint8_t *) 0x0600; ; ldx #$06 lda #$00 sta L000F stx L000F+1 ; ; uint8_t bytes = 0xff; ; lda #$FF sta L0011 ; ; uint8_t fill = 0x00; ; lda #$00 sta L0013 ; ; memset8(dest, fill, bytes); ; lda L000F ldx L000F+1 jsr pushax lda L0013 jsr pusha lda L0011 jsr _memset8 ; ; return 0; ; ldx #$00 txa ; ; } ; rts .endproc
13. Osmibitová lokální proměnná (počitadlo smyčky) s modifikátorem register
Mnoha překladačům programovacího jazyka C lze při optimalizacích do větší či menší míry „pomoci“ použitím modifikátoru register, kterým se označí ty proměnné, které by – alespoň podle názoru programátora – mohly být umístěny přímo do pracovního registru. V našem konkrétním příkladu máme jen jedinou takovou proměnnou, a to počitadlo smyčky i:
Navíc se omezíme na maximální velikost mazaného bloku – namísto 65535 bajtů budeme uvažovat o blocích o maximální velikosti 255 bajtů. To nám umožní zmenšit bitovou šířku proměnné i na osm bitů:
#include <stdint.h> void memset8(uint8_t * dest, uint8_t c, uint8_t n) { register uint8_t i; for (i = 0; i < n; i++) { dest[i] = c; } } int main(void) { uint8_t *dest = (uint8_t *) 0x0600; uint8_t bytes = 0xff; uint8_t fill = 0x00; memset8(dest, fill, bytes); return 0; }
14. Assembler vygenerovaný pro upravenou verzi funkce memset
Při použití osmibitového počitadla, navíc s modifikátorem register, dojde k mírné úpravě kódu, který již nemusí složitě počítat 16bitové ukazatele (komentáře byly opět přidány ručně):
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _memset8 .export _main ; --------------------------------------------------------------- ; void __near__ memset8 (__near__ unsigned char *, unsigned char, unsigned char) ; --------------------------------------------------------------- .segment "CODE" .proc _memset8: near .segment "BSS" L0002: .res 1,$00 ; místo pro uložení osmibitového počitadla .segment "CODE" ; ; { ; jsr pusha ; ; for (i = 0; i < n; i++) { ; lda #$00 sta L0002 ; nastavení počáteční hodnoty osmibitového počitadla L001B: lda L0002 ldy #$00 cmp (sp),y ; kontrola, zda počitadlo nedosáhlo konečné hodnoty bcs L0004 ; podmíněný výskok ze smyčky ; ; dest[i] = c; ; ldy #$03 jsr ldaxysp clc adc L0002 ; adresa prvku do pomocné buňky ptr1 bcc L001A ; zajímavý trik na zvýšení vyššího bajtu adresy jen při přetečení! inx ; dtto L001A: sta ptr1 stx ptr1+1 ldy #$01 lda (sp),y dey sta (ptr1),y ; modifikace prvku v poli přes pomocnou buňku ptr1 s adresou ; ; for (i = 0; i < n; i++) { ; inc L0002 ; zvýšení hodnoty počitadla na konci smyčky jmp L001B ; skok na začátek smyčky ; ; } ; L0004: jmp incsp4 .endproc ; --------------------------------------------------------------- ; int __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "BSS" L000F: .res 2,$00 L0011: .res 1,$00 L0013: .res 1,$00 .segment "CODE" ; ; uint8_t *dest = (uint8_t *) 0x0600; ; ldx #$06 lda #$00 sta L000F stx L000F+1 ; ; uint8_t bytes = 0xff; ; lda #$FF sta L0011 ; ; uint8_t fill = 0x00; ; lda #$00 sta L0013 ; ; memset8(dest, fill, bytes); ; lda L000F ldx L000F+1 jsr pushax lda L0013 jsr pusha lda L0011 jsr _memset8 ; ; return 0; ; ldx #$00 txa ; ; } ; rts .endproc
Obrázek 6: Assembler se změnil takto.
Zvýrazněné rozdíly v textové podobě:
L0002: L0002: .res 2,$00 | .res 1,$00 .segment "CODE" .segment "CODE" ; ; ; { ; { ; ; jsr pusha jsr pusha ; ; ; for (i = 0; i < n; i++) { ; for (i = 0; i < n; i++) { ; ; lda #$00 lda #$00 sta L0002 sta L0002 sta L0002+1 | L001B: lda L0002 L0003: lda L0002 < ldy #$00 ldy #$00 cmp (sp),y cmp (sp),y lda L0002+1 < sbc #$00 < bcs L0004 bcs L0004 ; ; ; dest[i] = c; ; dest[i] = c; ; ; ldy #$03 ldy #$03 jsr ldaxysp jsr ldaxysp clc clc adc L0002 adc L0002 sta ptr1 | bcc L001A txa | inx adc L0002+1 | L001A: sta ptr1 sta ptr1+1 | stx ptr1+1 ldy #$01 ldy #$01 lda (sp),y lda (sp),y dey dey sta (ptr1),y sta (ptr1),y ; ; ; for (i = 0; i < n; i++) { ; for (i = 0; i < n; i++) { ; ; lda L0002 | inc L0002 ldx L0002+1 | jmp L001B jsr incax1 < sta L0002 < stx L0002+1 < jmp L0003 < ; ; ; } ; } ; ; L0004: jmp incsp4 L0004: jmp incsp4 .endproc .endproc
15. Konstantní parametry funkce
V některých případech je možné překladači dále „pomoci“, například tak, že se explicitně specifikují konstantní parametry funkce. To může překladači usnadnit předávání hodnot (resp. tomto případě jejich nepředávání a nahrazení původními adresami) atd. Vyzkoušejme si, zda bude mít tato úprava nějaký vliv na výsledný programový kód:
#include <stdint.h> void memset8(uint8_t * dest, const uint8_t c, const uint8_t n) { register uint8_t i; for (i = 0; i < n; i++) { dest[i] = c; } } int main(void) { uint8_t *dest = (uint8_t *) 0x0600; uint8_t bytes = 0xff; uint8_t fill = 0x00; memset8(dest, fill, bytes); return 0; }
Ve skutečnosti bude v tomto konkrétním případě vygenerovaný assembler prakticky totožný s předchozím příkladem (pochopitelně až na odlišnou hlavičku funkce):
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _memset8 .export _main ; --------------------------------------------------------------- ; void __near__ memset8 (__near__ unsigned char *, const unsigned char, const unsigned char) ; --------------------------------------------------------------- .segment "CODE" .proc _memset8: near .segment "BSS" L0002: .res 1,$00 .segment "CODE" ; ; { ; jsr pusha ; ; for (i = 0; i < n; i++) { ; lda #$00 sta L0002 L001B: lda L0002 ldy #$00 cmp (sp),y bcs L0004 ; ; dest[i] = c; ; ldy #$03 jsr ldaxysp clc adc L0002 bcc L001A inx L001A: sta ptr1 stx ptr1+1 ldy #$01 lda (sp),y dey sta (ptr1),y ; ; for (i = 0; i < n; i++) { ; inc L0002 jmp L001B ; ; } ; L0004: jmp incsp4 .endproc ; --------------------------------------------------------------- ; int __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "BSS" L000F: .res 2,$00 L0011: .res 1,$00 L0013: .res 1,$00 .segment "CODE" ; ; uint8_t *dest = (uint8_t *) 0x0600; ; ldx #$06 lda #$00 sta L000F stx L000F+1 ; ; uint8_t bytes = 0xff; ; lda #$FF sta L0011 ; ; uint8_t fill = 0x00; ; lda #$00 sta L0013 ; ; memset8(dest, fill, bytes); ; lda L000F ldx L000F+1 jsr pushax lda L0013 jsr pusha lda L0011 jsr _memset8 ; ; return 0; ; ldx #$00 txa ; ; } ; rts .endproc
16. Přímý přístup do paměti přes ukazatel, využití smyčky while
Všechny předchozí implementace funkce memset byly založeny na počítané programové smyčce for a na přístupu do paměti přes selektor pole, tedy s využitím operátoru []. Moderní překladače sice dokážou i takový zápis optimalizovat (ostatně a[b] je identické s *(a+b))", ovšem to není případ překladače cc65. Pokusme se tedy o přepis, resp. optimalizaci na úrovni sémantiky – budeme přímo manipulovat s ukazatelem do paměti, která se má modifikovat a namísto programové smyčky for použijeme smyčku while:
#include <stdint.h> void memset8(uint8_t * dest, const uint8_t c, uint8_t n) { while (n > 0) { *dest++ = c; n--; } } int main(void) { uint8_t *dest = (uint8_t *) 0x0600; uint8_t bytes = 0xff; uint8_t fill = 0x00; memset8(dest, fill, bytes); return 0; }
Nyní bude výsledný kód vygenerovaný překladačem zcela odlišný (komentáře jsem opět dodal ručně):
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _memset8 .export _main ; --------------------------------------------------------------- ; void __near__ memset8 (__near__ unsigned char *, const unsigned char, unsigned char) ; --------------------------------------------------------------- .segment "CODE" .proc _memset8: near .segment "CODE" ; ; { ; jsr pusha ; ; while (n > 0) { ; jmp L0004 ; začínáme na konci kódu s testem počitadla ; ; *dest++ = c; ; L0002: ldy #$03 jsr ldaxysp sta regsave ; výpočet ukazatele stx regsave+1 jsr incax1 ldy #$02 jsr staxysp ldy #$01 lda (sp),y dey sta (regsave),y ; zápis hodnoty přes ukazatel ; ; n--; ; lda (sp),y ; snížení hodnoty počitadla o jedničku sec sbc #$01 sta (sp),y ; (šlo by provést přes jedinou instrukci dec!) ; ; while (n > 0) { ; L0004: ldy #$00 lda (sp),y ; načtení hodnoty počitadla s testem na nulu bne L0002 ; není nulové? -> pokračujeme další iterací ; ; } ; jmp incsp4 .endproc ; --------------------------------------------------------------- ; int __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "BSS" L000A: .res 2,$00 L000C: .res 1,$00 L000E: .res 1,$00 .segment "CODE" ; ; uint8_t *dest = (uint8_t *) 0x0600; ; ldx #$06 lda #$00 sta L000A stx L000A+1 ; ; uint8_t bytes = 0xff; ; lda #$FF sta L000C ; ; uint8_t fill = 0x00; ; lda #$00 sta L000E ; ; memset8(dest, fill, bytes); ; lda L000A ldx L000A+1 jsr pushax lda L000E jsr pusha lda L000C jsr _memset8 ; ; return 0; ; ldx #$00 txa ; ; } ; rts .endproc
17. Otočení smyčky – využití stejné sémantiky, jako při práci v assembleru
V případě, že nám nebude vadit, že funkce memset8 nebude funkční při nulové velikosti bloku, lze smyčku otočit a test na její ukončení přesunout až na konec. Kód se tedy začne podobat kódu, který bychom zapsali přímo v assembleru:
- Provedení jedné iterace: zápis hodnoty do paměti
- Zvýšení hodnoty ukazatele o jedničku
- Snížení hodnoty počitadla o jedničku
- Pokud jsme nedosáhli nuly, skok na první bod
V céčku by tento zápis mohl vypadat takto:
#include <stdint.h> void memset8(uint8_t * dest, const uint8_t c, uint8_t n) { /* pozor na hodnotu n=0! */ do { *dest++ = c; n--; } while (n > 0); } int main(void) { uint8_t *dest = (uint8_t *) 0x0600; uint8_t bytes = 0xff; uint8_t fill = 0x00; memset8(dest, fill, bytes); return 0; }
Opět se podívejme, jak bude vypadat výsledek po přeložení do assembleru:
; ; File generated by cc65 v 2.18 - Ubuntu 2.18-1 ; .fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1" .setcpu "6502" .smart on .autoimport on .case on .debuginfo off .importzp sp, sreg, regsave, regbank .importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4 .macpack longbranch .forceimport __STARTUP__ .export _memset8 .export _main ; --------------------------------------------------------------- ; void __near__ memset8 (__near__ unsigned char *, const unsigned char, unsigned char) ; --------------------------------------------------------------- .segment "CODE" .proc _memset8: near .segment "CODE" ; ; { ; jsr pusha ; ; *dest++ = c; ; L0002: ldy #$03 jsr ldaxysp sta regsave stx regsave+1 jsr incax1 ldy #$02 jsr staxysp ldy #$01 lda (sp),y dey sta (regsave),y ; ; n--; ; lda (sp),y sec sbc #$01 sta (sp),y ; ; } while (n > 0); ; lda (sp),y bne L0002 ; ; } ; jmp incsp4 .endproc ; --------------------------------------------------------------- ; int __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "BSS" L000A: .res 2,$00 L000C: .res 1,$00 L000E: .res 1,$00 .segment "CODE" ; ; uint8_t *dest = (uint8_t *) 0x0600; ; ldx #$06 lda #$00 sta L000A stx L000A+1 ; ; uint8_t bytes = 0xff; ; lda #$FF sta L000C ; ; uint8_t fill = 0x00; ; lda #$00 sta L000E ; ; memset8(dest, fill, bytes); ; lda L000A ldx L000A+1 jsr pushax lda L000E jsr pusha lda L000C jsr _memset8 ; ; return 0; ; ldx #$00 txa ; ; } ; rts .endproc
Až na jeden skok jsme dostali identický kód:
Obrázek 7: Assembler se změnil jen nepatrně.
18. Obsah navazujícího článku
V dnešním článku jsme si popsali pouze základní vlastnosti překladače cc65. Ve druhé a současně i závěrečné části článku si řekneme, jakým způsobem můžeme využít některé jeho speciální vlastnosti, například možnost mixovat kód psaný v jazyku C s assemblerem (na úrovni zdrojového kódu) nebo použití pseudoproměnných A, AX atd. Taktéž se zmíníme o některých vlastnostech standardního jazyka C, které nejsou v cc65 plně podporovány.
19. Repositář s demonstračními příklady
Demonstrační příklady napsané v jazyku C, které jsou určené pro překlad pomocí překladače cc65, byly uložen 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ář:
# | Příklad | Stručný popis | Adresa |
---|---|---|---|
1 | local_add.c | funkce pro součet dvou celých čísel | https://github.com/tisnik/8bit-fame/blob/master/cc65/local_add.c |
2 | memset1.c | vyplnění bloku paměti zadanou hodnotou, základní verze | https://github.com/tisnik/8bit-fame/blob/master/cc65/memset1.c |
3 | memset2.c | vyplnění bloku paměti zadanou hodnotou, použití klíčového slova register | https://github.com/tisnik/8bit-fame/blob/master/cc65/memset2.c |
4 | memset3.c | vyplnění bloku paměti zadanou hodnotou, použití konstantních parametrů | https://github.com/tisnik/8bit-fame/blob/master/cc65/memset3.c |
5 | memset4.c | vyplnění bloku paměti zadanou hodnotou, přepis na smyčku while | https://github.com/tisnik/8bit-fame/blob/master/cc65/memset4.c |
6 | memset5.c | vyplnění bloku paměti zadanou hodnotou, přepis na smyčku do-while | https://github.com/tisnik/8bit-fame/blob/master/cc65/memset5.c |
7 | Makefile | překlad všech demonstračních příkladů do assembleru s různými volbami | https://github.com/tisnik/8bit-fame/blob/master/cc65/Makefile |
20. Odkazy na Internetu
- When did people first start thinking ‚C is portable assembler‘?
https://stackoverflow.com/questions/3040276/when-did-people-first-start-thinking-c-is-portable-assembler - The Thirty Million Line Problem
https://www.youtube.com/watch?v=kZRE7HIO3vk - NesDev.org
https://www.nesdev.org/ - How to Program an NES game in C
https://nesdoug.com/ - Cycle reference chart
https://www.nesdev.org/wiki/Cycle_reference_chart - Getting Started Programming in C: Coding a Retro Game with C Part 2
https://retrogamecoders.com/getting-started-with-c-cc65/ - NES game development in 6502 assembly – Part 1
https://kibrit.tech/en/blog/nes-game-development-part-1 - NES 6502 Programming Tutorial – Part 1: Getting Started
https://dev.xenforo.relay.cool/index.php?threads/nes-6502-programming-tutorial-part-1-getting-started.858389/ - List of 6502-based Computers and Consoles
https://www.retrocompute.co.uk/list-of-6502-based-computers-and-consoles/ - 6502 – the first RISC µP
http://ericclever.com/6500/ - ca65 Users Guide
https://cc65.github.io/doc/ca65.html - cc65 Users Guide
https://cc65.github.io/doc/cc65.html - ld65 Users Guide
https://cc65.github.io/doc/ld65.html - da65 Users Guide
https://cc65.github.io/doc/da65.html - “Hello, world” from scratch on a 6502 — Part 1
https://www.youtube.com/watch?v=LnzuMJLZRdU - A Tour of 6502 Cross-Assemblers
https://bumbershootsoft.wordpress.com/2016/01/31/a-tour-of-6502-cross-assemblers/ - 6502 PRIMER: Building your own 6502 computer
http://wilsonminesco.com/6502primer/ - 6502 Instruction Set
https://www.masswerk.at/6502/6502_instruction_set.html - Chip Hall of Fame: MOS Technology 6502 Microprocessor
https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor - Single-board computer
https://en.wikipedia.org/wiki/Single-board_computer - www.6502.org
http://www.6502.org/ - 6502 PRIMER: Building your own 6502 computer – clock generator
http://wilsonminesco.com/6502primer/ClkGen.html - Great Microprocessors of the Past and Present (V 13.4.0)
http://www.cpushack.com/CPU/cpu.html - Jak se zrodil procesor?
https://www.root.cz/clanky/jak-se-zrodil-procesor/ - Osmibitové mikroprocesory a mikrořadiče firmy Motorola (1)
https://www.root.cz/clanky/osmibitove-mikroprocesory-a-mikroradice-firmy-motorola-1/ - Mikrořadiče a jejich použití v jednoduchých mikropočítačích
https://www.root.cz/clanky/mikroradice-a-jejich-pouziti-v-jednoduchych-mikropocitacich/ - Mikrořadiče a jejich aplikace v jednoduchých mikropočítačích (2)
https://www.root.cz/clanky/mikroradice-a-jejich-aplikace-v-jednoduchych-mikropocitacich-2/ - 25 Microchips That Shook the World
https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world - Comparison of instruction set architectures
https://en.wikipedia.org/wiki/Comparison_of_instruction_set_architectures - Vývojové nástroje používané v dobách osmibitových mikropočítačů
https://www.root.cz/clanky/vyvojove-nastroje-pouzivane-v-dobach-osmibitovych-mikropocitacu/ - Historie vývoje počítačových her (112. část – vývojové nástroje pro herní konzole)
https://www.root.cz/clanky/historie-vyvoje-pocitacovych-her-112-cast-vyvojove-nastroje-pro-herni-konzole/ - Programovací jazyky a vývojové nástroje pro mikropočítače společnosti Sinclair Research
https://www.root.cz/clanky/programovaci-jazyky-a-vyvojove-nastroje-pro-mikropocitace-spolecnosti-sinclair-research/ - Programovací jazyky používané na platformě osmibitových domácích mikropočítačů Atari
https://www.root.cz/clanky/programovaci-jazyky-pouzivane-na-platforme-osmibitovych-domacich-mikropocitacu-atari/ - Programovací jazyky používané na platformě osmibitových domácích mikropočítačů Atari (2)
https://www.root.cz/clanky/programovaci-jazyky-pouzivane-na-platforme-osmibitovych-domacich-mikropocitacu-atari-2/ - Cross assemblery a cross překladače pro platformu osmibitových domácích mikropočítačů Atari
https://www.root.cz/clanky/cross-assemblery-a-cross-prekladace-pro-platformu-osmibitovych-domacich-mikropocitacu-atari/ - C Isn't A Programming Language Anymore
https://faultlore.com/blah/c-isnt-a-language/ - Why the C Language Will Never Stop You from Making Mistakes
https://thephd.dev/your-c-compiler-and-standard-library-will-not-help-you - Benchmark: C compilers for the 6502 CPU
https://sgadrat.itch.io/super-tilt-bro/devlog/219534/benchmark-c-compilers-for-the-6502-cpu - Advanced optimizations in CC65
https://github.com/ilmenit/CC65-Advanced-Optimizations - The 6502/65C02/65C816 Instruction Set Decoded
https://llx.com/Neil/a2/opcodes.html - 6502 C Compilers Comparison
https://gglabs.us/node/2293 - 6502 C compilers benchmark
https://github.com/sgadrat/6502-compilers-bench - cc65: Differences to the ISO standard
https://cc65.github.io/doc/cc65.html#s4 - Compilers
http://www.6502.org/tools/lang/