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

Самомодифицирующийся код в экосистеме Cortex-M.

Как заработает - напишу.

Проверил в железе - работает (Cortex-M0).

 

#include "stdafx.h"
#include "conio.h"
#include "windows.h"

#define IS_ALIGN   (1)
#define VAR_NUM    (32)

//-----------------------------------------------------------------------------
//  enum eVAR_TYPE
//-----------------------------------------------------------------------------
enum eVAR_TYPE
{
 VAR_NONE = 0,
 VAR_DWORD,
 VAR_WORD,
 VAR_BYTE,
};

//-----------------------------------------------------------------------------
//  typedef struct sVAR
//-----------------------------------------------------------------------------
typedef struct sVAR
{
 union
 {
   DWORD          data_dw;
   WORD           data_w;
   BYTE           data_b;
 };
 enum  eVAR_TYPE  type;
} sVAR;

volatile sVAR    var[VAR_NUM] =
{
 {0x12345678,  VAR_DWORD},
 {0xABCD,      VAR_WORD},
 {0x11223344,  VAR_DWORD},
 {0x55,        VAR_BYTE},
 {0x00,        VAR_BYTE},
 {0x66,        VAR_BYTE},
 {0x77,        VAR_BYTE},
 {0x88,        VAR_BYTE},
 {0x99,        VAR_BYTE},
 {0xAA,        VAR_BYTE},
 {0xBB,        VAR_BYTE},
 {0xCC,        VAR_BYTE},
 {0xDD,        VAR_BYTE},
 {0xEE,        VAR_BYTE},
 {0xFF,        VAR_BYTE},
 {0x98765432,  VAR_DWORD},
};

BYTE    exec_ram[1024];

//-----------------------------------------------------------------------------
//  void set_new_mask(const DWORD  mask)
//-----------------------------------------------------------------------------
void set_new_mask(const DWORD  mask)
{
 int    offset;
 int    last_var = 0;
 int    s_pos = 0;
 int    exec_ram_pos = 0;

 printf("// r0 = *s, r1 = var\n");

 //  PUSH  {r2, lr}
 exec_ram[exec_ram_pos++] = 0x04;
 exec_ram[exec_ram_pos++] = 0xB5;

 printf("B5 04 - push {r2, pc}\n");

 for(int i = 0; i < VAR_NUM; i++)
 {
   if(mask & (1 << i))
   {
     switch(var[i].type)
     {
       case VAR_DWORD:
         printf("// DWORD[%d]\n", i);

         offset = (i - last_var) * 8;
         last_var = i;

         while(offset > 128)
         {
           offset -= 128;
           // ADD  r1, #128
           exec_ram[exec_ram_pos++] = 128;
           exec_ram[exec_ram_pos++] = 0x31;

           printf("31 80 - add  r1, #128\n");
         }

         if(offset)
         {
           // ADD  r1, #offset
           exec_ram[exec_ram_pos++] = offset;
           exec_ram[exec_ram_pos++] = 0x31;

           printf("31 %02X - add  r1, #%d\n", offset, offset);
         }

         if(IS_ALIGN && (s_pos & 1))
         {
           // byte align

           // LDRB r2, [r1, #0]
           exec_ram[exec_ram_pos++] = 0x0A;
           exec_ram[exec_ram_pos++] = 0x78;

           printf("78 0A - ldrb r2, [r1, #0]\n");

           // STRB r2, [r0, #0]
           exec_ram[exec_ram_pos++] = 0x02;
           exec_ram[exec_ram_pos++] = 0x70;

           printf("70 02 - strb r2, [r0, #0]\n");

           // LDRB r2, [r1, #1]
           exec_ram[exec_ram_pos++] = 0x4A;
           exec_ram[exec_ram_pos++] = 0x78;

           printf("78 4A - ldrb r2, [r1, #1]\n");

           // STRB r2, [r0, #1]
           exec_ram[exec_ram_pos++] = 0x42;
           exec_ram[exec_ram_pos++] = 0x70;

           printf("70 42 - strb r2, [r0, #1]\n");

           // LDRB r2, [r1, #2]
           exec_ram[exec_ram_pos++] = 0x8A;
           exec_ram[exec_ram_pos++] = 0x78;

           printf("78 8A - ldrb r2, [r1, #2]\n");

           // STRB r2, [r0, #2]
           exec_ram[exec_ram_pos++] = 0x82;
           exec_ram[exec_ram_pos++] = 0x70;

           printf("70 82 - strb r2, [r0, #2]\n");

           // LDRB r2, [r1, #3]
           exec_ram[exec_ram_pos++] = 0xCA;
           exec_ram[exec_ram_pos++] = 0x78;

           printf("78 CA - ldrb r2, [r1, #3]\n");

           // STRB r2, [r0, #3]
           exec_ram[exec_ram_pos++] = 0xC2;
           exec_ram[exec_ram_pos++] = 0x70;

           printf("70 C2 - strb r2, [r0, #3]\n");
         }
         else if(IS_ALIGN && (s_pos & 2))
         {
           // word align

           // LDRH r2, [r1, #0]
           exec_ram[exec_ram_pos++] = 0x0A;
           exec_ram[exec_ram_pos++] = 0x88;

           printf("88 0A - ldrh r2, [r1, #0]\n");

           // STRH r2, [r0, #0]
           exec_ram[exec_ram_pos++] = 0x02;
           exec_ram[exec_ram_pos++] = 0x80;

           printf("80 02 - strh r2, [r0, #0]\n");

           // LDRH r2, [r1, #2]
           exec_ram[exec_ram_pos++] = 0x4A;
           exec_ram[exec_ram_pos++] = 0x88;

           printf("88 4A - ldrh r2, [r1, #2]\n");

           // STRH r2, [r0, #2]
           exec_ram[exec_ram_pos++] = 0x42;
           exec_ram[exec_ram_pos++] = 0x80;

           printf("80 42 - strh r2, [r0, #2]\n");
         }
         else
         {
           // dword align

           // LDR  r2, [r1, #0]
           exec_ram[exec_ram_pos++] = 0x0A;
           exec_ram[exec_ram_pos++] = 0x68;

           printf("68 0A - ldr  r2, [r1, #0]\n");

           // STR  r2, [r0, #0]
           exec_ram[exec_ram_pos++] = 0x02;
           exec_ram[exec_ram_pos++] = 0x60;

           printf("60 02 - str  r2, [r0, #0]\n");
         }

         // ADDS  r0, #4
         exec_ram[exec_ram_pos++] = 0x04;
         exec_ram[exec_ram_pos++] = 0x30;

         printf("30 04 - adds r0, #4\n");

         s_pos += 4;
         break;

       case VAR_WORD:
         printf("// WORD[%d]\n", i);

         offset = (i - last_var) * 8;
         last_var = i;

         while(offset > 128)
         {
           offset -= 128;
           // ADD  r1, #128
           exec_ram[exec_ram_pos++] = 128;
           exec_ram[exec_ram_pos++] = 0x31;

           printf("31 80 - add  r1, #128\n");
         }

         if(offset)
         {
           // ADD  r1, #offset
           exec_ram[exec_ram_pos++] = offset;
           exec_ram[exec_ram_pos++] = 0x31;

           printf("31 %02X - add  r1, #%d\n", offset, offset);
         }

         if(IS_ALIGN && (s_pos & 1))
         {
           // byte align

           // LDRB r2, [r1, #0]
           exec_ram[exec_ram_pos++] = 0x0A;
           exec_ram[exec_ram_pos++] = 0x78;

           printf("78 0A - ldrb r2, [r1, #0]\n");

           // STRB r2, [r0, #0]
           exec_ram[exec_ram_pos++] = 0x02;
           exec_ram[exec_ram_pos++] = 0x70;

           printf("70 02 - strb r2, [r0, #0]\n");

           // LDRB r2, [r1, #1]
           exec_ram[exec_ram_pos++] = 0x4A;
           exec_ram[exec_ram_pos++] = 0x78;

           printf("78 4A - ldrb r2, [r1, #1]\n");

           // STRB r2, [r0, #1]
           exec_ram[exec_ram_pos++] = 0x42;
           exec_ram[exec_ram_pos++] = 0x70;

           printf("70 42 - strb r2, [r0, #1]\n");
         }
         else
         {
           // word align

           // LDRH r2, [r1, #0]
           exec_ram[exec_ram_pos++] = 0x0A;
           exec_ram[exec_ram_pos++] = 0x88;

           printf("88 0A - ldrh r2, [r1, #0]\n");

           // STRH r2, [r0, #0]
           exec_ram[exec_ram_pos++] = 0x02;
           exec_ram[exec_ram_pos++] = 0x80;

           printf("80 02 - strh r2, [r0, #0]\n");
         }

         // ADDS  r0, #2
         exec_ram[exec_ram_pos++] = 0x02;
         exec_ram[exec_ram_pos++] = 0x30;

         printf("30 02 - adds r0, #2\n");

         s_pos += 2;
         break;

       case VAR_BYTE:
         printf("// BYTE[%d]\n", i);

         offset = (i - last_var) * 8;
         last_var = i;

         while(offset > 128)
         {
           offset -= 128;
           // ADD  r1, #128
           exec_ram[exec_ram_pos++] = 128;
           exec_ram[exec_ram_pos++] = 0x31;

           printf("31 80 - add  r1, #128\n\r");
         }

         if(offset)
         {
           // ADD  r1, #offset
           exec_ram[exec_ram_pos++] = offset;
           exec_ram[exec_ram_pos++] = 0x31;

           printf("31 %02X - add  r1, #%d\n", offset, offset);
         }

         // LDRB r2, [r1, #0]
         exec_ram[exec_ram_pos++] = 0x0A;
         exec_ram[exec_ram_pos++] = 0x78;

         printf("78 0A - ldrb r2, [r1, #0]\n");

         // STRB r2, [r0, #0]
         exec_ram[exec_ram_pos++] = 0x02;
         exec_ram[exec_ram_pos++] = 0x70;

         printf("70 02 - strb r2, [r0, #0]\n");

         // ADDS  r0, #1
         exec_ram[exec_ram_pos++] = 0x01;
         exec_ram[exec_ram_pos++] = 0x30;

         printf("30 01 - adds r0, #1\n");

         s_pos += 1;
         break;
     }
   }
 }
 printf("// EXIT\n");

 //  POP  {r2, pc}
 exec_ram[exec_ram_pos++] = 0x04;
 exec_ram[exec_ram_pos++] = 0xBD;

 printf("BD 04 - pop  {r2, pc}\n");

 #if 0
 printf("SMC:\n");
 for(int i = 0; i < exec_ram_pos / 2; i++)
   printf("%02X %02X\n", exec_ram[i * 2 + 1], exec_ram[i * 2 + 0]);
 #endif
}

int _tmain(int argc, _TCHAR* argv[])
{
 set_new_mask(0xc7);
 _getch();
 return 0;
}


 

СМК:

// r0 = *s, r1 = var
B5 04 - push {r2, pc}
// DWORD[0]
68 0A - ldr  r2, [r1, #0]
60 02 - str  r2, [r0, #0]
30 04 - adds r0, #4
// WORD[1]
31 08 - add  r1, #8
88 0A - ldrh r2, [r1, #0]
80 02 - strh r2, [r0, #0]
30 02 - adds r0, #2
// DWORD[2]
31 08 - add  r1, #8
88 0A - ldrh r2, [r1, #0]
80 02 - strh r2, [r0, #0]
88 4A - ldrh r2, [r1, #2]
80 42 - strh r2, [r0, #2]
30 04 - adds r0, #4
// BYTE[6]
31 20 - add  r1, #32
78 0A - ldrb r2, [r1, #0]
70 02 - strb r2, [r0, #0]
30 01 - adds r0, #1
// BYTE[7]
31 08 - add  r1, #8
78 0A - ldrb r2, [r1, #0]
70 02 - strb r2, [r0, #0]
30 01 - adds r0, #1
// EXIT
BD 04 - pop  {r2, pc}

 

Результат на Cortex-M0

RESULT[20000F44]: 78 56 34 12 CD AB 44 33 22 11 77 88

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


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

Проверил в железе - работает (Cortex-M0).

мммм... неправильно :laughing:

См. моё сообщение #39 - какой должен быть результат работы.

К тому же: a) const в аргументе set_new_mask() - лишнее; б) R2,LR в вашей функции - не нужно сохранять, см. соглашения вызова компилятора; в) даже по Вашему алгоритму копирования - очень неоптимально, количество команд в 2 раза больше, чем можно было бы (для Cortex-M4).

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


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

мммм... неправильно

А что именно неправильно?

Есть таблица переменных состоящая из значений и типов.

Есть маска, где каждый бит связан с соответствующим элементом таблицы.

Есть функция-генератор, создающая в рантайме код, при выполнении которого

в выходной поток копируются данные из таблицы с учетом длины и заданной маски.

Для заданного var, RESULT (отработал СМК) такой ожидаете?

Если нет, то где ошибка?

 

a) const в аргументе set_new_mask() - лишнее;

Ну, функция set_new_mask не должна менять mask, поэтому и const. Или я вас не понял.

 

б) R2,LR в вашей функции - не нужно сохранять, см. соглашения вызова компилятора;

Вы же супер-скорость хотели. Выкидывайте push и pop - и вот вам готовый участок кода, дающий результат,

но не забудьте проинициализировать r0 и r1.

Если нужна именно как функция, то достаточно одного "bx lr" в конце.

 

в) даже по Вашему алгоритму копирования - очень неоптимально, количество команд в 2 раза больше, чем можно было бы (для Cortex-M4).

Какие именно команды лишние? Можно IS_ALIGN обнулить, тогда генератор будет работать с невыровненными данными.

Попробую сегодня избавиться от лишних adds.

 

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


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

А что именно неправильно?

Есть таблица переменных состоящая из значений и типов.

Вы же ассемблер понимаете? Тогда посмотрите на требуемый результат (код) в сообщении #39 и поймёте в чём разница.

Нет таблицы переменных, есть таблица указателей на переменные. Так как эти переменные разбросаны по всей программе и объединить их в одну область памяти (структуру) нельзя. Они находятся в разных обособленных службах программы, и там и должны оставаться. А адресация их идёт через таблицу указателей. Я писал об этом. И в примере результата это чётко видно.

Если си-исходник будет понятней, то:

enum {N = <максимум 64>};
enum {TYP_s16, TYP_u16, TYP_s32, TYP_u32, TYP_float, TYP_n};
struct {
 u8 main:3;
 u8 misc:5;
} const isTyp[N] = {{TYP_s32, ...}, {TYP_u16, ...}, ...};
u8 isTypSize[TYP_n] = {2, 2, 4, 4, 4};
char *psrc;
char *pdst;
if (map & 1 << 0) {
 psrc = varPtrs[0];
 if (isTypSize[isTyp[0].main]) == 4) {
   *(u32 *)pdst = *(u32 *)psrc;
   pdst += 4;
 } else {
   *(u16 *)pdst = *(u16 *)psrc;
   pdst += 2;
 }
}
if (map & 1 << 1) {
 psrc = varPtrs[1];
 if (isTypSize[isTyp[1].main]) == 4) {
   *(u32 *)pdst = *(u32 *)psrc;
   pdst += 4;
 } else {
   *(u16 *)pdst = *(u16 *)psrc;
   pdst += 2;
 }
}
//и так далее... так сейчас в си-исходнике сделано. Цель СМК - построить оптимальный код выкинув кучу ненужных операций из этого алгоритма по текущей карте map, до следующего её изменения.

 

Какие именно команды лишние? Можно IS_ALIGN обнулить, тогда генератор будет работать с невыровненными данными.

Попробую сегодня избавиться от лишних adds.

У меня Cortex-M4. Он поддерживает команды с пост- и пре- модификациями адреса. Например: LDR R0, [R1], #4 тогда ваш код сократится в 2 раза.

Да я уже вчера всё сделал. И даже больше. Мой генератор ещё и перемежение команд поддерживает для исключения штрафов между командами LDR. :rolleyes:

И я его на асме написал. Хотя это просто из спортивного интереса. B)

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


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

Да я уже вчера всё сделал

Покажите пожалуйста выход генератора для случая DWORD, WORD, DWORD, BYTE, BYTE в виде asm-инструкций.

У меня с таблицей переменных и невыровненными данным получилось так

// r0 = *s, r1 = var
// DWORD[0]
68 0A - ldr  r2, [r1, #0]
60 02 - str  r2, [r0, #0]
// WORD[1]
89 0A - ldrh r2, [r1, #8]
80 82 - strh r2, [r0, #4]
// DWORD[2]
8A 0A - ldrh r2, [r1, #16]
80 C2 - strh r2, [r0, #6]
8A 4A - ldrh r2, [r1, #18]
81 02 - strh r2, [r0, #8]
// BYTE[6]
31 30 - add  r1, #48
78 0A - ldrb r2, [r1, #0]
72 82 - strb r2, [r0, #10]
// BYTE[7]
7A 0A - ldrb r2, [r1, #8]
72 C2 - strb r2, [r0, #11]
30 0C - adds r0, #12
47 70 - bx   lr

 

Кста, а есть у кого-нить справочник машинных кодов для Cortex-M всех семейств?

Не путать со справочником asm-команд, который найти не составляет труда.

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


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

Кста, а есть у кого-нить справочник машинных кодов для Cortex-M всех семейств?

ARM®v7-M Architecture Reference Manual

 

Господа, вы тут явно пытаетесь эмулировать DMA. :biggrin:

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


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

Есть мысль использовать самомодифицирующийся код для оптимизации решения одной задачи.

Если "модификация" происходит под внешним управлением - то это называется эмуляцией.

Доказательство очень простое: информация от том что и как необходимо изменить - остаётся без изменений. Место хранения этой информации не важно, важно то что эта информация является избыточной.

 

Самомодифицирующийся код в явном виде делится на два типа поведения: модификация себя любимого под внешним управлением, и автономное поведение. Оба случая утрачивают начальное состояние кода. Более простое название этого процесса - полиморфный код.

Используется вирусами, защитой лицензии, и как не странно - антивирусами.

 

Насколько я верно понял, вам нужно выполнять внешний код из текстового ввода. То-есть все те-же команды ассемблера arm - записанные в текстовом массиве. Без разметки границ, без легирования меток, и без чёткой локализации хранения данных.

Поздравляю - это называется парсер.

 

Имеет смысл посмотреть блочную реализацию парсера бэйсика. На него кстати есть с десяток активных проектов на гите, в том числе и под арм.

Или реализацию джавы для арм, хотя там без тяжёлых наркотиков очень трудно разобраться.

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


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

Покажите пожалуйста выход генератора для случая DWORD, WORD, DWORD, BYTE, BYTE в виде asm-инструкций.

Да, как только поборю IAR. Если у Вас IAR, то как Вы ему сказали, чтобы он массив объявленный в программе как массив, показывал в окне дизасма как код? А то у меня он, собака, как только видит что это массив, показывает просто как кучу констант DC32 как этот массив не объявляй. Не могу заставить его дизассемблировать :((((

И мне не нужны байтовые операции - переменные только 16 и 32 бита.

 

Кста, а есть у кого-нить справочник машинных кодов для Cortex-M всех семейств?

Мне тоже хотелось-бы. Не нашёл в инете. Только если правильно выражаться: таблица маш.кодов набора инструкций Thumb-2.

 

Господа, вы тут явно пытаетесь эмулировать DMA. :biggrin:

Да причём тут DMA??? Каким боком он поможет в этой задаче? Разве чтобы только затормозить процедуру в несколько раз?

 

Более простое название этого процесса - полиморфный код.

Мне без разницы как это называть. Пускай будет полиморфный.

А что именно требуется - мне кажется уже каждый должен был понять, хоть немного почитав тред.

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


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

Да, как только поборю IAR.

gcc-шный objdump легко превращает бинарник в s-файл.

Можете скинуть бинарь - я его конвертну, если у вас нет gcc.

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


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

Да причём тут DMA??? Каким боком он поможет в этой задаче? Разве чтобы только затормозить процедуру в несколько раз?

Скопирует все переменные в одну область и вышлет куда надо.

Или даже без копирования вышлет.

 

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


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

Можете скинуть бинарь - я его конвертну, если у вас нет gcc.

Спасибо, уже не надо - просто копировал из массива результат в неиспользуемую память МК, а для такой памяти IAR позволяет делать дизасм. Так и отладил.

Вобщем: всё ок работает алгоритм. :rolleyes:

Ниже прикладываю результат его работы для 64-битного слова: 0000.002A.1003.0006.

При таких весах битов карты (см. установленные биты в бит-карте выше):

бит1=2 байта, бит2=2 байта, бит16=4 байта, бит17=2 байта, бит28=4 байта, бит33=4 байта, бит35=4 байта, бит37=2 байта.

 

Результат картинкой (не знаю как в IAR сохранить текстовое содержимое окна дизасма, а InqSoft Scaner не захватывает текст из этого окна):

post-38713-1530364627_thumb.png

А здесь результат в бинарнике:

smk00.zip

Итого - время выполнения этого кода думаю будет == примерно 31 такт (не измерял), при условии что dst на входе - выровнен на 4.

Как можно догадаться: функция строится таким образом, чтобы она была определена как:

extern "C" void * Func(void *dst, u32 *table);

где: dst - буфер для записи (передаётся в R0); table - массив из 64-х указателей на захватываемые переменные.

 

Скопирует все переменные в одну область и вышлет куда надо.

Или даже без копирования вышлет.

Ну-ну. А теперь объясните как он это скопирует, когда переменные разбросаны по всей памяти МК кусочками по 2 и по 4 байта? :smile3009:

Про передачу свЯзными списками я в курсе.

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


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

Ну-ну. А теперь объясните как он это скопирует, когда переменные разбросаны по всей памяти МК кусочками по 2 и по 4 байта? :smile3009:

Про передачу свЯзными списками я в курсе.

И чем не подошла передача связными списками?

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


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

И чем не подошла передача связными списками?

Тем что при программировании каждого сегмента списка, DMA-контроллер читает из памяти следующий блок-описатель сегмента, который в моём МК насколько помню == 5 слов. Это кроме собственно пересылки потом. И так - на каждую переменную. В итоге количество обращений к памяти в разы больше, и скорость выполнения в разы меньше чем с СМК. Уже не говоря о манипуляциях с IO-регистрами DMA.

Да и ОЗУ для описания такого связного списка нужно больше.

В то время как время выполнения результата генератора СМК приведённого выше, думаю должно быть == ~31 такт.

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


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

В то время как время выполнения результата генератора СМК приведённого выше, думаю должно быть == ~31 такт.

Что-то непонятная мне логика.

В управлении мотором или SMPS самые приоритетные прерывания и процедуры - это токовые контуры.

Они безальтернативно самые приоритетные и прервут любую пересылку или еще что-там.

Т.е. ваша программная пересылка гарантировано будет прервана и задержана как минимум на время прерываний в токовом контуре.

Таким образом теряете детерминизм и гарантированность времени окончания пересылки, так какой прок экономить такты?

DMA же этой проблемы не имеет.

 

Для справки, прерываний в токовом контуре у самых навороченных DSP от TI заточенных на это длится не менее 1 мкс, это 140 тактов в чипе с частотой 140 МГц.

Вот плюс минус сотня тактов и будет погрешностью. Так чего экономить 30 если погрешность больше 100?

 

Я честно думал, что речь идет об оптимизации именно алгоритмов в токовых контурах или алгоритмов модуляции.

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


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

jcxz, а зачем тут это? Вы сначала загружаете адрес интересуемой переменной из таблицы, потом загружаете значение переменной по этому адресу, и сохраняете в dst с постинкрементом, это понятно. А выделенное то зачем? Вы что-то говорили про перемежение инструкций для удаления штрафов LDR/STR. Могли бы озвучить, в чем там проблема?

image.png

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


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

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

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

Гость
К сожалению, ваш контент содержит запрещённые слова. Пожалуйста, отредактируйте контент, чтобы удалить выделенные ниже слова.
Ответить в этой теме...

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

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

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

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

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

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