ARM-ы для самых маленьких: тонкости компиляции и компоновщик, часть 1

от автора


Продолжая серию статей про разработку с нуля для ARM, сегодня я затрону тему написания скриптов компоновщика для GNU ld. Эта тема может пригодиться не только тем, кто работает со встраиваемыми системами, но и тем, кто хочет лучше понять строение исполняемых файлов. Хотя примеры так или иначе основаны на тулчейне arm-none-eabi, суть компоновки та же и у компоновщика Visual Studio, например.

Предидущие статьи:

Примеры кода из статьи: https://github.com/farcaller/arm-demos

Когда мы компилируем исходный файл, на выходе мы получаем файл объектный, который типично содержит в себе несколько секций с данными. Четыре самые распространенные секции это:

  • .text — скомпилированный машинный код;
  • .data — глобальные и статические переменные;
  • .rodata — аналог .data для неизменяемых данных;
  • .bss — глобальные и статические переменные, которые при старте содержат нулевое значение.

В бинарных файлах, с которыми мы работаем в рамках этого цикла, будут часто попадаться еще две секции:

  • .comment — информация о версии компилятора;
  • .ARM.attributes — ARM-специфичные атрибуты файла.

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

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

Заглянем внутрь

В качестве первого примера мы изучим следующий код на C: module_a.c:

static int local_function();  int external_counter; static int counter; static int preset_counter = 5; const int constant = 10;  int public_function() {     volatile int i = 3 + constant;     ++external_counter;     return local_function() * i; }  static int local_function() {     ++counter;     ++preset_counter;     return counter + preset_counter; } 

Скомпилируем его и посмотрим, какие секции мы получили:

% rake 'show:sections[a]' arm-none-eabi-gcc -mthumb -O2 -mcpu=cortex-m0 -c module_a.c -o build/module_a.o arm-none-eabi-objdump build/module_a.o -h  build/module_a.o:     file format elf32-littlearm  Sections: Idx Name          Size      VMA       LMA       File off  Algn   0 .text         00000034  00000000  00000000  00000034  2**2                   CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE   1 .data         00000004  00000000  00000000  00000068  2**2                   CONTENTS, ALLOC, LOAD, DATA   2 .bss          00000004  00000000  00000000  0000006c  2**2                   ALLOC   3 .rodata       00000004  00000000  00000000  0000006c  2**2                   CONTENTS, ALLOC, LOAD, READONLY, DATA   4 .comment      00000071  00000000  00000000  00000070  2**0                   CONTENTS, READONLY   5 .ARM.attributes 00000031  00000000  00000000  000000e1  2**0                   CONTENTS, READONLY 

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

% rake 'show:symbols:text[a]' arm-none-eabi-objdump build/module_a.o -j .text -t  build/module_a.o:     file format elf32-littlearm  SYMBOL TABLE: 00000000 l    d  .text  00000000 .text 00000000 g     F .text  00000034 public_function 

Откроем man по objdump для консультации. В этой секции мы видим два символа: .text — это отладочный символ, который указывает на начало секции, public_function — это символ, который указывает на нашу функцию. Для local_function символа нет, так как функция объявлена как static, т.е., она не экспортируется за пределы объектного файла.

% rake 'show:symbols:data[a]' arm-none-eabi-objdump build/module_a.o -j .data -j .bss -t  build/module_a.o:     file format elf32-littlearm  SYMBOL TABLE: 00000000 l    d  .data  00000000 .data 00000000 l    d  .bss 00000000 .bss 00000000 l     O .data  00000004 preset_counter 00000000 l     O .bss 00000004 counter 

В секциях .data и .bss находится два из наших счетчиков — preset_counter и counter. Они находятся в разных секциях, так как у preset_counter есть начальное значение, которое и сохранено в .data:

% rake 'show:contents[a,.data]' arm-none-eabi-objdump build/module_a.o -j .data -s  build/module_a.o:     file format elf32-littlearm  Contents of section .data:  0000 05000000 

У counter значения нет, так что он инициализируется в ноль и попадает в секцию .bss. Сама секция .bss физически в файле не присутствует, так как ее содержимое всегда фиксировано, — это нули. Если бы вы объявили char buffer[1024] в коде, то компилятору пришлось бы записать в объектный файл килобайт пустого места, что лишено смысла.

В этот момент у вас может появится вопрос — куда пропал external_counter?

% rake 'show:symbols:all[a]' arm-none-eabi-objdump build/module_a.o -t  build/module_a.o:     file format elf32-littlearm  SYMBOL TABLE: 00000000 l    df *ABS*  00000000 module_a.c 00000000 l    d  .text  00000000 .text 00000000 l    d  .data  00000000 .data 00000000 l    d  .bss 00000000 .bss 00000000 l    d  .rodata  00000000 .rodata 00000000 l     O .data  00000004 preset_counter 00000000 l     O .bss 00000004 counter 00000000 l    d  .comment 00000000 .comment 00000000 l    d  .ARM.attributes  00000000 .ARM.attributes 00000000 g     F .text  00000034 public_function 00000004       O *COM*  00000004 external_counter 00000000 g     O .rodata  00000004 constant 

external_counter отправился в секцию *COM*. В данном случае это означает, что он, возможно, находится за пределами этого объектного файла. Уже на этапе компоновки ld будет разбираться, объявлен ли символ в другом файле, или ему следует создать его самому — в данном случае, в секции .bss. Также обратите внимание на то, что const int constant попал в .rodata. Компилятор гарантирует, что коду не понадобится изменять значение по этому адресу, так что компоновщик может спокойно разместить его во флеш-памяти.

Мы можем посмотреть на .comment:

% rake 'show:contents[a,.comment]' arm-none-eabi-objdump build/module_a.o -j .comment -s  build/module_a.o:     file format elf32-littlearm  Contents of section .comment:  0000 00474343 3a202847 4e552054 6f6f6c73  .GCC: (GNU Tools  0010 20666f72 2041524d 20456d62 65646465   for ARM Embedde  0020 64205072 6f636573 736f7273 2920342e  d Processors) 4.  0030 372e3320 32303133 30333132 20287265  7.3 20130312 (re  0040 6c656173 6529205b 41524d2f 656d6265  lease) [ARM/embe  0050 64646564 2d345f37 2d627261 6e636820  dded-4_7-branch  0060 72657669 73696f6e 20313936 3631355d  revision 196615]  0070 00 

Тут действительно записана версия компилятора. Также мы можем заглянуть в .ARM.attributes, правда для этого стоит задействовать уже не objdump, а readelf:

% rake 'show:attrs[a]' arm-none-eabi-readelf build/module_a.o -A Attribute Section: aeabi File Attributes   Tag_CPU_name: "Cortex-M0"   Tag_CPU_arch: v6S-M   Tag_CPU_arch_profile: Microcontroller   Tag_THUMB_ISA_use: Thumb-1   Tag_ABI_PCS_wchar_t: 4   Tag_ABI_FP_denormal: Needed   Tag_ABI_FP_exceptions: Needed   Tag_ABI_FP_number_model: IEEE 754   Tag_ABI_align_needed: 8-byte   Tag_ABI_align_preserved: 8-byte, except leaf SP   Tag_ABI_enum_size: small   Tag_ABI_optimization_goals: Aggressive Speed 

Документацию по публичным тегам можно посмотреть на инфоцентре ARM.

Собираем все в кучу

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

Основная работа ld вертится вокруг карты памяти, которую мы видели в первой части. Если сильно упростить, компоновка — это процесс выдирания секций из объектных файлов, раскладывание их по указанным адресам и исправление перекрестных ссылок. В «стандартных» ОС ядро умеет читать выходной файл и загружать секции в память по ожидаемым виртуальным адресам. Также динамический компоновщик выполняет схожую работу, догружая в определенные места памяти внешние библиотеки и настраивая на них перекрестные ссылки.

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

Возьмем простой сценарий компоновщика и разберем по кусочкам. layout.ld:

MEMORY {    rom(RX)   : ORIGIN = 0x00000000, LENGTH = 0x8000    ram(WAIL) : ORIGIN = 0x10000000, LENGTH = 0x2000 }  ENTRY(public_function)  SECTIONS {   .text : { *(.text) } > rom   _data_start = .;   .data : { *(.data) } > ram AT> rom   _bss_start = .;   .bss  : { *(.bss) }  > ram   _bss_end = .; } 

Конфигурация компоновщика по умолчанию позволяет ему использовать всю доступную память (где-то около 0xFFFFFFFF байт в случае 32-битного ARM). Начнем с того, что определим регионы памяти, которые можно использовать: rom и ram. Буквы в скобках определяют атрибуты: доступ на чтение, запись, исполнение, выделение памяти. Секции, которые явно не указаны в сценарии, будут раскиданы по регионам с подходящими атрибутами автоматически. Если для секции не найдется места, компоновщик откажется работать, аргументируя свое поведение как-то так: error: no memory region specified for loadable section `.data'.

Два параметра, ORIGIN и LENGTH, задают начало и длину региона соответственно, еще можно встретить варианты org, o, len и l, они эквивалентны. Значение — это выражение, т.е., в нем можно выполнять арифметические операции или использовать суффиксы K, M, и т.п. Запись LENGTH = 0x8000, например, можно альтернативно выполнить так: l = 32K.

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

Исходные секции задаются в форме ИМЯ_ФАЙЛА(ИМЯ_СЕКЦИИ), символ * ведет себя стандартным образом, так что запись *(.text) означает: секции .text из всех файлов.

У секции есть два адреса: LMA (Load Memory Address) — откуда она загружается, и VMA (Virtual Memory Address) — по какому адресу она доступна в виртуальной памяти. Объясняя проще, LMA — это где она окажется в бинарном файле, а VMA — это куда будут перенаправлены символы, т.е., указатель на символ в коде будет ссылаться на VMA-адрес.

Нас интересуют три секции — код, данные и данные, которые нулевые по умолчанию. Таким образом, мы копируем код (.text) во флеш-память, данные (.data) – во флеш-память, но из расчета, что они будут доступны в оперативной памяти, и .bss — в оперативную память.

Для .bss, в общем случае, инициализация не требуется, так как при старте микроконтроллера оперативная память и так, вероятно, обнулена. Но вот с .data придется повозиться отдельно, проблема из-за двоякой натуры. С одной стороны, там хранятся конкретные данные (стартовое значение preset_counter), так что она должна быть во флеш-памяти. С другой стороны, это секция, доступная на запись, так что она должна быть в оперативной памяти. Эта проблема решается разными LMA и VMA, а также дополнительным кодом на C, который при запуске будет копировать содержимое из LMA в VMA. Для константных данных, которые обычно находятся в секции .rodata, такая процедура, например, не нужна, мы можем спокойно читать из прямо из флеш-памяти.

У компоновщика есть понятие курсора — это текущий LMA. В начале блока SECTIONS курсор равен нулю и постепенно увеличивается по мере добавления новых секций. Текущее значение курсора хранится в переменной . (точка).

Давайте запустим компоновщик и посмотрим результат его работы:

% rake 'show:map[a]' arm-none-eabi-ld -T layout.ld -M -o build/out.elf build/module_a.o  Allocating common symbols Common symbol       size              file  external_counter    0x4               build/module_a.o  Memory Configuration  Name             Origin             Length             Attributes rom              0x0000000000000000 0x0000000000008000 xr ram              0x0000000010000000 0x0000000000002000 awl *default*        0x0000000000000000 0xffffffffffffffff 

Во-первых, мы видим, как компоновщик выносит в отдельную категорию «общий» символ external_counter. Далее мы видим, что наша конфигурация памяти была загружена и добавлена к конфигурации по умолчанию (которая выделяет все адресное пространство).

Linker script and memory map   .text           0x0000000000000000       0x34  *(.text)  .text          0x0000000000000000       0x34 build/module_a.o                 0x0000000000000000                public_function                 0x0000000000000034                _data_start = . 

Далее компоновщик размещает в памяти секции, которые мы указали, в первую очередь .text.

.rodata         0x0000000000000034        0x4  .rodata        0x0000000000000034        0x4 build/module_a.o                 0x0000000000000034                constant  .glue_7         0x0000000000000038        0x0  .glue_7        0x0000000000000000        0x0 linker stubs  .glue_7t        0x0000000000000038        0x0  .glue_7t       0x0000000000000000        0x0 linker stubs  .vfp11_veneer   0x0000000000000038        0x0  .vfp11_veneer  0x0000000000000000        0x0 linker stubs  .v4_bx          0x0000000000000038        0x0  .v4_bx         0x0000000000000000        0x0 linker stubs  .iplt           0x0000000000000038        0x0  .iplt          0x0000000000000000        0x0 build/module_a.o  .rel.dyn        0x0000000000000038        0x0  .rel.iplt      0x0000000000000000        0x0 build/module_a.o 

Следом идут секции, которые мы не указывали явно — .rodata, .glue_7, .glue_7t, .vfp11_veneer, .v4_bx, .iplt, .rel.dyn. С .rodata все понятно, там в четырех байтах хранится наша константа constant. Что касается остальных секций, то их существование обязано всяческой поддержке работоспособности, например, трамплинам из ARM в Thumb. Все эти секции пустые и не попадают в итоговый образ.

.data           0x0000000010000000        0x4 load address 0x0000000000000038  *(.data)  .data          0x0000000010000000        0x4 build/module_a.o                 0x0000000010000004                _data_end = . 

Вот и наша секция .data, как видите, она находится по адресу 0x10000000, хотя физически хранится по адресу 0x38 (т.е., сразу после .rodata). Тут же мы видим значение нашей переменной, прочитанной из курсора, _data_end.

.igot.plt       0x0000000010000004        0x0 load address 0x000000000000003c  .igot.plt      0x0000000000000000        0x0 build/module_a.o  .bss            0x0000000010000004        0x8 load address 0x000000000000003c  *(.bss)  .bss           0x0000000010000004        0x4 build/module_a.o  COMMON         0x0000000010000008        0x4 build/module_a.o                 0x0000000010000008                external_counter                 0x000000001000000c                _bss_end = . 

Еще одна пустая секция, следом за ней — .bss.

LOAD build/module_a.o OUTPUT(build/out.elf elf32-littlearm)  .comment        0x0000000000000000       0x70  .comment       0x0000000000000000       0x70 build/module_a.o                                          0x71 (size before relaxing)  .ARM.attributes                 0x0000000000000000       0x31  .ARM.attributes                 0x0000000000000000       0x31 build/module_a.o 

Наконец, ld генерирует выходной файл и выбрасывает ненужные секции. Вроде все?

                0x0000000000000034                _data_start = . ... .data           0x0000000010000000        0x4 load address 0x0000000000000038 

Переменная, указывающая на начало .data, на самом деле указывает совсем не туда! А ведь и правда, курсор после .text указывает на его конец. Для правильной установки переменной ее надо перенести внутрь описания выходной секции:

.data : {   _data_start = .;   *(.data)   _data_end = .; } > ram AT> rom 

Скомпонуем и посмотрим что изменилось:

% rake 'show:map[a]' SCRIPT=layout2.ld arm-none-eabi-ld -T layout2.ld -M -o build/module_a.elf build/module_a.o  ...  .data           0x0000000010000000        0x4 load address 0x0000000000000038                 0x0000000010000000                _data_start = .  *(.data)  .data          0x0000000010000000        0x4 build/module_a.o                 0x0000000010000004                _data_end = .  ... 

Отлично, теперь все на месте.

Вы можете задаться вопросом — а какое нам дело, где находится .data? Как вы помните, данные физически хранятся во флеше, а работать с ними предстоит из оперативной памяти. По этой причине нам придется написать загрузочный код, который будет копировать .data в оперативную память, а эти переменные помогут нам узнать конкретные адреса, куда надо переносить секцию.

Усложним задачу

С одним модулем мы разобрались. Давайте добавим второй файл и посмотрим что поменяется. Второй файл будет содержать уже известный нам external_counter и немного кода на C++: module_b.cpp

int external_counter; extern "C" int public_function();  void function_b() {     external_counter += public_function(); }  void function_c() { }  void function_d() { } 

Как вы знаете, при компиляции кода на C++ имена функций и методов проходят «манглинг», когда в имени кодируются типы аргументов, имена классов и пространств имен:

% rake 'show:symbols:text[b]' arm-none-eabi-gcc -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -mthumb -O2 -mcpu=cortex-m0 -c module_b.cpp -o build/module_b.o arm-none-eabi-objdump build/module_b.o -j .text -t  build/module_b.o:     file format elf32-littlearm  SYMBOL TABLE: 00000000 l    d  .text  00000000 .text 00000000 g     F .text  00000014 _Z10function_bv 00000014 g     F .text  00000002 _Z10function_cv 00000018 g     F .text  00000002 _Z10function_dv 

Мы компилируем код с флагами -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables, чтобы избежать появления дополнительных секций, связанных с обработкой исключительных ситуаций. Имена функций были закодированы соответствующим образом.

Мы не можем сгенерировать карту для этого модуля, так как его нельзя скомпоновать самостоятельно, он зависит от функции public_function из модуля a. Компонуем оба модуля сразу:

% rake 'show:map[a|b]' SCRIPT=layout2.ld arm-none-eabi-ld -T layout2.ld -M -o build/out.elf build/module_a.o build/module_b.o  ...   .text          0x0000000000000000       0x34 build/module_a.o                 0x0000000000000000                public_function  .text          0x0000000000000034       0x1c build/module_b.o                 0x0000000000000034                function_b()                 0x0000000000000048                function_c()                 0x000000000000004c                function_d()  ... 

Блок общих символов пропал, все символы найдены в соответствующих модулях. Секции .text, равно как и остальные, компонуются друг за другом.

Соберем мусор!

Для встраиваемых приложений размер выходного файла как никогда актуален, потому стоит позаботиться о том, чтобы максимальное количество ненужных данных и мертвого кода было удалено. Компоновщик в состоянии избавиться от секций, на которые никто не ссылается и которые не были явно указаны как необходимые в сценарии компоновки. Делается это достаточно просто — с помощью флага --gc-sections:

% rake 'show:map[a|b]' SCRIPT=layout2.ld GC=1 arm-none-eabi-ld --gc-sections -T layout2.ld -M -o build/out.elf build/module_a.o build/module_b.o  Discarded input sections   .rodata        0x0000000000000000        0x4 build/module_a.o  COMMON         0x0000000000000000        0x0 build/module_a.o  .text          0x0000000000000000       0x1c build/module_b.o  .data          0x0000000000000000        0x0 build/module_b.o  ...  .text           0x0000000000000000       0x34  *(.text)  .text          0x0000000000000000       0x34 build/module_a.o                 0x0000000000000000                public_function  ... 

Как видите, секция .text из build/module_b.o была удалена полностью, так как содержала бесполезные функции! Заодно компоновщик выбросил неиспользуемые константы из первого модуля.

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

void function_b();  extern "C" int public_function() {     function_b(); } 

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

% rake 'show:map[b|c]' SCRIPT=layout2.ld GC=1 arm-none-eabi-gcc -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -mthumb -O2 -mcpu=cortex-m0 -c module_c.cpp -o build/module_c.o arm-none-eabi-ld --gc-sections -T layout2.ld -M -o build/out.elf build/module_b.o build/module_c.o  Discarded input sections   .data          0x0000000000000000        0x0 build/module_b.o  .data          0x0000000000000000        0x0 build/module_c.o  .bss           0x0000000000000000        0x0 build/module_c.o  ...  .text           0x0000000000000000       0x24  *(.text)  .text          0x0000000000000000       0x1c build/module_b.o                 0x0000000000000000                function_b()                 0x0000000000000014                function_c()                 0x0000000000000018                function_d()  .text          0x000000000000001c        0x8 build/module_c.o                 0x000000000000001c                public_function 

Хотя часть секций (впрочем, пустых) выбросить удалось, но мы все еще теряем бесценные байты на функции function_c() и function_d(), которые оказались в той же секции, что и function_b(), которая нам нужна. На помощь придут флаги компилятора, которые разбивают функции и данные в разные секции: -ffunction-sections и -fdata-sections:

% rake clean && rake 'show:symbols:all[b]' SPLIT_SECTIONS=1 arm-none-eabi-gcc -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -ffunction-sections -fdata-sections -mthumb -O2 -mcpu=cortex-m0 -c module_b.cpp -o build/module_b.o arm-none-eabi-objdump build/module_b.o -t  build/module_b.o:     file format elf32-littlearm  SYMBOL TABLE: 00000000 l    df *ABS*  00000000 module_b.cpp 00000000 l    d  .text  00000000 .text 00000000 l    d  .data  00000000 .data 00000000 l    d  .bss 00000000 .bss 00000000 l    d  .text._Z10function_bv  00000000 .text._Z10function_bv 00000000 l    d  .text._Z10function_cv  00000000 .text._Z10function_cv 00000000 l    d  .text._Z10function_dv  00000000 .text._Z10function_dv 00000000 l    d  .bss.external_counter  00000000 .bss.external_counter 00000000 l    d  .comment 00000000 .comment 00000000 l    d  .ARM.attributes  00000000 .ARM.attributes 00000000 g     F .text._Z10function_bv  00000014 _Z10function_bv 00000000         *UND*  00000000 public_function 00000000 g     F .text._Z10function_cv  00000002 _Z10function_cv 00000000 g     F .text._Z10function_dv  00000002 _Z10function_dv 00000000 g     O .bss.external_counter  00000004 external_counter 

Теперь, когда каждая функция и объект помещены в независимые секции, компоновщик может от них избавиться:

% rake clean && rake 'show:map[b|c]' SCRIPT=layout2.ld GC=1 SPLIT_SECTIONS=1 arm-none-eabi-gcc -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -ffunction-sections -fdata-sections -mthumb -O2 -mcpu=cortex-m0 -c module_b.cpp -o build/module_b.o arm-none-eabi-gcc -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -ffunction-sections -fdata-sections -mthumb -O2 -mcpu=cortex-m0 -c module_c.cpp -o build/module_c.o arm-none-eabi-ld --gc-sections -T layout2.ld -M -o build/out.elf build/module_b.o build/module_c.o  Discarded input sections   .text          0x0000000000000000        0x0 build/module_b.o  .data          0x0000000000000000        0x0 build/module_b.o  .bss           0x0000000000000000        0x0 build/module_b.o  .text._Z10function_cv                 0x0000000000000000        0x4 build/module_b.o  .text._Z10function_dv                 0x0000000000000000        0x4 build/module_b.o  .text          0x0000000000000000        0x0 build/module_c.o  .data          0x0000000000000000        0x0 build/module_c.o  .bss           0x0000000000000000        0x0 build/module_c.o  ... 

Вместо заключения

И снова объем статьи растет, теперь он в два раза больше первой части. К сожалению, компоновка — это сложная тема, и ее сложно осилить «влет». Через неделю мы продолжим изучение компоновщика и сделаем полноценный сценарий компоновки для наших встраиваемых приложений.

P.S. Как всегда, большое спасибо pfactum за вычитку текста.

Лицензия Creative Commons Это произведение доступно по лицензии Creative Commons «Attribution-NonCommercial-NoDerivs» 3.0 Unported. Программный текст примеров доступен по лицензии Unlicense (если иное явно не указано в заголовках файлов). Это произведение написано исключительно в образовательных целях и никаким образом не аффилировано с текущим или предыдущими работодателями автора.

ссылка на оригинал статьи http://habrahabr.ru/post/191058/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *