...

пятница, 23 августа 2013 г.

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 (если иное явно не указано в заголовках файлов). Это произведение написано исключительно в образовательных целях и никаким образом не аффилировано с текущим или предыдущими работодателями автора.

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends: 'You Say What You Like, Because They Like What You Say' - http://www.medialens.org/index.php/alerts/alert-archive/alerts-2013/731-you-say-what-you-like-because-they-like-what-you-say.html


Комментариев нет:

Отправить комментарий