Перейти к содержанию
    

arm-none-eabi-gcc не создаёт статический конструктор C++

Добрый день.

 

Использую тулчейн arm-none-eabi-gcc для ядра ARM Cortex-A8.  Язык C++.  Столкнулся с проблемой:  не создаётся статический конструктор класса и не происходит инициализация в нём.

 

Вот хедер модуля с классом:

#ifndef KEX_H
#define KEX_H

class IClass {

    public:
        IClass(int sa, int sb);

    private:
        int a, b;

};

class TestClass {

    public:
        TestClass(const IClass& ic);

    private:
        int a1, b1;
};

#endif

Вот сам класс:

#include "kex.h"
#include "printf.h"

extern void hardware_init(void);

IClass::IClass(int sa, int sb)
{
 a = sa; b = sb;
}

TestClass::TestClass(const IClass& ic)
{
 hardware_init();                        //инитим железо
 printf("\nHi! I'm Constructor ;)");    //выводим по UART строку
}

 

Сама программа

//.........

IClass ic(2,5);      //делаем экземпляр класса - уже на момент создания класса, С++ рантайм должен  проинитить железо и вывести строку
TestClass tc(ic);

int main(void)

{
 while(1);

//..........

При запуске верхний код не работает - класс не создаётся, нет рантаймовского инита и ничего не печатается по  UART.

Если сделать так:

//.........


int main(void)
{
 IClass ic(2,5);      //инит железа и вывод строки
 TestClass tc(ic);

  
 while(1);

//..........

 

То работает.  

 

Вопрос, как заставить компилятор и линковщик  инициализировать и запускать статические классы? 

 

Подозреваю, надо в стартапе правильно вызвать инит, а в скрипте линковщика прописать нужные секции.

 

Программист, писавший код, построил программу таким образом, что такого "добра" просто навалом!

 На C переписать не выйдет, так как много кода с STL, <vector>, list, class

 

P.S.  Код успешно работает на TI C6000+ compiler  и на ПК

Изменено пользователем repstosw

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Гуглить про __libc_init_array,
Копировать рекомендованные секции из примеров что идут с arm-none-eabi уже не буду...

Spoiler


// Используется в случае наличия ключа ld -nostartfiles
// Так же смотреть вокруг software_init_hook

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

extern int main(void);
extern void __libc_init_array(void);

void __NO_RETURN _start(void)
{
	__libc_init_array();	// invoke constructors
    /* Branch to main function */
    main();

     /* Infinite loop */
	for (;;)
		;
}

// call after __preinit_array_xxx and before __init_array_xxx passing
void _init(void)
{
}

void * __dso_handle;

void _fini(void)
{
	for (;;)
		;
}

void __gxx_personality_v0(void)
{

}

#ifdef __cplusplus
}
#endif /* __cplusplus */

 

 

Изменено пользователем GenaSPB

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

18 минут назад, __inline__ сказал:

Вопрос, как заставить компилятор и линковщик  инициализировать и запускать статические классы? 

Очень просто: не использовать в конструкторах обращение к железу.

Ну или (если сильно нужно использовать), то инитить это железо до сишного стартапа, в ассемблерном коде после reset-вектора.

18 минут назад, __inline__ сказал:

P.S.  Код успешно работает на TI C6000+ compiler  и на ПК

Возможно где-то внутри hardware_init() или printf() используются статически инициализированные члены. А на этапе си-стартап-а (из которого вызываются конструкторы), эти члены ещё не проинициализированы в данном случае. А другой компилятор строит инит статических объектов в другом порядке и поэтому там всё работает.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Вообще-то порядок вызова статических конструкторов не определён.

Если есть завязки на это, то создаем класс, и вот в нем уже будет в порядке описания вызываться.

У топикстартера проблема иная - не вызывается вообще конструктор (я написал выше что сделать).
 

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

24 минуты назад, GenaSPB сказал:

У топикстартера проблема иная - не вызывается вообще конструктор

Откуда Вы знаете? Из исходного сообщения это никак не следует.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

4 minutes ago, jcxz said:

Откуда Вы знаете? Из исходного сообщения это никак не следует.

там только эта проблема и есть... Хотя там и завязка на порядок.

Изменено пользователем GenaSPB

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Сталкивался с необходимостью определить порядок инициализации классов. В их конструкторах происходил сетап хардвара.

Решил вопрос синглтонами. То есть создавался синглотон класса хардварного модуля и при первом его вызове происходила инициализация этого модуля.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

OK,  попробую обмозговать. Результат напишу.

 

Проблема возникла с этим движком: https://github.com/moai/moai-beta/tree/master/3rdparty/box2d-2.2.1

 

Под ПК и C6745 работает отлично, а под ARM не заработал. В ходе разбирательств с другим программистом, выяснили, что движок зацикливается при обходе двоичного дерева. Это привело к тому что мы обнаружили, что не вызывается конструктор класса.

 

b2World::b2World(const b2Vec2& gravity)

 И при создании ground Fixture входит в бесконечный цикл.

 

Если же внутренности вставить в main, то движок начинает работать.  Но это не совсем хорошее решение, так как предполагаем, что ещё кучу всего остального нужно инитить для верной работы :) Поэтому желается принудительно заствить GCC-компилер  сохранять и исполнять конструкторы.

 

Задача может не эмбиддерская, но  портирование софта с ПК на МК вполне себе имеет право существовать. :)

 

Нечто подобное было для Keil ARM, под STM32.  Там всё зависало и решилось удачно с флагами:

 

--no-exceptions --no-exceptions_unwind --force_new_nothrow

 

А для thumb-режима ещё нужно было принудительно задать выравнивание данных на 4, так как используются пересылки словами:

 

--pointer_alignment=4 --min_array_alignment=4

 

Изменено пользователем repstosw

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

2 часа назад, GenaSPB сказал:

Гуглить про __libc_init_array

__lib_init_array достаточно тяжёлая. Если размер актуален, то можно её подменить. На Cortex-Mx я вот так делают. Думаю на Cortex-A буде что-то подобное.

 

Код из стартапа.

  void SystemInit();
  void __libc_init_array();
  int main() __attribute__((noreturn));

  // These magic symbols are provided by the linker.
  extern void *_estack;
  extern void *_sidata, *_sdata, *_edata;
  extern void *_sbss, *_ebss;
  extern void (*__preinit_array_start[]) (void) __attribute__((weak));
  extern void (*__preinit_array_end[]) (void) __attribute__((weak));
  extern void (*__init_array_start[]) (void) __attribute__((weak));
  extern void (*__init_array_end[]) (void) __attribute__((weak));
  extern void (*__fini_array_start[]) (void) __attribute__((weak));
  extern void (*__fini_array_end[]) (void) __attribute__((weak));

  // Iterate over all the preinit/init routines (mainly static constructors).
  inline void __attribute__((always_inline)) __run_init_array (void)
  {
    int count;
    int i;

    count = __preinit_array_end - __preinit_array_start;
    for (i = 0; i < count; i++)
      __preinit_array_start[i] ();

    count = __init_array_end - __init_array_start;
    for (i = 0; i < count; i++)
      __init_array_start[i] ();
  }

  void __attribute__((naked, noreturn)) Reset_Handler()
  {
    #ifdef __DEBUG_SRAM__
      __set_MSP((uint32_t)&_estack);
    #endif

    SystemInit();  // User hardware initialization

    void **pSource, **pDest;
    for (pSource = &_sidata, pDest = &_sdata; pDest != &_edata; pSource++, pDest++)
      *pDest = *pSource;
    for (pDest = &_sbss; pDest != &_ebss; pDest++)
      *pDest = 0;

    //__libc_init_array(); // Use with libc start files instead __run_init_array();
    __run_init_array();    // Use with the "-nostartfiles" linker option instead __libc_init_array();
    (void)main();
  }

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Всем спасибо!  Задача решена.

 

Дополнительно настроил проект, чтобы при выкидывании секций, оптимизатор не задевал  нужное.  Плюс интегрировал в стартап eGON-заголовок и сделал перенос векторов прерываний.  Раньше была куча С- и АСМ- файлов, ссылающихся друг на друга.

 

Стартап (часть на ассемблере):

Spoiler

         .global Header
         .global Jump
         .global CLI
         .global STI

         .global _stack                  
         .global _bss_start
         .global _bss_end
 
         .set  UND_STACK_SIZE, 0x8
         .set  ABT_STACK_SIZE, 0x8
         .set  FIQ_STACK_SIZE, 0x8
         .set  IRQ_STACK_SIZE, 0x100
         .set  SVC_STACK_SIZE, 0x8

         .set  MODE_USR, 0x10            
         .set  MODE_FIQ, 0x11
         .set  MODE_IRQ, 0x12
         .set  MODE_SVC, 0x13
         .set  MODE_ABT, 0x17
         .set  MODE_UND, 0x1B
         .set  MODE_SYS, 0x1F            

         .equ  I_F_BIT, 0xC0

         .text
         .code 32
         .section .keep,"a"

Header:
         b Entry             @ точка входа в код
         .long 0x4E4F4765    @ eGON
         .long 0x3054422E    @ .BT0
         .long 0x00000000    @ checksum for boot0 (must be calculated !!!)
         .long 0x00007E00    @ length for boot0 (32256 B)
         .long 0x00000000
         .long 0x00000000
         .long 0x00000000

         .align 5                @ align 32 byte

Vector:
         LDR pc,=ResetHandler    @ Reset
         LDR pc,=UndefHandler    @ Undef
         LDR pc,=SWIHandler      @ SWI
         LDR pc,=PrefetchHandler @ Prefetch
         LDR pc,=AbortHandler    @ Abort
         NOP                     @ Reserved
         LDR pc,=IRQHandler      @ IRQ
         LDR pc,=FIQHandler      @ FIQ NMI

ResetHandler:
         B ResetHandler

UndefHandler:
         B UndefHandler

SWIHandler:
         B SWIHandler

PrefetchHandler:
         B PrefetchHandler

AbortHandler:
         B AbortHandler

FIQHandler:
         B FIQHandler

IRQHandler:
         PUSH {r0-r3,r12,lr}

@         BL IRQ_HANDLER

         POP {r0-r3,r12,lr}
         SUBS pc,lr,#4

Entry:
         MRC     p15, 0, r0, c1, c0, 0       @ Read CP15 System Control register
         BIC     r0, r0, #(0x1 << 12)        @ Clear I bit 12 to disable I Cache
         BIC     r0, r0, #(0x1 <<  2)        @ Clear C bit  2 to disable D Cache
         BIC     r0, r0, #0x1                @ Clear M bit  0 to disable MMU
         BIC     r0, r0, #(0x1 << 11)        @ Clear Z bit 11 to disable branch prediction
         MCR     p15, 0, r0, c1, c0, 0       @ Write value back to CP15 System Control register

         MOV     r0,#0
         MCR     p15, 0, r0, c8, c7, 0      @ I-TLB and D-TLB invalidation
         MCR     p15, 0, r0, c7, c5, 6      @ BPIALL - Invalidate entire branch predictor array

         LDR   r0, =_stack                     @ Read the stack address
         MSR   cpsr_c, #MODE_UND|I_F_BIT       @ switch to undef  mode
         MOV   sp,r0                           @ write the stack pointer
         SUB   r0, r0, #UND_STACK_SIZE         @ give stack space

         MSR   cpsr_c, #MODE_ABT|I_F_BIT       @ Change to abort mode
         MOV   sp, r0                          @ write the stack pointer
         SUB   r0,r0, #ABT_STACK_SIZE          @ give stack space

         MSR   cpsr_c, #MODE_FIQ|I_F_BIT       @ change to FIQ mode
         MOV   sp,r0                           @ write the stack pointer
         SUB   r0,r0, #FIQ_STACK_SIZE          @ give stack space

         MSR   cpsr_c, #MODE_IRQ|I_F_BIT       @ change to IRQ mode
         MOV   sp,r0                           @ write the stack pointer
         SUB   r0,r0, #IRQ_STACK_SIZE          @ give stack space

         MSR   cpsr_c, #MODE_SVC|I_F_BIT       @ change to SVC mode
         MOV   sp,r0                           @ write the stack pointer
         SUB   r0,r0, #SVC_STACK_SIZE          @ give stack space

         MSR   cpsr_c, #MODE_SYS|I_F_BIT       @ change to system mode
         MOV   sp,r0                           @ write the stack pointer

@ Invalidate and Enable Branch Prediction  
         MOV     r0, #0
         MCR     p15, #0, r0, c7, c5, #6
         ISB
         MRC     p15, #0, r0, c1, c0, #0
         ORR     r0, r0, #0x00000800
         MCR     p15, #0, r0, c1, c0, #0

@Neon
@		MRC p15, #0, r1, c1, c0, #2           @ r1 = Access Control Register
@		ORR r1, r1, #(0xf << 20)              @ enable full access for p10,11
@		MCR p15, #0, r1, c1, c0, #2           @ Access Control Register = r1
@		MOV r1, #0
@		MCR p15, #0, r1, c7, c5, #4           @flush prefetch buffer
@		MOV r0,#0x40000000
@		FMXR FPEXC, r0                        @ Set Neon/VFP Enable bit

@BSS Clear
@         LDR   r0, =_bss_start                 @ Start address of BSS
@         LDR   r1, =(_bss_end - 0x04)          @ End address of BSS
@         MOV   r2, #0
@Loop:
@         STR   r2, [r0], #4                    @ Clear one word in BSS
@         CMP   r0, r1
@         BLE   Loop                            @ Clear till BSS end

@SetVector
          mrc p15,0,r0,c1,c0,0
          bic r0,r0,#0x2000
          mcr p15,0,r0,c1,c0,0
          ldr r0,=Vector
          mcr p15,0,r0,c12,c0,0

@RuntimeInit
          LDR   r10,=RuntimeInit
          BX    r10

Jump:
          mov pc,r0

CLI:
          mrs r0, cpsr
          orr r0, r0, #I_F_BIT
          msr cpsr_c, r0
          mov pc, lr
 
STI:
          mrs r0, cpsr
          bic r0, r0, #I_F_BIT
          msr cpsr_c, r0
          mov pc, lr

         .end

 

 

Стартап (часть на С):

Spoiler

extern void HardwareInit(void);              //инит железа

//extern void __libc_init_array();           //стандартная функция
extern int main() __attribute__((noreturn)); //main.cpp

//These magic symbols are provided by the linker.
extern void *_estack;
extern void *_sidata,*_sdata,*_edata;
extern void *_sbss,*_ebss;
extern void (*__preinit_array_start[]) (void) __attribute__((weak));
extern void (*__preinit_array_end[]) (void) __attribute__((weak));
extern void (*__init_array_start[]) (void) __attribute__((weak));
extern void (*__init_array_end[]) (void) __attribute__((weak));
extern void (*__fini_array_start[]) (void) __attribute__((weak));
extern void (*__fini_array_end[]) (void) __attribute__((weak));

/*
Iterate over all the preinit/init routines (mainly static constructors)
Use with the "-nostartfiles" linker option instead __libc_init_array()
*/
inline void __attribute__((always_inline)) __run_init_array(void)
{
 int count;
 int i;

 count=__preinit_array_end-__preinit_array_start;
 for(i=0;i<count;i++)__preinit_array_start[i]();

#ifdef HAVE_INIT_FINI
 _init();
#endif
 
 count=__init_array_end-__init_array_start;
 for(i=0;i<count;i++)__init_array_start[i]();
}

void __attribute__((naked, noreturn)) RuntimeInit(void)
{
 HardwareInit();

 void **pSource,**pDest;
 for(pSource=&_sidata,pDest=&_sdata;pDest!=&_edata;pSource++,pDest++)*pDest=*pSource;
 for(pDest=&_sbss;pDest!=&_ebss;pDest++)*pDest=0;

 __run_init_array(); //__libc_init_array();

 (void)main();
}

 

 

Работают оба способа - через стандартный __libc_init_array и через облегчённый.

Скрипт линкера:

Spoiler

/* linker script */

MEM_SIZE = 0x00007E00 ; /* размер доступной памяти */

ROM_BASE = 0x00000000 ; /* стартовый адрес */
ROM_SIZE = 0x00007000 ;

RAM_BASE = ROM_BASE + ROM_SIZE ;
RAM_SIZE = MEM_SIZE - ROM_SIZE ;

ENTRY(Header)

_Minimum_Stack_Size = 0x100 ;

MEMORY
{
 ROM (XR) : ORIGIN = ROM_BASE, LENGTH = ROM_SIZE
 RAM (RW) : ORIGIN = RAM_BASE, LENGTH = RAM_SIZE
}

/* higher address of the user mode stack */
PROVIDE ( _estack = ALIGN(ORIGIN(RAM) + LENGTH(RAM) - 8 ,8) );

SECTIONS
{

.text :
{
	. = ALIGN(4);

        KEEP(*(.keep));

	*(.text)                   /* remaining code */
	*(.text.*)
	*(.rodata)                 /* read-only data (constants) */
	*(.rodata*)

	*(.eh_frame_hdr)
	*(.eh_frame)
	*(.ARM.extab* .gnu.linkonce.armextab.*)
	*(.gcc_except_table)
	*(.eh_frame_hdr)
	*(.eh_frame)

	*(.glue_7)
	*(.glue_7t)
	. = ALIGN(4);
} > ROM

.preinit_array     :
{
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array*))
    PROVIDE_HIDDEN (__preinit_array_end = .);
} > ROM

.init_array :
{
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*)))
    KEEP (*(.init_array*))
    PROVIDE_HIDDEN (__init_array_end = .);
} > ROM

.fini_array :
{
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT(.fini_array.*)))
    KEEP (*(.fini_array*))
    PROVIDE_HIDDEN (__fini_array_end = .);
} > ROM

/* .ARM.exidx is sorted, so has to go in its own output section.  */

__exidx_start = .;
.ARM.exidx :
{
	*(.ARM.exidx* .gnu.linkonce.armexidx.*)
} > ROM
__exidx_end = .;

.text.align :
{
	. = ALIGN(8);
	_etext = .;
	_sidata = _etext;		/* start of initialized data label */
} > ROM

.data : AT ( _sidata )		/* AT makes the LMA follow on in the binary image */
{
	. = ALIGN(4);
	_sdata = .;				/* start of .data label */
	KEEP( *(.data) )
	KEEP( *(.data.*) )
	. = ALIGN(4);
	_edata = .;				/* end of .data label */
} > RAM

/* .bss section - uninitialized data */
.bss :
{
	. = ALIGN(4);
	_sbss = .;				/* start of .bss label (for startup) */
	 *(.bss)
	 *(.bss.*)
	 *(COMMON)
	. = ALIGN(4);
	_ebss = .;				/* end of .bss label (for startup) */
	_end = .;				/* end of used ram (start of free memory, for malloc) */
	__end = .;				/* the same */
} > RAM
__bss_start__ = _sbss ;
__bss_end__ = _ebss ;

/*
* This is the user stack section
* This is just to check that there is enough RAM left for the User mode stack
* It should generate an error if it's full.
*/
   ._usrstack :
   {
    . = ALIGN(4);
     _susrstack = . ;
     . = . + _Minimum_Stack_Size ;
     . = ALIGN(4);
     _eusrstack = . ;
   } > RAM
_stack = _eusrstack ;

PROVIDE( _heap = _ebss );
PROVIDE ( _eheap = ALIGN(ORIGIN(RAM) + LENGTH(RAM) - 8 ,8) );

/*
* after that it's only debugging information.
*/

/* remove the debugging information from the standard libraries */
DISCARD :
{
	libc.a ( * )
	libm.a ( * )
	libgcc.a ( * )
}

}

 

 

Команды GCC:

del *.o
del *.elf
del *.asm
del *.bin
del *.fil
del *.a13

echo MAIN FILE
arm-none-eabi-g++.exe -O3 -Ofast -marm -march=armv7-a -fmax-errors=1 -ffunction-sections -fdata-sections -fno-exceptions -include aw_sdk/sdk.h -c led.cpp

echo CPP CLASS
arm-none-eabi-g++.exe -O3 -Ofast -marm -march=armv7-a -fmax-errors=1 -ffunction-sections -fdata-sections -fno-exceptions -Iaw_sdk/uart -c kex.cpp

  ...

echo C STARTUP
arm-none-eabi-gcc.exe -O3 -Ofast -marm -march=armv7-a -fmax-errors=1 -ffunction-sections -fdata-sections -fno-exceptions -Iaw_sdk -Iaw_sdk/core_v7 -Iaw_sdk/uart -c startup_c.c

echo HARDWARE INIT
arm-none-eabi-gcc.exe -O3 -Ofast -marm -march=armv7-a -fmax-errors=1 -ffunction-sections -fdata-sections -fno-exceptions -Iaw_sdk -Iaw_sdk/core_v7 -Iaw_sdk/uart -c hardware.c

echo ASM STARTUP
arm-none-eabi-as.exe -o startup_asm.o startup_asm.S


echo LINKER
arm-none-eabi-g++.exe -T led.lds -Wl,--gc-sections -Wl,--static -nostartfiles led.o kex.o startup_c.o startup_asm.o hardware.o -o led.elf -Xlinker -Map=led.map

echo DISASSEMBLER
arm-none-eabi-objdump.exe -D led.elf > led.asm

echo ELF to BIN
arm-none-eabi-objcopy.exe -O binary led.elf led.bin

echo ZERO-FILLER up to 0x7E00 bytes
filler.exe led.bin led.fil

echo CHECKSUMM for eGON
checksum.exe led.fil led.a13

echo LOAD ALLWINNER A13 PROGRAM via USB
sunxi-fel.exe -v spl led.a13

 

Сам главный файл и класс:

Spoiler

//kex.h

#ifndef KEX_H
#define KEX_H

class IClass {

	public:
		IClass(int sa, int sb);

	private:
		int a, b;

};

class TestClass {

	public:
		TestClass(const IClass& ic);

	private:
		int a1, b1;
};

#endif

//kex.cpp
  
#include "kex.h"

#include "printf.h"

IClass::IClass(int sa,int sb)
{
 a=sa;
 b=sb;
}

TestClass::TestClass(const IClass& ic)
{
 printfx("\nHi! I'm Constructor ;)");
}

//main

#include "kex.h"

IClass ic(2,5);
TestClass tc(ic);

int main(void)
{
 printf("\nA13 Class Test...\n");

 Loop:
 goto Loop;

 return 0;
}

 

 

Результат:

result.thumb.jpg.494106affd9b2f4786f6e2455aa2a452.jpg

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Вот ещё информация по теме. + конструкторы-дестуркторы в "обычном" С:

 

 

Кстати, так как в моём случае вся память R/W, то надобность в копировании LMA в VMA отпадает и скрипт линкера можно упростить: переменные с начальным ненулевым инитом пойдут в RO-data в секцию text.  И туда можно писать, так как это тоже RAM, а не  Flash.   Это позволило сэкономить немного памяти и упростить жизнь си-шному рантайму :)

 

Вот это можно исключить, ненулевые переменные пойдут в .text,  а нулевые в .bss

.text.align :
{
	. = ALIGN(8);
	_etext = .;
	_sidata = _etext;		/* start of initialized data label */
} > ROM

.data : AT ( _sidata )		/* AT makes the LMA follow on in the binary image */
{
	. = ALIGN(4);
	_sdata = .;				/* start of .data label */
	KEEP( *(.data) )
	KEEP( *(.data.*) )
	. = ALIGN(4);
	_edata = .;				/* end of .data label */
} > RAM

 

Изменено пользователем repstosw

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Присоединяйтесь к обсуждению

Вы можете написать сейчас и зарегистрироваться позже. Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.

Гость
Ответить в этой теме...

×   Вставлено с форматированием.   Вставить как обычный текст

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...