Obsah
1. Dokončení popisu překladače jazyka C pro osmibitové mikroprocesory MOS 6502: cc65
2. Volba, do jaké paměťové oblasti se mají uložit lokální proměnné
3. Vliv klíčového slova register na způsob uložení lokálních proměnných
4. Přímý přístup k akumulátoru s využitím identifikátoru __A_
5. Přístup k registrovému páru A + X s využitím identifikátoru __AX__
6. Přístup k 32bitové hodnotě uložené ve dvou buňkách RAM a v registrovém páru A + X
7. Rozdíl mezi chováním operátoru pro pre-inkrementaci a post-inkrementaci
8. Součet celých čísel bez znaménka a se znaménkem
9. Modifikace bloku paměti s využitím ukazatele a offsetu
10. Přímý zápis instrukcí v assembleru do céčkového kódu
11. Použití reference na lokální proměnnou v assembleru
12. Nepatrně složitější příklad: ručně optimalizovaný součet dvou proměnných
13. Přístup ke globálním proměnným z assembleru
14. Bitové posuny a další operace prováděné s akumulátorem
15. Násobení a dělení dvěma popř. mocninou dvou
16. cc65 nakonec není tak hloupý – násobení třemi a pěti
19. Repositář s demonstračními příklady
1. Dokončení popisu překladače jazyka C pro osmibitové mikroprocesory MOS 6502: cc65
Na úvodní článek o překladačích jazyka C určených pro různé osmibitové procesory dnes navážeme. Připomeňme si, že jsme se zabývali překladem zdrojových kódů napsaných v céčku do assembleru (a posléze i strojového kódu) osmibitových mikroprocesorů MOS 6502, které byly použity například v domácích počítačích Atari a Commodore, v osobních mikropočítačích Apple, v herních konzolích Atari 2600 (zjednodušený MOS 6507), NES atd. Jednalo se tedy o populární mikroprocesor, pro nějž vzniklo hned několik překladačů jazyka C. Nás však zajímá především cross překladač cc65, který umožňuje překlad zdrojových kódů na PC s přenosem výsledného strojového kódu na zvolenou osmibitovou architekturu. Cross překladač cc65 je poměrně populární, ovšem neprovádí žádné složitější optimalizace a pro jeho úspěšné použití je nutné znát jeho limity a různá temná zákoutí – v opačném případě nebude produkovaný kód kvalitní.
Na úvod se podívejme na to, jakým způsobem se přeloží prázdná funkce main, kterou jsme navíc (poněkud v rozporu se standardem céčka) nadeklarovali bez parametrů a bez návratové hodnoty:
void main(void) { }
Překlad do assembleru provedeme příkazem:
$ cc65 -T -Cl -O -o empty_main.asm empty_main.c
Význam jednotlivých přepínačů:
- -T zajistí, že se do kódu v assembleru v poznámkách vloží i původní zdrojový kód, takže bude zřejmé, jaké strojové instrukce odpovídají danému řádku v céčku.
- -Cl povolí použití statických lokálních proměnných, což vede ke kratšímu a současně i rychlejšímu kódu, protože MOS 6502 neumí (dobře) adresovat operandy na zásobníku.
- -O povolí základní optimalizace prováděně překladačem. Bez tohoto přepínače nemá smysl cc65 vůbec používat (skutečně!).
- -o je volba určující jméno výsledného souboru s assemblerem.
Výsledkem bude soubor nazvaný empty_main.asm, jenž bude vypadat 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" ; ; } ; rts .endproc
Povšimněte si, že překlad je alespoň v tomto případě velmi úsporný – samotný kód obsahuje pouze jedinou instrukci rts neboli návrat z podprogramu (subrutiny).
2. Volba, do jaké paměťové oblasti se mají uložit lokální proměnné
Nyní do původně prázdné funkce main přidáme deklaraci a inicializaci lokální proměnné:
void main(void) { unsigned char x = 42; }
Na moderních mikroprocesorových architekturách by se tato proměnná uložila do zásobníkového rámce (stack frame), což je ovšem na MOS 6502 obtížná operace (zásobníkové rámce by se musely relativně složitě emulovat). Proto existuje možnost uložit tuto proměnnou do segmentu BSS, který není obsažen v objektovém kódu – jedná se jen o pojmenování paměťové oblasti:
.segment "BSS" L0002: .res 1,$00
Přístup k této paměťové oblasti vyžaduje „dlouhé“ adresování, tedy použití 16bitové adresy v instrukci:
lda #$2A sta L0002
Úplný kód napsaný v assembleru vzniklý překladem původního céčkového kódu 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 "BSS" L0002: .res 1,$00 .segment "CODE" ; ; unsigned char x = 42; ; lda #$2A sta L0002 ; ; } ; rts .endproc
Alternativně je možné přepínačem -Or zajistit, že se původně lokální nestatické proměnné uloží do nulté stránky paměti popř. přímo do pracovního registru A (akumulátoru):
; ; 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" ; ; unsigned char x = 42; ; lda #$2A jsr pusha ; ; } ; jmp incsp1 .endproc
3. Vliv klíčového slova register na způsob uložení lokálních proměnných
Programovací jazyk C umožňuje v deklaraci proměnných (resp. lokálních proměnných) použít klíčové slovo register, kterým se překladači říká, že daná proměnná je využívána často (například v programové smyčce) a bylo by ji tedy vhodné umístit přímo do registru. Co však toto klíčové slovo říká překladači cc65, který musí pracovat pouze se třemi registry? Můžeme si to relativně snadno vyzkoušet:
void main(void) { register unsigned char x = 42; }
Překlad provedeme dvakrát, nejdříve s přepínači -Cl a -O:
; ; 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 .segment "CODE" ; ; register unsigned char x = 42; ; lda #$2A sta L0002 ; ; } ; rts .endproc
Druhý překlad je proveden s přepínači -Or a -O:
; ; 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 x = 42; ; lda regbank+5 jsr pusha lda #$2A sta regbank+5 ; ; } ; ldy #$00 lda (sp),y sta regbank+5 jmp incsp1 .endproc
Nyní je stejná proměnná umístěna na nultou stránku paměti, takže přístup k ní bude rychlejší a i výsledný kód může být kratší.
Podívejme se, jak rozdíl v obou generovaných assemblerech zvýrazní diff:
; ------------------------------------------- ; ------------------------------------------- ; void __near__ main (void) ; void __near__ main (void) ; ------------------------------------------- ; ------------------------------------------- .segment "CODE" .segment "CODE" .proc _main: near .proc _main: near .segment "BSS" < < L0002: < .res 1,$00 < < .segment "CODE" .segment "CODE" ; ; ; register unsigned char x = 42; ; register unsigned char x = 42; ; ; > lda regbank+5 > jsr pusha lda #$2A lda #$2A sta L0002 | sta regbank+5 ; ; ; } ; } ; ; rts | ldy #$00 > lda (sp),y > sta regbank+5 > jmp incsp1 .endproc .endproc
4. Přímý přístup k akumulátoru s využitím identifikátoru __A__
Překladač cc65 umožňuje přímou manipulaci s akumulátorem s využitím identifikátoru __A__. Z pohledu programátora se jedná o proměnnou typu unsigned char. Podívejme se na jednoduchý příklad manipulace s akumulátorem:
void main(void) { __A__ = 42; ++__A__; }
Po překladu do assembleru uvidíme, že výsledkem je do značné míry optimální kód – viz zvýrazněné instrukce:
; ; 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__ = 42; ; lda #$2A ; ; ++__A__; ; clc adc #$01 ; ; } ; rts .endproc
5. Přístup k registrovému páru A + X s využitím identifikátoru __AX__
V předchozí kapitole jsme si ukázali, jak lze velmi snadno využít __A__ ve funkci velmi rychle přístupné proměnné typu unsigned char (jejíž obsah je ovšem později přepsán dalšími příkazy). Podobně lze použít identifikátor __AX__, který představuje registrový pár A + X, ve funkci lokální proměnné typu unsigned short int, tedy 16bitového celého čísla bez znaménka:
void main(void) { __AX__ = 0x1234; ++__AX__; }
Podívejme se nyní na vygenerovaný kód, který je velmi zajímavý. Povšimněte si, že registr X obsahuje horní bajt, zatímco akumulátor A dolní bajt šestnáctibitové hodnoty:
; ; 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" ; ; __AX__ = 0x1234; ; ldx #$12 lda #$34 ; ; ++__AX__; ; jsr incax1 ; ; } ; rts .endproc
Zajímavé je, že při zapnutí všech optimalizací se ++__AX__; přeloží takovým způsobem, že se zvyšuje pouze hodnota akumulátoru, což je chyba překladače (překlad bude stejný, i když použijeme například konstantu 0×00ff):
; ; 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" ; ; __AX__ = 0x1234; ; lda #$34 ; ; ++__AX__; ; clc adc #$01 ; ; } ; rts .endproc
6. Přístup k 32bitové hodnotě uložené ve dvou buňkách RAM a v registrovém páru A + X
Pokračujme dále v popisu speciálních vlastností překladače céčka cc65. V některých případech je nutné pracovat s 32bitovými hodnotami. To již pro osmibitový mikroprocesor MOS 6502 vybavený pouhými třemi osmibitovými registry představuje poměrně zásadní problém. Překladač cc65 sice programátorům nabízí pseudoproměnnou nazvanou __EAX__, která je 32bitová, ovšem interně nemůže být tato hodnota uložena v registrech mikroprocesoru (alespoň ne celá). Proto cc65 přistupuje ke kompromisu – horních šestnáct bitů je uloženo v nulté stránce paměti a dolních šestnáct bitů v nám již známé dvojici registrů X a A (Y se používá při adresování, předávání hodnot interním subrutinám atd.):
void main(void) { __EAX__ = 0x12345678; ++__EAX__; }
Zajímavý bude překlad tohoto céčkového kódu 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 _main ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; __EAX__ = 0x12345678; ; ldx #$56 lda #$34 sta sreg lda #$12 sta sreg+1 lda #$78 ; ; ++__EAX__; ; ldy #$01 jsr inceaxy ; ; } ; rts .endproc
V tomto případě žádné optimalizace nepomůžou – optimalizován je jen poslední skok do subrutiny jsr následovaný návratem ze subrutiny rts za běžný skok jmp:
; ; 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" ; ; __EAX__ = 0x12345678; ; ldx #$56 lda #$34 sta sreg lda #$12 sta sreg+1 lda #$78 ; ; ++__EAX__; ; ldy #$01 jmp inceaxy .endproc
7. Rozdíl mezi chováním operátoru pro pre-inkrementaci a post-inkrementaci
Na stránce cc65 coding hints jsou uvedeny některé užitečné tipy pro práci s překladačem cc65. Mj. se zde dozvíme, že je vhodnější používat prefixový operátor ++ (resp. –) a nikoli postfixovou variantu stejného operátoru. Teoreticky – pokud výsledek nijak nepoužijeme (tedy zaměříme se jen na vedlejší efekt tohoto operátoru) by měl být přeložený kód totožný, ovšem v případě cc65 tomu tak není. Snadno si to můžeme otestovat na těchto dvou příkladech, které oba de facto provádí stejnou operaci – zvýšení hodnoty interní proměnné x o jedničku:
void main(void) { register unsigned int x = 42; x++; }
void main(void) { register unsigned int x = 42; ++x; }
Výsledkem překladu prvního příkladu s x++ je kód, v němž jsou pro výpočet použity registry A a X, přičemž se testuje přetečení do vyššího bajtu (instrukce bcc):
; ; 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 2,$00 .segment "CODE" ; ; register unsigned int x = 42; ; ldx #$00 lda #$2A sta L0002 stx L0002+1 ; ; x++; ; clc adc #$01 bcc L0005 inx L0005: sta L0002 stx L0002+1 ; ; } ; rts .endproc
Při zapnutí optimalizací bude výsledný kód vypadat jinak. Až na odlišný způsob uložení proměnné se ovšem stále provádí stejný výpočet s podmíněným skokem:
; ; 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 int x = 42; ; lda #$2A jsr pusha0 ; ; x++; ; ldy #$01 lda (sp),y tax dey lda (sp),y clc adc #$01 bcc L0004 inx L0004: jsr stax0sp ; ; } ; jmp incsp2 .endproc
Výsledek překladu příkladu s ++x vypadá odlišně – celý výpočet je proveden s hodnotou uloženou v operační paměti a nikoli v pracovních registrech, a to s využitím pouhých třech instrukcí (ve výpisu jsou zvýrazněny):
; ; 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 2,$00 .segment "CODE" ; ; register unsigned int x = 42; ; ldx #$00 lda #$2A sta L0002 stx L0002+1 ; ; ++x; ; inc L0002 bne L0005 inc L0002+1 ; ; } ; L0005: rts .endproc
Po zapnutí optimalizací se paradoxně volá podprogram pro součet hodnot:
; ; 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 int x = 42; ; lda #$2A jsr pusha0 ; ; ++x; ; lda #$01 jsr addeq0sp ; ; } ; jmp incsp2 .endproc
8. Součet celých čísel bez znaménka a se znaménkem
Mikroprocesor MOS 6502 sice dokáže do určité míry pracovat s hodnotami se znaménkem (signed char, signed int atd.), ovšem výsledný kód bude poměrně komplikovaný. Ostatně můžeme se o tom přesvědčit sami při porovnání assembleru vygenerovaného pro následující dva céčkové zdrojové kódy, které se liší „jen“ rozdílem mezi signed a unsigned (což na moderních mikroprocesorech není vůbec problematické).
Operace s hodnotami bez znaménka:
void main(void) { register unsigned char a; register unsigned char b; register unsigned char c; a = 10; b = 20; c = a + b; }
Operace s hodnotami se znaménkem:
void main(void) { register signed char a; register signed char b; register signed char c; a = 10; b = 20; c = a + b; }
První příklad, tj. součet dvou hodnot bez znaménka, se přeloží přímočaře (i když ruční optimalizace jsou stále na místě):
; ; 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
Ovšem u druhého příkladu, tj. při součtu dvou hodnot se znaménkem, je již vygenerovaný kód dosti komplikovaný a vyžaduje dvojici podmíněných skoků bpl (skok, pokud je hodnota kladná):
; ; 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; ; ldx #$00 lda L0002 bpl L000B dex L000B: sta ptr1 stx ptr1+1 ldx #$00 lda L0003 bpl L000C dex L000C: clc adc ptr1 pha txa adc ptr1+1 pla cmp #$80 sta L0004 ; ; } ; rts .endproc
9. Modifikace bloku paměti s využitím ukazatele a offsetu
Minule jsme si ukázali několik způsobů implementace funkce nazvané memset8, která dokáže vyplnit určitý blok paměti (menší než 256 bajtů) zadanou hodnotou. Používali jsme přitom přístup do bloku s využitím operátoru [], tj. chovali jsme se k celému bloku jako k poli:
#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; }
Výsledný kód vypadat po optimalizaci 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 _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
Kdysi, zhruba v dobách vlády DOSu (a nastupující vlády Linuxu) se tvrdilo, že indexování prvků pole operátorem [] je pomalé a že je lepší ho nahradit za operaci *(bázová_adresa+offset), tedy za přímou manipulaci s pamětí (nebo ještě lépe kompletní eliminací offsetu a posunem bázové adresy). Jak je tomu ovšem v případě překladače cc65? Pokusme se předchozí příklad upravit tak, aby se používalo adresování bajtů v bloku paměti pomocí bázové adresy a offsetu:
#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; }
Zajímavé je, že tento příklad se přeloží do zcela totožné sekvence instrukcí, jako příklad předchozí, takže naše „optimalizace“ na úrovni céčkového kódu v tomto případě postrádá smysl:
; ; 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
10. Přímý zápis instrukcí v assembleru do céčkového kódu
V rámci předchozích kapitol jsme si ukázali některé problematické rysy překladače cc65. V případě, že narazíme na neoptimální kód, lze situaci zachránit, a to přímým zápisem instrukcí v assembleru. Pro tento účel se používá „funkce“ nazvaná __asm__, které se v řetězci předá jak vlastní instrukce, tak i popř. proměnná, s níž má instrukce operovat. Podívejme se na ten nejjednodušší možný případ, tj. na vygenerování sekvence instrukcí, které nepracují s žádnou lokální ani globální proměnnou:
void main(void) { __asm__ ("lda #1"); __asm__ ("clc"); __asm__ ("adc #2"); }
Sekvence instrukcí, které jsme zapsali do __asm__, se objeví ve výsledném assemblerovském kódu:
; ; 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" ; ; __asm__ ("lda #1"); ; lda #1 ; ; __asm__ ("clc"); ; clc ; ; __asm__ ("adc #2"); ; adc #2 ; ; } ; rts .endproc
11. Použití reference na lokální proměnnou v assembleru
Jak jsme si již řekli v předchozí kapitole, je možné pseudofunkci __asm__ předat i proměnnou, s níž má instrukce pracovat. Například v následujícím příkladu vypočteme součet 1+2 a uložíme jeho výsledek do lokální proměnné a:
void main(void) { register unsigned char a; __asm__ ("lda #1"); __asm__ ("clc"); __asm__ ("adc #2"); __asm__ ("sta %v", a); }
V assemblerovském výpisu si povšimněte zejména toho, jakým způsobem je přeložena poslední instrukce – tedy zápis výsledku do lokální proměnné a:
; ; 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 ; ; __asm__ ("lda #1"); ; lda #1 ; ; __asm__ ("clc"); ; clc ; ; __asm__ ("adc #2"); ; adc #2 ; ; __asm__ ("sta %v", a); ; sta regbank+5 ; ; } ; ldy #$00 lda (sp),y sta regbank+5 jmp incsp1 .endproc
12. Nepatrně složitější příklad: ručně optimalizovaný součet dvou proměnných
Se znalostí toho, jak lze assemblerovským instrukcím předávat proměnné (tj. jejich jména, nikoli hodnoty nebo reference), můžeme součet c=a+b realizovat optimalizovaným assemblerem, a to například takto:
void main(void) { register unsigned char a; register unsigned char b; register unsigned char c; __asm__ ("lda %v", a); __asm__ ("clc"); __asm__ ("adc %v", b); __asm__ ("sta %v", c); }
Ve vygenerovaném assembleru je ona čtveřice instrukcí zvýrazněna, aby bylo zřejmé, že se skutečně pracuje s adresami proměnných:
; ; 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 ; ; __asm__ ("lda %v", a); ; lda regbank+5 ; ; __asm__ ("clc"); ; clc ; ; __asm__ ("adc %v", b); ; adc regbank+4 ; ; __asm__ ("sta %v", c); ; sta regbank+3 ; ; } ; ldy #$00 L000A: lda (sp),y sta regbank+3,y iny cpy #$03 bne L000A jmp incsp3 .endproc
13. Přístup ke globálním proměnným z assembleru
S naší znalostí toho, jakým způsobem je možné kombinovat céčko a assembler, se můžeme pustit i do nepatrně „složitějšího“ příkladu. Například se můžeme pokusit o realizaci součtu dvou globálních proměnných, přičemž výsledek bude taktéž uložen do globální proměnné. Samotná realizace takového programu, v němž budeme přímo generovat instrukce v assembleru, může vypadat následovně:
unsigned char a; unsigned char b; unsigned char c; void main(void) { __asm__ ("lda %v", a); __asm__ ("clc"); __asm__ ("adc %v", b); __asm__ ("sta %v", c); }
Tento program se přeloží následujícím způsobem (zvýrazněny jsou bloky s globálními proměnnými i dotčená čtveřice instrukcí):
; ; 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 _a .export _b .export _c .export _main .segment "BSS" _a: .res 1,$00 _b: .res 1,$00 _c: .res 1,$00 ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; __asm__ ("lda %v", a); ; lda _a ; ; __asm__ ("clc"); ; clc ; ; __asm__ ("adc %v", b); ; adc _b ; ; __asm__ ("sta %v", c);konkrétní ; sta _c ; ; } ; rts .endproc
Zajímavé bude vysledovat, jak vlastně budou instrukce, které ke globálním proměnným přistupují, přeloženy do strojového kódu:
$ ca65 --listing assembly4_list.asm assembly4.asm
S následujícím výsledkem:
000000r 1 ; 000000r 1 ; __asm__ ("lda %v", a); 000000r 1 ; 000000r 1 AD rr rr lda _a 000003r 1 ; 000003r 1 ; __asm__ ("clc"); 000003r 1 ; 000003r 1 18 clc 000004r 1 ; 000004r 1 ; __asm__ ("adc %v", b); 000004r 1 ; 000004r 1 6D rr rr adc _b 000007r 1 ; 000007r 1 ; __asm__ ("sta %v", c); 000007r 1 ; 000007r 1 8D rr rr sta _c 00000Ar 1 ; 00000Ar 1 ; } 00000Ar 1 ; 00000Ar 1 60 rts
14. Bitové posuny a další operace prováděné s akumulátorem
Vyzkoušejme si nyní, jakým způsobem se přeloží céčkovské příkazy, které manipulují s akumulátorem s využitím základních céčkovských operátorů. Začneme operací __A__ += 1, protože způsob překladu operátoru ++ (jak v prefixové, tak i v postfixové variantě) jsme si již ukázali v rámci předchozích kapitol:
void main(void) { __A__ = 42; __A__ += 1; }
Překlad je v tomto případě (ovšem pouze při zapnutí optimalizací) přímočarý a odpovídá tomu, co bychom sami zapsali 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 _main ; --------------------------------------------------------------- ; void __near__ main (void) ; --------------------------------------------------------------- .segment "CODE" .proc _main: near .segment "CODE" ; ; __A__ = 42; ; lda #$2A ; ; __A__ += 1; ; clc adc #$01 ; ; } ; rts .endproc
Podobně si můžeme vyzkoušet překlad céčkovského kódu, v němž se obsah akumulátoru posune doleva:
void main(void) { __A__ = 42; __A__ <<= 1; }
Výsledek je v tomto případě opět optimální:
; ; 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__ = 42; ; lda #$2A ; ; __A__ <<= 1; ; asl a ; ; } ; rts .endproc
A pro úplnost si ukažme i posun doprava, tedy vlastně dělení dvěma (bez znaménka!):
void main(void) { __A__ = 42; __A__ >>= 1; }
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" ; ; __A__ = 42; ; lda #$2A ; ; __A__ >>= 1; ; lsr a ; ; } ; rts .endproc
Pro úplnost doplňme posun o dva bity doleva i doprava:
void main(void) { __A__ = 42; __A__ >>= 2; __A__ <<= 2; }
Což se přeloží do kódu:
; ; 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__ = 42; ; lda #$2A ; ; __A__ >>= 2; ; lsr a lsr a ; ; __A__ <<= 2; ; asl a asl a ; ; } ; rts .endproc
15. Násobení a dělení dvěma popř. mocninou dvou
V případě, že budeme obsah proměnné (a ještě lépe obsah akumulátoru) násobit dvěma či nějakou vyšší mocninou dvojky, bude tato operace překladačem cc65 převedena na instrukci pro bitový posun doleva – ovšem pouze v případě, že povolíme optimalizace (bez těch není možné cc65 reálně používat). Můžeme si to ostatně velmi snadno otestovat na tomto příkladu:
void main(void) { __A__ = 42; __A__ *= 2; }
Po překladu je zřejmé, že se skutečně použila instrukce ASL určená pro bitový posun doleva:
; ; 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__ = 42; ; lda #$2A ; ; __A__ *= 2; ; asl a ; ; } ; rts .endproc
Naopak při dělení dvěma nebo dělení vyšší mocninou dvojky se použije bitový posun doprava. Opět si to otestujeme:
void main(void) { __A__ = 42; __A__ /= 2; }
Výsledek bude po překladu vypadat 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" ; ; __A__ = 42; ; lda #$2A ; ; __A__ /= 2; ; lsr a ; ; } ; rts .endproc
Při násobení čtyřmi se posun provede dvakrát, což je taktéž korektní:
void main(void) { __A__ = 42; __A__ *= 4; }
S výsledkem, v němž můžeme vidět dvojí použití instrukce ASL:
; ; 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__ = 42; ; lda #$2A ; ; __A__ *= 4; ; asl a asl a ; ; } ; rts .endproc
Pozor ovšem na situaci, kdy jsou optimalizace zakázány. V takovém případě dostaneme zcela neoptimální kód:
ldx #$00 lda #$2A jsr shlax1 rts
16. cc65 nakonec není tak hloupý – násobení třemi a pěti
V předchozích kapitolách jsme si uvedli poměrně velké množství příkladů, z nichž je zřejmé, že překladač cc65 nedokáže provádět mnohé optimalizace, na které jsme zvyklí z moderních překladačů určených pro moderní mikroprocesory. Na druhou stranu ovšem není cc65 úplně hloupý, protože dokáže optimalizovat alespoň násobení určitými konstantami. Ukázali jsme si, jak je rozpoznáno násobení mocninou dvojky, ovšem cc65 dokáže nahradit i násobení dalšími konstantami optimalizovaným kódem. Začněme vynásobením akumulátoru třemi (násobíme akumulátor, aby výsledný kód neobsahoval zbytečné paměťové operace):
void main(void) { __A__ = 42; __A__ *= 3; }
V tomto případě je násobení nahrazeno posunem doleva (tedy vynásobením dvěma) a součtem s původní hodnotou, tedy A=A<<1+A=A*2+A=A*3, což je poměrně příjemné zjiště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 "CODE" ; ; __A__ = 42; ; lda #$2A ; ; __A__ *= 3; ; sta tmp1 asl a clc adc tmp1 ; ; } ; rts .endproc
V dalším příkladu zkusíme vynásobení pěti:
void main(void) { __A__ = 42; __A__ *= 5; }
V tomto případě je násobení nahrazeno posunem doleva o dva bity (tedy vynásobením čtyřmi) a součtem s původní hodnotou, tedy A=A<<2+A=A*4+A=A*5:
; ; 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__ = 42; ; lda #$2A ; ; __A__ *= 5; ; sta tmp1 asl a asl a clc adc tmp1 ; ; } ; rts .endproc
17. Násobení větší konstantou
V případě, že konstanta, kterou je násoben obsah akumulátoru, je složitější, tj. pokud ji nelze snadno rozepsat na několik mocnin dvojky, bude namísto optimalizovaného kódu zavolán podprogram (subrutina) pro násobení. Připomeňme si totiž, že osmibitový mikroprocesor MOS 6502 neobsahoval instrukci pro součin, což je ostatně s ohledem na jeho cenu a interní jednoduchost pochopitelné:
void main(void) { __A__ = 42; __A__ *= 42; }
Ve výše uvedeném příkladu se pokoušíme násobit konstantou 42, což se přeloží do tohoto kódu:
; ; 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__ = 42; ; lda #$2A ; ; __A__ *= 42; ; jsr pusha0 jmp tosumulax .endproc
jsr pusha0 jsr tosumulax rts
Překladač dvojici jsr+rts zaměnil za jmp, protože ono rts je vlastně „schováno“ ve volané subrutině.
18. Upravený soubor Makefile
Pro úplnost si ještě ukažme, jak vypadá soubor Makefile, kterým byly přeloženy všechny minule i dnes popsané demonstrační příklady. Tento soubor předpokládá, že je cc65 nainstalován tak, že je dostupný bez nutnosti uvedení celé cesty k překladači. V opačném případě je nutné změnit prvních několik řádků tohoto souboru:
CC65=cc65 STD_FLAGS = INCLUDE_SOURCE =-T STATIC_LOCAL_VARS = -Cl STD_OPTIMIZATION = -O REGISTER_VARS = -Or INLINE_FUNC = -Oi ALL_OPTIMIZATIONS = -Osir all: local_add_1.asm local_add_2.asm local_add_3.asm local_add_4.asm local_add_5.asm local_add_6.asm local_add_7.asm \ memset1.asm memset2.asm memset3.asm memset4.asm memset5.asm memset6.asm \ empty_main.asm local_var_1.asm local_var_2.asm register_var_1.asm register_var_2.asm \ a_register.asm ax_register_1.asm ax_register_2.asm eax_register_1.asm eax_register_2.asm \ post_increment_1.asm post_increment_2.asm pre_increment_1.asm pre_increment_2.asm \ assembly1.asm assembly2.asm assembly3.asm assembly4.asm \ a_register_add.asm a_register_shift.asm a_register_rshift.asm a_register_double.asm \ a_register_triple.asm a_register_quadruple.asm \ a_register_half.asm a_register_times5.asm a_register_times42.asm clean: rm *.asm local_add_1.asm: local_add.c ${CC65} ${STD_FLAGS} -o $@ $< local_add_2.asm: local_add.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} -o $@ $< local_add_3.asm: local_add.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} -o $@ $< local_add_4.asm: local_add.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} -o $@ $< local_add_5.asm: local_add.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${REGISTER_VARS} -o $@ $< local_add_6.asm: local_add.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${INLINE_FUNC} -o $@ $< local_add_7.asm: local_add.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${ALL_OPTIMIZATIONS} -o $@ $< local_add_B.asm: local_add_B.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${INLINE_FUNC} -o $@ $< local_mul.asm: local_mul.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${ALL_OPTIMIZATIONS} -o $@ $< memset1.asm: memset1.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} -o $@ $< memset2.asm: memset2.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} -o $@ $< memset3.asm: memset3.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} -o $@ $< memset4.asm: memset4.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} -o $@ $< memset5.asm: memset5.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} -o $@ $< memset6.asm: memset6 .c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} -o $@ $< empty_main.asm: empty_main.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} -o $@ $< local_var_1.asm: local_var.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} -o $@ $< local_var_2.asm: local_var.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${REGISTER_VARS} ${STD_OPTIMIZATION} -o $@ $< register_var_1.asm: register_var.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} -o $@ $< register_var_2.asm: register_var.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${REGISTER_VARS} ${STD_OPTIMIZATION} -o $@ $< a_register.asm: a_register.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< ax_register_1.asm: ax_register.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} -o $@ $< ax_register_2.asm: ax_register.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< eax_register_1.asm: eax_register.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} -o $@ $< eax_register_2.asm: eax_register.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< post_increment_1.asm: post_increment.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} ${INLINE_FUNC} -o $@ $< post_increment_2.asm: post_increment.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${post_incrementS} ${STD_OPTIMIZATION} ${INLINE_FUNC} -o $@ $< pre_increment_1.asm: pre_increment.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STATIC_LOCAL_VARS} ${STD_OPTIMIZATION} ${INLINE_FUNC} -o $@ $< pre_increment_2.asm: pre_increment.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${pre_incrementS} ${STD_OPTIMIZATION} ${INLINE_FUNC} -o $@ $< assembly1.asm: assembly1.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${pre_incrementS} ${STD_OPTIMIZATION} ${REGISTER_VARS} -o $@ $< assembly2.asm: assembly2.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${pre_incrementS} ${STD_OPTIMIZATION} ${REGISTER_VARS} -o $@ $< assembly3.asm: assembly3.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${pre_incrementS} ${STD_OPTIMIZATION} ${REGISTER_VARS} -o $@ $< assembly4.asm: assembly4.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${pre_incrementS} ${STD_OPTIMIZATION} ${REGISTER_VARS} -o $@ $< a_register_add.asm: a_register_add.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< a_register_shift.asm: a_register_shift.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< a_register_rshift.asm: a_register_rshift.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< a_register_double.asm: a_register_double.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< a_register_triple.asm: a_register_triple.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< a_register_quadruple.asm: a_register_quadruple.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< a_register_times5.asm: a_register_times5.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< a_register_times42.asm: a_register_times42.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $< a_register_half.asm: a_register_half.c ${CC65} ${STD_FLAGS} ${INCLUDE_SOURCE} ${STD_OPTIMIZATION} -o $@ $<
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ář:
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 - cc65 coding hints
https://cc65.github.io/doc/coding.html - 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/ - Překladače jazyka C pro historické osmibitové mikroprocesory
https://www.root.cz/clanky/prekladace-jazyka-c-pro-historicke-osmibitove-mikroprocesory/