А Вы понимаете: собрать проект, который в архиве занимает около 200K (понятно, что там не только исходники,но «копать» проект не сильно хочется).
2) Зачем Вы говорите про какой-то make, если у Вас ошибка при запуске avr-gcc. Вбейте «руками» строку запуска gcc и убедитесь, что проблема осталась (т.е make тут вообще «никаким боком»).
Добавьте в строку запуска gcc ключик вывода расширенной информации (-v) и сюда все что Вам в ответ «вывалилось»
Зарегистрируйтесь и получите два купона по 5$ каждый:https://jlcpcb.com/cwc
———- Build: Release in avr (compiler: GNU GCC Compiler for AVR)———-
Есть пара варнингов, но вряд ли дело в них.
_________________
[CppCon 2017] Matt Godbolt: Что мой компилятор сделал для меня?
Продолжение цикла обзорных статей с конференции CppCon 2017.
На этот раз очень интересное выступление от автора Compiler Explorer (godbolt.org). Обязательно читать всем, кто для быстроты умножает на 2 с помощью сдвига (по крайней мере, на x86-64). Если вы знакомы с ассемблером x86-64, то можете перемотать до разделов с примерами («Умножение», «Деление» и т.д). Далее слова автора. Мои комментарии в квадратных скобках курсивом.
Моя цель сделать так, чтобы вы не боялись ассемблер, это полезная вещь. И использовали его. Не обязательно все время. И я не говорю, что вы должны все бросить и учить ассемблер. Но вы должны уметь просмотреть результат работы компилятора. И когда вы это сделаете, то оцените, как много работы проделал компилятор, и какой он умный.
Предыстория
Итак, есть классический способ суммирования (до C++11):
С появлением Range-For-Loop мы задумались, можем ли мы заменить его на более приятный код:
Наши опасения были связаны с тем, что мы уже были «покусаны» итераторами в других языках. У нас был опыт итерирования контейнеров при котором конструировались итераторы, что добавляло работы сборщику мусора. Мы знали, что в C++ такого нет, но должны были проверить.
Предупреждение
Чтение ассемблерного кода: можно легко ввести в себя в заблуждение, если думать, что вы можете видеть что здесь происходит, что это работает быстрее того из-за меньшего количества инструкций. В современном процессоре происходят очень сложные вещи и вы не сможете предсказать. Если вы делаете предсказания производительности, то всегда запускайте бенчмарки. Например от Google или интерактивный онлайновый http://quick-bench.com.
Основы ассемблера x86-64
Регистры
Существуют соглашения вызова функций ABI, это то, как функции «общаются» друг с другом, в частности:
Также существуют правила, которые говорят, какие регистры вы можете перезаписать, какие сохранить. Но если вы не пишете на ассемблере, то знать их не обязательно.
Регистры общего назначения 64-битные, но имеют различные имена. Например, обратившись к регистру eax, мы получим нижние 32 бита регистра rax. Но по сложным причинам запись в eax приведет к обнулению верхних 32 бит rax (почему). Это не относится к ax, ah, al.
Операции
Я использую синтакс Intel. Кто в зале использует синтакс Intel? А AT&T? О, кажется сегодня я наделал себе врагов.
Виды операций (синтаксис Intel):
Примеры мнемокодов (op): call, ret, add, sub, cmp. Операндами (dest, src) могут являться регистры или ссылки на память вида:
Ссылка на память редко бывает абсолютным значением. Мы можем использовать значение регистра, а также добавить некоторое смещение, задаваемым значением другого регистра.
Разберем по строкам:
Compiler Explorer v0.1
Сначала, чтобы изучать работу компилятора, я делал следующее. Компилировал командой:
Compiler Explorer
Теперь вкратце о современной версии Compiler Explorer. Чтобы создать новое окошко с кодом нужно щелкнуть по пункту «Editor» и удерживая перетащить куда нужно. Для создания окошка с результатом компиляции нужно щелкнуть по кнопке со стрелкой вверх и удерживая перетащить. Если я наведу курсор на желтую строку, то соответствующие строки в ассемблерной части станут жирнее.
Теперь я покажу результат компиляции без оптимизации (-O0).
Офигенно, использованы все эти крутые операции! Но нужно использовать бенчмарки, чтобы убедиться, что такой код быстрее простой версии.
Вернемся к нашему примеру:
Слева цикл от 0 до size, справа range for. Код, реализующий циклы, идентичен в обоих случаях. Попробуем такой способ:
Подробный разбор
Начнем с первых двух строк:
Мы передавали вектор по ссылке. Но здесь нет такой штуки как ссылка. Только указатели. Регистр rdi указывает на переданный вектор. Производится чтение значений по адресам rdi и rdi + 8. Ошибочно думать, что rdi — это указатель непосредственно на массив int-ов. Это указатель на вектор. По крайней мере в GCC реализация вектора такая:
То есть, вектор — это структура, содержащая три указателя. Первый указатель — начало массива, второй — конец, третий — конец зарезервированной памяти (allocated) для этого вектора. Здесь явно не хранится размер массива. Это интересно. Посмотрим на различия в реализациях до начала цикла. Первый пример (обычный цикл с индексом):
В первой строчке вычисляется количество байт массива. В третьей это значение делится на 4, чтобы найти количество элементов (так как int — 4-байтовый). То есть это просто функция size:
Если получили размер 0 после операции сдвига, то выставляется Equal Flag и срабатывает операция перехода je (Jump If Equal), и программа возвращает 0. Таким образом осуществилась проверка равенства размера нулю. Далее, мы предполагаем, что во время итераций будет осуществляться проверка индекса с размером. Но компилятор догадывается, что фактически нам этот индекс и не нужен, и в цикле он будет сравнивать текущий указатель на элемент массива с концом массива (rcx).
Теперь второй пример (с range for):
Здесь просто сравнивается, равен ли начальный указатель конечному, и если да, то прыжок в конец программы. Фактически, произошло это:
Сам цикл идентичен в обоих случаях:
Умножение
Далее я покажу маленькие, но классные примеры.
edi — первый параметр, esi — второй. Обычное умножение с помощью imul. Вот так выглядит 4-битовое перемножение вручную:
Нам пришлось выполнить 4 сложения. Это чудо, что на Haswell 32-битное перемножение осуществляется всего за 4 такта. Сложение за 1 такт. Но посмотрим примеры в которых еще быстрее:
Вы можете ожидать, что произойдет сдвиг влево на 1. Проверим:
Одна операция. Преимущество lea в том, что можно очень гибко задавать источник. Для реализации с помощью сдвига пришлось бы делать 2 операции: копировать значение в eax и сдвигать.
lea поддерживает умножение на 2, 4, 8. Поэтому, умножение на 8 будет аналогичным:
На 16 уже со сдвигом:
Ага, эти разработчики компиляторов не такие уж умные, какими они себя считают. Я могу реализовать эффективнее:
Ну-ка, вернем обратно return a * 65599; :
Позвольте компилятору делать все эти подобные вещи за вас.
Деление
На Haswell 32-битовое деление выполняется за 22-29 циклов. Можем ли мы сделать лучше?
Никакой магии, просто сдвиг вправо. Также никаких сюрпризов, если будем делить на 4, 8, 16 и т. д. Попробуем на 3:
Значение, равное 2/3*x оказалось в edx — старших 32 битах 64-разрядного результата умножения. То есть, фактически, произошло умножение на 2/3 и сдвиг на 1 вправо. [Если не очень понятно, то поумножайте в каком-нибудь hex-калькуляторе 0xaaaaaaab на 3, 4, 5, 6… и понаблюдайте за старшими разрядами].
Остаток от деления
В частности, остаток от деления на 3:
Первые 5 строчек — уже рассмотренное деление. А в следующих результат деления умножается на 3 и вычитается из исходного значения. То есть, x-3*[x/3]. Почему я уделяю внимание делению по модулю? Потому что оно используется в hash-map — любимом всеми контейнере. Для нахождения или добавления объекта в контейнер вычисляется остаток от деления хеша на размер хеш-таблицы. Этот остаток является индексом элемента таблицы (bucket). Очень важно, чтобы эта операция работала как можно быстрее. В общем случае она работает довольно долго. Поэтому, libc++ использует степени двойки для допустимых размеров таблиц. Но это не очень удачные значения. Реализация Boost multi_index содержит множество допустимых размеров, и switch с их перечислением.
[Судя по вопросу из зала, не все поняли это объяснение. Грубо говоря, такая реализация:]
Подсчет битов
Следующая функция осуществляет подсчет единичных битов:
На каждой итерации у числа пропадает самая младшая единичка, пока все число не превратится ноль. GCC сделал умно:
Эта инструкцию, осуществляющую подсчет бит, которую народ так хотел, что intel добавил ее. Только подумайте, что clang должен быть выполнить. Для него не было никаких подсказок «я хочу посчитать биты».
Сложение
Сложение всех целых чисел от 0 до x включительно:
Интересно, цикл пропал. В первых двух строчках проверка аргумента на ноль. В следующих 5 просто формула суммы: x(x+1)/2 == x + x(x-1)/2. Реализован второй вариант формулы, потому что первый вариант вызовет переполнение, если передать INT_MAX.
Как работает Compiler Explorer
[Я совсем не разбираюсь с Frontend, Docker, виртуализацией и прочим, о чем рассказывал автор, поэтому решил не описывать все тонкости реализации, чтобы не ляпнуть глупость]
Написан на node.js. Запущено на Amazon EC2. Это второе мое выступление на этой конференции, в которой я говорю про JavaScript и чувствую себя так плохо. Конечно, мы иногда ругаем C++… но если только взглянуть на JavaScript, ох.
Все это выглядит примерно так:
Amazon EC2
Компиляторы
Я установил их с помощью apt-get. Компиляторы от Microsoft работают через WINE.
Безопасность
Frontend
В качестве онлайн редактора кода используется Microsoft Monaco. Drag-drop для окошек реализован с помощью GoldenLayout.
Исходники
Скоро
Вопросы
Хотелось бы видеть поддержку других библиотек, например Boost.
Некоторые уже есть:
Вы говорили про switch для всех возможных размеров таблиц в Boost multi_index. Но такая реализация неэффективна — так как значения сильно разрежены, то не получится использовать jump table, а только бинарный поиск.
В switch перечислены не сами размеры, а их индексы.
Планируется ли поддержка Web assembly
Есть много компиляторов, которые не имеют backends для webassembly. Ну да, неплохо бы, но пока занят другими вещами.
Я трачу примерно 120-150$ в месяц на сервера Амазона.
Планируется ли поддержка llvm intermediate representation?
Как вы подкрашиваете соответствующие строки исходного кода и ассемблерного?
Просто запускаю с флагом «-g». Получающийся листинг имеет комментарии с номерами строк.
Кросскомпиляция под ARM
Достаточно давно хотел освоить сабж, но всё были другие более приоритетные дела. И вот настала очередь кросскомпиляции.
В данном посте будут описаны:
Вводная
Одно из развивающихся направлений в современном IT это IoT. Развивается это направление достаточно быстро, всё время выходят всякие крутые штуки (типа кроссовок со встроенным трекером или кроссовки, которые могут указывать направление, куда идти (специально для слепых людей)). Основная масса этих устройств представляют собой что-то типа «блютуз лампочки», но оставшаяся часть являет собой сложные процессорные системы, которые собирают данные и управляют этим огромным разнообразием всяких умных штучек. Эти сложные системы, как правило, представляют собой одноплатные компьютеры, такие как Raspberry Pi, Odroid, Orange Pi и т.п. На них запускается Linux и пишется прикладной софт. В основном, используют скриптовые языки и Java. Но бывают приложения, когда необходима высокая производительность, и здесь, естественно, требуются C и C++. К примеру, может потребоваться добавить что-то специфичное в ядро или, как можно быстрее, высчитать БПФ. Вот тут-то и нужна кросскомпиляция.
Если проект не очень большой, то его можно собирать и отлаживать прямо на целевой платформе. А если проект достаточно велик, то компиляция на целевой платформе будет затруднительна из-за временных издержек. К примеру, попробуйте собрать Boost на Raspberry Pi. Думаю, ожидание сборки будет продолжительным, а если ещё и ошибки какие всплывут, то это может занять ох как много времени.
Поэтому лучше собирать на хосте. В моём случае, это i5 с 4ГБ ОЗУ, Fedora 24.
Инструменты
Для кросскомпиляции под ARM требуются toolchain и эмулятор платформы либо реальная целевая платформа.
Т.к. меня интересует компиляция для ARM, то использоваться будет и соответствующий toolchain.
Toolchain’ы делятся на несколько типов или триплетов. Триплет обычно состоит из трёх частей: целевой процессор, vendor и OS, vendor зачастую опускается.
Сперва я пытался использовать toolchain’ы, которые лежат в репах Fedora 24. Но был неприятно удивлён этим:
Поискав, наткнулся на toolchain от компании Linaro. И он меня вполне устроил.
Второй инструмент- это QEMU. Я буду использовать его, т.к. мой Odroid-C1+ пал смертью храбрых (нагнулся контроллер SD карты). Но я таки успел с ним чуток поработать, что не может не радовать.
Элементарная технология кросскомпиляции
Собственно, ничего необычного в этом нет. Просто используется toolchain в роли компилятора. А стандартные библиотеки поставляются вместе с toolchain’ом.
Какие ключи у toolchain’а можно посмотреть на сайте gnu, в соответствующем разделе.
Для начала нужно запустить эмуляцию с интересующей платформой. Я решил съэмулировать Cortex-A9.
После нескольких неудачных попыток наткнулся на этот how2, который оказался вполне вменяемым, на мой взгляд.
Ну сперва, само собою, нужно заиметь QEMU. Установил я его из стандартных репов Fedor’ы.
Далее создаём образ жёсткого диска, на который будет установлен Debian.
По этой ссылке скачал vmlinuz и initrd и запустил их в эмуляции.
Далее просто устанавливаем Debian на наш образ жёсткого диска (у меня ушло
После установки нужно вынуть из образа жёсткого диска vmlinuz и initrd. Делал я это по описанию отсюда.
Сперва узнаём смещение, где расположен раздел с нужными нам файлами:
Теперь по этому смещению примонтируем нужный нам раздел.
Копируем файлы vmlinuz и initrd и размонтируем жёсткий диск.
Теперь можно запустить эмуляцию.
И вот заветное приглашение:
Теперь с хоста по SSH можно подцепиться к симуляции.
Теперь можно и собрать программку. По Makefile’у ясно, что будет калькулятор. Простенький.
Собираем на хосте исполняемый файл.
Отмечу, что проще собрать с ключом -static, если нет особого желания предаваться плотским утехам с библиотеками на целевой платформе.
Копируем исполняемый файл на таргет и проверяем.
Собственно, вот такая она, эта кросскомпиляция.
UPD: Подправил информацию по toolchain’ам по комментарию grossws.
ARM-ы для самых маленьких: тонкости компиляции и компоновщик
Продолжая серию статей про разработку с нуля для ARM, сегодня я затрону тему написания скриптов компоновщика для GNU ld. Эта тема может пригодиться не только тем, кто работает со встраиваемыми системами, но и тем, кто хочет лучше понять строение исполняемых файлов. Хотя примеры так или иначе основаны на тулчейне arm-none-eabi, суть компоновки та же и у компоновщика Visual Studio, например.
В бинарных файлах, с которыми мы работаем в рамках этого цикла, будут часто попадаться еще две секции:
После того, как у нас получилось несколько таких файлов, за дело берется компоновщик, который по заданным правилам соберет все секции, выбросит ненужные и сделает итоговый исполняемый файл. Для «стандартных» ОС определены правила, где что должно находиться, но в случае микроконтроллеров нам обычно нужно заниматься распихиванием всего по флешу и оперативной памяти вручную.
Заглянем внутрь
В качестве первого примера мы изучим следующий код на C: module_a.c :
Скомпилируем его и посмотрим, какие секции мы получили:
Как мы видим — шесть секций, назначение которых нам уже более-менее известно. Второй строкой идут атрибуты секции, они будут интереснее позднее, при компоновке. Посмотрим, какие символы определены в этих секциях:
Документацию по публичным тегам можно посмотреть на инфоцентре ARM.
Собираем все в кучу
Теперь, когда мы заглянули внутрь объектных файлов, давайте разберемся, как ld собирает их в одно успешное приложение.
Основная работа ld вертится вокруг карты памяти, которую мы видели в первой части. Если сильно упростить, компоновка — это процесс выдирания секций из объектных файлов, раскладывание их по указанным адресам и исправление перекрестных ссылок. В «стандартных» ОС ядро умеет читать выходной файл и загружать секции в память по ожидаемым виртуальным адресам. Также динамический компоновщик выполняет схожую работу, догружая в определенные места памяти внешние библиотеки и настраивая на них перекрестные ссылки.
Со встраиваемыми системами проще, программа для прошивки берет ваш бинарный файл и заливает на флешку как есть. Его не волнуют ни мачо, ни эльфы, он работает с бинарными дампами.
Возьмем простой сценарий компоновщика и разберем по кусочкам. layout.ld:
Вторая часть файла — конфигурация секций. В целом, это означает копирование из одних протобафов в другие протобафы заданных исходных секций в выходные секции.
У секции есть два адреса: LMA (Load Memory Address) — откуда она загружается, и VMA (Virtual Memory Address) — по какому адресу она доступна в виртуальной памяти. Объясняя проще, LMA — это где она окажется в бинарном файле, а VMA — это куда будут перенаправлены символы, т.е., указатель на символ в коде будет ссылаться на VMA-адрес.
Давайте запустим компоновщик и посмотрим результат его работы:
Наконец, ld генерирует выходной файл и выбрасывает ненужные секции. Вроде все?
Скомпонуем и посмотрим что изменилось:
Отлично, теперь все на месте.
Усложним задачу
С одним модулем мы разобрались. Давайте добавим второй файл и посмотрим что поменяется. Второй файл будет содержать уже известный нам external_counter и немного кода на C++: module_b.cpp
Как вы знаете, при компиляции кода на C++ имена функций и методов проходят «манглинг», когда в имени кодируются типы аргументов, имена классов и пространств имен:
Соберем мусор!
На самом деле, эта оптимизация не полная, в чем мы легко можем убедиться с помощью несложного эксперимента, см. module_c.cpp
Мы заменим модуль a на модуль c и посмотрим, сможет ли компоновщик удалить секцию.
Теперь, когда каждая функция и объект помещены в независимые секции, компоновщик может от них избавиться:
Вместо заключения
И снова объем статьи растет, теперь он в два раза больше первой части. К сожалению, компоновка — это сложная тема, и ее сложно осилить «влет». Через неделю мы продолжим изучение компоновщика и сделаем полноценный сценарий компоновки для наших встраиваемых приложений.
GNU Compiler Collection, первые шаги
Эта заметка призвана на простых примерах познакомить начинающего nix-разработчика с инструментами GNU, в частности с компилятором GCC.
С его помощью мы и создадим простейшую программу. По большому счету все, как обычно. Заводим специальную папку, в которой будет размещаться проект. Создаем в ней файл с именем: hello.c Открываем файл в любом текстовом редакторе и пишем простейший код:
#include int main(void) < printf(«Hello world!»); return(0); >
Сохраняем файл и выполняем команду: gcc hello.c
В созданной нами папке появился новый файл — a.out, это название присваивается по умолчанию, если специально не задано другого.
И радуемся в связи с первой написанной программой в линуксе!
Идем далее. При запуске исполняемого файла, если мы укажем только его название, система будет искать его в каталогах /usr/bin и /usr/local/bin, и, естественно, не найдет. Первый из них предназначен для размещения стабильных версий программ, как правило, входящих в дистрибутив Linux. Второй – для программ, устанавливаемых самим пользователем (за стабильность которых никто не ручается). По умолчанию, при сборке программы, устанавливаются в каталог /usr/local/bin.
Флаги используемые при компиляции
Название получаемого файла такое же, но компилятор изменяет расширение .c на .o (но указать можно и вручную).
Флаг -x используем, если создаётся объектный файл из исходника, уже обработанного препроцессором (например такого, какой мы получили выше), мы должны обязательно указать явно, что компилируемый файл является файлом исходного кода, обработанный препроцессором, и имеющий теги препроцессора. В противном случае он будет обрабатываться, как обычный файл C++, без учёта тегов препроцессора, а значит связь с объявленными функциями не будет устанавливаться.
Для чего нужна вся эта возня с промежуточными этапами? Программы редко состоят из одного файла. Как правило исходных файлов несколько, и они объединены в проект. И в некоторых исключительных случаях программу приходится компоновать из нескольких частей, написанных, возможно, на разных языках. В этом случае приходится запускать компиляторы разных языков, чтобы каждый получил объектный файл из своего исходника, а затем уже эти полученные объектные файлы компоновать в исполняемую программу.