Obsah
1. Minimalistické překladače jazyka C: tcc a Chibicc
3. Překladače programovacího jazyka C v Linuxu
5. Získání, konfigurace a překlad Tiny C Compileru
7. Překlad překladače (sic) Chibicc
8. Programy použité pro porovnání Tiny C Compileru a překladače Chibicc s GCC
9. Testujeme rychlost překladu
10. Zhodnocení rychlosti překladu
11. Vliv (absence) optimalizací na rychlost výsledných programů
12. Rychlost vykreslení fraktálu
14. Tiny C Compiler ve funkci „interpretru“
15. Hlášení o chybách a varování
16. Zdrojový kód s množstvím potenciálních problémů
17. Chybová hlášení a varování vypisovaná překladači céčka: GCC a Clang
18. Chybová hlášení a varování vypisovaná tcc a Chibiccem
19. Kde tedy „malé“ překladače použít?
1. Minimalistické překladače jazyka C: tcc a Chibicc
Když vývojář používající operační systém Linux (nebo abychom neurazili, tak GNU/Linux) použije sousloví „překladač céčka“, je prakticky stoprocentně jisté, že má na mysli překladač programovacího jazyka C patřící do rodiny GCC (GNU Compiler Collection) nebo Clang (postavený nad LLVM). Ovšem ve skutečnosti existuje pro Linux mnohem větší množství překladačů programovacího jazyka C. Mezi ně patří například minimalisticky pojatý projekt Tiny C Compiler, jehož největší předností je velká rychlost překladu i možnost použít tento překladač ve funkci interpretru – je jím tedy možné céčkové programy přímo „spouštět“, podobně jako skripty psané v Perlu či v BASHi, Pythonu, Ruby atd. atd. Ovšem existuje i relativně velké množství dalších překladačů céčka. V dnešním článku se kromě již zmíněného Tiny C Compileru (zkráceně jen tcc) zaměříme i na překladač nazvaný chibicc: A Small C Compiler (zkracovat to budeme na chibicc).
2. Programovací jazyk C
Jedním z nejpopulárnějších v současnosti používaných programovacích jazyků je stále (což je mimochodem zajímavé a hodně to vypovídá o realitě v IT) programovací jazyk C navržený Dennisem Ritchiem. Jazyk C se postupně vyvinul z programovacích jazyků BPCL (autor Martin Richards, 1966) a B (autor Ken Thompson, 1970) až do současné podoby, která byla standardizována v několika normách, z nichž nejznámější je pravděpodobně stále ještě ISO/IEC 9899:1999 známá pod zkráceným označením C99 (následoval C11 neboli ISO/IEC 9899:2011, C17 neboli ISO/IEC 9899:2018 a konečně C23 neboli ISO/IEC 9899:2024 ). Starší, dodnes v některých případech stále ještě používaný standard se jmenuje ISO 9899:1990, tento starší standard je prakticky shodný (až na jiné číslování jednotlivých paragrafů) s normou ANSI C (ANSI X3.159–1989 „Programming Language C“) a zkráceně se označuje C89 či méně často C90. Se všemi zkratkami C89, C90, C99, C11, C17 a C23 se ještě v tomto článku setkáme.
Programovací jazyk C je i přes absenci některých důležitých vlastností (například mu chybí automatický správce paměti či podpora silného typování a práce s objekty, výjimkami, uzávěry atd.) využívaný jak pro tvorbu open source aplikací, tak i v čistě komerční oblasti – nejedná se jen o vývoj aplikací pro desktopy a servery, ale i pro mikrořadiče či digitální signálové procesory (DSP). Céčko je mnohdy využíváno i ve funkci cílového jazyka, do něhož se překládají (transpilují) programy zapsané v některých vyšších programovacích jazycích – vývojáři, kteří překladače těchto jazyků vytváří, se tak nemusí starat například o nízkoúrovňové optimalizace, protože je za ně již naprogramovali vývojáři překladače céčka. Z historického hlediska je zajímavé, že právě tímto způsobem vznikla první verze jazyka C++ (nástroj Cpre), i když moderní překladače C++ jsou již řešeny odděleně.
To však není zdaleka vše, protože programovací jazyk C je dodnes důležitý i z toho důvodu, že jak rozhraní jader některých operačních systémů (Linux, Microsoft Windows i dalších systémů), tak rozhraní systémových knihoven bylo navrženo s ohledem na jmenné konvence céčka i s ohledem na možnosti jeho linkeru (a ostatní jazyky toto rozhraní s většími či menšími problémy dodržují).
3. Překladače programovacího jazyka C v Linuxu
Vývoj operačního systému GNU/Linux byl do značné míry ovlivněn, a možná bych si dokonce dovolil říci, že vůbec umožněn tím, že na něj byly portovány nástroje označované souhrnným názvem GNU Compiler Collection neboli zkráceně GCC. Tyto nástroje obsahují překladače několika programovacích jazyků, především pak překladače jazyků C, C++, Objective C, Objective C++, Java, Ada a Fortran (později byly přidány překladače pro Go a nedávno i pro Rust, ovšem prozatím ve fázi vývoje). Kromě překladačů jsou součástí GCC i další nástroje, zejména preprocesor používaný jazyky C a C++, dále GNU assembler (gas), linker, nástroj sledující pokrytí kódu testy (gcov) atd. Jedná se o velmi kvalitní nástroje, které navíc podporují obrovské množství různých platforem, operačních systémů a procesorových architektur, samozřejmě včetně procesorů řady x86 a x86_64.
Kromě překladače céčka patřícího do skupiny nástrojů GCC (i samotný překladač se jmenuje GCC – GNU C Compiler, což může být poněkud matoucí, je tedy nutné vždy sledovat kontext, ve kterém se o GCC mluví či píše) je možné v operačním systému Linux použít i další překladače. Mezi ně patří například Clang z projektu LLVM, jenž je zajímavý především po technologické stránce a s jehož technologií se seznámíme v některém z navazujících článků. Dále se pak můžeme (či jsme se mohli) setkat s překladači komerčních firem, například s překladačem vytvořeným společností Intel, který v případě některých typů optimalizací překonával GCC a Clang/LLVM. V neposlední řadě je pak možné v Linuxu (a nutno říci, že nejenom v něm) použít překladače nazvané Tiny C Compiler (zkráceně tcc) a Chibicc, jejichž popisem a porovnáním s GCC se budeme zabývat v navazujících kapitolách.
4. Tiny C Compiler
Tiny C Compiler (zkracovat ho budeme na tcc) je překladač programovacího jazyka C, který byl původně vytvořen Fabricem Bellardem a nyní se o jeho další vývoj a portaci na nové platformy stará komunita vývojářů, protože se samozřejmě jedná o open-source projekt (i když je nutno poznamenat, že vývoj v žádném případě není nijak bouřlivý). Tiny C Compiler v sobě kromě vlastního překladače obsahuje i linker, což znamená, že jeden binární program může sloužit jak pro překlad zdrojových textů (včetně preprocesingu) do objektového kódu, tak pro vytvoření výsledného spustitelného binárního programu. Všechny tři zmíněné funkce jsou implementovány v jediném spustitelném souboru, jehož velikost na platformě x86 nepřesahuje tři sta kilobajtů, což je například v porovnání s GCC zcela zanedbatelná velikost (dokonce i pouze GNU assembler je v binární podobě větší než celý tcc).
Tiny C Compiler podporuje standard C89/C90 i velkou část standardu C99, a to do té míry, že úpravy zdrojových kódů určených pro GCC většinou nejsou zapotřebí. Největší devizou překladače tcc je ovšem blesková rychlost překladu, protože vlastní překlad je jednoprůchodový. Na stránkách tohoto projektu se uvádí, že tcc je přibližně osmkrát rychlejší než překladač GCC (s použitím standardních voleb, tj. bez optimalizací), ovšem jak se můžete dozvědět z dalšího textu, může být tcc v extrémním případě rychlejší zhruba čtyřicetkrát! (a v některých jiných případech jen třikrát rychlejší). Na druhou stranu však tcc za většinou ostatních moderních překladačů céčka významně pokulhává v případě optimalizací prováděných při překladu. O tom, do jaké míry se absence většiny optimalizačních metod projeví na demonstračních příkladech (benchmarcích), si řekneme v navazujících kapitolách.
Co to však znamená pro každodenní praxi vývojáře? Podle mého názoru je ideální zkombinovat vlastnosti tcc (doslova blesková rychlost překladu) a GCC či Clang (optimalizace na několika úrovních) takovým způsobem, že pro samotný vývoj se použije překladač tcc a pro tvorbu výsledné binární podoby programu se naopak využije překladač GCC s příslušnými přepínači (-O3 atd.). Tiny C Compiler lze použít především na architekturách x86 a x86_64, i když existují i verze využitelné na procesorech ARM a DSP čipech řady TMS320C67×x. Všechna měření, jejichž výsledky jsou uvedeny v dalších kapitolách, byly provedeny na počítači s architekturou x86–64.
5. Získání, konfigurace a překlad Tiny C Compileru
Některé distribuce Linuxu obsahují balíček s Tiny C Compilerem. Poté je jeho instalace triviální. Příkladem mohou být distribuce postavené na Debianu/Mintu atd., v nichž je možné Tiny C Compiler nainstalovat takto:
$ sudo apt-get install tcc
Samotná instalace vyžaduje pouze minimum místa na disku (alespoň z dnešního pohledu):
The following NEW packages will be installed: tcc 0 upgraded, 1 newly installed, 0 to remove and 22 not upgraded. Need to get 242 kB of archives. After this operation, 709 kB of additional disk space will be used. Get:1 http://archive.ubuntu.com/ubuntu focal/universe amd64 tcc amd64 0.9.27-8 [242 kB] Fetched 242 kB in 1s (215 kB/s) Selecting previously unselected package tcc. (Reading database ... 330120 files and directories currently installed.) Preparing to unpack .../tcc_0.9.27-8_amd64.deb ... Unpacking tcc (0.9.27-8) ... Setting up tcc (0.9.27-8) ... Processing triggers for install-info (6.7.0.dfsg.2-5) ... Processing triggers for doc-base (0.10.9) ... Processing 1 added doc-base file... Processing triggers for man-db (2.9.1-1) ...
Zjistíme, která verze se skutečně nainstalovala:
$ tcc -v tcc version 0.9.27 (x86_64 Linux)
V případě, že není balíček dostupný nebo pokud si budete chtít vyzkoušet jeho nejnovější verzi, můžeme provést překlad a instalaci přímo ze zdrojových kódů. Je to ve skutečnosti velmi snadné a potřebujeme k tomu pouze jiný překladač céčka (nebo samotný tcc, který umí sám sebe přeložit), git a některé další nástroje dostupné na počítači vývojářů.
Nejprve si naklonujeme repositář s tcc:
$ git clone git://repo.or.cz/tinycc.git Cloning into 'tinycc'... remote: Counting objects: 16864, done. remote: Compressing objects: 100% (4304/4304), done. remote: Total 16864 (delta 12447), reused 16819 (delta 12405) Receiving objects: 100% (16864/16864), 4.77 MiB | 1.34 MiB/s, done. Resolving deltas: 100% (12447/12447), done.
Následně spustíme skript configure, který zjistí aktuálně použitý systém, architekturu procesoru a překladač, který bude použitý pro překlad. Výsledek se uloží, takže bude možné použít make pro překlad:
$ ./configure Binary directory /usr/local/bin TinyCC directory /usr/local/lib/tcc Library directory /usr/local/lib Include directory /usr/local/include Manual directory /usr/local/share/man Info directory /usr/local/share/info Doc directory /usr/local/share/doc Source path /tmp/ramdisk/tinycc Build OS Linux x86_64 C compiler gcc (9.4) Target OS Linux CPU x86_64 Triplet x86_64-linux-gnu Creating config.mak and config.h
Dále již stačí spustit klasický make:
$ make
Po několika sekundách či desítkách sekund by měl překlad doběhnout. Výsledkem bude spustitelný soubor tcc s velikostí nepřesahující 400kB:
$ ls -sh tcc 348K tcc
Tento soubor lze ještě zmenšit na cca 320 kB (předchozí verze tcc dokonce na velikosti 100kB):
$ strip tcc $ ls -sh tcc 320K tcc
Nakonec ověříme, jakou verzi tcc jsme vlastně přeložili:
$ ./tcc -v tcc version 0.9.28rc 2024-03-13 mob@2b0a663d (x86_64 Linux)
6. Překladač céčka Chibicc
Dalším minimalisticky pojatým překladačem programovacího jazyka C, s nímž se v dnešním článku ve stručnosti seznámíme, se jmenuje Chibicc. Tento překladač, jehož repositář naleznete na adrese https://github.com/rui314/chibicc, na rozdíl od některých „malých“ překladačů či interpretrů céčka (picoc) splňuje prakticky celý standard C11 (a tím pádem i starší standardy, zejména C89/C90 a C99, které se stále používají). Ovšem již na tomto místě je dobré upozornit na to, že vzhledem k tomu, že tento překladač NEpodporuje optimalizace, bude běh výsledných přeložených programů většinou pomalejší, než v případě GCC či Clangu, což si ostatně ukážeme na demonstračních příkladech, které jsou popsány v osmé kapitole.
7. Překlad překladače (sic) Chibicc
Samotný překlad překladače Chibicc je velmi snadný a opět nám k tomu bude postačovat starý dobrý GCC (či již nainstalovaný tcc – ostatně si to můžete ověřit). Nejprve si naklonujeme adresář s tímto projektem:
$ git clone git@github.com:rui314/chibicc.git Cloning into 'chibicc'... remote: Enumerating objects: 4584, done. remote: Counting objects: 100% (686/686), done. remote: Compressing objects: 100% (292/292), done. remote: Total 4584 (delta 409), reused 664 (delta 394), pack-reused 3898 Receiving objects: 100% (4584/4584), 822.69 KiB | 1.61 MiB/s, done. Resolving deltas: 100% (3406/3406), done.
Přejdeme do adresáře s naklonovaným repositářem:
$ cd chibicc/
Samotný překladač je realizován v několika zdrojových souborech. Není jich mnoho, pokud si počet a velikost souborů porovnáme se zdrojovými kódy GCC či Clangu:
$ ls -l *.[hc] -rw-rw-r-- 1 ptisnovs ptisnovs 9811 Mar 19 14:26 chibicc.h -rw-rw-r-- 1 ptisnovs ptisnovs 43920 Mar 19 14:26 codegen.c -rw-rw-r-- 1 ptisnovs ptisnovs 4564 Mar 19 14:26 hashmap.c -rw-rw-r-- 1 ptisnovs ptisnovs 18392 Mar 19 14:26 main.c -rw-rw-r-- 1 ptisnovs ptisnovs 91158 Mar 19 14:26 parse.c -rw-rw-r-- 1 ptisnovs ptisnovs 33021 Mar 19 14:26 preprocess.c -rw-rw-r-- 1 ptisnovs ptisnovs 690 Mar 19 14:26 strings.c -rw-rw-r-- 1 ptisnovs ptisnovs 19317 Mar 19 14:26 tokenize.c -rw-rw-r-- 1 ptisnovs ptisnovs 7503 Mar 19 14:26 type.c -rw-rw-r-- 1 ptisnovs ptisnovs 6980 Mar 19 14:26 unicode.c
Překlad provedeme bez nutnosti konfigurace, pouze přímým vyvoláním make:
$ make cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o strings.o strings.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o tokenize.o tokenize.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o hashmap.o hashmap.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o type.o type.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o main.o main.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o parse.o parse.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o codegen.o codegen.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o preprocess.o preprocess.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o unicode.o unicode.c cc -std=c11 -g -fno-common -Wall -Wno-switch -o chibicc strings.o tokenize.o hashmap.o type.o main.o parse.o codegen.o preprocess.o unicode.o
Pro zajímavost se podívejme na čas čistého překladu celého Chibiccu. Jedná se o prakticky okamžitý výsledek:
$ make clean $ time make cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o strings.o strings.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o tokenize.o tokenize.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o hashmap.o hashmap.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o type.o type.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o main.o main.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o parse.o parse.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o codegen.o codegen.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o preprocess.o preprocess.c cc -std=c11 -g -fno-common -Wall -Wno-switch -c -o unicode.o unicode.c cc -std=c11 -g -fno-common -Wall -Wno-switch -o chibicc strings.o tokenize.o hashmap.o type.o main.o parse.o codegen.o preprocess.o unicode.o real 0m0,667s user 0m0,563s sys 0m0,098s
Výsledný překladač má velikost nepatrně přesahující 315kB:
$ ls -l chibicc -rwxrwxr-x 1 ptisnovs ptisnovs 315128 Mar 19 14:28 chibicc
Po odstranění „tuku“ se jeho velikost zmenší na 166kB:
$ strip chibicc -rwxrwxr-x 1 ptisnovs ptisnovs 166960 Mar 19 18:01 chibicc
Zkusme si nyní překladač spustit:
$ ./chibicc --help chibicc [ -o <path> ] <file>
Mimochodem – nelze použít hlavičkové soubory GCC s intrinsic (SIMD atd.), což je ale do jisté míry pochopitelné:
$ chibicc -I/usr/include -I/usr/include/SDL2/ main.c gfx.c /usr/include/SDL2/SDL_cpuinfo.h:86: #include <immintrin.h> ^ immintrin.h: cannot open file: No such file or directory
8. Programy použité pro porovnání Tiny C Compileru a překladače Chibicc s GCC
Pro porovnání vlastností Tiny C Compileru, překladače Chibicc a překladače céčka, jenž je součástí skupiny překladačů GCC, jsem zvolil tři diametrálně odlišné zdrojové kódy. Prvním zdrojovým kódem je kód projektu tth (plným jménem TeX to HTML Translator), který je dostupný na adrese http://silas.psfc.mit.edu/tth/tars/download.html. Tento projekt je v několika ohledech extrémní, protože je uložen v jediném céčkovém zdrojovém kódu o velikosti 1192 kB, který má celkem 29 068 řádků! (v jediném zdrojovém souboru). Překlad takto velkého souboru je pro překladač (přesněji řečeno pro překladač GCC) skutečně tvrdým oříškem, o čemž se ostatně přesvědčíme v další kapitole.
Druhým testovacím programem je generátor fraktálů, který při svém spuštění velmi intenzivně používá operace s hodnotami s plovoucí řádovou čárkou (floating point). V tomto programu se názorně ukáže, jak dobře (či naopak špatně) provádí překladač různé optimalizace – a to jak optimalizace výpočtů, tak například i rozbalení smyček. Zdrojový kód tohoto programu vypadá následovně:
#include <stdlib.h> #include <stdio.h> #include "palette_mandmap.h" void calc_mandelbrot(unsigned int width, unsigned int height, unsigned int maxiter, unsigned char palette[][3]) { puts("P3"); printf("%d %d\n", width, height); puts("255"); double cy = -1.5; int y; for (y=0; y<height; y++) { double cx = -2.0; int x; for (x=0; x<width; x++) { double zx = 0.0; double zy = 0.0; unsigned int i = 0; while (i < maxiter) { double zx2 = zx * zx; double zy2 = zy * zy; if (zx2 + zy2 > 4.0) { break; } zy = 2.0 * zx * zy + cy; zx = zx2 - zy2 + cx; i++; } unsigned char *color = palette[i % 256]; unsigned char r = *color++; unsigned char g = *color++; unsigned char b = *color; printf("%d %d %d\n", r, g, b); cx += 3.0/width; } cy += 3.0/height; } } int main(int argc, char **argv) { if (argc < 4) { puts("usage: ./mandelbrot width height maxiter"); return 1; } int width = atoi(argv[1]); int height = atoi(argv[2]); int maxiter = atoi(argv[3]); calc_mandelbrot(width, height, maxiter, palette); return 0; }
Výše uvedený zdrojový soubor navíc ještě vyžaduje barvovou paletu ve formátu pole 256×3 celočíselných hodnot typu bajt:
/* taken from Fractint */ unsigned char palette[][3] = { {255, 255, 255}, {224, 224, 224}, {216, 216, 216}, {208, 208, 208}, {200, 200, 200}, {192, 192, 192}, {184, 184, 184}, {176, 176, 176}, {168, 168, 168}, {160, 160, 160}, {152, 152, 152}, {144, 144, 144}, {136, 136, 136}, {128, 128, 128}, {120, 120, 120}, {112, 112, 112}, {104, 104, 104}, {96, 96, 96}, {88, 88, 88}, {80, 80, 80}, {72, 72, 72}, {64, 64, 64}, {56, 56, 56}, {48, 48, 56}, {40, 40, 56}, {32, 32, 56}, {24, 24, 56}, {16, 16, 56}, {8, 8, 56}, {000, 000, 60}, {000, 000, 64}, {000, 000, 72}, {000, 000, 80}, {000, 000, 88}, {000, 000, 96}, {000, 000, 104}, {000, 000, 108}, {000, 000, 116}, {000, 000, 124}, {000, 000, 132}, {000, 000, 140}, {000, 000, 148}, {000, 000, 156}, {000, 000, 160}, {000, 000, 168}, {000, 000, 176}, {000, 000, 184}, {000, 000, 192}, {000, 000, 200}, {000, 000, 204}, {000, 000, 212}, {000, 000, 220}, {000, 000, 228}, {000, 000, 236}, {000, 000, 244}, {000, 000, 252}, {000, 4, 252}, {4, 12, 252}, {8, 20, 252}, {12, 28, 252}, {16, 36, 252}, {20, 44, 252}, {20, 52, 252}, {24, 60, 252}, {28, 68, 252}, {32, 76, 252}, {36, 84, 252}, {40, 92, 252}, {40, 100, 252}, {44, 108, 252}, {48, 116, 252}, {52, 120, 252}, {56, 128, 252}, {60, 136, 252}, {60, 144, 252}, {64, 152, 252}, {68, 160, 252}, {72, 168, 252}, {76, 176, 252}, {80, 184, 252}, {80, 192, 252}, {84, 200, 252}, {88, 208, 252}, {92, 216, 252}, {96, 224, 252}, {100, 232, 252}, {100, 228, 248}, {96, 224, 244}, {92, 216, 240}, {88, 212, 236}, {88, 204, 232}, {84, 200, 228}, {80, 192, 220}, {76, 188, 216}, {76, 180, 212}, {72, 176, 208}, {68, 168, 204}, {64, 164, 200}, {64, 156, 196}, {60, 152, 188}, {56, 144, 184}, {52, 140, 180}, {52, 132, 176}, {48, 128, 172}, {44, 120, 168}, {40, 116, 160}, {40, 108, 156}, {36, 104, 152}, {32, 96, 148}, {28, 92, 144}, {28, 84, 140}, {24, 80, 136}, {20, 72, 128}, {16, 68, 124}, {16, 60, 120}, {12, 56, 116}, {8, 48, 112}, {4, 44, 108}, {000, 36, 100}, {4, 36, 104}, {12, 40, 108}, {16, 44, 116}, {24, 48, 120}, {28, 52, 128}, {36, 56, 132}, {40, 60, 140}, {48, 64, 144}, {52, 64, 148}, {60, 68, 156}, {64, 72, 160}, {72, 76, 168}, {76, 80, 172}, {84, 84, 180}, {88, 88, 184}, {96, 92, 192}, {104, 100, 192}, {112, 112, 196}, {124, 120, 200}, {132, 132, 204}, {144, 140, 208}, {152, 152, 212}, {164, 160, 216}, {172, 172, 220}, {180, 180, 224}, {192, 192, 228}, {200, 200, 232}, {212, 212, 236}, {220, 220, 240}, {232, 232, 244}, {240, 240, 248}, {252, 252, 252}, {252, 240, 244}, {252, 224, 232}, {252, 208, 224}, {252, 192, 212}, {252, 176, 204}, {252, 160, 192}, {252, 144, 184}, {252, 128, 172}, {252, 112, 164}, {252, 96, 152}, {252, 80, 144}, {252, 64, 132}, {252, 48, 124}, {252, 32, 112}, {252, 16, 104}, {252, 000, 92}, {236, 000, 88}, {228, 000, 88}, {216, 4, 84}, {204, 4, 80}, {192, 8, 76}, {180, 8, 76}, {168, 12, 72}, {156, 16, 68}, {144, 16, 64}, {132, 20, 60}, {124, 20, 60}, {112, 24, 56}, {100, 24, 52}, {88, 28, 48}, {76, 32, 44}, {64, 32, 44}, {52, 36, 40}, {40, 36, 36}, {28, 40, 32}, {16, 44, 28}, {20, 52, 32}, {24, 60, 36}, {28, 68, 44}, {32, 76, 48}, {36, 88, 56}, {40, 96, 60}, {44, 104, 64}, {48, 112, 72}, {52, 120, 76}, {56, 132, 84}, {48, 136, 84}, {40, 144, 80}, {52, 148, 88}, {68, 156, 100}, {80, 164, 112}, {96, 168, 124}, {108, 176, 136}, {124, 184, 144}, {136, 192, 156}, {152, 196, 168}, {164, 204, 180}, {180, 212, 192}, {192, 220, 200}, {208, 224, 212}, {220, 232, 224}, {236, 240, 236}, {252, 248, 248}, {252, 252, 252}, {252, 252, 240}, {252, 252, 228}, {252, 252, 216}, {248, 248, 204}, {248, 248, 192}, {248, 248, 180}, {248, 248, 164}, {244, 244, 152}, {244, 244, 140}, {244, 244, 128}, {244, 244, 116}, {240, 240, 104}, {240, 240, 92}, {240, 240, 76}, {240, 240, 64}, {236, 236, 52}, {236, 236, 40}, {236, 236, 28}, {236, 236, 16}, {232, 232, 0}, {232, 232, 12}, {232, 232, 28}, {232, 232, 40}, {236, 236, 56}, {236, 236, 68}, {236, 236, 84}, {236, 236, 96}, {240, 240, 112}, {240, 240, 124}, {240, 240, 140}, {244, 244, 152}, {244, 244, 168}, {244, 244, 180}, {244, 244, 196}, {248, 248, 208}, {248, 248, 224}, {248, 248, 236}, {252, 252, 252}, {248, 248, 248}, {240, 240, 240}, {232, 232, 232}};
Třetím příkladem je program pro řešení Sudoku. Algoritmus řešení je velmi jednoduchý a naivní (k dispozici jsou mnohem sofistikovanější algoritmy), což však není v tomto případě podstatné, jelikož nás zajímá rychlost překladu a kvalita výsledného kódu. V tomto programu se velmi často používají operace pro práci s prvky dvourozměrných a trojrozměrných polí, takže se zde ukáže, jak dokáže překladač optimalizovat tento typ operací:
/* ------------------------------------------------------------------ */ /* Benchmark na praci s celociselnymi poli: */ /* reseni Sudoku metodou brute-force */ /* */ /* Autor: Pavel Tisnovsky */ /* ------------------------------------------------------------------ */ #include <stdio.h> #include <stdlib.h> #include <string.h> #define ODDELOVAC puts("+--------+--------+--------+"); /* vstupni data: */ /* 1-9 znama hodnota */ /* 0 neznama hodnota (prazdne policko) */ const int src[9][9]={ {5, 6, 9, 0, 0, 7, 2, 1, 8}, {7, 0, 2, 0, 0, 1, 0, 3, 0}, {1, 3, 8, 9, 2, 0, 4, 0, 0}, {0, 0, 7, 2, 0, 9, 6, 5, 0}, {0, 0, 4, 0, 0, 5, 8, 9, 3}, {9, 0, 0, 8, 6, 0, 0, 4, 0}, {8, 0, 0, 6, 1, 0, 3, 0, 0}, {4, 1, 0, 3, 9, 2, 0, 0, 0}, {2, 0, 0, 0, 5, 8, 1, 0, 4} }; /* pracovni matice */ int wrk[9][9]; /* matice obsahujici povolene hodnoty pro kazde policko */ int povol[9][9][9]; /* pomocna matice s minimalni povolenou hodnotou pro kazde policko */ int minim[9][9]; /* ------------------------------------------------------------------ */ /* Snizeni poctu moznych kombinaci zjistenim duplicit ve sloupcich */ /* ------------------------------------------------------------------ */ void testSloupce0(void) { int radek, sloupec; int cifry[9], cifra; int i; for (sloupec=0; sloupec<9; sloupec++) { for (i=0; i<9; i++) cifry[i]=0; /* projdi vsechny bunky v radku a zjisti zapsana cisla */ for (radek=0; radek<9; radek++) { cifra=src[radek][sloupec]; if (cifra>0) { /* obsad cifru */ cifry[cifra-1]=1; } } /* v matici povol zmen cely radek */ for (radek=0; radek<9; radek++) { if (src[radek][sloupec]==0) { for (i=0; i<9; i++) { if (cifry[i]) povol[radek][sloupec][i]=0; } } } } } /* ------------------------------------------------------------------ */ /* Snizeni poctu moznych kombinaci zjistenim duplicit na radcich */ /* ------------------------------------------------------------------ */ void testRadku0(void) { int radek, sloupec; int cifry[9], cifra; int i; for (radek=0; radek<9; radek++) { for (i=0; i<9; i++) cifry[i]=0; /* projdi vsechny bunky ve sloupci a zjisti zapsana cisla */ for (sloupec=0; sloupec<9; sloupec++) { cifra=src[radek][sloupec]; if (cifra>0) { /* obsad cifru */ cifry[cifra-1]=1; } } /* v matici povol zmeni cely sloupec */ for (sloupec=0; sloupec<9; sloupec++) { if (src[radek][sloupec]==0) { for (i=0; i<9; i++) { if (cifry[i]) povol[radek][sloupec][i]=0; } } } } } /* ------------------------------------------------------------------ */ /* Snizeni poctu moznych kombinaci zjistenim duplicit v blocich 3x3 */ /* ------------------------------------------------------------------ */ void testBloky0(void) { int blokx, bloky; int cifry[9], cifra; int i, j; for (bloky=0; bloky<9; bloky+=3) { for (blokx=0; blokx<9; blokx+=3) { for (i=0; i<9; i++) /* v kazdem bloku 3x3 nejprve vymazeme seznam cifer */ cifry[i]=0; for (j=0; j<3; j++) { /* projdi blok a zjisti cisla */ for (i=0; i<3; i++) { cifra=src[j+bloky][i+blokx]; if (cifra>0) { /* obsad cifru */ cifry[cifra-1]=1; } } } for (j=0; j<3; j++) { /* projdi blok a znuluj nalezene cifry */ for (i=0; i<3; i++) { if (src[j+bloky][i+blokx]==0) { /* v tomto poli ma smysl menit cifry */ int k; for (k=0; k<9; k++) { if (cifry[k]) povol[j+bloky][i+blokx][k]=0; } } } } } } } /* ------------------------------------------------------------------ */ /* Test, zda je ve vsech sloupcich devet ruznych hodnot */ /* ------------------------------------------------------------------ */ int testSloupce(void) { int radek, sloupec; int cifry[9]; for (sloupec=0; sloupec<9; sloupec++) { for (radek = 0; radek<9; radek++) { cifry[radek] = 0; } /* projdu vsechny polozky v radku a zjistim cisla */ for (radek=0; radek < 9; radek++) { /* cifra je uz obsazena? */ if (cifry[wrk[radek][sloupec]-1] != 0) return 0; else /* obsad cifru */ cifry[wrk[radek][sloupec]-1]=1; } } return 1; } /* ------------------------------------------------------------------ */ /* Test, zda je na vsech radcich devet ruznych hodnot */ /* ------------------------------------------------------------------ */ int testRadky(void) { int radek, sloupec; int cifry[9]; for (radek=0; radek<9; radek++) { for (sloupec=0; sloupec<9; sloupec++) { cifry[sloupec] = 0; } /* projdu vsechny polozky v radku a zjistim cisla */ for (sloupec=0; sloupec<9; sloupec++) { /* cifra je uz obsazena? */ if (cifry[wrk[radek][sloupec]-1] != 0) return 0; else /* obsad cifru */ cifry[wrk[radek][sloupec]-1]=1; } } return 1; } /* ------------------------------------------------------------------ */ /* Test, zda je ve vsech blocich 3x3 devet ruznych hodnot */ /* ------------------------------------------------------------------ */ int testBloky(void) { int blokx, bloky; int cifry[9], cifra; int i, j; for (bloky=0; bloky<9; bloky+=3) { for (blokx=0; blokx<9; blokx+=3) { memset(cifry, 0, 9 * sizeof(int)); for (j=0; j<3; j++) { for (i=0; i<3; i++) { cifra=wrk[j+bloky][i+blokx]; if (cifry[cifra-1]) return 0; else /* obsad cifru */ cifry[cifra-1]=1; } } } } return 1; } /* ------------------------------------------------------------------ */ /* Tisk nalezeneho reseni */ /* ------------------------------------------------------------------ */ void tiskVysledku(void) { int i, j; putchar('\n'); ODDELOVAC for (j=0; j<9; j++) { printf("| "); for (i=0; i<9; i++) { printf("%d ", wrk[j][i]); if (i % 3 == 2) printf(" | "); } putchar('\n'); if (j % 3 == 2) ODDELOVAC } } /* ------------------------------------------------------------------ */ /* Reseni Sudoku */ /* ------------------------------------------------------------------ */ void solve(void) { int i, j, k; int done=0; double counter=0; double max=1; int cnt=0; /* zjisteni poctu kombinaci pro vypis */ for (j=0; j<9; j++) { for (i=0; i<9; i++) { int cnt=0; for (k=0; k<9; k++) { if (povol[j][i][k]) cnt++; } max=max*cnt; } } while (!done) { /* test, zda je Sudoku vyreseno */ if (testRadky() && testSloupce() && testBloky()) { tiskVysledku(); return; } /* vypis prubeznych vysledku */ if (cnt==5e6) { printf("Kombinaci: %.0f z %.f zbyva %.f = %7.3f%%\n", counter, max, max-counter, 100.0 - 100.0*counter/max); cnt=0; } for (j=0; j<9; j++) { for (i=0; i<9; i++) { if (!src[j][i]) { /* jde o nevyplnenou polozku -> zvysime jeji hodnotu */ if (wrk[j][i]<9) { do { /* prejit na dalsi povolenou cifru */ wrk[j][i]++; k=wrk[j][i]; } while (povol[j][i][k-1]==0 && k<=9); if (k>9 || povol[j][i][k-1]==0) { /* uz jsme na konci rady povolenych cisel? */ wrk[j][i]=minim[j][i]; /* prejdeme na dalsi policko */ } else { goto KONEC; } } else { /* jsme na konci -> nastavime prvni povolenou cifru */ wrk[j][i]=minim[j][i]; if (j==8 && i==8) done=1; } } } } KONEC: counter++; cnt++; } } /* ------------------------------------------------------------------ */ /* Vypis vsech kombinaci cisel pro kazde policko */ /* ------------------------------------------------------------------ */ void vypisPovol() { double counter=1; int i, j, k; /* vypsani povol matice */ for (j=0; j<9; j++) { for (i=0; i<9; i++) { int cnt=0; int prvniCislo=1; putchar('('); for (k=0; k<9; k++) { if (povol[j][i][k]) { if (prvniCislo) prvniCislo = 0; else putchar(' '); putchar('1'+k); cnt++; } } putchar(')'); putchar(' '); putchar(' '); counter=counter*cnt; } putchar('\n'); } printf("Kombinaci celkem: %g\n\n", counter); } /* ------------------------------------------------------------------ */ /* Funkce zavolana po startu konzolove aplikace */ /* ------------------------------------------------------------------ */ int main(void) { int i, j, k; /* inicializace pracovni matice */ for (j=0; j<9; j++) for (i=0; i<9; i++) { wrk[j][i]=src[j][i]; if (wrk[j][i]==0) wrk[j][i]=1; } /* inicializace povol matice */ for (j=0; j<9; j++) { for (i=0; i<9; i++) { for (k=0; k<9; k++) { povol[j][i][k]=1; } } } puts("Pocet kombinaci v nevyplnenych polickach:"); vypisPovol(); /* zruseni polozek v matici SRC */ for (j=0; j<9; j++) { for (i=0; i<9; i++) { if (src[j][i]>0) for (k=0; k<9; k++) { if (k+1!=src[j][i]) povol[j][i][k]=0; } } } testSloupce0(); puts("Odstraneni jiz vyplnenych cislic ve sloupcich:"); vypisPovol(); testRadku0(); puts("Odstraneni jiz vyplnenych cislic v radcich:"); vypisPovol(); testBloky0(); puts("Odstraneni jiz vyplnenych cislic v blocich 3x3:"); vypisPovol(); /* inicializace minimalnich polozek */ for (j=0; j<9; j++) { for (i=0; i<9; i++) { if (src[j][i]==0) { /* volna polozka */ for (k=0; k<9; k++) { if (povol[j][i][k]) { minim[j][i]=k+1; break; } } } } } /* nastaveni prvnich volnych polozek */ for (j=0; j<9; j++) { for (i=0; i<9; i++) { if (src[j][i]==0) /* volna polozka */ wrk[j][i]=minim[j][i]; } } puts("Mozne kombinace cisel:"); vypisPovol(); puts("Jdeme na to ... trpelivost ..."); solve(); return 0; } /* ------------------------------------------------------------------ */ /* finito */ /* ------------------------------------------------------------------ */
9. Testujeme rychlost překladu
Překladač Tiny C Compiler je slovy svých tvůrců přibližně devětkrát rychlejší než překladač GCC, a to dokonce za předpokladu, že GCC nebude provádět žádné optimalizace (výchozím stavem je tedy přepínač -O0). O tom, do jaké míry je či není toto tvrzení pravdivé, se ale raději přesvědčíme sami. V následujících třech tabulkách jsou zobrazeny časy překladu pro všechny tři výše zmíněné demonstrační příklady. Nejprve byl překlad proveden s využitím Tiny C Compileru a Chibiccu, posléze byl použit GCC bez dalších voleb a poslední dva překlady proběhly taktéž s využitím překladače GCC, ovšem v tomto případě byly použity volby -O3 (optimalizace na rychlost výsledného kódu) a -Os (optimalizace na velikost výsledného kódu). V posledních sloupcích je ukázáno, kolikrát rychlejší je překlad pomocí tcc či Chibiccu:
Překlad aplikace „tth“:
Překlad | Čas překladu | GCC/tcc | GCC/Chibbicc |
---|---|---|---|
gcc | 1,193s | 11× | 3,6× |
gcc -Os | 35,052s | 340× | 105× |
gcc -O3 | 47,507s | 461× | 142× |
tcc | 0,103s | ||
chibicc | 0,333s |
Obrázek 1: Rychlost překladu „tth.c“ (menší sloupec = rychlejší překlad).
Překlad aplikace „mandelbrot.c“:
Překlad | Čas překladu | GCC/tcc | GCC/Chibbicc |
---|---|---|---|
gcc | 0,050s | 6,25× | 1,11× |
gcc -Os | 0,049s | 6,12× | 1,08× |
gcc -O3 | 0,079s | 9,87× | 1,75× |
tcc | 0,008s | ||
chibicc | 0,045s |
Obrázek 2: Rychlost překladu „mandelbrot.c“.
Překlad aplikace „sudoku.c“:
Překlad | Čas překladu | GCC/tcc | GCC/Chibbicc |
---|---|---|---|
gcc | 0,095s | 6,33× | 1,6× |
gcc -Os | 0,113s | 7,53× | 1,9× |
gcc -O3 | 0,721s | 48× | 12,2× |
tcc | 0,015s | ||
chibicc | 0,059s |
Obrázek 3: Rychlost překladu „sudoku.c“.
10. Zhodnocení rychlosti překladu
Z výše uvedených tabulek je patrné, že tvůrci Tiny C Compileru jsou ve svých tvrzeních o rychlosti svého překladače ještě zbytečně skromní, protože tcc dokázal přeložit aplikaci „tth“ zhruba jedenáctkrát rychleji než GCC, nemluvě o 461× rychlejším překladu v případě, pokud GCC tutéž aplikaci překládal s optimalizacemi (ovšem to skutečně není férové srovnání, jen ukazuje, jak jsou optimalizace časově náročné). Samozřejmě se jedná o dosti mezní případ, který však názorně ukazuje některé přednosti jednoprůchodového překladu (zdá se, že v tomto případě doba překladu roste pouze lineárně s počtem překládaných řádků).
U dalších dvou demonstračních příkladů nejsou rozdíly tak patrné (zrychlení je „pouze“ zhruba šestinásobné), a to zejména proto, že se jedná o relativně malé projekty, u nichž se ještě naplno neprojeví rychlost interní překladové smyčky tcc, protože je nutné provést závěrečné slinkování, které je zhruba stejně rychlé (zpracovávají se tytéž sdílené i statické knihovny atd.).
Naproti tomu Chibicc sice taktéž ve všech případech přeložil program rychleji než GCC (3,6× u prvního příkladu), ovšem rozdíly byly v dalších dvou příkladech pouze nepatrné. V tomto ohledu tedy tento překladač nijak nezazářil, ovšem nutno poznamenat, že se jeho tvůrci nesnažili o dosažení velké rychlosti překladu, na rozdíl od tcc.
11. Vliv (absence) optimalizací na rychlost výsledných programů
Další věc, která nás při porovnávání různých překladačů céčka zajímá, je pochopitelně kvalita výsledného spustitelného binárního kódu (ostatně pokud by nám nezáleželo na rychlosti výsledného programu, nemá asi smysl vůbec zvolit si jazyk C). Zde je zřejmé, že Tiny C Compiler ani Chibicc pravděpodobně nebudou dosahovat kvalit GCC nebo Clangu, zejména při povolení všech optimalizací, které GCC podporuje (v případě Clangu je to ještě markantnější). Ovšem zajímavé bude zjistit a vzájemně porovnat kód vygenerovaný Tiny C Compilerem a Chibiccem s kódem, který byl vytvořen překladačem GCC, ovšem bez zapnutí optimalizací. V ideálním případě by kódy mohly být srovnatelně rychlé, ale opět se o tom raději přesvědčíme změřením doby běhu dvou demonstračních příkladů – výpočet a vykreslení fraktálu a vyřešen Sudoku.
12. Rychlost vykreslení fraktálu
Po překladu všech variant programu mandelbrot.c ho budeme spouštět následujícím způsobem pro zjištění úrovně optimalizace zdrojových kódů:
$ time ./a.out 4096 4096 256 > /dev/null
Takto vypadají naměřené výsledky:
Spuštění | Čas běhu |
---|---|
gcc | 6,569s |
gcc -Os | 4,598s |
gcc -O3 | 4,506s |
tcc | 6,696s |
chibicc | 22,780s |
Obrázek 4: Rychlost doby běhu výpočtu Mandelbrotovy množiny (kratší sloupec = rychlejší výpočet).
Z předchozí tabulky je možné vyčíst, že strojový kód generovaný překladačem Tiny C Compiler je sice na platformě x86–86 (kterou jsme pro benchmarky použili) pomalejší než GCC, ovšem rozdíl oproti neoptimalizovanému kódu generovanému s využitím GCC není nijak závratně velký (časy jsou prakticky shodné). Když navíc přihlédneme k dobám kompilace, kde tcc jasně vyhrává, je patrné, že Tiny C Compiler není jen další projekt na hraní, ale reálně a kvalitně naprogramovaný nástroj. Horší je tomu v případě Chibiccu, který generuje mnohem pomalejší kód, který je navíc větší v porovnání s ostatními dvěma konkurenty: 20kB oproti cca 16kB v případě GCC a pouhými pěti kilobajty v případě tcc. Chibicc je interně velmi jednoduchý překladač a zde je to jasně patrné.
13. Rychlost vyřešení Sudoku
Naprosto stejným způsobem můžeme změřit dobu běhu prográmku na luštění Sudoku:
Spuštění | Čas běhu |
---|---|
gcc | 4,860s |
gcc -Os | 1,335s |
gcc -O3 | 0,902s |
tcc | 5,431s |
chibicc | 13,908s |
Obrázek 5: Rychlost doby vyřešení Sudoku (kratší sloupec = rychlejší výpočet).
Zde je již tcc znatelněji horší, než výsledek překladače GCC bez zapnutí optimalizací (rozdíl přibližně 12%). A nejpomalejší je, již pravděpodobně podle očekávání, výsledek překladu překladače Chibicc, který se opět nijak zvlášť nevyznamenal.
14. Tiny C Compiler ve funkci „interpretru“
Cílem Tiny C Compileru zajisté není konkurovat ve všech oblastech překladači céčka, který je ústřední součástí nástrojů GCC či Clangu, ovšem v některých případech může být jeho použití výhodné. TCC je možné použít především při vývoji, protože rychlost překladu je tak velká, že u menších aplikací (řádově stovky, tisíce až deseti tisíce řádků) vlastně ani není nutné program dělit do více modulů a pro překlad používat soubory Makefile (toto rozdělení je samozřejmě užitečné, ovšem mnohdy se již při vývoji provede takový návrh modulů, který se posléze ukáže být nepříliš vhodný). Tiny C Compiler také může mít své opodstatnění při tvorbě „přímo spustitelných C programů“, protože i bez provádění optimalizací je výsledkem práce TCC vždy nativně spustitelný kód, který je proveden mnohem rychleji než různé verze bajtkódu (Python, částečně i Java) či přímo interpretovaného kódu (BASH).
Pro tento účel lze použít přepínač -run, takže není nutné striktně oddělovat fázi překladu s fází běhu.
15. Hlášení o chybách a varování
Mohlo by se zdát, že i když Tiny C Compiler negeneruje optimalizovaný kód, mohl by být užitečný pro detekci chyb a potenciálních chyb, které překladače většinou vypisují formou varování (warnings). Tiny C Compiler je totiž obvykle tak rychlý, že dokáže překládat zdrojové kódy prakticky „on-the-fly“ v průběhu jejich editace (a zcela jistě při ukládání). Takže by bylo snadné integrovat tcc do programátorských textových editorů a integrovaných vývojových rozhraní (ať již přímo či přes technologii LSP, s níž jsme se již na stránkách Roota několikrát setkali). Bude tedy zajímavé zjistit, do jaké míry Tiny C Compiler dokáže najít potenciálně problematické části kódu. Jeho možnosti si porovnáme jak s Chibiccem, tak i s klasickým GCC a taktéž s Clangem.
16. Zdrojový kód s množstvím potenciálních problémů
Pro test oznamování chyb a varování použijeme následující zdrojový kód naprogramovaný v jazyku C, který jsem získal z článku Warnings Are Your Friend – A Code Quality Primer dostupného na adrese https://hackaday.com/2018/11/06/warnings-are-your-friend-a-code-quality-primer/:
/* Author: https://hackaday.com/2018/11/06/warnings-are-your-friend-a-code-quality-primer/ */ #include <stdio.h> enum count {NONE, ONE, OTHER}; int main(int argc, char **argv) { int ret; enum count count = argc - 1; // [1] signedness silently changed* switch (count) { case NONE: printf("%s\n", argc); // [2] integer printed as string ret = 1; // [3] missing break case ONE: printf(argv[1]); // [4] format string is not a string literal ret = 2; break; } // [5] enum value OTHER not considered // [6] no default case return ret; // [7] possibly uninitialized value }
Tento zdrojový kód je sice napsán v C a plně odpovídá syntaxi tohoto programovacího jazyka, na druhou stranu ovšem obsahuje poměrně velké množství sémantických chyb, které by sice neměly céčkovému překladači zabránit v překladu, ovšem bylo by vhodné, aby na ně překladač vývojáře alespoň upozornil. Tyto potenciální chyby jsou popsány v komentářích přímo ve zdrojovém kódu.
Jak se s tímto problematickým zdrojovým kódem vyrovnají všechny popisované překladače jazyka C. Podívejme se nyní na jejich výpisy chyb a varování v průběhu překladu.
17. Chybová hlášení a varování vypisovaná překladači céčka: GCC a Clang
Nejprve se podívejme na výsledky vypsané překladačem GCC. V první variantě se použil pouze příkaz gcc bez dalších přepínačů:
warnings.c: In function ‘main’: warnings.c:14:22: warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=] 14 | printf("%s\n", argc); // [2] integer printed as string | ~^ ~~~~ | | | | | int | char * | %d warnings.c:18:13: warning: format not a string literal and no format arguments [-Wformat-security] 18 | printf(argv[1]); // [4] format string is not a string literal | ^~~~~~
V další variantě již při volání GCC použijeme přepínač -Wall, který povolí detekci a výpis většího množství varování:
warnings.c: In function ‘main’: warnings.c:14:22: warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=] 14 | printf("%s\n", argc); // [2] integer printed as string | ~^ ~~~~ | | | | | int | char * | %d warnings.c:18:13: warning: format not a string literal and no format arguments [-Wformat-security] 18 | printf(argv[1]); // [4] format string is not a string literal | ^~~~~~ warnings.c:12:5: warning: enumeration value ‘OTHER’ not handled in switch [-Wswitch] 12 | switch (count) { | ^~~~~~
Pro úplnost přidejme ještě přepínač -Wextra:
warnings.c: In function ‘main’: warnings.c:14:22: warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=] 14 | printf("%s\n", argc); // [2] integer printed as string | ~^ ~~~~ | | | | | int | char * | %d warnings.c:18:13: warning: format not a string literal and no format arguments [-Wformat-security] 18 | printf(argv[1]); // [4] format string is not a string literal | ^~~~~~ warnings.c:12:5: warning: enumeration value ‘OTHER’ not handled in switch [-Wswitch] 12 | switch (count) { | ^~~~~~ warnings.c:15:17: warning: this statement may fall through [-Wimplicit-fallthrough=] 15 | ret = 1; | ~~~~^~~ warnings.c:17:9: note: here 17 | case ONE: | ^~~~
Další překladač programovacího jazyka C, který dokáže dobře detekovat potenciální chyby, je překladač Clang. Opět si nejprve ukažme, jaké chyby vypíše tento překladač v případě, že nepoužijeme žádné další přepínače:
warnings.c:8:16: error: cannot initialize a variable of type 'enum count' with an rvalue of type 'int' 8 | enum count count = argc - 1; // [1] signedness silently changed* | ^ ~~~~~~~~ warnings.c:12:28: warning: format specifies type 'char *' but the argument has type 'int' [-Wformat] 12 | printf("%s\n", argc); // [2] integer printed as string | ~~ ^~~~ | %d warnings.c:16:20: warning: format string is not a string literal (potentially insecure) [-Wformat-security] 16 | printf(argv[1]); // [4] format string is not a string literal | ^~~~~~~ warnings.c:16:20: note: treat the string as an argument to avoid this 16 | printf(argv[1]); // [4] format string is not a string literal | ^ | "%s", warnings.c:10:13: warning: enumeration value 'OTHER' not handled in switch [-Wswitch] 10 | switch (count) { | ^~~~~ 3 warnings and 1 error generated. Compiler returned: 1
Přepínače -Wall zkombinované, popř. s přepínačem -Wextra poskytnou stejný výsledek:
warnings.c:8:16: error: cannot initialize a variable of type 'enum count' with an rvalue of type 'int' 8 | enum count count = argc - 1; // [1] signedness silently changed* | ^ ~~~~~~~~ warnings.c:12:28: warning: format specifies type 'char *' but the argument has type 'int' [-Wformat] 12 | printf("%s\n", argc); // [2] integer printed as string | ~~ ^~~~ | %d warnings.c:16:20: warning: format string is not a string literal (potentially insecure) [-Wformat-security] 16 | printf(argv[1]); // [4] format string is not a string literal | ^~~~~~~ warnings.c:16:20: note: treat the string as an argument to avoid this 16 | printf(argv[1]); // [4] format string is not a string literal | ^ | "%s", warnings.c:10:13: warning: enumeration value 'OTHER' not handled in switch [-Wswitch] 10 | switch (count) { | ^~~~~ 3 warnings and 1 error generated. Compiler returned: 1
Nejvíce problémů (5 ze sedmi) nalezneme při použití přepínače -Weverything:
warnings.c:8:16: error: cannot initialize a variable of type 'enum count' with an rvalue of type 'int' 8 | enum count count = argc - 1; // [1] signedness silently changed* | ^ ~~~~~~~~ warnings.c:12:28: warning: format specifies type 'char *' but the argument has type 'int' [-Wformat] 12 | printf("%s\n", argc); // [2] integer printed as string | ~~ ^~~~ | %d warnings.c:16:20: warning: format string is not a string literal (potentially insecure) [-Wformat-security] 16 | printf(argv[1]); // [4] format string is not a string literal | ^~~~~~~ warnings.c:16:20: note: treat the string as an argument to avoid this 16 | printf(argv[1]); // [4] format string is not a string literal | ^ | "%s", warnings.c:10:5: warning: 'switch' missing 'default' label [-Wswitch-default] 10 | switch (count) { | ^ warnings.c:10:13: warning: enumeration value 'OTHER' not handled in switch [-Wswitch] 10 | switch (count) { | ^~~~~ 4 warnings and 1 error generated. Compiler returned: 1
18. Chybová hlášení a varování vypisovaná tcc a Chibiccem
Nyní již následují výstupy obou „malých“ překladačů céčka, tedy Tiny C Compileru a Chibicu. Začněme prvním zmíněným překladačem, tedy tcc zavolaným pouze s předáním zdrojového kódu pro překlad (bez dalších přepínačů):
$ tcc warnings.c
V tomto případě jsme nedostali žádné informace o varování. Zkusme tedy přepínač -Wall:
$ tcc -Wall warnings.c
Opět se nevypsalo žádné varování, takže v tomto ohledu Tiny C Compiler dosti zklamal.
A nakonec jsme si nechali „malý“ překladač céčka Chibicc. Ten žádné další přepínače nenabízí, takže je zde naše práce snadná:
$ chibicc warnings.c
Opět se nevypsalo absolutně žádné varování ani chybové hlášení.
19. Kde tedy „malé“ překladače použít?
Již jsme si řekli, že Tiny C Compiler lze využít i jako „interpret“ céčka (což je ovšem jen zdání, protože se stále používá fáze překladu). Zajímavé je využití Tiny C Compileru či Chibiccu (či obou!) ve schématu „důvěřivé důvěry“, což je velmi zajímavé téma, kterému jsme se věnovali v tomto článku.
20. Odkazy na Internetu
- Chibicc na GitHubu
https://github.com/rui314/chibicc - Původní domovská stránka Tiny C Compileru
https://bellard.org/tcc/ - Tiny C Compiler na Wikipedii
https://en.wikipedia.org/wiki/Tiny_C_Compiler - Repositář Tiny C Compileru
https://repo.or.cz/w/tinycc.git - Warnings Are Your Friend – A Code Quality Primer
https://hackaday.com/2018/11/06/warnings-are-your-friend-a-code-quality-primer/ - Defending Against Compiler-Based Backdoors
https://blog.regehr.org/archives/1241 - Reflections on Trusting Trust
https://www.win.tue.nl/~aeb/linux/hh/thompson/trust.html - Coding Machines (povídka)
https://www.teamten.com/lawrence/writings/coding-machines/ - Stage0
https://bootstrapping.miraheze.org/wiki/Stage0 - Projekt stage0 na GitHubu
https://github.com/oriansj/stage0 - Bootstraping wiki
https://bootstrapping.miraheze.org/wiki/Main_Page - Bootstrapped 6502 Assembler
https://github.com/robinluckey/bootstrap-6502 - IBM Basic assembly language and successors (Wikipedia)
https://en.wikipedia.org/wiki/IBM_Basic_assembly_language_and_successors - X86 Assembly/Bootloaders
https://en.wikibooks.org/wiki/X86_Assembly/Bootloaders - run6502, lib6502 — 6502 microprocessor emulator
http://piumarta.com/software/lib6502/ - Simple Computer Simulator Instruction-Set
http://www.science.smith.edu/dftwiki/index.php/Simple_Computer_Simulator_Instruction-Set - Bootstrapping#Computing (Wikipedia)
https://en.wikipedia.org/wiki/Bootstrapping#Computing - Bootstrapping (compilers)
https://en.wikipedia.org/wiki/Bootstrapping_%28compilers%29 - Bootstrapable Builds
http://bootstrappable.org/ - What is a coder's worst nightmare?
https://www.quora.com/What-is-a-coders-worst-nightmare/answer/Mick-Stute - Linux Assembly
http://asm.sourceforge.net/ - Tombstone diagram (Wikipedia)
https://en.wikipedia.org/wiki/Tombstone_diagram - History of compiler construction (Wikipedia)
https://en.wikipedia.org/wiki/History_of_compiler_construction - Self-hosting (Wikipedia)
https://en.wikipedia.org/wiki/Self-hosting - GNU Mes: Maxwell Equations of Software
https://gitlab.com/janneke/mes - Tiny C Compiler
https://bellard.org/tcc/ - Welcome to C–
https://www.cs.tufts.edu/~nr/c--/index.html - c4 – C in four functions
https://github.com/rswier/c4 - Tiny BASIC (Wikipedia)
https://en.wikipedia.org/wiki/Tiny_BASIC - David A. Wheeler’s Page on Fully Countering Trusting Trust through Diverse Double-Compiling (DDC) – Countering Trojan Horse attacks on Compilers
https://www.dwheeler.com/trusting-trust/ - Reviewing Microsoft's Automatic Insertion of Telemetry into C++ Binaries
https://www.infoq.com/news/2016/06/visual-cpp-telemetry - Visual Studio adding telemetry function calls to binary?
https://www.reddit.com/r/cpp/comments/4ibauu/visual_studio_adding_telemetry_function_calls_to/d30dmvu/ - LWN – The Trojan Horse
https://www.dwheeler.com/trusting-trust/spencer-19981123.txt - reproducible-builds.org
https://reproducible-builds.org/ - Other Assemblers
http://asm.sourceforge.net/howto/other.html - Projekt bootstrap
https://github.com/ras52/bootstrap - Projekt bcompiler
https://github.com/certik/bcompiler - Zadní vrátka
https://cs.wikipedia.org/wiki/Zadn%C3%AD_vr%C3%A1tka#Reflections_on_Trusting_Trust - David A. Wheeler’s Personal Home Page
https://www.dwheeler.com/ - David A. Wheeler
https://en.wikipedia.org/wiki/David_A._Wheeler - Multics Security Evaluation: Vulnerability Analysis
http://csrc.nist.gov/publications/history/karg74.pdf - Reflections on Rusting Trust
https://manishearth.github.io/blog/2016/12/02/reflections-on-rusting-trust/ - Reflections on Trusting Trust for Go (slajdy)
https://www.slideshare.net/yeokm1/reflections-on-trusting-trust-for-go - Reflections on Trusting Trust for Go (zdrojové materiály)
https://github.com/yeokm1/reflections-on-trusting-trust-go - Reflections on Trusting Trust for Go – GopherConSG 2018
https://www.youtube.com/watch?v=T82JttlJf60 - Reproducing Go binaries byte-by-byte
https://blog.filippo.io/reproducing-go-binaries-byte-by-byte/ - Trojský kůň
https://cs.wikipedia.org/wiki/Trojsk%C3%BD_k%C5%AF%C5%88_%28program%29