LINUX.ORG.RU

Избранные сообщения RisuX3

Динамические библиотеки, конспект

Форум — Development

Привет. Так вышло, что пришлось основательно разобраться в теме и пока память свежа изложил всё в виде небольшой памятки. Удобно по прошествии некоторого времени освежить память прочитав небольшой конспект. Вообще, по-хорошему, блог что ли какой завести )). Просьба - не флудить, ссылки/комментарии/дополнения по теме приветствуются. ЗЫ: подразумевается, что либы -fpic

1. Утилиты readelf, objdump. Читать man elf, man ld.so. N в именах структор
   подразумевает 32 или 64.
2. Структура ELF файла:
   1. заголовок (смещение 0, struct ElfN_Ehdr). Readelf::ELF Header
   2. program header table (массив struct ElfN_Phdr). Содержит информацию о том
      как отображать секции в память процесса. Readelf::Program Headers
   3. section header table (массив struct ElfN_Shdr). Readelf::Section Headers
3. link_map   
3.1. Загруженные в память модули попадают в список (массив) из struct link_map.
     Списков может быть много, каждый список - "пространство имён". Для
     загрузки модулей в неглобальный список (создание нового) используется
     dlmopen().
3.1. Получать link_map модуля через dlinfo() или dladdr1():
     [--code--]
     #define _GNU_SOURCE
     #include <link.h>
     #include <dlfcn.h>
     #include <stdio.h>
     int main()
     {
        static char addr_in_mod;
        Dl_info __info;
        struct link_map *lm;
        if(dladdr1(&addr_in_mod, &__info, (void*)&lm, RTLD_DL_LINKMAP) != 0) {
           printf("link_map:\n");
           struct link_map *i = lm;
           for(; i->l_prev != NULL; i = i->l_prev);
           for (; i != NULL; i = i->l_next)
              printf("addr diff=%p  name=%s%s",(void*)i->l_addr,  i->l_name, i==lm?"  <--cur\n":"\n");
        }
     }
     //output:
     //link_map:
     //addr diff=0x41f000  name=  <--current module
     //addr diff=0xb7fc4000  name=linux-gate.so.1
     //addr diff=0xb7fa3000  name=/lib/libdl.so.2
     //addr diff=0xb7dc5000  name=/lib/libc.so.6
     //addr diff=0xb7fc6000  name=/lib/ld-linux.so.2
     [/--code--]
3.2. Во время переразмещений символ ищется в модулях указанных в link_map
     списке начиная от начала списка т.е. порядок важен, "gcc -ls1 -ls2"
     libs1.so находится в списке раньше, чем libs2.so.
3.3. При добавлении библиотеки через LD_PRELOAD, она попадает перед остальными
     разделяемыми библиотеками в глобальном link_map списке.
3.4. Опция RTLD_DEEPBIND для dlopen - собственные символы модуля приоритетнее
     символов из вышестоящих в link_map списке модулей.
     Собственные символы загружаемой библиотеки содержат:
      1. символы из самой загружаемой библиотеке
      2. символы из библиотек, которые были слинкованы с загружаемой из
         командной строки (у первых приоритет выше).
3.5. При загрузки через dlopen, библиотеки добавленные с флагом RTLD_GLOBAL
     имеют приоритет над RTLD_LOCAL, не смотря на то, что находятся в link_map
     списке позже (не относится к получению void f() через dlsym()). Например:
     [--code--]
     // предоставляет void f(), ссылается на void f().
     dlopen("lib1.so", RTLD_LOCAL);
     // предоставляет void f().
     dlopen("lib2.so", RTLD_GLOBAL);
     // при ленивом переразмещении, lib1.so будет ссылаться на lib2.so::f().
     [/--code--]
4. RTLD_GLOBAL - символы из загруженного модуля будут участвовать в
   переразмещениях для заргуженных в дальнейшем библиотек. RTLD_LOCAL - не будут.
   Если lib2.so линкуется с lib1.so через командную строку
   "gcc -fpic -shared -l2 s.c -o lib1.so", то видимость символов из lib2.so
   наследуется от видимости символов из lib1.so:
   [--code--]
   dlopen("./lib1.so", RTLD_LAZY|RTLD_GLOBAL);    // символы из lib2.so глобальные
   dlopen("./lib1.so", RTLD_LAZY|RTLD_LOCAL);     // символы из lib2.so локальные
   [/--code--]
   Если lib2.so подгружается из lib1.so через dlopen(), то видимость символов
   из lib2.so контролируется флагом dlopen() при загрузке lib2.so. Способ
   загрузки (через командную строку или dlopen) и флаг для dlopen при
   загрузки lib1.so значения не имеет.
5. Переразмещение (relocation).
5.1. Переразмещение - процесс соединения символьной ссылки с символьным
     определением.
     Переразмещение: ленивое - загрузчик вызывается при ссылке на символ, и
     ненеленивое - переразмещение при загрузке. Переразмещение переменных всегда
     неленивое.
5.2. Символы, требующие переразмещения, содержатся в .rel... секциях. В них
     находятся ElfN_Rel структуры.
     [--code--]
     typedef struct {
         Elf32_Addr r_offset;    \\ адрес внесения правки (адрес в GOT, например. readelf::Offset).
         uint32_t   r_info;      \\ содержит тип переразмещения и индекс в таблице символов (массив Elf32_Sym[]).
     } Elf32_Rel;
     typedef struct {
         uint32_t      st_name;   \\ индекс в таблице строк. Т.е. сопостовляет символ с Си строкой.
         Elf32_Addr    st_value;  \\ адрес символа в текущем модуле (readelf::Sym.Value).
         uint32_t      st_size;
         unsigned char st_info;
         unsigned char st_other;
         uint16_t      st_shndx;
     } Elf32_Sym;
     [/--code--]
5.3. Механизм обращения к переменным (требующим переразмещений):
     1. линкер на старте правит .got секцию, она начинает указывать на нужные
        данные.
     2. ссылка на переменную в коде (в .text секции):
          [--code--]
          call   44c <__x86.get_pc_thunk.ax>  # получаем в eax адрес следующей инструкции
          add    $0x1bcb,%eax                 # в eax адрес .got секции
          mov    0x14(%eax),%edx              # отступ от края .got на адрес переменной,
                                              # разыменовываем в edx
          [/--code--]
5.4. Механизм обращения к функциям, для пример - exfn():
     1. ссылка на exfn() в коде (в .text секции)
     2. переход на "трамплин" в .plt секции - plt@exfn()
     3. переход на разыменованный указатель из .got.plt, если переразмещение
        уже было произведено, то попадаем на exfn(), иначе:
        3.1. возврат в plt@exfn(), в стек кладётся смещение в .rel.plt
             секции Elf32_Rel структуры и указатель на link_map список
        3.2. вызов ld.so, правится указатель в .got.plt
        3.3. переход на exfn().
6. .dynamic секция может быть прочитана из программы через массив _DYNAMIC[],
   который содержит struct ElfN_Dyn, автоматически заполняется линкером.
7. Экспортируемые символы из elf модуля указываются в .dynsym секции.
8. -rdynamic опция линкера (для исполняемого ELF) - символы из exe, которые не
   были востребованы библиотеками, указанными в командной строке, не
   экспортируются (не указываются в .dynsym секции) и не участвуют в
   переразмещениях в библиотеках, которые подргружаются через dlopen. Данная
   опция заставляет линкер помещать в таблицу все функции.
9. Управление экспортом из модуля
   * Управление экспортом по умолчанию:
     gcc -fvisibility=default
     -fvisibility=hidden
     -fvisibility=internal
     -fvisibility=protected
   * Управление экспортом посимвольно:
     __attribute__ ((visibility ("hidden")));
     __attribute__ ((visibility ("hidden")))
   * Для группы:
     #pragma GCC visibility push(hidden)
     ...
     #pragma GCC visibility pop
   * static и анонимные namespace
   * Управление эспортом через export map, через опцию --version-script

 , , ,

pavlick
()

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

Новости — Документация
Группа Документация

В третьей статье из цикла «прозрачный брандмауэр с маршрутизатором» рассмотрена задача плавного перехода на новые адреса другого провайдера и особенности фильтрации пакетов через встроенный мост Linux на ядрах 4.X

>>> Статья полностью

 , , , policy-routing

vodz
()

Arch + Openbox + Tint2

Галерея — Скриншоты

Всем привет, еще один скриншот Openbox.
Более наглядно в видео: youtube.com.

  • Слева запускается лаунчер: rofi, а разноцветные иконки Tint2.
  • Курсы криптовалют: Conky.

>>> Просмотр (1366x768, 135 Kb)

 , ,

stupid
()

Нужен компилятор ANSI C, который не умирает

Форум — Development

Хочу добавить в свое приложение возможность расширения. Сначала хотел использовать любой скриптовый язык, но потом понял, что все они отстой и тормоза. По идее, если взять сишечку и скомпилять ее по быстрому, то времени это много не займет, компилять прямо в память, зато работать будет со скоростью света.

В принципе, можно просто вызывать gcc и создавать шаред-объект во временном файле, но это очень долго, если файлов будет много. Это как php, который умирает после каждого запроса. А есть ли компиляторы, которые могут работать как демоны, не тратя время на инициализацию при выполнении запросов, быстро компилить и требовать минимум ресурсов?

Алсо, подойдет и какая-то либа, в принципе можно и с собой таскать, внутри бинаря

 

ruzisufaka
()

Regexp in C

Форум — Development

Есть две либы, гнушная regex.h и pcre. С какой начать знакомство для новичка Си? И вкратце плюсы/минусы можете рассказать?

Update: прикрутил pcreposix.h — все зашибись. Всем спасибо :)

 , ,

gh0stwizard
()

Мой редактор уровней, в разработке

Галерея — Скриншоты

Пишу вот редактор уровней, Tiled мне оказалось мало, решил сделать что-то более сложное, чтобы можно было расставлять объекты как угодно, возможность создавать тайловые карты, менять топологию объектов (например чтобы сделать красивый блоб), назначать классы, менять ограничивающие полигоны, настраивать анимацию, ну и базовые деформации (хотя возможно их и не будет), так же хотелось сделать обрезание объектов, если слишком большой, чтобы не рисовать 2 разные текстуры, можно просто обрезать топологию.

Да дизайн похож на блендеровский, и не с проста, я ориентировался на бледеровский интерфейс, иконки тоже от туда взяты, со временем конечно придется их перерисовать. Так же позаимствовал панельку сверху тоже из идею нового UI для блендера. К сожалению толком своего ничего придумать не могу, а программа нужна, ну и интересно было ее поделать и реализовать интерфейс полностью на OpenGL.

Сейчас программа активно используется мною, для создани игры на Haxe, написал простой фреймворк для загрузки карт созданных в этом редакторе для него, в будущем будет еще и C++. Для меня программа оказалась очень даже полезной )

В будущем хочу сделать открытый доступ к программе, но не уверен что буду открывать исходники, т.к. скажу честно, боюсь критики )) И самое наверное элементарное здесь, что я не через makefile сделал, а через башскрипты, и один файл main.cpp инклудит все заголовочные файлы и исходный код! Я до этого много работал на дельфи, и пересел на C++ года 2 назад, и когда уже было очень многое написано, я только понял что я налажал, но уже поздно, а все переписывать не очень хочется.
Еще скриншоты:
http://habrastorage.org/files/7b3/c85/958/7b3c85958c004fafbd1200b9aab3abc7.png
http://habrastorage.org/files/f1b/c5e/62a/f1bc5e62a6ea4967abcb5940d0b9e6c5.png
http://habrastorage.org/files/1bf/504/3a4/1bf5043a42c444ba84f5b4c64614ba1f.png

А раньше он выглядел вот так:
http://habrastorage.org/files/667/d86/820/667d86820eb4476ab90bc2e3fd4895c2.png

Виде игры которую я делаю на Haxe, только приступил к работе:
http://www.youtube.com/watch?x-yt-ts=1421828030&x-yt-cl=84411374&v=CO...

>>> Просмотр (1920x1080, 238 Kb)

 ,

Int64
()

Vertex

Галерея — Скриншоты

Пользуясь случаем, показываю красивую тему оформления GTK, которую нарисовал horst3180.

GNOME, Docky.
Иконки Faience вперемешку с Flattr.

PNG (2560x1440, 2467 Kb)

>>> Просмотр (2560x1440, 1281 Kb)

 ,

zezic
()

Программирование на С

Форум — Development

Здравствуйте мои дорогие любители погромирования. Прочитал K&R «Язык программирования Си». Теперь нужна книга, которая расскажет как правильно писать код на С, общепринятые приёмы и стандартные алгоритмы решения типичных задач. Цель: пишу быдлокод для МК и хочу повысить свой скилл.

Перемещено mono из talks

ramon13666
()

Arch, i3, numix

Галерея — Скриншоты

Собственно долго пиленные конфиги i3 и xfce4-terminal дали свой результат.

i3bar собственно по-умолчанию скрыт и показывается только когда зажат mod, для скриншота включил.

WM: i3 + i3status + dmenu-xft-fuzzy + j4-dmenu-desktop

Шелл: zsh + oh-my-zsh

Терминал: xfce4-terminal

Тема gtk: Numix

Курсор: DMZ

ШГ: Droid (везде 10ый, в i3bar и dmenu 9ый, сглаживание, полный хинтинг)

Обои: через feh

Чего не видно: conky, vim, moc, vifm, иконки faience, chromium, 

Если кому интересно выложу еще скриншотов и конфиги.

>>> Просмотр (1366x768, 510 Kb)

 , ,

Deleted
()

Производительность; илитный запил оптимальных реализаций и основы матчасти.

Форум — Development

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

Это будет формат для самых маленьких, где я буду показывать как что-то пилится по-пацаночке. Его задача - на примерах пересказать штеудмануал тем, кому лень его читать, но кто очень любит спорить про код, перфоманс и матчасть. Ну и просто интересные наблюдения.

Изначально я хотел написать про то: что такое бесплатные вычисления на примере is_range() + сумма елементов массива, но тут выявилась смешная особенность, поэтому пока без is_range().

Начнём с простого - сумма елементов(float) массива. Как написать её быстро? Обычный крестопоц сделает так:

auto summ = accumulate(begin(vec), end(vec), 0.)

Этот код выдаёт 5.6GB/s(мы всё бенчим в л1д 32килобайта массив). Казалось бы, если бы мы слушали всяких «гуру», которые нам говорят: accumulate() - оптимизирован, «ты что умнее создатели stl"а?», «конпелятор умнее тебе - сам всё делает оптимально», «руками что-то делать слишком сложно и не нужно» - то мы бы там и остались с этими 5.6ГБ, но мы пойдём дальше и поймём почему так, и является ли это тем, что намн ужно.

Но посмотрев на код - он не векторизован:

	addq	$4, %rdx
	vcvtss2sd	-4(%rdx), %xmm2, %xmm2
	vaddsd	%xmm2, %xmm1, %xmm1

Почему? Патамучто это основная флоатпроблема: Он не ассоциативен - флоат не имеет в себе точных представлений всех чисел входящих в диапазон его «представления» т.е. порядкопроблемы.

Поэтому конпелятор НЕ ВЕКТОРИЗУЕТ флоат по умолчанию, ну никак. Даже такую банальщину.

Для решения этих проблем - есть ключик -funsafe-math-optimizations, который входит в -ffast-math, который кладёт на точность при вычислениях. Добавив его мы получаем уже 44.9GB/s.

Но теперь мы получаем ещё одну проблему - надо думать: «как бэ сунуть эту ключик не повредив там, где этот ключик не нужен».

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

double memadd_autovec(buf_t buf) { //5.609465GB/s, либо 44.969652GB/s с ffast-math
  float * it = buf_begin(buf), * end = buf_end(buf), summ = 0.;
  do {
    summ += *it++;
  } while(it != end);
  return summ;
}

double hsumf(__v8sf v) {
  return (v[0] + v[1] + v[2] + v[3] + v[4] + v[5] + v[6] + v[7]);
}

double memadd_vec(buf_t buf) { //45.652002GB/s и класть на ffast-math
  __v8sf * it = buf_begin(buf), * end = buf_end(buf), summ = {};
  do {
    summ += *it++;
  } while(it != end);
  return hsumf(summ);
}

Т.е. разницы никакой нет, кроме нужной нам реализации горизантального сложение вектора. Когда я говорил пацану: «векторную сишку для написания быстрого кода юзать намного проще, чем плюсы» - поцан нипонимэ, да и любые пацаны скажут - ну дак с -ffast-math оба выдают по 45гигов - нахрен эта сишка нужна?

А вот зачем:

double memadd(buf_t buf) { //132.878440GB/s
  __v8sf * it = buf_begin(buf), * end = buf_end(buf), summ = {};
  do {
    summ += *it++;summ += *it++;summ += *it++;summ += *it++;
  } while(it != end);
  return hsumf(summ);
}

Это называется пацанский анролл копипастой, а вот заставить конпелятор нормально что-то разанролить очень сложно.

Если бы мы слушали всяких «гуру», которые нам вещают: «анрол говно и не нужен» - мы бы так и седели с 45-ю гигами, а так мы сидим с 132.878440GB/s. Т.е. анролл нам дал немного не мало ~300%.

Но основная мысль, которую толкают всякие «гуру» - это не надо следить за тактами/считать такты и прочее. Но мы о5 сделаем наоборот и посмотрим что будет.

Т.к. наш юзкейс упирается на 99% в throughput и дёргается одна инструкция, то нам достаточно просто считать теоретическую производительность для моего камня. 4.5(частота камня)*8(т.е. у нас камень с avx, то там вектор 32байта, либо 8флоатов.)*1(throughput нашей инструкции - в данном случае vpaddps из интел мануала). Т.е. 36гигафлопс, либо ~144гига. Т.е. мы сняли овер 90% теоретической производительности - остальные 10% у нас ушли в наши циклы, всякие горизонтальные суммы вектора и прочее, ну и конечно же чтение данных из кеша.

Но самое смешное - на моём хасвеле умножение имеет throughput 0.5 - т.е. на хасвеле умножение быстрее сложения. Это новая забористая трава у интела.

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

Поэтому очень смешно слушать, когда какие-то пацаны говорят: «float point имеет такую же производительность как и инты» - нет, оно имеет такоу же производительность лишь по причине того, что на штеуде инты тормазят так же, как и float.

И чтобы окончательно в этом убедится - мы взглянем на fma(вариации умножения со сложением/вычитанем), которые имеют throughput 0.5 - да, да - на хасвеле умножение+сложение в 2раза быстрее просто сложения. Это уже не просто трава - это что-то принципиально новое.

У целочисленного сложения же throughput 0.5 и казалось бы, если мы поменяем в нашей функции float на int - у нас будет сложение работать в 2раза быстрее, но это не так. Оно выдаёт те же 130гигов, а почему?

Вообще у камня есть такая фича, допустим у нас:

add $1, %reg0//вот тут инструкция add залочит регистр reg0
add $1, %reg0//а эта инструкция уйдёт в лок до особождения предыдущей инструкцией регистра reg0

Чтобы такой жопы небыло - есть специальная фича:

add $1, %reg0//lock reg0
add $1, %reg0//И тут вместо того, чтобы уйти в лок - камень вместо reg0 даёт инструкции любой свободный регистр.

Эта фича называется прееименование регистров, либо как-то так - мне лень гуглить.

Дак вот штука в том, что фича работает через жопу. Мне лень читать мануал и искать почему так, но штука в том, что она ограничивает throughput. На умножении и целочисленном сложении она огранивает throughput c 0.5 до 1.

И вот я решил заюзать сложении через fma:

__v8sf fmaadd(__v8sf a, __v8sf b) {
  return _mm256_fmadd_ps(_mm256_set1_ps(1.), a, b);// a + b * 1. == a + b.
}

double memadd_fma(buf_t buf) {
  __v8sf * it = buf_begin(buf), * end = buf_end(buf), summ = {};
  do {
    summ = fmaadd(summ, *it++);
  } while(it != end);
  return hsumf(summ);
}

Но меня ждала жопа: 27.347290GB/s, причем не анролл и ничего не помогал. Я уж подумал, что мануал наврал, но позже до меня допёрло: у неё latency 5тактов и ((4.5×8)÷5)×4 ~= 29гигов - т.е. я получаю производительность с её latency, но какой жопой оно так?

Потом я вспомнил, что гцц гинерит анрольный код вида:

add $1, %reg0
add $1, %reg0
//а не
add $1, %reg0
add $1, %reg1

Т.е. на неё вообще не работает переименовывание регистров - и инструкции постоянно в локе. Я это проверил и оказался прав. Ну и я написал такой мемадд:


__v8sf fmaadd(__v8sf a, __v8sf b) {
  return _mm256_fmadd_ps(_mm256_set1_ps(1.), a, b);
}

inline void fma_10way_finality(__v8sf * cache, __v8sf * it, __v8sf * end) {
  switch(end - it) {
    case 8:
      *(cache + 7) = fmaadd(*(cache + 7), *(it + 7));
      *(cache + 6) = fmaadd(*(cache + 6), *(it + 6));
    case 6:
      *(cache + 5) = fmaadd(*(cache + 5), *(it + 5));
      *(cache + 4) = fmaadd(*(cache + 4), *(it + 4));
    case 4:
      *(cache + 3) = fmaadd(*(cache + 3), *(it + 3));
      *(cache + 2) = fmaadd(*(cache + 2), *(it + 2));
    case 2:
      *(cache + 1) = fmaadd(*(cache + 1), *(it + 1));
      *(cache + 0) = fmaadd(*(cache + 0), *(it + 0));
    case 0:
      break;
    default: error_at_line(-1, 0, __FILE__, __LINE__, "bad_aligned");
  }
}

double memaddfma_10way(buf_t buf) {
  __v8sf * it = buf_begin(buf), * end = buf_end(buf), summ = (__v8sf){};
  __v8sf * cache = (__v8sf[10]){{}};
  uint64_t i = 0;
  while((it += 10) <= end) {
    *(cache + i) = fmaadd(*(cache + i), *(it - i - 1));++i;
    *(cache + i) = fmaadd(*(cache + i), *(it - i - 1));++i;
    *(cache + i) = fmaadd(*(cache + i), *(it - i - 1));++i;
    *(cache + i) = fmaadd(*(cache + i), *(it - i - 1));++i;
    *(cache + i) = fmaadd(*(cache + i), *(it - i - 1));++i;
    *(cache + i) = fmaadd(*(cache + i), *(it - i - 1));++i;
    *(cache + i) = fmaadd(*(cache + i), *(it - i - 1));++i;
    *(cache + i) = fmaadd(*(cache + i), *(it - i - 1));++i;
    *(cache + i) = fmaadd(*(cache + i), *(it - i - 1));++i;
    *(cache + i) = fmaadd(*(cache + i), *(it - i - 1));++i;
    i = 0;
  }
  fma_10way_finality(cache, (it - 10), end);
  summ = (*(cache + 0) + *(cache + 1) + *(cache + 2) + *(cache + 3) +
	  *(cache + 4) + *(cache + 5) + *(cache + 6) + *(cache + 7) +
	  *(cache + 8) + *(cache + 9));
  return hsumf(summ);
}

Пришлось хреначить финалити, ибо тут «анролл» на 10, а почему на 10 - для максимального throughput"а - надо, чтобы каждый каждый регистр юзался через 5тактов - т.е. 10регистров.

И вся эта порятнка нужна для борьбы с тупостью конпелятора.

Это уже: 214.167252GB/s(раельно там в районе 250 - просто мой бенч говно). 107 гигафлопс на ведро. Из теоретических 144, но тут уже влияние кеша. Причем 50+ из которых выкидываются и просто бесплатные.

Теперь вопрос к пацанам - что нам дадут эти гагфлопсы, когда у нас будет массив не 32килобайта, а 32мегабайта? Зачем нужно выживать максимум, когда скорость памяти отсилы 20-30гигабайт и нам хватит даже С++ кода с ffast-math?

Ну и призываются упомянутые мною пацаны: mv - этот тот експерт, что вещал про «руками переименовывать регистры не надо» и «анрол ваще ненужен», emulek вещал про ненужность счёта тактов, и не понимал что такое «беслпатно», AIv - не понимал в чем проблема плюсов, ck114 - так же не понимал в чем проблема плюсов.

Бенчи: https://gist.github.com/superhackkiller1997/606be26fa158ef75501d - вроде я там ничего не напутал.

P.S. - не выпиливайте пж, пусть пацаны «нужно» или «не нужно». Мне интеерсно. Ну и там рекомендации пацанов.

 , , ,

Carb_blog
()