Neviem napísať "Hello world" v C++

08.12.2019 | 18:19 | Mirecove dristy | Miroslav Bendík

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?

    • RE: Neviem napísať "Hello world" v C++ 08.12.2019 | 19:21
      Avatar debian+   Návštevník

      Neviem ako ty, ale ja mam taky rozdiel medzi tymi 2 subormi:

      $ diff main.txt main_O3.txt 
      2c2
      < main.o:     file format elf32-littlearm
      ---
      > main_O3.o:     file format elf32-littlearm
      18,33c18,25
      <   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
      ---
      >   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>
      42,50d33
      < 
      < Disassembly of section .rodata.str1.1:
      < 
      < 00000000 <.LC0>:
      <    0:	6c6c6548 	cfstr64vs	mvdx6, [ip], #-288	; 0xfffffee0
      <    4:	6f77206f 	svcvs	0x0077206f
      <    8:	21646c72 	smccs	18114	; 0x46c2
      <    c:	Address 0x000000000000000c is out of bounds.
      <
      • RE: Neviem napísať "Hello world" v C++ 08.12.2019 | 20:17
        Avatar Miroslav Bendík Gentoo  Administrátor

        Medzi -Os a -O3 je rozdiel, ale medzi -O3 s textom Hello world a O3 s textom napr. Hello moto nie je rozdiel.

        • RE: Neviem napísať "Hello world" v C++ 08.12.2019 | 21:04
          Avatar debian+   Návštevník

          Ako si to prekladal? Ja:

          all:
          	arm-none-eabi-g++ -c -o main.o main.cpp -Os -mcpu=cortex-m3
          	arm-none-eabi-g++ -c -o main_O3.o main.cpp -Os -O3 -mcpu=cortex-m3
          	arm-none-eabi-gcc -c -o main_c.o main.c -Os -mcpu=cortex-m3
          	arm-none-eabi-gcc -c -o main_c_O3.o main.c -Os -O3 -mcpu=cortex-m3
          
          	arm-none-eabi-objdump -D  main.o > main.txt
          	arm-none-eabi-objdump -D  main_O3.o > main_O3.txt
          	arm-none-eabi-objdump -D  main_c.o > main_c.txt
          	arm-none-eabi-objdump -D  main_c_O3.o > main_c_O3.txt
          
          • RE: Neviem napísať "Hello world" v C++ 09.12.2019 | 07:33
            Avatar Miroslav Bendík Gentoo  Administrátor

            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)

    • RE: Neviem napísať "Hello world" v C++ 08.12.2019 | 22:10
      Avatar debian+   Návštevník

      Zrejme nespravne optimalizacie: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#index-O3

    • RE: Neviem napísať "Hello world" v C++ 09.12.2019 | 07:28
      Avatar Miroslav Bendík Gentoo  Administrátor

      Doplňujúce informácie:

      Zmenil som const std::string msg("Hello World!\n"); na const 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:

      arm-none-eabi-g++ -c -o build/program.o program.cpp -mcpu=cortex-m3 -O1 -finline-small-functions -fipa-sra -DMSG='"Hello World!"'
      arm-none-eabi-g++ -c -o build/program2.o program.cpp -mcpu=cortex-m3 -O1 -finline-small-functions -fipa-sra -DMSG='"Hello Moto!"'

      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:

      arm-none-eabi-g++ -c -o build/program.o program.cpp -mcpu=cortex-m3 -O1 -finline-small-functions -fipa-sra -DMSG='"Hello World + nejaky dalsi zbytocny text"'
      cat build/program.o|grep Hello
      Zhody v binárnom súbore (štandardný vstup)

      Zrazu po zväčšení textu to v binárke je.

    • RE: Neviem napísať "Hello world" v C++ 09.12.2019 | 22:08
      Avatar mark   Používateľ

      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

      extern "C"

      K čomu ho potrebuješ?

      • RE: Neviem napísať "Hello world" v C++ 09.12.2019 | 22:14
        Avatar Miroslav Bendík Gentoo  Administrátor

        Po vymazaní sa nič nemení. Extern je pretože v linker skripte nechcem používať mangoovaný názov.

        • RE: Neviem napísať "Hello world" v C++ 09.12.2019 | 22:24
          Avatar mark   Používateľ

          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.

    • RE: Neviem napísať "Hello world" v C++ 10.12.2019 | 03:00
      Avatar debian+   Návštevník

      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.

    • RE: Neviem napísať "Hello world" v C++ 10.12.2019 | 03:42
      Avatar debian+   Návštevník

      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:

      #include <string>
      
      #ifdef DISABLE_MY_ASM
       #include <cstdio>
      #endif
      
      #define SVC_WRITE0 0x04
      
      
      int svc_call(int command, const void* message) {
          int output;
      #ifdef DISABLE_MY_ASM
          printf("%s\n", message);
      #else
          __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"
          );
      #endif
          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"; 
      
          const std::string msg("Hello world!\n");
          cout << msg << "\n";
          for (;;) {
          }
      }
      //}
      
      • RE: Neviem napísať "Hello world" v C++ 10.12.2019 | 07:41
        Avatar Miroslav Bendík Gentoo  Administrátor

        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.

        #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 cout;
        
        
        void hlavny_program(void) {
            const std::string msg("Hello World!\n");
            cout << msg;
            for (;;) {
            }
        }
        

        K tomu príslušný disassembler:

        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 <_Z14hlavny_programv>:
          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 <_Z14hlavny_programv+0xe>

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

    • RE: Neviem napísať "Hello world" v C++ 10.12.2019 | 09:01
      Avatar Miroslav Bendík Gentoo  Administrátor

      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:

      int svc_call(int command, const void* message) {
          int output;
          __asm volatile
          (
              "mov r0, %[com] \n"
              "ldr r1, %[msg] \n"
              "bkpt #0xAB \n"
              "mov %[out], r0"
              : [out] "=r" (output)
              : [com] "r" (command), [msg] "m" (message)
              : "r0", "r1"
          );
          return output;
      }
      

      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?

      • RE: Neviem napísať "Hello world" v C++ 10.12.2019 | 16:00
        Avatar mark   Používateľ

        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.

      • RE: Neviem napísať "Hello world" v C++ 18.01 | 03:08
        Avatar debian+   Návštevník
        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?

        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.

    • RE: Neviem napísať "Hello world" v C++ 10.12.2019 | 11:34
      Avatar Miroslav Bendík Gentoo  Administrátor

      Alebo

      int __attribute__((noinline)) svc_call(int command, const void* message) {
          (void)message;
          __asm volatile("bkpt #0xAB":::"memory");
          return command;
      }
      

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

      Rozloženie sekcie .text._Z8svc_calliPKv:
      
      00000000 <svc_call(int, void const*)>:
         0:	beab      	bkpt	0x00ab
         2:	4770      	bx	lr
      
      Rozloženie sekcie .text.hlavny_program:
      
      00000000 <hlavny_program>:
         0:	b580      	push	{r7, lr}
         2:	b086      	sub	sp, #24
         4:	ad02      	add	r5, sp, #8
         6:	462c      	mov	r4, r5
         8:	270d      	movs	r7, #13
         a:	2600      	movs	r6, #0
         c:	4b06      	ldr	r3, [pc, #24]	; (28 <hlavny_program+0x28>)
         e:	9500      	str	r5, [sp, #0]
        10:	cb0f      	ldmia	r3, {r0, r1, r2, r3}
        12:	c407      	stmia	r4!, {r0, r1, r2}
        14:	4629      	mov	r1, r5
        16:	7023      	strb	r3, [r4, #0]
        18:	2004      	movs	r0, #4
        1a:	9701      	str	r7, [sp, #4]
        1c:	f88d 6015 	strb.w	r6, [sp, #21]
        20:	f7ff fffe 	bl	0 <hlavny_program>
        24:	e7fe      	b.n	24 <hlavny_program+0x24>
        26:	bf00      	nop
        28:	00000000 	.word	0x00000000
    • RE: Neviem napísať "Hello world" v C++ 10.12.2019 | 20:09
      Avatar Miroslav Bendík Gentoo  Administrátor

      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:

      int svc_call(int command, const void* message) {
          register int cmd __asm__ ("r0") = command;
          register const void *msg __asm__ ("r1") = message;
          __asm volatile
          (
              "bkpt #0xAB"
              : "+r" (cmd) /* Vystupy */
              : "r" (msg) /* Vstupy */
              : "memory"
          );
          return cmd;
      }
      

      Generovaný kód inlinuje funkciu, ale registre sú použité korektne.

      00000000 <hlavny_program>:
         0:	b480      	push	{r7}
         2:	b087      	sub	sp, #28
         4:	ad02      	add	r5, sp, #8
         6:	462c      	mov	r4, r5
         8:	270d      	movs	r7, #13
         a:	2600      	movs	r6, #0
         c:	4b05      	ldr	r3, [pc, #20]	; (24 <hlavny_program+0x24>)
         e:	9500      	str	r5, [sp, #0]
        10:	cb0f      	ldmia	r3, {r0, r1, r2, r3}
        12:	c407      	stmia	r4!, {r0, r1, r2}
        14:	9701      	str	r7, [sp, #4]
        16:	7023      	strb	r3, [r4, #0]
        18:	4629      	mov	r1, r5
        1a:	f88d 6015 	strb.w	r6, [sp, #21]
        1e:	2004      	movs	r0, #4
        20:	beab      	bkpt	0x00ab
        22:	e7fe      	b.n	22 <hlavny_program+0x22>