Překladače jazyka C pro historické osmibitové mikroprocesory

15. 9. 2022
Doba čtení: 33 minut

Sdílet

 Autor: Michal Tauchman, podle licence: CC BY-SA 4.0
O programovacím jazyku C se někdy s nadsázkou říká, že je to „přenositelný assembler“. Programy psané v 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.

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

3. cc65

4. vbcc

5. 6502-gcc

6. Instalace ca65 a cc65

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

9. Statické lokální proměnné

10. Zapnutí optimalizací společně s využitím statických lokálních proměnných, proměnných v registrech atd.

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

18. Obsah navazujícího článku

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

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.

Poznámka: tyto zvláštnosti MOS 6502 dosti podstatným způsobem ovlivňují i backend céčkovského překladače.

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/wi­ki/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!");
$)
Poznámka: což připomíná trigraphy C.

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.

Poznámka: existuje i český termín křížový překladač, ale musím se přiznat, že mi připadá jako výsledek otrockého překladu a navíc se slovo „cross“ přeložilo ve špatném kontextu.

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í).

Poznámka: další informace o překladačích i cross překladačích Aztec C lze najít na stránce http://aztecmuseum.ca/compilers.htm.

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).

Poznámka: současně cc65 negeneruje optimální kód, což si ostatně ukážeme v navazujících kapitolách.

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

Poznámka: v dalším textu se budeme téměř výhradně zabývat překladačem cc65, což je nejznámější, avšak nikoli nejkvalitnější překladač céčka pro osmibitové mikroprocesory MOS 6502.

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
Poznámka: nainstalován je ale například i disassembler a dokonce i simulátor mikroprocesoru MOS 6502 atd.

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;
}
Poznámka: tento zdrojový kód je uložen na adrese https://github.com/tisnik/8bit-fame/blob/master/cc65/local_add.c.

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
Poznámka: I při letmém pohledu je zřejmé, že se v žádném případě nejedná o příliš optimalizovaný kód. V dalších kapitolách si proto ukážeme vliv přepínačů překladače na strukturu vygenerovaného kódu.

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.

Poznámka: teoreticky ne vždy je možné tuto jednoznačnou vazbu zajistit, zejména při aplikaci složitých optimalizací, kdy dochází k promíchávání různých částí kódu. Takových optimalizací však stejně cc65 není schopen.

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
Poznámka: neustálé ukládání nuly do index registru X je zcela zbytečné, což ostatně platí i pro předchozí příklad.

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
Poznámka: z takto reprezentovaného kódu to není zřejmé, ovšem regbank je adresa do nulté stránky paměti, takže instrukce lda, sta atd. mohou být optimalizovány do kratší a rychlejší verze (ušetří se vždy jeden bajt a jeden strojový cyklus).

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.

Poznámka: zdrojový kód této funkce naleznete na adrese https://github.com/tisnik/8bit-fame/blob/master/cc65/memset1.c.

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;
}
Poznámka: zdrojový kód této funkce naleznete na adrese https://github.com/tisnik/8bit-fame/blob/master/cc65/memset2.c.

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
Poznámka: sice se jedná o kratší kód, ovšem ruční vyplnění bloku paměti je stále mnohem lepší – lze ho provést se třetinou instrukcí.

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:

  1. Provedení jedné iterace: zápis hodnoty do paměti
  2. Zvýšení hodnoty ukazatele o jedničku
  3. Snížení hodnoty počitadla o jedničku
  4. 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ě.

bitcoin školení listopad 24

Poznámka: stále se však nejedná o optimální kód!

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

  1. When did people first start thinking ‚C is portable assembler‘?
    https://stackoverflow.com/qu­estions/3040276/when-did-people-first-start-thinking-c-is-portable-assembler
  2. The Thirty Million Line Problem
    https://www.youtube.com/wat­ch?v=kZRE7HIO3vk
  3. NesDev.org
    https://www.nesdev.org/
  4. How to Program an NES game in C
    https://nesdoug.com/
  5. Cycle reference chart
    https://www.nesdev.org/wi­ki/Cycle_reference_chart
  6. Getting Started Programming in C: Coding a Retro Game with C Part 2
    https://retrogamecoders.com/getting-started-with-c-cc65/
  7. NES game development in 6502 assembly – Part 1
    https://kibrit.tech/en/blog/nes-game-development-part-1
  8. 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/
  9. List of 6502-based Computers and Consoles
    https://www.retrocompute.co.uk/list-of-6502-based-computers-and-consoles/
  10. 6502 – the first RISC µP
    http://ericclever.com/6500/
  11. ca65 Users Guide
    https://cc65.github.io/doc/ca65.html
  12. cc65 Users Guide
    https://cc65.github.io/doc/cc65.html
  13. ld65 Users Guide
    https://cc65.github.io/doc/ld65.html
  14. da65 Users Guide
    https://cc65.github.io/doc/da65.html
  15. “Hello, world” from scratch on a 6502 — Part 1
    https://www.youtube.com/wat­ch?v=LnzuMJLZRdU
  16. A Tour of 6502 Cross-Assemblers
    https://bumbershootsoft.wor­dpress.com/2016/01/31/a-tour-of-6502-cross-assemblers/
  17. 6502 PRIMER: Building your own 6502 computer
    http://wilsonminesco.com/6502primer/
  18. 6502 Instruction Set
    https://www.masswerk.at/6502/6502_in­struction_set.html
  19. Chip Hall of Fame: MOS Technology 6502 Microprocessor
    https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor
  20. Single-board computer
    https://en.wikipedia.org/wiki/Single-board_computer
  21. www.6502.org
    http://www.6502.org/
  22. 6502 PRIMER: Building your own 6502 computer – clock generator
    http://wilsonminesco.com/6502pri­mer/ClkGen.html
  23. Great Microprocessors of the Past and Present (V 13.4.0)
    http://www.cpushack.com/CPU/cpu.html
  24. Jak se zrodil procesor?
    https://www.root.cz/clanky/jak-se-zrodil-procesor/
  25. Osmibitové mikroprocesory a mikrořadiče firmy Motorola (1)
    https://www.root.cz/clanky/osmibitove-mikroprocesory-a-mikroradice-firmy-motorola-1/
  26. 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/
  27. 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/
  28. 25 Microchips That Shook the World
    https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world
  29. Comparison of instruction set architectures
    https://en.wikipedia.org/wi­ki/Comparison_of_instructi­on_set_architectures
  30. 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/
  31. 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/
  32. Programovací jazyky a vývojové nástroje pro mikropočítače společnosti Sinclair Research
    https://www.root.cz/clanky/pro­gramovaci-jazyky-a-vyvojove-nastroje-pro-mikropocitace-spolecnosti-sinclair-research/
  33. Programovací jazyky používané na platformě osmibitových domácích mikropočítačů Atari
    https://www.root.cz/clanky/pro­gramovaci-jazyky-pouzivane-na-platforme-osmibitovych-domacich-mikropocitacu-atari/
  34. Programovací jazyky používané na platformě osmibitových domácích mikropočítačů Atari (2)
    https://www.root.cz/clanky/pro­gramovaci-jazyky-pouzivane-na-platforme-osmibitovych-domacich-mikropocitacu-atari-2/
  35. 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/
  36. C Isn't A Programming Language Anymore
    https://faultlore.com/blah/c-isnt-a-language/
  37. Why the C Language Will Never Stop You from Making Mistakes
    https://thephd.dev/your-c-compiler-and-standard-library-will-not-help-you
  38. Benchmark: C compilers for the 6502 CPU
    https://sgadrat.itch.io/super-tilt-bro/devlog/219534/benchmark-c-compilers-for-the-6502-cpu
  39. Advanced optimizations in CC65
    https://github.com/ilmenit/CC65-Advanced-Optimizations
  40. The 6502/65C02/65C816 Instruction Set Decoded
    https://llx.com/Neil/a2/opcodes.html
  41. 6502 C Compilers Comparison
    https://gglabs.us/node/2293
  42. 6502 C compilers benchmark
    https://github.com/sgadrat/6502-compilers-bench
  43. cc65: Differences to the ISO standard
    https://cc65.github.io/doc/cc65­.html#s4
  44. Compilers
    http://www.6502.org/tools/lang/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.