Neviem napísať "Hello world" v C++
Možno by tento blog mal byť skôr vo fóre, ale nakoneic je to trochu viacej textu a možno trochu viacej otázok na zamyslenie sa. V každom prípade situácia je taká, že posledných pár týždňov sa snažím napísať hello world v C++ a jednoducho to nefunguje.
Samozrejme nebol by som to ja, keby v tom nebol malý háčik. Háčik spočíva v tom, že pracujem s hardvérom, ktorý má pár bytov pamäte. Takže som napísal malý objekt, ktorý má podobné rozhranie ako std:cout
. V momente keď chcem vypísať dáta vložím do registra r1 adresu adresu textového buffera a vyvolám výnimku, ktorú odchytím debuggerom. Dosť bolo kecov, poďme na kód:
#include <string> #define SVC_WRITE0 0x04 int svc_call(int command, const void* message) { int output; __asm volatile ( "mov r0, %[com] \n" "mov r1, %[msg] \n" "bkpt #0xAB \n" "mov %[out], r0" : [out] "=r" (output) : [com] "r" (command), [msg] "r" (message) : "r0", "r1" ); return output; } struct OutputStream { OutputStream() = default; const OutputStream& operator<<(const std::string &value) const { svc_call(SVC_WRITE0, value.data()); return *this; } const OutputStream& operator<<(const char *value) const { svc_call(SVC_WRITE0, value); return *this; } }; const OutputStream cout; extern "C" { void hlavny_program(void) { //cout << "Hello world!\n"; FUnguje const std::string msg("Hello world!\n"); cout << msg; for (;;) { } } }
Po skompilovaní nasledujúcim príkazom program normálne funguje.
arm-none-eabi-g++ -c -o build/program.o program.cpp -Os -mcpu=cortex-m3
Disassembler (arm-none-eabi-objdump -D build/program.o
) vyzerá normálne.
00000000 <_Z8svc_calliPKv>: 0: 4603 mov r3, r0 2: 460a mov r2, r1 4: 4618 mov r0, r3 6: 4611 mov r1, r2 8: beab bkpt 0x00ab a: 4603 mov r3, r0 c: 4618 mov r0, r3 e: 4770 bx lr 00000010 <hlavny_program>: 10: b57f push {r0, r1, r2, r3, r4, r5, r6, lr} 12: 4a08 ldr r2, [pc, #32] ; (34 <hlavny_program+0x24>) 14: a802 add r0, sp, #8 16: f1a2 010d sub.w r1, r2, #13 1a: 9000 str r0, [sp, #0] 1c: f7ff fffe bl 0 <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE13_S_copy_charsEPcPKcS7_> 20: 230d movs r3, #13 22: 9301 str r3, [sp, #4] 24: 2300 movs r3, #0 26: 9900 ldr r1, [sp, #0] 28: 2004 movs r0, #4 2a: 734b strb r3, [r1, #13] 2c: f7ff fffe bl 0 <_Z8svc_calliPKv> 30: e7fe b.n 30 <hlavny_program+0x20> 32: bf00 nop 34: 0000000d andeq r0, r0, sp Rozloženie sekcie .rodata.str1.1:
Objektový kód program.o
obsahuje text Hello world a po spustení program normálne vypíše "Hello world!" do okna gdb.
Teraz preložím ten istý program s flagom -O3
.
00000000 <_Z8svc_calliPKv>: 0: 4603 mov r3, r0 2: 460a mov r2, r1 4: 4618 mov r0, r3 6: 4611 mov r1, r2 8: beab bkpt 0x00ab a: 4603 mov r3, r0 c: 4618 mov r0, r3 e: 4770 bx lr 00000010 <hlavny_program>: 10: b086 sub sp, #24 12: 2304 movs r3, #4 14: aa02 add r2, sp, #8 16: 4618 mov r0, r3 18: 4611 mov r1, r2 1a: beab bkpt 0x00ab 1c: 4603 mov r3, r0 1e: e7fe b.n 1e <hlavny_program+0xe>
Výsledný program hrabe do neinicializovanej pamäte. Za bežných okolností by som povedal, že mám zlý linker skript, ale ja kontrolujem generovaný kód pred linkovaním. Výsledný objektový kód neobsahuje konštantu "Hello world!". Môžem zmeniť text "Hello world!" na iný a binárny súbor program.o zostane bez zmeny. Kompilátor jednoducho z nejakého dôvodu odstránil konštantu a celý kód, ktorý s ňou pracuje okrem asm funkcie, ktorá je označená ako volatile.
Ak vo funkcii svc_call pridám do int output
príznak volatile program začne generovať konštantu "Hello world!" a zároveň aj začne fungovať.
Nejaké nápady, prečo sa pri -O3 stratila časť generovaného kódu?
Pre pridávanie komentárov sa musíte prihlásiť.
Neviem ako ty, ale ja mam taky rozdiel medzi tymi 2 subormi:
Medzi -Os a -O3 je rozdiel, ale medzi -O3 s textom Hello world a O3 s textom napr. Hello moto nie je rozdiel.
Ako si to prekladal? Ja:
Ja podobne, akurát pri O3 som vyhadzoval Os. Nižšie do komentára som doplnil ďalšie informácie + skúsil som si postupne zapínať optimalizácie z O2, aby som zistil ktorá mi vyhadzuje konštantu. Teraz ešte zistiť prečo. Teoreticky viem zabrániť kompilátoru vyhodiť ten kód ak dám k premennej output príznak volatile, ale output by to aspoň podľa mňa nemal ovplyvňovať. Teda nejde mi až tak o vyriešenie problému, ale o pochopenie prečo kompilátor tak agresívne optimalizoval tento kód. (O2 by zase až tak agresívne byť nemali)
Zrejme nespravne optimalizacie: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#index-O3
Doplňujúce informácie:
Zmenil som
const std::string msg("Hello World!\n");
naconst std::string msg(MSG "\n");
, takže môžem definovať text cez parameter kompilátora -D.Ďalej som zistil, že problematické optimalizácie sú -finline-small-functions -fipa-sra z O2. Nie samostatne, ale obe súčasne.
Skompilujem teda:
Vygenerované binárky sú identické a text Hello World sa nenachádza ani v jednej.
A teraz trik. Nezmením kód, ale zväčším text:
Zrazu po zväčšení textu to v binárke je.
Možno strieľam vedľa, ale skúsil by som na skúšku vymazať
const OutputStream& operator<<(const char *value) const { svc_call(SVC_WRITE0, value); return *this; }
a porovnať správanie. Takisto vo mne srší podozrenie na
K čomu ho potrebuješ?
Po vymazaní sa nič nemení. Extern je pretože v linker skripte nechcem používať mangoovaný názov.
Ktovie či náhodou kvôli tomu nechaosia tie optimalizácie. Kedysi dávno aj podpora C++ pre uC bola biedna, takže zo zotrvačnosti používam "C-like" kód na takomto druhu HW.
V zdrojak je "Hello world". Citaj https://www.evilsocket.net/2015/05/02/using-inline-assembly-and-naked-functions-to-fool-disassemblers/ ;).
Ak menim u seba string (predlzujem o ddddddddddddddddddddddddddd), tak sa meni dlzka .rodata.str1.4.
A pis poriadne kod. Ak pises kod C, tak pis, kod C. Ak pises kod C++, tak pises C++. Ty to mixujes a blbne to. Tu mas spravne, upravene, ako mi to ide na AMD64. Tvoj kod mi blbol.
V C kode moze byt len C kod. Cize ziadne vytvaranie C objektov v extern "C". Rozhranie na vymenu su funkcie. C a C++ pouzivaju rozdielny sposob linkovania. Preto v C++ tu mame to extern "C".;
Nie je div, ze link si s tym neporadi, ak miesas 2 svety.
U mna:
Ten extern s problémom nemá nič spoločné. Je tam kvôli tomu, aby som v linker skripte nemusel používať manglované meno u entry pointu (nepoužívam main pretože je trochu inak handlovaný kompilátorom). Mám teda takýto kód.
K tomu príslušný disassembler:
Žiadne ďalšie sekcie tam nie sú (nemám .rodata). Zmenou textu, alebo skrátením textu sa binárka nemení. Pri predĺžení textu mi už .rodata vznikne a program normálne funguje.
Generovaná binárka je identická až na názov symbolu hlavny_program, ktorý sa zmenil na _Z14hlavny_programv (pretože C++) a prestalo fungovať linkovanie (/usr/libexec/gcc/arm-none-eabi/ld: warning: cannot find entry symbol hlavny_program; defaulting to 0000000020000000).
Ok, problém vyriešený.
Všetci sa tu akosi zamerali na okrajové časti a nikto vrátane mňa sa poriadne nepozrel na assembler.
Kompilátor nemá žiadnu informáciu o tom, čo robím po inštrukcii bkpt. Môže vidieť, že som do registra zobral adresu, ale už nemá žiadnu informáciu, či s tou adresou niečo robím.
Takže pri vyššej optimalizácii kompilátor poslal len výslednú adresu textu ak by text bol v binárke (čo nebol). Z pohľadu kompilátora to vyzerá ok pretože na pamäť sa vlastne neodvolávam. To, že hrabem do pamäte by sa kompilátor mohol dozvedieť keby som u vstupného operandu message použil constraint m (memory). Takže po úprave assembleru nasledujúcim spôsobom to funguje dobre:
Prípadne som do clobber mohol pridať "memory", čo by malo rovnaký efekt.
Ostáva tu ešte otázka ako sa program bude správať ak message nebude jednoduchý pointer, ale pointer na štruktúru. Nebude za určitých okolností vyhadzovať atribúty štruktúry?
Celkom poučné, že gcc analyzuje aj programátorom zadaný kód v asembleri. V C sa premenné deklarovali ako volatile, ak kompilátor nevedel zistiť, ktorá časť kódu k nim kedy pristupuje. Či to funguje so štruktúrami, to neviem. Neskúšal som.
Pouzivaj oddeleny preklad. Tj. svc_call() daj zvlast v .o subore a tym padom nebude to pred optimalizovat. Resp. vies pri preklade aj striktne definovat, ake optimalizacie ma pouzivat. Tj. aj vypnut pre C optimalizacie. Vid parameter -O.
Alebo
Čo využíva fakt, že parametre funkcie sú predávané pomocou registrov (prvé 4 registre, toto správanie je definované v ARM ABI). Ak zabránim inlinovaniu, tak sa môžem spoľahnúť, že v r0 a r1 budú správne hodnoty ak to kompilátor nejak neoptimalizuje.
Disassembler vyzerá byť v poriadku a program funguje.
Ešte jedna hádam posledná úprava. Tentoraz vynútim použitie registrov r0, r1, takže môžem vypnúť zákaz inline.
Upravený kód vyzerá takto:
Generovaný kód inlinuje funkciu, ale registre sú použité korektne.