Русский
Русский
English
Статистика
Реклама

Gcc

Перевод Низкоуровневое программирование микроконтроллеров tinyAVR 0-series

10.10.2020 18:11:17 | Автор: admin


Вы 8-битный или 32-битный программист? Мы, в компании OMZLO, сосредоточили основные усилия на новых 32-битных ARM Cortex-чипах (STM32 и SAMD), которые, в сравнении с более старыми 8-битными микроконтроллерами (Micro Controller Unit, MCU) обычно предлагают больше RAM, более высокую производительность, поддержку большего количества периферийных устройств. И всё это за ту же, или за более низкую цену. Но 8-битные MCU ещё не утратили своей актуальности. В частности, компания Microchip выпустила новую серию чипов, tinyAVR 0-series, которые, в сравнении с AVR-чипами, выпущенными ранее, дают возможность работать с более современной периферией. Новые чипы, при этом, отличаются весьма привлекательной ценой. Возникает такое ощущение, что эти чипы отлично подойдут для разработки простых устройств, которым не нужны те возможности, что предлагают более новые 32-битные MCU. 8-битные микроконтроллеры, кроме того, значительно легче программировать, что приводит к увеличению скорости разработки программной части устройств, создаваемых на их основе.

Благодаря успеху Arduino UNO в интернете можно найти множество руководств, разъясняющих особенности программирования 8-битных микроконтроллеров ATmega328 и их собратьев вроде ATtiny85. Речь идёт о прямом доступе к регистрам без использования языка программирования, используемого для Arduino, и без применения IDE, созданных производителями чипов, вроде Atmel Studio. Чтобы в этом убедиться просто поищите в Google по словам atmega328 blinky. Для программирования микроконтроллеров вам понадобится лишь C-компилятор для AVR, текстовой редактор, avrdude и AVR-программатор. На некоторых ресурсах даже можно найти руководства, посвящённые тому, как, пользуясь универсальными макетными платами, завести ATmega328. Правда, если говорить о более новых чипах tinyAVR 0-series, по ним найти информацию такого рода непросто.

Конечно, Microchip предлагает все необходимые инструменты для программирования новых tinyAVR, представленные в виде IDE, рассчитанной исключительно на Windows. Для некоторых из новых чипов существуют ядра Arduino. Благодаря этому такие чипы можно программировать с использованием IDE Arduino. Но, опять же, если некто предпочитает писать код для микроконтроллеров в низкоуровневом стиле, используя свой любимый текстовой редактор, Makefile и компилятор C, то он сможет найти очень мало информации о таком подходе к работе с tinyAVR.

В этом материале мы расскажем о том, как, с нуля, применяя простейшие инструменты, создать прошивку blinky для ATtiny406. Большинство того, о чём пойдёт речь, справедливо и для других MCU tinyAVR. Это руководство рассчитано на тех, кто пользуется macOS и Linux, но нашими советами, с небольшими изменениями, смогут воспользоваться и те, кто работает в среде Windows.

Аппаратная часть проекта


Исследование ATtiny406


Мы решили поэкспериментировать с ATtiny406, рассчитывая на то, что в будущем этот микроконтроллер придёт на смену ATtiny45, который в настоящее время используется в PiWatcher в нашей разработке, которая позволяет, при возникновении такой необходимости, полностью выключить или перезагрузить Raspberry Pi. У ATtiny406 имеется 4 Кб флеш-памяти, 256 байт RAM, микрочип может работать на частоте 20 МГц без внешнего источника тактовых сигналов.

Одним из главных различий между новыми MCU tinyAVR и более старыми, широко известными чипами, вроде ATtiny85, является то, что более новые чипы используют протокол программирования UPDI. Для его работы нужно всего 3 пина, а для работы протокола ISP, используемого старыми чипами, нужно 6 пинов.

После непродолжительного изучения вопроса мы узнали, что программировать tinyAVR по UPDI можно, воспользовавшись простым USB-to-Serial-кабелем и резистором. Мы выяснили это благодаря Python-инструменту pyupdi, который предложил следующую схему подключения для загрузки прошивки на микроконтроллер.


Схема подключения микроконтроллера

Проектирование платы для ATtiny406


Мы создали минималистичную коммутационную плату для ATtiny406. На эту плату можно подать питание в 5В от USB. Кроме того, можно подать на неё более низкое напряжение в 3,3В, воспользовавшись для этого выделенными VCC/GND-пинами. На плате нашлось место для кнопки и светодиода. Для проведения экспериментов мы решили встроить в плату резистор на 4,7 кОм, необходимый для использования протокола UPDI (это резистор R2). В результате у нас получилась следующая схема платы.


Схема платы

Готовая плата


Готовая коммутационная плата оказалась весьма компактной, она хорошо стала на маленькую макетную плату. Схемы платы можно найти здесь.


Коммутационная плата, установленная на макетную плату

Для программирования ATtiny406 к плате, с использованием имеющихся на ней контактов, подключается USB-to-Serial-кабель.


Схема подключения кабеля

Программная часть проекта


pyupdi


Мы установили pyupdi, следуя инструкциям из репозитория проекта.

USB-to-Serial-кабель был подключён к плате с использованием четырёх UPDI-контактов. Наш USB-to-Serial-конвертер был виден в macOS как файл /dev/tty.usbserial-FTF5HUAV.

Для того чтобы убедиться в том, что программатор распознаёт ATtiny406, можно выполнить команду, напоминающую следующую, отредактировав путь к файлу:

pyupdi -d tiny406 -c /dev/tty.usbserial-FTF5HUAV -i

Если всё настроено правильно, то выполнение подобной команды должно привести к выводу примерно таких данных:

Device info: {'family': 'tinyAVR', 'nvm': 'P:0', 'ocd': 'D:0', 'osc': '3', 'device_id': '1E9225', 'device_rev': '0.1'}

C-компилятор


Оказалось, что обычный компилятор avr-gcc, который можно установить на macOS, используя Homebrew, не позволяет задать ATtiny406 в виде цели компиляции. Поэтому мы решили установить avr-gcc, предоставляемый компанией Microchip. Для загрузки компилятора надо создать учётную запись на сайте Microchip, что слегка раздражает.


Загрузка компилятора

После загрузки необходимых материалов, представленных в виде архива, мы распаковали этот архив в отдельную папку. Путь к директории bin, которая окажется в этой папке, нужно добавить в PATH. Это, в дальнейшем, облегчит работу. Если исходить из предположения о том, что компилятор хранится в папке $HOME/Src/avr8-gnu-toolchain-darwin_x86_64, то отредактировать PATH можно, добавив следующую команду в файл .bash_profile:

export PATH=$PATH:$HOME/Src/avr8-gnu-toolchain-darwin_x86_64/bin/

Самые новые MCU ATtiny не поддерживаются компилятором avr-gcc от Microchip без дополнительных настроек. Для обеспечения их поддержки нужно загрузить ATtiny Device Pack.


Загрузка ATtiny Device Pack

Мы, в результате, загрузили пакет Atmel.ATtiny_DFP.1.6.326.atpack (этот файл может называться иначе, в состав его имени может входить другой номер версии). Хотя расширением файла является .atpack, это, на самом деле, обычный .zip-архив. Мы поменяли его расширение на .zip и извлекли содержимое пакета в папку $HOME/Src/Atmel.ATtiny_DFP.1.6.326, то есть туда же, где уже имелись файлы компилятора.

Написание программы на C


Мы написали следующую программу, которая, с частотой в 1 Гц, мигает светодиодом, подключённым к выходу B5 на нашей плате для ATtiny.

#include <avr/io.h>#include <util/delay.h>int main() {_PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, 0); // установлено в 20МГц (предполагается, что фьюз 0x02 установлен в 2)PORTB.DIRSET = (1<<5);for (;;) {PORTB.OUTSET = (1<<5);_delay_ms(500);PORTB.OUTCLR = (1<<5);_delay_ms(500);}}

Этот код очень похож на программу, мигающую светодиодом, написанную для привычных микроконтроллеров AVR. Первым заметным изменением является использование структур для доступа к регистрам MCU. Например, вместо обращения к PORTB выполняется обращение к PORTB.DIRSET.

Ещё одно изменение представлено кодом настройки частоты (_PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, 0)). Новый ATtiny406 после перезагрузки работает на частоте 3,33 МГц, что соответствует базовой частоте в 20 МГц, которую разделили на 6. Для того чтобы чип работал бы на полной скорости в 20 МГц, мы очищаем регистр CLKCTRL.MCLKCTRLB. Так как этот регистр должен быть защищён от случайных изменений, на ATtiny406 для его модификации необходимо применить особую программную конструкцию. К счастью, решение этой задачи облегчает макрос _PROTECTED_WRITE. Подробности об этом можно почитать здесь.

Если сравнить этот код с тем, который пишут для STM32 или для SAMD21, то он окажется гораздо проще.

Файл Makefile


Тут мы пользуемся следующей структурой директорий:

  • src/Atmel.ATtiny_DFP.1.6.326/ путь к Microchip Device Pack.
  • src/attiny406-test/ папка, в которой, в файле main.c, хранится вышеприведённый код.

Компиляцию кода, находясь в директории attiny406-test/, можно выполнить следующей командой:

avr-gcc -mmcu=attiny406 -B ../Atmel.ATtiny_DFP.1.6.326/gcc/dev/attiny406/ -O3 -I ../Atmel.ATtiny_DFP.1.6.326/include/ -DF_CPU=20000000L -o attiny406-test.elf main.c

Флаг -O позволяет выполнить оптимизацию, необходимую для успешной работы вызовов функции _delay_ms(). То же самое относится и к переменной -DF_CPU, содержимое которой отражает ожидаемую частоту чипа. Остальные параметры содержат сведения о расположении файлов для ATtiny406, которые мы ранее скачали и извлекли из архива Device Pack.

Для загрузки прошивки на MCU нужно преобразовать то, что получилось в формат Intel HEX. После этого надо воспользоваться pyupdi. Мы создали простой Makefile, автоматизирующий решение этих задач:

OBJS=main.oELF=$(notdir $(CURDIR)).elfHEX=$(notdir $(CURDIR)).hexF_CPU=20000000LCFLAGS=-mmcu=attiny406 -B ../Atmel.ATtiny_DFP.1.6.326/gcc/dev/attiny406/ -O3CFLAGS+=-I ../Atmel.ATtiny_DFP.1.6.326/include/ -DF_CPU=$(F_CPU)LDFLAGS=-mmcu=attiny406 -B ../Atmel.ATtiny_DFP.1.6.326/gcc/dev/attiny406/CC=avr-gccLD=avr-gccall:  $(HEX)$(ELF): $(OBJS)$(LD) $(LDFLAGS) -o $@ $(OBJS) $(LDLIBS)$(HEX): $(ELF)avr-objcopy -O ihex -R .eeprom $< $@flash: $(HEX)pyupdi -d tiny406 -c /dev/tty.usbserial-FTF5HUAV -f attiny406-test.hexread-fuses:pyupdi -d tiny406 -c /dev/tty.usbserial-FTF5HUAV -frclean:rm -rf $(OBJS) $(ELF) $(HEX)

Для компиляции кода достаточно выполнить команду make. Загрузка кода на микроконтроллер выполняется командой make flash. Представленный нами Makefile может быть, при необходимости, доработан.

Итоги


Программировать новые TinyAVR так же просто, как и MCU предыдущих поколений. Главное подобрать правильные инструменты. Если у вас есть советы по программированию AVRTiny, поделитесь ими с нами в Twitter или в комментариях ниже.

Планируете ли вы пользоваться новыми TinyAVR в своих проектах?



Подробнее..

С23 международный стандарт на удалёнке

08.12.2020 10:15:01 | Автор: admin


C++20 прошёл все бюрократические инстанции и теперь официально готов! Международный комитет переехал в онлайн, и теперь мы вовсю работаем над C++23. Под катом вас ждут:

  • std::stacktrace,
  • z и uz,
  • 61 с половиной багфикс в ядре языка,
  • string::contains,
  • Executors & Networking,
  • и прочие новости.

std::stacktrace


Ура! РГ21 вместе с Fails протащили в стандарт std::stacktrace. Скоро можно будет делать программы с хорошей диагностикой:

#include <stacktrace>// ...std::cout << std::stacktrace::current();

И получать читаемый трейс:

0# bar(int) at /path/to/source/file.cpp:701# bar(int) at /path/to/source/file.cpp:702# bar(int) at /path/to/source/file.cpp:703# bar(int) at /path/to/source/file.cpp:704# main at /path/to/main.cpp:935# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.66# _start

Stacktrace незаменим для диагностирования сложных проблем. Он меня много раз спасал при отладке асинхронного кода. Например, в Такси можно включить особый режим работы сервиса, после чего порождение подзадач и исключения начинают печатать трейсы.

Библиотека приехала в стандарт C++ из Boost, но есть отличие в поведении: конструктор по умолчанию для std::stacktrace создаёт пустой объект, а не захватывает trace текущего потока, как это делает boost::stacktrace. Описание интерфейса и особенности дизайна класса доступны в P0881R7.

Кстати, обсуждается идея о том, чтобы исключения C++ при выставлении рантайм-флага могли сохранять трейсы. Разумеется, всё должно быть в лучших традициях C++ без слома ABI, максимально эффективно, с возможностью не платить за неиспользуемую функциональность.

Z и UZ


Если вы не любите печатать std::size_t или std::ssize_t, то вам приглянутся суффиксы C++2b, uz и z соответственно:

for (auto i = 0uz; i < vec.size(); ++i) {      assert(i == vec[i].size());}

Должен признать, что это нововведение из P0330R8 меня не особо будоражит Так что перейдём к следующей новинке.

Большой рефакторинг стандарта


К C++20 успели отрефакторить описание стандартной библиотеки, явно разделив static_assert, SFINAE и рантайм условия для классов и функций. К C++23 подгруппа ядра языка решилась переработать часть стандарта, отвечающую за декларации и их поиск.

В итоге закрыли 61 ошибку в ядре языка, частично решили ещё четыре проблемы. Любители артхаусной литературы могут оценить новый слог в P1787R6.

string::contains


Долой str.find(str2) != std::string_view::npos! Теперь, благодаря P1679R3, можно писать более вменяемый код str.contains(str2). Лично меня весьма радует, что с C++20 люди в комитете активно работают над красотой и лаконичностью языка, отодвинув концепцию итераторов и позиций на второй план:
Было Стало
str.substr(0, str2.size()) == str2
str.starts_with(str2);
str.size() >= str2.size() && !str.compare(    str.size() - str2.size(),    std::string_view::npos,    str2)
str.ends_with(str2);
std::sort(std::begin(d), std::end(d));
std::ranges::sort(d);

Executors & Networking


Увы, но Executors всё ещё не приняли в стандарт.

Однако комитет сдвинулся с мёртвой точки! Интерфейс практически устаканился, P0443 почти готов к принятию. Появилось множество реализаций предложенного интерфейса:


Работа над Networking идёт одновременно с Executors, постоянно вносятся небольшие правки. Networking продолжает зависеть от Executors, есть шанс, что успеют сделать обе вещи к C++23.

Прочие новости


В ближайшие полтора года международный комитет планирует работать удалённо. Планы на C++2b остаются в силе выпустить новый стандарт в 2023 году.

Конференции по C++ тоже продолжаются в онлайн-формате, ближайшая С++ meetup Moscow #11 в Технопарке Сколково. Следите за новостями и анонсами в канале t.me/ProCxx.

Напоследок ещё немного приятных новостей: в рамках подготовки к выпуску userver мы начали актвнее апстримить наши таксишные наработки в смежные проекты. segoon уже успел заапстримить в clang-tidy-проверку на безопасность функции для многопоточных приложений. Надеюсь, вам пригодится!
Подробнее..

Как можно и как нельзя использовать нулевой указатель в С

30.07.2020 16:13:15 | Автор: admin


Некоторым этот банальный вопрос уже набил оскомину, но мы взяли 7 примеров и попытались объяснить их поведение при помощи стандарта:


struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};/*1*/ *p;/*2*/ foo((*p, 5));                     /*3*/ A a{*p};/*4*/ p->data_mem;/*5*/ int b{p->data_mem};/*6*/ p->non_static_mem_fn();/*7*/ p->static_mem_fn();

Очевидная, но важная деталь: p, инициализированный нулевым указателем, не может указывать на объект типа А, потому что его значение отлично от значения любого указателя на объект типа А conv.ptr#1.


Disclaimer: статья содержит вольный перевод терминов и выдержек из стандарта на русский язык. Мы рекомендуем английскую версию статьи на dev.to, лишенную неточностей перевода.


Пример 1


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


*p;

Синтаксически это оператор выражения (expression statement, stmt.expr#1), в котором *p является выражением с отброшенным результатом, который, тем не менее, нужно вычислить. Определение унарного оператора * expr.unary.op#1 гласит, что этот оператор осуществляет косвенное обращение (indirection), и результатом является l-значение, которое обозначает объект или функцию, на которую указывает выражение. Его семантика понятна, чего не скажешь о том, должен ли объект существовать. Нулевой указатель в определении не упоминается ни разу.


Можно попробовать зацепиться за косвенное обращение, потому что есть basic.stc#4, в котором четко написано, что поведение при косвенном обращении через недопустимое значение указателя (indirection through an invalid pointer value) не определено. Но там же дается описание недопустимого значения указателя, под которое нулевой не подходит, и дается ссылка на basic.compound#3.4, где видно, что нулевой указатель и недопустимый это различные значения указателя.


Еще есть примечание в dcl.ref#5, которое гласит, что the only way to create such a reference would be to bind it to the object obtained by indirection through a null pointer, which causes undefined behavior, т.е. единственный способ создать такую ссылку привязать ее к объекту, полученному за счет косвенного обращения через нулевой указатель, что приводит к неопределенному поведению. Но придаточное в конце может относиться не только к косвенному обращению, но и к привязать (to bind), и в этом случае неопределенное поведение вызвано тем, что нулевой указатель не указывает на объект, о чем и говорится в основном тексте пункта dcl.ref#5.


Раз стандарт вместо однозначных формулировок оставляет пространство для интерпретаций в разрезе нашего вопроса, можно обратиться к списку дефектов языковой части стандарта, где Core Working Group среди прочего поясняет текст стандарта. Наш вопрос выделен в отдельный дефект, где CWG довольно давно пришла к неформальному консенсусу (так определен статус drafting), что неопределенное поведение влечет не разыменование само по себе, а конвертация результата разыменования из l-значения в r-значение. Если неформальный консенсус CWG звучит недостаточно весомо, то есть другой дефект, в котором рассматривается пример, аналогичный нашему примеру 7. Такой код назван корректным по этой же причине в официальной аргументации CWG.


В дальнейших рассуждениях мы будем опираться на этот консенсус. Если в будущем стандарт запретит разыменовывать нулевые указатели по примеру Си (N2176, 6.5.3.2 и сноска 104), значит, все примеры содержат неопределенное поведение, и на этом разговор можно закончить.


Пример 2


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


foo((*p, 5));  

Чтобы вызвать foo, требуется проинициализировать его параметр, для чего нужно вычислить результат оператора запятая. Его операнды вычисляются слева направо, причем все, кроме последнего, являются выражениями с отброшенным значением так же, как и в примере 1 (expr.comma#1). Следовательно, этот пример также корректен.


Пример 3


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


A a{*p};

Для инициализации a будет выбран неявный конструктор копирования, и для того, чтобы его вызвать, нужно проинициализировать параметр const A& допустимым объектом, в противном случае поведение не определено (dcl.ref#5). В нашем случае допустимого объекта нет.


Пример 4


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


p->data_mem;

Выражение этого оператора выражения при вычислении будет раскрыто в (*(p)).data_mem согласно expr.ref#2, которое обозначает (designate) соответствующий подобъект объекта, на который указывает выражение до точки (expr.ref#6.2). Параллели с примером 1 становятся особенно явными, если открыть, скажем, basic.lookup.qual#1, и увидеть, как to refer и to designate взаимозаменяемо используются в том же смысле, что и в expr.ref. Из чего мы делаем вывод, что это корректный код, однако некоторые компиляторы не согласны (см. про проверку константными выражениями в конце статьи).


Пример 5


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


int b{p->data_mem};

В продолжение предыдущего примера не будем отбрасывать результат, а проинициализируем им int. В этом случае результат нужно конвертировать в pr-значение, потому что выражения именно этой категории инициализируют объекты (basic.lval#1.2). Так как речь идет об int, будет осуществлен доступ к объекту результата (conv.lval#3.4), что в нашем случае ведет к неопределенному поведению, потому что ни одно из условий basic.lval#11 не соблюдается.


Пример 6


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


p->non_static_mem_fn();

class.mfct.non-static#1 гласит, что функции-члены разрешено вызывать для объекта типа, к которому они принадлежат (или унаследованного от него), или напрямую из определений функций-членов класса. Именно разрешено такой смысл вкладывается в глагол may be в директивах ИСО/МЭК, которым следуют все стандарты ИСО. Раз объекта нет, то и поведение при таком вызове не определено.


Пример 7


Открыть начало кода
struct A {    int data_mem;    void non_static_mem_fn() {}    static void static_mem_fn() {}};void foo(int) {}A* p{nullptr};


p->static_mem_fn();

Как говорилось в рассуждениях к примеру 1, Core Working Group считает этот код корректным. Добавить можно лишь то, что согласно сноске 59, выражение слева от оператора -> разыменовывается, даже его результат не требуется.


Проверка с помощью constexpr


Раз константные выражения не могут полагаться на неопределенное поведение (expr.const#5), то можно узнать мнение компиляторов о наших примерах. Пусть они и несовершенны, но как минимум нередко правы. Мы взяли три популярных компилятора, подправили пример под constexpr и для наглядности закомментировали те примеры, которые не компилируются, потому что сообщения об ошибках что у GCC, что у MSVC оставляют желать лучшего на данных примерах: godbolt.


Что получилось в итоге:


#
Код
Предположение
GCC 10.1
Clang 10
MSVC 19.24
1
*p;
+
+
+
+
2
foo((*p, 5));
+
+
+
+
3
A a{*p};
4
p->data_mem;
+
+
5
int b{p->data_mem};
6
p->non_static_mem_fn();
+
+
7
p->static_mem_fn();
+
+
+
+

Результаты заставляют несколько усомниться в выводе из примера 6 и в большей степени из примера 4. Но также интересно, что мы все сходимся во мнении о ключевом примере 1.


Спасибо, что остались с нами до конца, чтобы проследить за приключениями нулевого указателя в С++! :-) Обычно мы делимся на Хабре кусками кода из реальных проектов по разработке встроенного ПО для электроники, но этот раз нас заинтересовали чисто философские вопросы, поэтому примеры синтетические.


Если вы разделяете нашу любовь к противоречиям в С++, делитесь наболевшим в комментариях.

Подробнее..

Использование SEH в 32 разрядных приложениях Windows с компилятором Mingw-W64

23.12.2020 22:19:01 | Автор: admin

Что такое SEH


Из всех механизмов, предоставляемых операционными системами семейства Windows, возможно наиболее широко используемым, но не полностью документированным, является механизм структурной обработки исключений (он же Structured Exception Handling, или просто SEH). Структурная обработка исключений это сервис, предоставляемый операционной системой, механизм обработки программных и аппаратных исключений в операционной системе Microsoft Windows, позволяющий программистам управлять обработкой исключений. Исключение это событие при выполнении программы, которое приводит к её ненормальному или неправильному поведению.

Вся документация по SEH, которую вы, вероятно, найдете, описывает одну лишь компиляторно-зависимую оболочку, созданную функциями библиотеки времени выполнения (Run-Time-Library, RTL) вокруг реализации SEH операционной системы. В ключевых словах _try, _finally, или _except, нет ничего магического. Группы разработчиков из Microsoft, занимающиеся разработкой операционных систем и компиляторов, определили эти ключевые слова, и то, что они делают. Другие поставщики компиляторов просто поддержали эту семантику. Видимые программисту инструменты SEH уровня компилятора призваны скрыть базовый механизм SEH уровня операционной системы, что позволяет не обнародовать детали функционирования последнего. Основные детали базового механизма SEH уровня ОС будут рассмотрены в этой статье. В статье отражены личные взгляды и предпочтения автора.

Основной смысл SEH состоит в следующем: когда выполняющийся поток совершает ошибку, ОС дает возможность узнать об этом. Точнее, когда поток совершает ошибку, ОС приостанавливает поток, сохраняет все регистры процессора в специальной структуре и из служебного потока, связанного с тем, который вызвал ошибку, вызывает определенную пользователем callback-функцию. Функциональность этой callback-функции в значительной степени не регламентирована, т.е. в процессе своей работы она может делать все, что угодно. Например, она может устранить причину ошибки, если это возможно. Независимо оттого, что эта callback-функция делает, она должна возвратить значение, которое скажет системе, что делать дальше. Эта callback-функция называется exception handler(обработчик исключения). Для того, чтобы быть вызванной:
1. Функция обработчик исключения должна соответствовать прототипу
2. Адрес функции обработчика исключения должен быть указан в специальной структуре данных EXCEPTION_REGISTRATION
3. Структура данных EXCEPTION_REGISTRATION должна располагаться в стеке потока, который вызвал исключение с выравниванием адреса структуры и ее компонентов на адрес, кратный 4 (биты 0 и 1 адреса равны нулю)
4. Тело функции обработчика исключения не должно располагаться в стеке потока, который вызвал исключение

Значение, которое возвращает обработчик исключения, указывает ОС обработано ли исключение или нет.

Технические детали SEH


Прототип функции обработчика исключения:
EXCEPTION_DISPOSITION __cdecl _except_handler(     struct _EXCEPTION_RECORD *ExceptionRecord,     void * EstablisherFrame,     struct _CONTEXT *ContextRecord,     void * DispatcherContext     );

EXCEPTION_DISPOSITION это макроопределение, имеющее тип int, _EXCEPTION_RECORD содержит информацию о произошедшем исключении, EstablisherFrame адрес структуры EXCEPTION_REGISTRATION в стеке потока, вызвавшего исключение, _CONTEXT адрес структуры содержащей все регистры процессора в момент возникновения исключения, DispatcherContext служебая информация

Структура данных EXCEPTION_REGISTRATION
_EXCEPTION_REGISTRATION struc
prev dd ?
handler dd ?
_EXCEPTION_REGISTRATION ends


image
Как видно из определения, структура состоит из 2х элементов по 32 бита, prev адрес структуры с предыдущим обработчиком, handler адрес функции обработчика исключения. Наличие элемента prev позволяет выстраивать цепочку из обработчиков, адрес последней структуры с актуальным обработчиком доступен через регистр fs:0, отсюда и начинается просмотр и вызов последовательности обработчиков, до тех пор, пока один из них не вернет значение ExceptionContinueExecution (0). Если ни один обработчик из цепочки обработчиков не может обработать исключение, то его обрабатывает первый элемент цепочки обработчик исключений по умолчанию (стандартный обработчик), у которого нет варианта отказаться от обработки исключения.
image

SEH это ошибка.


Я совершенно уверен в том, что сама изначальная идея SEH ошибочна. Операционная система будет сообщать программе о возникновении незапланированных исключительных ситуаций, таких как деление на 0, неверный HANDLE, попытка исполнения неверной инструкции процессора попытка доступа к памяти при отсутствии соответствующих разрешений и так далее. Совершенно нереально исправить такие ошибки во время исполнения программы, ошибка уже случилась, программа уже пошла по неверному пути, который привел ее к этой ошибке. Нельзя уже сделать ничего разумного, но SEH дает возможности:
1. Скрыть ошибку, заменив стандартный обработчик ошибок своим, и обработать исключение
2. Позволить программе прожить немного дольше, сделав вид, что ничего не случилось
И первое, и второе очень плохо.
К сожалению, стандартные обработчики ошибок современных ОС семейства Windows не показывают пользователю ничего, что могло бы информировать его о том, что случилось и что надо делать, после паузы и показа неинформативного окна приложение аварийно закрывается. Моя идея состоит в том, чтобы при возникновении исключения программист должен иметь возможность информировать пользователя об ошибке с помощью специально сформированных сообщений, после чего выполнить минимально возможную приборку за собой и постараться корректно закрыть программу. Еще раз напоминаю, что в большинстве случаев нормальное продолжение выполнения программы после возникновения исключения невозможно.

Компилятор GCC


GCC является главным компилятором для сборки ряда операционных систем; среди них различные варианты Linux и BSD, а также ReactOS, Mac OS X, OpenSolaris, NeXTSTEP, BeOS и Haiku.
GCC часто выбирается для разработки программного обеспечения, которое должно работать на большом числе различных аппаратных платформ.
GCC является лидером по количеству процессоров и операционных систем, которые он поддерживает. Имеет развитые средства для программирования на встроенном ассемблере (ассемблерные вставки). Генерирует компактный и быстрый исполняемый код. Имеет возможности оптимизации, в том числе с учетом ассемблерных вставок. К относительным недостаткам GCC можно отнести непривычный синтаксис AT&T используемый во встроенном ассемблере.
GCC/MinGW является полностью надежным качественным компилятором, который, на мой взгляд, превосходит любой доступный на сегодняшний день компилятор языка Си по качеству сгенерированного кода. Это несколько менее выражено с самыми последними версиями MSVC, но все еще заметно. Особенно для всего, что связано с inline assembly, GCC на мой взгляд, превосходит MSVC.
Соответствие стандартам, как мне кажется, также намного лучше в GCC.

MinGW-W64 компилятор


MinGW набор инструментов разработки программного обеспечения для создания приложений под Windows. Включает в себя компилятор, родной программный порт GNU Compiler Collection (GCC) под Windows вместе с набором свободно распространяемых библиотек импорта и заголовочных файлов для Windows API. В MinGW включены расширения для библиотеки времени выполнения Microsoft Visual C++ для поддержки функциональности C99.
В связи с тем, что в рамках изначального проекта MinGW не обещалось, что в его кодовую базу будут вноситься обновления, связанные с добавлением некоторых новых ключевых элементов Win32 API, а также наиболее необходимой поддержки 64-битной архитектуры, был создан проект MinGW-w64. Он является новой чистой реализацией портирования GNU Compiler Collection (GCC) под Microsoft Windows, осуществленной изначально компанией OneVision и переданной в 2008 году в общественное пользование (Public Domain). Сначала он был предложен на рассмотрение для интеграции с оригинальным проектом MinGW, но был отклонен в связи с подозрением на использование несвободного или проприетарного кода. По многим серьезным причинам этического характера, связанным с отношением со стороны авторов MinGW, ведущие разработчики кода MinGW-w64 решили больше не пытаться кооперироваться с проектом MinGW.
MinGW-w64 обеспечивает более полную реализацию Win32 API, включая:
  • лучшую поддержку стандарта C99
  • лучшую поддержку pthreads (включая возможность задействовать функциональность стандарта C++11 библиотеки libstdc++ компилятора GCC)
  • GCC multilib
  • точки входа в программу с поддержкой Unicode (wmain/wWinMain)
  • DDK (из проекта ReactOS)
  • DirectX (из проекта WINE)
  • поддержку больших файлов
  • поддержку 64-битной архитектуры Windows

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

Поддержка SEH в Mingw-w64


Совершенно неудовлетворительна поддержка SEH, проще сказать ее нет, несмотря на наличие макросов __try1() и __except1. Мало того, что ни синтаксически (поскольку синтаксис __try __except() не поддерживается), ни семантически (макросы __try1 __except1 только позволяют установить/удалить обработчик исключений) невозможно программирование SEH, как с компиляторами Microsoft или Borland/Embarcadero, даже само использование макросов __try1 __except1 может приводить к проблемам

Проблемы


Использование стандартных макросов __try1 и __except1 может приводить к проблемам, так как в этих макросах явно изменяется состояние регистра ESP, но компилятор, который должен вести учет использования стека (что означает, что компилятор всегда должен знать, каково значение регистра ESP), об этом не уведомляется. Может быть, стандартные макросы будут работать, а может быть и нет.

Решение


Во-первых, однозначно придется отказаться от использования стандартных макросов __try1 и __except1, вместо них будут ассемблерные вставки, не затрагивающие регистр ESP и локальная переменная типа EXCEPTION_REGISTRATION в стеке. Приблизительно вот так:

static int exception=0;EXCEPTION_REGISTRATION seh_ex_reg;seh_ex_reg.handler = (PEXCEPTION_ROUTINE) exception_handler;/*ассемблерная вставка, которая в поле prev записывает адрес предыдущей структуры из fs:0 и устанавливает новый адрес этой структуры в fs:0 */asm ("\t movl %%fs:0, %%eax; movl %%eax, %0 \n" : "=r" (seh_ex_reg.prev) : : "%eax");asm volatile("\t movl %0,%%eax; movl %%eax, %%fs:0 \n"::"r" (&seh_ex_reg) : "%eax");// блок try{// codeif(exception == 0){//ассемблерная вставка: убираем обработчик исключений asm (// restore previous handler, ESP is not modified "\t movl %0, %%eax \n""\t movl %%eax, %%fs:0 \n" : :"r" (seh_ex_reg.prev) : "%eax");goto end;}}// блок except{trusted_code://сюда мы попадаем при исключении//ассемблерная вставка: убираем обработчик исключений//выполняем очистку}end:exit();

Во-вторых, необходимо отделить доверяемый код (т.е., отлаженный, не генерирующий исключений) от недоверяемого (где возможны исключения). Это достаточно стандартная практика, где за блоком try следует блок except С помощью нескольких глобальных переменных, ассемблерных вставок и имеющего дурную славу опрератора goto удалось решить такую задачу. Решение не претендует на универсальность, над этим еще надо думать, пока это выглядит как сваленный в кучу набор трюков. Недостатки: много ассемблерного кода, используются статические переменные, не производится размотка стека, состояние локальных переменных в стеке try блока теряется безвозвратно. Корректное завершение программы возможно, используя только переменные и функции из except блока.
От использования статических переменных, впрочем, легко отказаться, для чего придется расширить структуру EXCEPTION_REGISTRATION, дополнив ее недостающими элементами, перенести туда все бывшие ранее статическими переменные. Это сделает код примера не намного сложнее, но зато код будет по-настоящему структурным, способным обрабатывать исключения на нескольких уровнях вложенности. Доступ к дополнительным элементам расширенной структуры EXCEPTION_REGISTRATION_EX из функции обработчика исключения осуществляется через параметр EstablisherFrame который нужно преобразовать к соответствующему типу. Заодно сохраним значения регистров ESP и EBP, чтобы восстановить стек фрейм к тому состоянию, которое было в начале блока try. Теперь наш пример обретает законченную форму. К сожалению, при возникновении исключения, содержимое локальных переменных блока try безвозвратно теряется.
Ссылка на исходный текст
Подробнее..

Простое сложное программирование

15.04.2021 16:11:54 | Автор: admin


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

Но понятие сложность сродни термину куча. Для кого-то и пять кокосов это не куча, а кого-то съел один и раз больше не хочется значит для него и единственного кокоса будет много.

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

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

Выбор попугаев для измерения


Я не стал придумывать свои или вычислять эмпирические метрики программного кода, и в качестве попугая решил взять самую простую метрику SLOC (англ.Source Lines of Code) количество строк исходного текста компилятора, которая очень легко вычисляется.

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

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

Но для численной оценки сложности кода в рамках одного проекта, метрика SLOC подходит хорошо.

Методика подсчета SLOC


Изначально попробовал использовать простой bash скрипт с поиском по маске и подсчетом числа строк в файлах исходника через wc -l. Но через некоторое время стало понятно, что приходится изобретать очередной велосипед.
Ну вы поняли:



Поэтому решил взять уже готовый. После быстрого поиска остановился на утилите SLOCCount, которая умеет анализировать почти три десятка типов исходников.
Список типов файлов для автоматического анализа
1. Ada (.ada, .ads, .adb)
2. Assembly (.s, .S, .asm)
3. awk (.awk)
4. Bourne shell and variants (.sh)
5. C (.c)
6. C++ (.C, .cpp, .cxx, .cc)
7. C shell (.csh)
8. COBOL (.cob, .cbl) as of version 2.10
9. C# (.cs) as of version 2.11
10. Expect (.exp)
11. Fortran (.f)
12. Haskell (.hs) as of version 2.11
13. Java (.java)
14. lex/flex (.l)
15. LISP/Scheme (.el, .scm, .lsp, .jl)
16. Makefile (makefile) not normally shown.
17. Modula-3 (.m3, .i3) as of version 2.07
18. Objective-C (.m)
19. Pascal (.p, .pas)
20. Perl (.pl, .pm, .perl)
21. PHP (.php, .php[3456], .inc) as of version 2.05
22. Python (.py)
23. Ruby (.rb) as of version 2.09
24. sed (.sed)
25. SQL (.sql) not normally shown.
26. TCL (.tcl, .tk, .itk)
27. Yacc/Bison (.y)



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

Меня изначально интересовал объем исходников на С/С++ и может быть еще на Ассемблере, если таких файлов окажется достаточно для много. Но после начала работы очень обрадовался, что не стал изобретать велосипед, а взял готовую тулзу, т.к. она отдельно считала статистику исходных файлов синтаксического анализатора Yacc/Bison (.y), который и определяет фактическую сложность парсера (читай сложность синтаксиса языка программирования).

Старые исходники gcc брал с gcc.gnu.org/mirrors.html, но перед запуском анализатора удалили каталоги других компиляторов (ada, fortran, java и т.д.), чтобы они не попадали в итоговую статистику.

Результаты в попугаях.


Таблица




Объем кода синтаксического анализатора Yacc/Bison


Объем общей которой базы GCC (только для языков C и C++)

Выводы


К сожалению синтаксический анализатор Yacc/Bison использовался только до 3 версии, а после его использование свелось на нет. Поэтому оценить сложность синтаксиса С/С++ с помощью объема кода парсера можно лишь примерно до 1996-98 года, после чего его стали постепенно выпиливать, т.е. чуть менее, чем за десять лет. Но даже за этот период объем кодовой базы синтаксического анализатора вырос двукратно, что примерно соответствует по времени реализации стандарта C99.

Но даже если не учитывать код синтаксического анализатора, то объем общей кодовой базы так же коррелирует с внедрением новых стандартов C++: C99, С11 и C14.

На графике не видно выраженного пика для С+17 и следующих версий, но могу предположить, что при текущем объеме кода (более 4 миллионов строк только С и С++ кода), несколько тысяч строк, необходимых для поддержки синтаксических конструкций новых стандартов просто незаметно.

Вывод первый очевидный. Рост сложности инструментов разработки


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

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

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

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

Вывод второй порог входа


Представьте, что нужно сделать свой сайт. Естественно нужно определить, какую CMS для него использовать и какой брать хостинг. И если с хостингом вопрос решается очень просто, конечно же в TimeWeb, да еще и с бонусом по ссылке, то при выборе CMS приходится подумать.

И если для простых сайтов существуют и простые решения, то для тех, кто не ищет легких путей существует CMS Drupal, который примечателен тем, что имеет фантастически высокий порог входа для начала использования.


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

Итоговый вывод не утешительный


Если рассматривать увеличение сложности только самого ПО, то это одно дело. Вот к примеру
Статистика ядра Linux с вики
17 сентября 1991: Linux версии 0.01 (10 239 строк кода).
14 марта 1994: Linux версии 1.0.0 (176 250 строк кода).
Март 1995: Linux версии 1.2.0 (310 950 строк кода).
9 июня 1996: Linux версии 2.0.0 (777 956 строк кода).
25 января 1999: Linux версии 2.2.0, изначально довольно недоработанный (1 800 847 строк кода).
4 января 2001: Linux версии 2.4.0 (3 377 902 строки кода).
18 декабря 2003: Linux версии 2.6.0 (5 929 913 строк кода).
23 марта 2009: Linux версии 2.6.29, временный символ Linux тасманский дьявол Tuz (11 010 647 строк кода).
22 июля 2011: релиз Linux 3.0 (14,6 млн строк кода).
24 октября 2011: релиз Linux 3.1.
15 января 2012: релиз Linux 3.3 преодолел отметку в 15 млн строк кода.
23 февраля 2015: первый релиз-кандидат Linux 4.0 (более 19 млн строк кода).
7 января 2019: первый релиз-кандидат Linux 5.0 (более 26 млн строк кода).

А что делать, если сложность программного обеспечения накладывается на тенденцию постоянного усложнения самих рабочих инструментов? Ведь постоянное развитие языков программирования неизбежно повышает порог входа для всех начинающих и только усугубляет проблему сложности разработки ПО.

Понятно, что чем дальше, тем проще уже не будет. Ведь область IT, это среда с самой высокой конкуренцией. И как уж тут не вспомнить Льюиса Кэррола, что его крылатое выражение
Нужно бежать со всех ног, чтобы только оставаться на месте, а чтобы куда-то попасть, надо бежать как минимум вдвое быстрее!
Ведь это относится не только к Алисе в стране чудес, но и к всем информационным технологиям в целом!
Подробнее..

Из песочницы Девиртуализация в последних версиях gcc и clang

16.07.2020 02:04:59 | Автор: admin

Что это вообще такое

Девиртуализация (devirtualization) оптимизация виртуальных функций. Если компилятор точно знает тип объекта, он может вызывать его виртуальные функции напрямую, не используя таблицу виртуальных функций.
В этой статье мы проверим насколько хорошо с этой задачей справляются компиляторы gcc и clang.

Тестирование

Все тесты производились на Arch Linux x86-64. Использовались gcc 4.8.2 и clang 3.3.

вывод gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/lto-wrapper
Target: x86_64-unknown-linux-gnu
Configured with: /build/gcc-multilib/src/gcc-4.8.2/configure --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://bugs.archlinux.org/ --enable-languages=c,c++,ada,fortran,go,lto,objc,obj-c++ --enable-shared --enable-threads=posix --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-clocale=gnu --disable-libstdcxx-pch --disable-libssp --enable-gnu-unique-object --enable-linker-build-id --enable-cloog-backend=isl --disable-cloog-version-check --enable-lto --enable-plugin --with-linker-hash-style=gnu --enable-multilib --disable-werror --enable-checking=release
Thread model: posix
gcc version 4.8.2 (GCC)

вывод clang -v
clang version 3.3 (tags/RELEASE_33/final)
Target: x86_64-unknown-linux-gnu
Thread model: posix


Чтобы было проще разбираться в дизассемблированном коде, использовался флаг -nostartfiles. Если его указать, то компилятор не будет генерировать код, вызывающий функцию main с нужными параметрами. Функция, которая получает управление первой, называется _start.

В коде, который мы будем компилировать, содержится два класса:
  • класс A абстрактный класс с трёмя методами: increment(), decrement() и get()
    class A {public:virtual ~A() {}virtual void increment() = 0;virtual void decrement() = 0;virtual int get() = 0;};
    
  • класс B класс наследующийся от А и реализующий все абстрактные методы
    class B : public A {public:B() : x(0) {}virtual void increment() {x++;}virtual void decrement() {x--;}virtual int get() {return x;}private:int x;};
    


Версия 1
Всё в одном файле.
код
class A {public:virtual ~A() {}virtual void increment() = 0;virtual void decrement() = 0;virtual int get() = 0;};class B : public A {public:B() : x(0) {}virtual void increment() {x++;}virtual void decrement() {x--;}virtual int get() {return x;}private:int x;};extern "C" {int printf(const char * format, ...);void exit(int status);void _start() {B b;b.increment();b.increment();b.decrement();printf("%d\n", b.get());exit(0);}}

Результат: gcc с флагами -O1, -O2, -O3, -Os и clang с флагами -O2, -O3, -Os произвели девиртуализацию и поняли, что второй аргумент функции printf всегда равен 1. Код, сгенерированный с помощью gcc -O1:
<_start>:    sub    rsp,0x8     ; вызов printf    mov    esi,0x1       ; записываем значение b.get() в ESI    mov    edi,0x4003a2  ; записываем адрес строки "%s\n" в EDI    mov    eax,0x0    call   400360 <printf@plt>  ; вызываем printf     ; вызов exit    mov    edi,0x0            ; записываем код ошибки в регистр EDI    call   400370 <exit@plt>  ; вызываем exit

Версия 2
Всё в одном файле, вызываем виртуальные методы через указатель на базовый класс
код
class A {public:virtual ~A() {}virtual void increment() = 0;virtual void decrement() = 0;virtual int get() = 0;};class B : public A {public:B() : x(0) {}virtual void increment() {x++;}virtual void decrement() {x--;}virtual int get() {return x;}private:int x;};extern "C" {int printf(const char * format, ...);void exit(int status);void _start() {A * a = new B;a->increment();a->increment();a->decrement();printf("%d\n", a->get());exit(0);}}

Результат: clang с флагами -O2, -O3, -Os генерирует такой же код, что и в варианте 1. gcc ведёт себя странно: с флагами -O1, -O2, -O3, -Os он генерирует такой код:
<_start>:    push   rbx     ; выделение памяти    mov    edi,0x10            ; кол-во байт (16)    call   400560 <_Znwm@plt>  ; вызываем функцию, выделяющую память (возвращает указатель в RAX)    mov    rbx,rax             ; сохраняем указатель на экземпляр класса в RBX     ; конструктор    mov    QWORD PTR [rax],0x4006d0  ; инициализируем таблицу виртуальных функций    mov    DWORD PTR [rax+0x8],0x1   ; инициализируем поле x единицей (первый вызов increment заинлайнился)     ; второй вызов increment    mov    rdi,rax                     ; записываем указатель на экземпляр класса в RDI    call   4005ca <_ZN1B9incrementEv>  ; вызываем increment     ; вызов decrement    mov    rax,QWORD PTR [rbx]   ; записываем указатель на таблицу виртуальных функций в RAX    mov    rdi,rbx               ; записываем указатель на экземпляр класса в RDI    call   QWORD PTR [rax+0x18]  ; вызываем decrement через таблицу виртуальных функций     ; вызов get    mov    rax,QWORD PTR [rbx]   ; записываем указатель на таблицу виртуальных функций в RAX    mov    rdi,rbx               ; записываем указатель на экземпляр класса в RDI    call   QWORD PTR [rax+0x20]  ; вызываем get через таблицу виртуальных функций (результат в EAX)     ; вызов printf    mov    esi,eax              ; записываем значение b.get() в ESI    mov    edi,0x400620         ; записываем адрес строки "%s\n" в EDI    mov    eax,0x0    call   400520 <printf@plt>  ; вызываем printf     ; вызов exit    mov    edi,0x0            ; записываем код ошибки в регистр EDI    call   400370 <exit@plt>  ; вызываем exit

Версия 3
Для каждого класса отдельный .hpp и .cpp файл
код
a.hpp
#pragma onceclass A {public:virtual ~A();virtual void increment() = 0;virtual void decrement() = 0;virtual int get() = 0;};

a.cpp
#include "a.hpp"A::~A() {}

b.hpp
#pragma once#include "a.hpp"class B : public A {public:B();virtual void increment();virtual void decrement();virtual int get();private:int x;};

b.cpp
#include "b.hpp"B::B() : x(0) {}void B::increment() {x++;}void B::decrement() {x--;}int B::get() {return x;}

test.cpp
#include "b.hpp"extern "C" {int printf(const char * format, ...);void exit(int status);void _start() {B b;b.increment();b.increment();b.decrement();printf("%d\n", b.get());exit(0);}}

Результат: оба компилятора успешно девиртуализировали все функции, но не смогли их заинлайнить, так как они находятся в разных единицах трансляции:
<_start>:    push   rbx    sub    rsp,0x10     ; выделяем пямять на стеке     ; вызов конструктора    lea    rbx,[rsp]           ; сохраняем указатель на экземпляр класса в RBX    mov    rdi,rbx             ; записываем указатель на экземпляр класса в RDI    call   400720 <_ZN1BC1Ev>  ; вызываем конструктор     ; вызов increment    mov    rdi,rbx                     ; записываем указатель на экземпляр класса в RDI    call   400740 <_ZN1B9incrementEv>  ; вызываем increment     ; вызов increment    lea    rdi,[rsp]                   ; записываем указатель на экземпляр класса в RDI    call   400740 <_ZN1B9incrementEv>  ; вызываем increment     ; вызов decrement    lea    rdi,[rsp]                   ; записываем указатель на экземпляр класса в RDI    call   400750 <_ZN1B9decrementEv>  ; вызываем decrement     ; вызов get    lea    rdi,[rsp]             ; записываем указатель на экземпляр класса в RDI    call   400760 <_ZN1B3getEv>  ; вызываем get     ; вызов printf    mov    edi,0x400820         ; записываем адрес строки "%s\n" в EDI    mov    esi,eax              ; записываем значение b.get() в ESI    xor    al,al    call   4005d0 <printf@plt>  ; вызываем printf     ; вызов exit    xor    edi,edi            ; записываем код ошибки в регистр EDI    call   4005e0 <exit@plt>  ; вызываем exit

Версия 4
Для каждого класса отдельный .hpp и .cpp файл, LTO (Link Time Optimization, она же Interprocedural optimization, флаг -flto)
Код тот же, что и в предыдущем примере
Результат: clang девиртуализировал и заинлайнил все методы (ассемблерный код как в примере 1), gcc по какой-то причине заинлайнил всё кроме конструктора:
<_start>:    push   rbx    sub    rsp,0x10  ; выделяем пямять на стеке     ; вызов конструктора    mov    rdi,rsp                  ; записываем указатель на экземпляр класса в регистр RDI    call   400660 <_ZN1BC1Ev.2444>  ; вызываем конструктор     ; вычисление значения поля x    mov    eax,DWORD PTR [rsp+0x8]  ; загружаем старое значение поля x (0)    lea    esi,[rax+0x1]            ; увеличиваем его не 1    mov    DWORD PTR [rsp+0x8],esi  ; записываем результат     ; вызов printf    mov    edi,0x400700         ; записываем адрес строки "%s\n" в EDI    mov    eax,0x0              ; записываем значение b.get() в ESI    call   4005f0 <printf@plt>  ; вызываем printf     ; вызов exit    mov    edi,0x0            ; записываем код ошибки в регистр EDI    call   400620 <exit@plt>  ; вызываем exit


Версия 5
Для каждого класса отдельный .hpp и .cpp файл, LTO, вызываем виртуальные методы через указатель на базовый класс
код
a.hpp
#pragma onceclass A {public:virtual ~A();virtual void increment() = 0;virtual void decrement() = 0;virtual int get() = 0;};

a.cpp
#include "a.hpp"A::~A() {}

b.hpp
#pragma once#include "a.hpp"class B : public A {public:B();virtual void increment();virtual void decrement();virtual int get();private:int x;};

b.cpp
#include "b.hpp"B::B() : x(0) {}void B::increment() {x++;}void B::decrement() {x--;}int B::get() {return x;}

test.cpp
#include "b.hpp"extern "C" {int printf(const char * format, ...);void exit(int status);void _start() {A * a = new B;a->increment();a->increment();a->decrement();printf("%d\n", a->get());exit(0);}}

Результат: и gcc, и clang смогли девиртуализировать только первый вызов increment:
<_start>:    push   rbx     ; выделение памяти    mov    edi,0x10            ; кол-во байт (16)    call   400480 <_Znwm@plt>  ; вызываем функцию, выделяющую память (возвращает указатель в RAX)    mov    rbx,rax             ; сохраняем указатель на экземпляр класса в RBX     ; конструктор    mov    QWORD PTR [rbx],0x4005b0  ; инициализируем таблицу виртуальных функций    mov    DWORD PTR [rbx+0x8],0x0   ; инициализируем поле x     ; первый вызов increment    mov    rdi,rbx                     ; записываем указатель на экземпляр класса в RDI    call   400520 <_ZN1B9incrementEv>  ; вызываем increment     ; второй вызов increment    mov    rax,QWORD PTR [rbx]   ; записываем указатель на таблицу виртуальных функций в RAX    mov    rdi,rbx               ; записываем указатель на экземпляр класса в RDI    call   QWORD PTR [rax+0x10]  ; вызываем increment     ; вызов decrement    mov    rax,QWORD PTR [rbx]   ; записываем указатель на таблицу виртуальных функций в RAX    mov    rdi,rbx               ; записываем указатель на экземпляр класса в RDI    call   QWORD PTR [rax+0x18]  ; вызываем decrement     ; вызов get    mov    rax,QWORD PTR [rbx]   ; записываем указатель на таблицу виртуальных функций в RAX    mov    rdi,rbx               ; записываем указатель на экземпляр класса в RDI    call   QWORD PTR [rax+0x20]  ; вызываем get     ; вызов printf    mov    edi,0x400570         ; записываем адрес строки "%s\n" в EDI    mov    esi,eax              ; записываем значение b.get() в ESI    xor    al,al    call   400490 <printf@plt>  ; вызываем printf     ; вызов exit    xor    edi,edi            ; записываем код ошибки в регистр EDI    pop    rbx    jmp    4004a0 <exit@plt>  ; вызываем exit

Выводы

  • Наилучший результат достигается когда все классы в одной единице трансляции
  • Во всех тестах результаты clang не хуже или лучше результатов gcc


Исходники: github.com/alkedr/devirtualize-test
Подробнее..

Разработка firmware на С словно игра в бисер. Как перестать динамически выделять память и начать жить

07.04.2021 10:06:45 | Автор: admin

C++ is a horrible language. It's made more horrible by the fact that a lotof substandard programmers use it, to the point where it's much mucheasier to generate total and utter crap with it.

Linus Benedict Torvalds

Собеседование шло уже второй час. Мы наконец-то закончили тягучее и вязкое обсуждение моей скромной персоны, и фокус внимания плавно переполз на предлагаемый мне проект. Самый бойкий из трех моих собеседников со знанием дела и без лишних деталей принялся за его описание. Говорил он быстро и уверенно явно повторяет весь этот рассказ уже не первый раз. По его словам, работа велась над неким чрезвычайно малым, но очень важным устройством на базе STM32L4. Потребление энергии должно быть сведено к минимуму... USART... SPI... ничего необычного, уже неоднократно слышал подобное. После нескольких убаюкивающих фраз собеседник внезапно подался чуть вперед и, перехватив мой сонный взгляд, не без гордости произнес:

А firmware мы пишем на C++! мой будущий коллега заулыбался и откинулся в кресле, ожидая моей реакции на свою провокативную эскападу.

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

У вас есть какие-то опасения? поспешил спросить он с искренней озабоченностью в голосе.

Опасения у меня всегда имеются в избытке. Я с ностальгическим умилением вспомнил, как писал первую прошивку на MbedOS для одной из демонстрационных плат. Память тогда закончилась быстрее, чем я успел моргнуть светодиодом второй раз. Эх, опыт, сын ошибок... Надо все же признать, что страшные темные времена, когда о прошивке на плюсах никто и не заикался, давно прошли. Все разумные возражения в настоящее время рассыпались в труху и превратились в мифы. Вроде бы...

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

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

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

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

IAR

Так уж получилось, что мы впервые встретились на этом проекте. "Ну, это же специальный компилятор для железок", наивно думал я, "сработаемся". Не скажу, что я жестоко ошибся и проклял тот день, но использование именно этого компилятора доставляет определенный дискомфорт. Дело в том, что в проекте уже начали внедрение относительно нового стандарта С++17. Я уже потирал потные ладошки, представляя, как перепишу вон то и вот это, как станет невероятно красиво, но IAR может охладить пыл не хуже, чем вид нововоронежской Аленушки.

Новый стандарт реализован для нашего любимого коммерческого компилятора лишь частично, несмотря на все заверения о поддержке всех возможностей новейших стандартов. Например, structured binding declaration совсем не работает, сколько не уговаривай упрямца. Еще IAR весьма нежен и хрупок, какая-нибудь относительно сложная конструкция может довести его до истерики: компиляция рухнет из-за некой внутренней ошибки. Это самое неприятное, поскольку нет никаких подсказок, по какой причине все так неприятно обернулось. Такие провалы огорчают даже сильнее финала Игры престолов.

Можно справедливо заметить, что всему виной сложные шаблонные конструкции. Да, но у GCC с пониманием аналогичных шаблонов никогда не было проблем.

SIL

Для некоторых классов устройств существует такое понятие, как стандарты SIL. Safety integrity level уровень полноты безопасности, способность системы обеспечивать функциональную безопасность.

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

Одно только это правило привело ко всем последующим значительным изменениям в коде, о которых речь пойдет ниже.

std::exception

Несмотря на утверждение Герба Саттера, что без исключений C++ уже перестает быть тем языком, который мы знаем и любим, они были беспощадно выпилены. Немудрено, ведь обычный механизм исключений использует динамическое выделение памяти, что недопустимо в нашем проекте.

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

__cxa_allocate_exception

Название у нее уже какое-то нехорошее, и действительно, выделяет память для объекта исключения и делает это весьма неприятным образом прямо в куче. Вполне возможно эту функцию подменить на собственную реализацию и работать со статическим буфером. Если не ошибаюсь, то в руководстве для разработчиков autosar для с++14 так и предлагают делать. Но есть нюансы. Для разных компиляторов реализация может отличаться, нужно точно знать, что делает оригинальная функция, прежде чем грубо вмешиваться в механизм обработки. Проще и безопаснее от исключений отказаться вовсе.Что и было сделано, и соответствующий флаг гордо реет теперь над компилятором! Только вот стандартную библиотеку нужно будет использовать вдвойне осторожнее, поскольку пересобрать ее с нужными опциями под IAR возможности нет.

std::vector

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

Все вектора подлежали замене на std::array, но были особо сложные случаи, которые использовали возможности векторов по полной и были очень хитро вплетены в код. Для таких случаев можно написать простой аллокатор, который позволит изобразить вектор из стандартного массива.

template <class T, std::size_t Size>class StaticArray { using ssize_t = int;public: using value_type = T; template <class U> struct rebind {   using other = StaticArray<U, Size>; }; StaticArray() = default; ~StaticArray() = default; template <class U, std::size_t S> StaticArray(const StaticArray<U, S>&); auto allocate(std::size_t n) -> value_type*; auto deallocate(value_type* p, std::size_t n) -> void; auto max_size() const -> std::size_t;};

Ключевые функции, конечно, allocate и deallocate. Передаваемый им параметр n это не размер в байтах, а размер в попугаях, которые хранятся в векторе. Функция max_size используется при проверке вместимости аллокатора и возвращает максимально возможное теоретически число, которое можно передать в функцию allocate.

Тут очевиднейший пример использования аллокатора
std::vector<int, StaticArray<int, 100>> v;    v.push_back(1000);std::cout<<"check size "<<v.size()<<std::endl;    v.push_back(2000);std::cout<<"check size "<<v.size()<<std::endl;

Результат выполнения такой программы (скомпилировано GCC) будет следующий:

max_size() -> 100

max_size() -> 100

allocate(1)

check size 1

max_size() -> 100

max_size() -> 100

allocate(2)

deallocate(1)

check size 2

deallocate(2)

std::shared_ptr

Умные указатели, безусловно, хорошая вещь, но нужная ли в bare metal? Требование безопасности, запрещающее динамическую аллокацию памяти, делает использование умных указателей в этой области крайне сомнительным мероприятием.

Конечно, контролировать управление памятью путем использования кастомных аллокаторов вполне возможно. В стандартной библиотеке есть замечательная функция std::allocate_shared, которая создаст разделяемый объект именно там, где мы укажем. Указать же можно самолепным аллокатором примерно такого вида:

template <class Element,           std::size_t Size,           class SharedWrapper = Element>class StaticSharedAllocator {  public:  static constexpr std::size_t kSize = Size;  using value_type = SharedWrapper;  using pool_type = StaticPool<Element, kSize>;  pool_type &pool_;  using ElementPlaceHolder = pool_type::value_type;  template <class U>  struct rebind {    using other = StaticSharedAllocator<Element, kSize, U>;  };  StaticSharedAllocator(pool_type &pool) : pool_{pool} {}  ~StaticSharedAllocator() = default;  template <class Other, std::size_t OtherSize>  StaticSharedAllocator(const StaticSharedAllocator<Other, OtherSize> &other)     : pool_{other.pool_} {}  auto allocate(std::size_t n) -> value_type * {    static_assert(sizeof(value_type) <= sizeof(ElementPlaceHolder));    static_assert(alignof(value_type) <= alignof(ElementPlaceHolder));    static_assert((alignof(ElementPlaceHolder) % alignof(value_type)) == 0u);      return reinterpret_cast<value_type *>(pool_.allocate(n));  }  auto deallocate(value_type *p, std::size_t n) -> void {    pool_.deallocate(reinterpret_cast<value_type *>(p), n);  }};

Очевидно, Element тип целевого объекта, который и должен храниться как разделяемый объект. Size максимальное число объектов данного типа, которое можно создать через аллокатор. SharedWrapper это тип объектов, которые будут храниться в контейнере на самом деле!

Конечно, вы знаете, что для работы shared_ptr необходима некоторая дополнительная информация, которую нужно где-то хранить, лучше прямо с целевым объектом вместе. Поэтому для этого аллокатора очень важна структура rebuild. Она используется в недрах стандартной библиотеки, где-то в районе alloc_traits.h, чтобы привести аллокатор к виду, который необходим для работы разделяемого указателя:

using type = typename _Tp::template rebind<_Up>::other;

где _Tp это StaticSharedAllocator<Element, Size>,

_Up это std::_Sp_counted_ptr_inplace<Object, StaticSharedAllocator<Element, Size>, __gnu_cxx::_S_atomic>

К сожалению, это верно только для GCC, в IAR тип будет немного другой, но общий принцип неизменен: нам нужно сохранить немного больше информации, чем содержится в Element. Для простоты тип целевого объекта и расширенный тип должны быть сохранены в шаблонных параметрах. Как вы уже догадались, SharedWrapper и будет расширенным типом, с которым непосредственно работает shared_ptr.

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

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

Еще немного кода для иллюстрации

Сам пул объектов основан на StaticArray аллокаторе. А чего добру пропадать?

template <class Type, size_t Size>struct StaticPool {  static constexpr size_t kSize = Size;  static constexpr size_t kSizeOverhead = 48;  using value_type = std::aligned_storage_t<sizeof(Type)+kSizeOverhead,                                             alignof(std::max_align_t)>;  StaticArray<value_type, Size> pool_;    auto allocate(std::size_t n) -> value_type * {    return pool_.allocate(n);  }  auto deallocate(value_type *p, std::size_t n) -> void {    pool_.deallocate(p, n);  }};

А теперь небольшой пример, как это все работает вместе:

struct Object {  int index;};constexpr size_t kMaxObjectNumber = 10u;StaticPool<Object, kMaxObjectNumber> object_pool {};StaticSharedAllocator<Object, kMaxObjectNumber> object_alloc_ {object_pool};std::shared_ptr<Object> MakeObject() {  return std::allocate_shared<Object>(object_alloc_);}

Все эти изуверства позволили избежать динамического выделения памяти и здесь. Хотя, мне кажется, лучше реорганизовать код так, чтоб не было необходимости использовать shared_ptr вообще. К сожалению, не всегда рефакторинг можно сделать быстро и безболезненно.

std::function

Универсальная полиморфная обертка над функциями или функциональными объектами. Очень удобная штука. Точно была бы полезна в embedded проекте, хотя бы для каких-нибудь функций обратного вызова (callbacks).

Чем мы платим за универсальность?

Во-первых, std::function может использовать динамическую аллокацию памяти.

Небольшой и несколько искусственный пример:

int x[] = {1, 2, 3, 4, 5};    auto sum = [=] () -> int {      int sum = x[0];      for (size_t i = 1u; i < sizeof(x) / sizeof(int); i++) {        sum += x[i];      }      return sum;    };        std::function<int()> callback = sum; 

Когда элементов массива 5, то размер функции 20 байт. В этом случае, когда мы присваиваем переменной callback экземпляр нашей лямбда-функции, будет использована динамическая аллокация.

Дело в том, что в классе нашей универсальной обертки содержится небольшой участок памяти (place holder), где может быть определена содержащаяся функция.

Любая функция в С++ может быть определена с помощью двух указателей максимум. Для свободных функций или функторов достаточно одного указателя, если нужно вызвать метод класса, то нужен указатель на объект и смещение внутри класса. Собственно, у нас есть небольшое укромное местечко для пары указателей. Конечно, небольшие функциональные объекты можно хранить прямо на месте этих указателей! Если размер лямбды, например, не позволяет целиком запихать ее туда, то на помощь снова придет динамическая аллокация.

Для GCC

Опции -specs=nano.specs уже не будет хватать для std::function.

Сразу появится сообщения подобного вида:

abort.c:(.text.abort+0xa): undefined reference to _exit

signalr.c:(.text.killr+0xe): undefined reference to _kill

signalr.c:(.text.getpidr+0x0): undefined reference to _getpid

Правильно, ведь пустая функция должна бросать исключение.

Нужна другая опция -specs=nosys.specs, где включены все необходимые заглушки для всяких системных функций.

Соберем небольшую прошивку, чтоб проверить как повлияет включение std::function на потребление памяти различных видов. Прошивка стандартный пример от ST для подмигивающего светодиода. Изменения в размере секций файла-прошивки в таблице:

text

data

bss

67880

2496

144

Невооруженным взглядом видно, что секция .text выросла просто фантастически (на 67Кб!). Как одна функция могла сделать такое?

Внутри std::function явно вызываются исключения, которые внутри себя используют demangling для имен функций, и это дорогое удовольствие. Нужно ли это в небольших устройствах? Скорее всего, любоваться красивыми именами не придется.

Если заглянуть в получившийся elf-файл, то можно увидеть много новых символов. Отсортируем их по размеру и посмотрим на самые жирные.

00000440cplus_demangle_operators0000049e__gxx_personality_v0000004c4 d_encoding000004fed_exprlist00000574_malloc_r0000060cd_print_mod000007f0d_type00000eec_dtoa_r00001b36_svfprintf_r0000306cd_print_comp

Много функций с префиксом d_* функции из файла cp-demangle.c библиотеки libiberty, которая, как я понимаю, встроена в gcc, и не так просто выставить ее за дверь.

Также имеются функции для обработки исключений (bad_function_call, std::unexpected, std::terminate)

_sbrk, malloc, free функции для работы с динамическим выделением памяти.

Результат ожидаемый флаги -fno-exceptions и -fno-rtti не спасают.

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

text

data

bss

67992

2504

144

Вторая std::function обошлась не так уж и дорого.

Показательно также то, сколько объектных файлов и из каких библиотек мы используем для этих случаев.

Для случая без std::function список короткий
libc_nano.alibg_nano.alibg_nano.a(lib_a-exit.o)libg_nano.a(lib_a-exit.o) (_global_impure_ptr)libg_nano.a(lib_a-impure.o)libg_nano.a(lib_a-init.o)libg_nano.a(lib_a-memcpy-stub.o)libg_nano.a(lib_a-memset.o)libgcc.alibm.alibstdc++_nano.a
Для случая с std::function список гораздо длиннее
libc.alibg.alibg.a(lib_a-__atexit.o)libg.a(lib_a-__call_atexit.o)libg.a(lib_a-__call_atexit.o) (__libc_fini_array)libg.a(lib_a-__call_atexit.o) (atexit)libg.a(lib_a-abort.o)libg.a(lib_a-abort.o) (_exit)libg.a(lib_a-abort.o) (raise)libg.a(lib_a-atexit.o)libg.a(lib_a-callocr.o)libg.a(lib_a-closer.o)libg.a(lib_a-closer.o) (_close)libg.a(lib_a-ctype_.o)libg.a(lib_a-cxa_atexit.o)libg.a(lib_a-cxa_atexit.o) (__register_exitproc)libg.a(lib_a-dtoa.o)libg.a(lib_a-dtoa.o) (_Balloc)libg.a(lib_a-dtoa.o) (__aeabi_ddiv)libg.a(lib_a-exit.o)libg.a(lib_a-exit.o) (__call_exitprocs)libg.a(lib_a-exit.o) (_global_impure_ptr)libg.a(lib_a-fclose.o)libg.a(lib_a-fflush.o)libg.a(lib_a-findfp.o)libg.a(lib_a-findfp.o) (__sread)libg.a(lib_a-findfp.o) (_fclose_r)libg.a(lib_a-findfp.o) (_fwalk)libg.a(lib_a-fini.o)libg.a(lib_a-fputc.o)libg.a(lib_a-fputc.o) (__retarget_lock_acquire_recursive)libg.a(lib_a-fputc.o) (__sinit)libg.a(lib_a-fputc.o) (_putc_r)libg.a(lib_a-fputs.o)libg.a(lib_a-fputs.o) (__sfvwrite_r)libg.a(lib_a-freer.o)libg.a(lib_a-fstatr.o)libg.a(lib_a-fstatr.o) (_fstat)libg.a(lib_a-fvwrite.o)libg.a(lib_a-fvwrite.o) (__swsetup_r)libg.a(lib_a-fvwrite.o) (_fflush_r)libg.a(lib_a-fvwrite.o) (_free_r)libg.a(lib_a-fvwrite.o) (_malloc_r)libg.a(lib_a-fvwrite.o) (_realloc_r)libg.a(lib_a-fvwrite.o) (memchr)libg.a(lib_a-fvwrite.o) (memmove)libg.a(lib_a-fwalk.o)libg.a(lib_a-fwrite.o)libg.a(lib_a-impure.o)libg.a(lib_a-init.o)libg.a(lib_a-isattyr.o)libg.a(lib_a-isattyr.o) (_isatty)libg.a(lib_a-locale.o)libg.a(lib_a-locale.o) (__ascii_mbtowc)libg.a(lib_a-locale.o) (__ascii_wctomb)libg.a(lib_a-locale.o) (_ctype_)libg.a(lib_a-localeconv.o)libg.a(lib_a-localeconv.o) (__global_locale)libg.a(lib_a-lock.o)libg.a(lib_a-lseekr.o)libg.a(lib_a-lseekr.o) (_lseek)libg.a(lib_a-makebuf.o)libg.a(lib_a-makebuf.o) (_fstat_r)libg.a(lib_a-makebuf.o) (_isatty_r)libg.a(lib_a-malloc.o)libg.a(lib_a-mallocr.o)libg.a(lib_a-mallocr.o) (__malloc_lock)libg.a(lib_a-mallocr.o) (_sbrk_r)libg.a(lib_a-mbtowc_r.o)libg.a(lib_a-memchr.o)libg.a(lib_a-memcmp.o)libg.a(lib_a-memcpy.o)libg.a(lib_a-memmove.o)libg.a(lib_a-memset.o)libg.a(lib_a-mlock.o)libg.a(lib_a-mprec.o)libg.a(lib_a-mprec.o) (_calloc_r)libg.a(lib_a-putc.o)libg.a(lib_a-putc.o) (__swbuf_r)libg.a(lib_a-readr.o)libg.a(lib_a-readr.o) (_read)libg.a(lib_a-realloc.o)libg.a(lib_a-reallocr.o)libg.a(lib_a-reent.o)libg.a(lib_a-s_frexp.o)libg.a(lib_a-sbrkr.o)libg.a(lib_a-sbrkr.o) (_sbrk)libg.a(lib_a-sbrkr.o) (errno)libg.a(lib_a-signal.o)libg.a(lib_a-signal.o) (_kill_r)libg.a(lib_a-signalr.o)libg.a(lib_a-signalr.o) (_getpid)libg.a(lib_a-signalr.o) (_kill)libg.a(lib_a-sprintf.o)libg.a(lib_a-sprintf.o) (_svfprintf_r)libg.a(lib_a-stdio.o)libg.a(lib_a-stdio.o) (_close_r)libg.a(lib_a-stdio.o) (_lseek_r)libg.a(lib_a-stdio.o) (_read_r)libg.a(lib_a-strcmp.o)libg.a(lib_a-strlen.o)libg.a(lib_a-strncmp.o)libg.a(lib_a-strncpy.o)libg.a(lib_a-svfiprintf.o)libg.a(lib_a-svfprintf.o)libg.a(lib_a-svfprintf.o) (__aeabi_d2iz)libg.a(lib_a-svfprintf.o) (__aeabi_dcmpeq)libg.a(lib_a-svfprintf.o) (__aeabi_dcmpun)libg.a(lib_a-svfprintf.o) (__aeabi_dmul)libg.a(lib_a-svfprintf.o) (__aeabi_dsub)libg.a(lib_a-svfprintf.o) (__aeabi_uldivmod)libg.a(lib_a-svfprintf.o) (__ssprint_r)libg.a(lib_a-svfprintf.o) (_dtoa_r)libg.a(lib_a-svfprintf.o) (_localeconv_r)libg.a(lib_a-svfprintf.o) (frexp)libg.a(lib_a-svfprintf.o) (strncpy)libg.a(lib_a-syswrite.o)libg.a(lib_a-syswrite.o) (_write_r)libg.a(lib_a-wbuf.o)libg.a(lib_a-wctomb_r.o)libg.a(lib_a-writer.o)libg.a(lib_a-writer.o) (_write)libg.a(lib_a-wsetup.o)libg.a(lib_a-wsetup.o) (__smakebuf_r)libgcc.alibgcc.a(_aeabi_uldivmod.o)libgcc.a(_aeabi_uldivmod.o) (__aeabi_ldiv0)libgcc.a(_aeabi_uldivmod.o) (__udivmoddi4)libgcc.a(_arm_addsubdf3.o)libgcc.a(_arm_cmpdf2.o)libgcc.a(_arm_fixdfsi.o)libgcc.a(_arm_muldf3.o)libgcc.a(_arm_muldivdf3.o)libgcc.a(_arm_unorddf2.o)libgcc.a(_dvmd_tls.o)libgcc.a(_udivmoddi4.o)libgcc.a(libunwind.o)libgcc.a(pr-support.o)libgcc.a(unwind-arm.o)libgcc.a(unwind-arm.o) (__gnu_unwind_execute)libgcc.a(unwind-arm.o) (restore_core_regs)libm.alibnosys.alibnosys.a(_exit.o)libnosys.a(close.o)libnosys.a(fstat.o)libnosys.a(getpid.o)libnosys.a(isatty.o)libnosys.a(kill.o)libnosys.a(lseek.o)libnosys.a(read.o)libnosys.a(sbrk.o)libnosys.a(write.o)libstdc++.alibstdc++.a(atexit_arm.o)libstdc++.a(atexit_arm.o) (__cxa_atexit)libstdc++.a(class_type_info.o)libstdc++.a(cp-demangle.o)libstdc++.a(cp-demangle.o) (memcmp)libstdc++.a(cp-demangle.o) (realloc)libstdc++.a(cp-demangle.o) (sprintf)libstdc++.a(cp-demangle.o) (strlen)libstdc++.a(cp-demangle.o) (strncmp)libstdc++.a(del_op.o)libstdc++.a(del_ops.o)libstdc++.a(eh_alloc.o)libstdc++.a(eh_alloc.o) (std::terminate())libstdc++.a(eh_alloc.o) (malloc)libstdc++.a(eh_arm.o)libstdc++.a(eh_call.o)libstdc++.a(eh_call.o) (__cxa_get_globals_fast)libstdc++.a(eh_catch.o)libstdc++.a(eh_exception.o)libstdc++.a(eh_exception.o) (operator delete(void*, unsigned int))libstdc++.a(eh_exception.o) (__cxa_pure_virtual)libstdc++.a(eh_globals.o)libstdc++.a(eh_personality.o)libstdc++.a(eh_term_handler.o)libstdc++.a(eh_terminate.o)libstdc++.a(eh_terminate.o) (__cxxabiv1::__terminate_handler)libstdc++.a(eh_terminate.o) (__cxxabiv1::__unexpected_handler)libstdc++.a(eh_terminate.o) (__gnu_cxx::__verbose_terminate_handler())libstdc++.a(eh_terminate.o) (__cxa_begin_catch)libstdc++.a(eh_terminate.o) (__cxa_call_unexpected)libstdc++.a(eh_terminate.o) (__cxa_end_cleanup)libstdc++.a(eh_terminate.o) (__gxx_personality_v0)libstdc++.a(eh_terminate.o) (abort)libstdc++.a(eh_throw.o)libstdc++.a(eh_type.o)libstdc++.a(eh_unex_handler.o)libstdc++.a(functional.o)libstdc++.a(functional.o) (std::exception::~exception())libstdc++.a(functional.o) (vtable for __cxxabiv1::__si_class_type_info)libstdc++.a(functional.o) (operator delete(void*))libstdc++.a(functional.o) (__cxa_allocate_exception)libstdc++.a(functional.o) (__cxa_throw)libstdc++.a(pure.o)libstdc++.a(pure.o) (write)libstdc++.a(si_class_type_info.o)libstdc++.a(si_class_type_info.o) (__cxxabiv1::__class_type_info::__do_upcast(__cxxabiv1::__class_type_info const*, void**) const)libstdc++.a(si_class_type_info.o) (std::type_info::__is_pointer_p() const)libstdc++.a(tinfo.o)libstdc++.a(tinfo.o) (strcmp)libstdc++.a(vterminate.o)libstdc++.a(vterminate.o) (__cxa_current_exception_type)libstdc++.a(vterminate.o) (__cxa_demangle)libstdc++.a(vterminate.o) (fputc)libstdc++.a(vterminate.o) (fputs)libstdc++.a(vterminate.o) (fwrite)

А что IAR?

Все устроено немного иначе. Он не требует явного указания спецификации nano или nosys, ему не нужны никакие заглушки. Этот компилятор все знает и сделает все в лучшем виде, не нужно ему мешать.

text

ro data

rw data

2958

38

548

О, добавилось всего-то каких-то жалких 3Кб кода! Это успех. Фанат GCC во мне заволновался, почему так мало? Смотрим, что же добавил нам IAR.

Добавились символы из двух новых объектных файлов:

dlmalloc.o 1'404 496

heaptramp0.o 4

Конечно же, появились функции и данные для работы с динамической памятью, как в случае GCC.

Естественно, никаких выделений в куче нет, но IAR приготовился: видно, что он создал структуру gm (global malloc: a malloc_state holds all of the bookkeeping for a space) и некоторые функции для работы с этой структурой.

Объектный файл того юнита, в котором была добавлена функция, тоже ощутимо располнел:

до

main.cpp.obj 3'218 412 36'924

после

main.cpp.obj 4'746 451 36'964

Файл прибавил более 1Кб. Появилась std::function, ее сопряжение с лямбдой, аллокаторы.

Добавление второго такого функционального объекта в другую единицу трансляции дает нам очередной прирост:

text

ro data

rw data

3 998

82

600

Прибавили более 1Кб. Т.е. каждая новая функция добавляет нам по килобайту кода в каждой единице трансляции. Это не слишком помогает экономить: в проекте не один и не два колбэка, больше десятка наберется. Хорошо, что большинство таких функций имеют сигнатуру void(*)(void) или void(*)(uint8_t *, int), мы можем быстро накидать свою реализацию std::function без особых проблем. Что я и сделал.

Убедившись, что моя примитивная реализация function работает и не требует много памяти, перенес ее в проект, заменив все std::function, до которых смог дотянуться. С чувством выполненного долга я отправился на набережную любоваться закатом.

Дома меня поджидало письмо от коллеги, преисполненное благодарности. Он писал, что благодаря отказу от богомерзких std::function проект сильно схуднул, мы все молодцы! Сочившееся из меня самодовольство брызнуло во все стороны. Прилагался также классический рекламно-наглядный график до-после, вопивший об уменьшении размера отладочной версии прошивки аж на 30 процентов. В абсолютных величинах цифра была еще страшнее, это, на минуточку, целых 150 килобайт! Что-о-о-о? Улыбка довольного кота медленно отделилась от лица и стремительным домкратом полетела вниз, пробивая перекрытия. В коде просто нет столько колбэков, чтоб хоть как-то можно было оправдать этот странный феномен. В чем дело?

Смотря на сонное спокойствие темной улицы, раскинувшейся внизу, я твердо решил, что не сомкну глаз, пока не отыщу ответ. Проснувшись утром, в первую очередь сравнил два разных elf-файла: до и после замены std::function. Тут все стало очевидно!

В одном забытом богом и кем-то из разработчиков заголовочном файле были такие строчки:

using Handler = std::function<void()>;static auto global_handlers = std::pair<Handler, Handler> {};

И это в заголовочном файле. Насколько я смог понять, эти функции использовались для создания бледного подобия shared_ptr, увеличивая и уменьшая счетчик ссылок на ресурс в конструкторе и деструкторе соответственно. В некотором роде такой подход даже работал, только для каждой единицы трансляции отдельно. Впрочем, это почти нигде уже не использовалось, но за каждое включение заголовочного файла приходилось платить примерно одним килобайтом памяти. А включений было предостаточно. Да, в релизной версии почти все вырезалось, но в отладочной оптимизация выключена и компилятор честно создавал все эти бесполезные объекты.

Понятно, чего хотел добиться неизвестный мне автор, и это вполне могло получиться. Начиная с 17-го стандарта, в заголовочном файле можно разместить некие глобальные объекты, которые будут видны и в других единицах трансляции. Достаточно вместо static написать inline. Это работает даже для IAR. Впрочем, я не стал изменять себе и просто все убрал.

Вот тут я все же не удержатся от объяснения очевидных вещей

Если у вас несколько единиц трансляции и создание глобального объекта вынесено в заголовочный файл, то при сборке проекта вы неизбежно получите ошибку multiple definition. Ежели добавить static, как сделал неизвестный мне разработчик, то все пройдет гладко, но в итоге будет занято несколько участков памяти и от глобальности ничего не останется.

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

// a.h

#pragma once

int a();

// a.cpp

#include "a.h"

#include "c.hpp"

int a() { return cglob * 2; }

// b.h

#pragma once

int b();

// b.cpp

#include "b.h"

#include "c.hpp"

int b() { return cglob * 4; }

// main.cpp

#include "a.h"

#include "b.h"

int main() { return a() + b(); }

// c.hpp

#pragma once

int c_glob = 0;

Пробуем собрать наш небольшой и бесполезный проект.

$ g++ a.cpp b.cpp main.cpp -o test

/usr/lib/gcc/x8664-pc-cygwin/10/../../../../x8664-pc-cygwin/bin/ld: /tmp/cccXOcPm.o:b.cpp:(.bss+0x0): повторное определение cglob; /tmp/ccjo1M9W.o:a.cpp:(.bss+0x0): здесь первое определение

collect2: ошибка: выполнение ld завершилось с кодом возврата 1

Неожиданно получаем ошибку. Так, теперь меняем содержимое файла c.hpp:

static int c_glob = 0;

Вот теперь все собирается! Полюбуемся на символы:

$ objdump.exe -t test.exe | grep glob | c++filt.exe

[ 48](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000000 c_glob

[ 65](sec 7)(fl 0x00)(ty 0)(scl 3) (nx 0) 0x0000000000000010 c_glob

Вот и второй лишний символ, что и требовалось доказать.

А ежели изменить c.hpp таким образом:

inline int c_glob = 0;

Объект c_glob будет единственным, все единицы трансляции будут ссылаться на один и тот же объект.

Вывод будет весьма банален: нужно понимать, что делаешь... и соответствовать стандартам SIL!

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

Всем спасибо, всем удачи!

Подробнее..

Перевод Находим и устраняем уязвимости бинарных файлов в Linux с утилитой checksec и компилятором gcc

12.06.2021 16:15:59 | Автор: admin

Изображение: Internet Archive Book Images. Modified by Opensource.com. CC BY-SA 4.0

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

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

Checksec это простая утилита, позволяющая определить, какие свойства были включены при компиляции. В этой статье я расскажу:

  • как использовать утилиту checksec для поиска уязвимостей;
  • как использовать компилятор gcc для устранения найденных уязвимостей.

Установка checksec


Для Fedora OS и других систем на базе RPM:

$ sudo dnf install checksec

Для систем на базе Debian используйте apt.

Быстрый старт с checksec


Утилита сhecksec состоит из единственного скриптового файла, который, впрочем, довольно большой. Благодаря такой прозрачности вы можете узнать, какие системные команды для поиска уязвимостей в бинарных файлах выполняются под капотом:

$ file /usr/bin/checksec/usr/bin/checksec: Bourne-Again shell script, ASCII text executable, with very long lines$ wc -l /usr/bin/checksec2111 /usr/bin/checksec

Давайте запустим checksec для утилиты просмотра содержимого каталогов (ls):

$ checksec --file=/usr/bin/ls<strong>RELRO      STACK CANARY   NX      PIE       RPATH   RUNPATH   Symbols    FORTIFY Fortified    Fortifiable  FILE</strong>Full RELRO   Canary found   NX enabled  PIE enabled   No RPATH  No RUNPATH  No Symbols    Yes  5    17       /usr/bin/ls

Выполнив команду в терминале, вы получите отчёт о том, какими полезными свойствами обладает этот бинарник, и какими не обладает.

Первая строка это шапка таблицы, в которой перечислены различные свойства безопасности RELRO, STACK CANARY, NX и так далее. Вторая строка показывает значения этих свойств для бинарного файла утилиты ls.

Hello, бинарник!


Я скомпилирую бинарный файл из простейшего кода на языке С:

#include <stdio.h>int main(){printf(Hello World\n);return 0;}

Обратите внимание, что пока я не передал компилятору ни одного флага, за исключением -o (он не относится к делу, а просто говорит, куда выводить результат компиляции):

$ gcc hello.c -o hello$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped$ ./helloHello World

Теперь запущу утилиту checksec для моего бинарника. Некоторые свойства отличаются от свойств

ls (для него я запускал утилиту выше):$ checksec --file=./hello<strong>RELRO      STACK CANARY   NX      PIE       RPATH   RUNPATH   Symbols     FORTIFY Fortified    Fortifiable   FILE</strong>Partial RELRO  No canary found  NX enabled  No PIE     No RPATH  No RUNPATH  85) Symbols    No  0    0./hello

Checksec позволяет использовать различные форматы вывода, которые вы можете указать с помощью опции --output. Я выберу формат JSON и сделаю вывод более наглядным с помощью утилиты jq:

$ checksec --file=./hello --output=json | jq{./hello: {relro: partial,canary: no,nx: yes,pie: no,rpath: no,runpath: no,symbols: yes,fortify_source: no,fortified: 0,fortify-able: 0}}

Анализ (checksec) и устранение (gcc) уязвимостей


Созданный выше бинарный файл имеет несколько свойств, определяющих, скажем так, степень его уязвимости. Я сравню свойства этого файла со свойствами бинарника ls (тоже указаны выше) и объясню, как это сделать с помощью утилиты checksec.

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

1. Отладочные символы


Начну с простого. Во время компиляции в бинарный файл включаются определённые символы. Эти символы используются при разработке программного обеспечения: они нужны для отладки и исправления ошибок.

Отладочные символы обычно удаляются из той версии бинарника, которую разработчики выпускают для широкого использования. Это никак не влияет на работу программы. Такая очистка (она обозначена словом strip) часто выполняется для экономии места, так как файл становится легче после удаления символов. А в проприетарном программном обеспечении эти символы часто удаляют ещё и потому, что у злоумышленников есть возможность прочитать их в двоичном формате и использовать в своих целях.

Сhecksec показывает, что отладочные символы присутствуют в моём бинарнике, но их нет в файле ls.

$ checksec --file=/bin/ls --output=json | jq | grep symbolssymbols: no,$ checksec --file=./hello --output=json | jq | grep symbolssymbols: yes,


То же самое может показать запуск команды file. Символы не удалены (not stripped).

$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, <strong>not stripped</strong>

Как работает checksec


Запустим эту команду с опцией --debug:

$ checksec --debug --file=./hello

Так как утилита checksec это один длинный скрипт, то для его изучения можно использовать функции Bash. Выведем команды, которые запускает скрипт для моего файла hello:

$ bash -x /usr/bin/checksec --file=./hello

Особое внимание обратите на echo_message вывод сообщения о том, содержит ли бинарник отладочные символы:

+ readelf -W --symbols ./hello+ grep -q '\.symtab'+ echo_message '\033[31m96) Symbols\t\033[m ' Symbols, ' symbols=yes' 'symbols:yes,'

Утилита checksec использует для чтения двоичного файла команду readelf со специальным флагом --symbols. Она выводит все отладочные символы, которые содержатся в бинарнике.

$ readelf -W --symbols ./hello

Из содержимого раздела .symtab можно узнать количество найденных символов:

$ readelf -W --symbols ./hello | grep -i symtab

Как удалить отладочные символы после компиляции


В этом нам поможет утилита strip.

$ gcc hello.c -o hello$$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=322037496cf6a2029dcdcf68649a4ebc63780138, for GNU/Linux 3.2.0, <strong>not stripped</strong>$$ strip hello$$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=322037496cf6a2029dcdcf68649a4ebc63780138, for GNU/Linux 3.2.0, <strong>stripped</strong>

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


При компиляции используйте флаг -s:

$ gcc -s hello.c -o hello$$ file hellohello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=247de82a8ad84e7d8f20751ce79ea9e0cf4bd263, for GNU/Linux 3.2.0, <strong>stripped</strong>

Убедиться, что символы удалены, можно и с помощью утилиты checksec:

$ checksec --file=./hello --output=json | jq | grep symbolssymbols: no,

2. Canary


Canary (осведомители) это секретные значения, которые хранятся в стеке между буфером и управляющими данными. Они используются для защиты от атаки переполнения буфера: если эти значения оказываются изменены, то стоит бить тревогу. Когда приложение запускается, для него создаётся свой стек. В данном случае это просто структура данных с операциями push и pop. Злоумышленник может подготовить вредоносные данные и записать их в стек. В этом случае буфер может быть переполнен, а стек повреждён. В дальнейшем это приведёт к сбою работы программы. Анализ значений canary позволяет быстро понять, что произошёл взлом и принять меры.

$ checksec --file=/bin/ls --output=json | jq | grep canarycanary: yes,$$ checksec --file=./hello --output=json | jq | grep canarycanary: no,$Чтобы проверить, включен ли механизм canary, скрипт checksec запускает следующую команду:$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'

Включаем canary


Для этого при компиляции используем флаг -stack-protector-all:

$ gcc -fstack-protector-all hello.c -o hello$ checksec --file=./hello --output=json | jq | grep canarycanary: yes,

Вот теперь сhecksec может с чистой совестью сообщить нам, что механизм canary включён:

$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'2: 0000000000000000   0 FUNC  GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (3)83: 0000000000000000   0 FUNC  GLOBAL DEFAULT UND __stack_chk_fail@@GLIBC_2.4$

3. PIE


Включённое свойство PIE позволяет произвольно размещать в памяти исполняемый код независимо от его абсолютного адреса:

PIE (Position Independent Executable) исполняемый позиционно-независимый код. Возможность предсказать, где и какие области памяти находятся в адресном пространстве процесса играет на руку взломщикам. Пользовательские программы загружаются и выполняются с предопределённого адреса виртуальной памяти процесса, если они не скомпилированы с опцией PIE. Использование PIE позволяет операционной системе загружать секции исполняемого кода в произвольные участки памяти, что существенно усложняет взлом.

$ checksec --file=/bin/ls --output=json | jq | grep piepie: yes,$ checksec --file=./hello --output=json | jq | grep piepie: no,

Часто свойство PIE включают только при компиляции библиотек. В выводе ниже hello помечен как LSB executable, а файл стандартной библиотеки libc (.so) как LSB shared object:

$ file hellohello: ELF 64-bit <strong>LSB executable</strong>, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped$ file /lib64/libc-2.32.so/lib64/libc-2.32.so: ELF 64-bit <strong>LSB shared object</strong>, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4a7fb374097fb927fb93d35ef98ba89262d0c4a4, for GNU/Linux 3.2.0, not stripped

Checksec получает эту информацию следующим образом:

$ readelf -W -h ./hello | grep EXECType:               EXEC (Executable file)

Если вы запустите эту же команду для библиотеки, то вместо EXEC увидите DYN:

$ readelf -W -h /lib64/libc-2.32.so | grep DYNType:               DYN (Shared object file)

Включаем PIE


При компиляции программы нужно указать следующие флаги:

$ gcc -pie -fpie hello.c -o hello

Чтобы убедиться, что свойство PIE включено, выполним такую команду:

$ checksec --file=./hello --output=json | jq | grep piepie: yes,$

Теперь у нашего бинарного файла (hello) тип сменится с EXEC на DYN:

$ file hellohello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bb039adf2530d97e02f534a94f0f668cd540f940, for GNU/Linux 3.2.0, not stripped$ readelf -W -h ./hello | grep DYNType:               DYN (Shared object file)

4. NX


Средства операционной системы и процессора позволяют гибко настраивать права доступа к страницам виртуальной памяти. Включив свойство NX (No Execute), мы можем запретить воспринимать данные в качестве инструкций процессора. Часто при атаках переполнения буфера злоумышленники помещают код в стек, а затем пытаются его выполнить. Однако, если запретить выполнение кода в этих сегментах памяти, можно предотвратить такие атаки. При обычной компиляции с использованием gcc это свойство включено по умолчанию:

$ checksec --file=/bin/ls --output=json | jq | grep nxnx: yes,$ checksec --file=./hello --output=json | jq | grep nxnx: yes,

Чтобы получить информацию о свойстве NX, checksec вновь использует команду readelf. В данном случае RW означает, что стек доступен для чтения и записи. Но так как в этой комбинации отсутствует символ E, на выполнение кода из этого стека стоит запрет:

$ readelf -W -l ./hello | grep GNU_STACKGNU_STACK   0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10

Отключение NX


Отключать свойство NX не рекомендуется, но сделать это можно так:

$ gcc -z execstack hello.c -o hello$ checksec --file=./hello --output=json | jq | grep nxnx: no,

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

$ readelf -W -l ./hello | grep GNU_STACKGNU_STACK   0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10

5. RELRO


В динамически слинкованных бинарниках для вызова функций из библиотек используется специальная таблица GOT (Global Offset Table). К этой таблице обращаются бинарные файлы формата ELF (Executable Linkable Format). Когда защита RELRO (Relocation Read-Only) включена, таблица GOT становится доступной только для чтения. Это позволяет защититься от некоторых видов атак, изменяющих записи таблицы:

$ checksec --file=/bin/ls --output=json | jq | grep relrorelro: full,$ checksec --file=./hello --output=json | jq | grep relrorelro: partial,

В данном случае включено только одно из свойств RELRO, поэтому checksec выводит значение partial. Для отображения настроек сhecksec использует команду readelf.

$ readelf -W -l ./hello | grep GNU_RELROGNU_RELRO   0x002e10 0x0000000000403e10 0x0000000000403e10 0x0001f0 0x0001f0 R  0x1$ readelf -W -d ./hello | grep BIND_NOW

Включаем полную защиту (FULL RELRO)


Для этого при компиляции нужно использовать соответствующие флаги:

$ gcc -Wl,-z,relro,-z,now hello.c -o hello$ checksec --file=./hello --output=json | jq | grep relrorelro: full,

Всё, теперь наш бинарник получил почётное звание FULL RELRO:

$ readelf -W -l ./hello | grep GNU_RELROGNU_RELRO   0x002dd0 0x0000000000403dd0 0x0000000000403dd0 0x000230 0x000230 R  0x1$ readelf -W -d ./hello | grep BIND_NOW0x0000000000000018 (BIND_NOW) 

Другие возможности checksec


Тему безопасности можно изучать бесконечно. Даже, рассказывая про простую утилиту checksec в этой статье, я не смогу охватить всё. Тем не менее, упомяну ещё несколько интересных возможностей.

Проверка нескольких файлов


Нет необходимости для каждого файла запускать отдельную команду. Можно запустить одну команду сразу для нескольких бинарных файлов:

$ checksec --dir=/usr/bin

Проверка процессов


Утилита checksec также позволяет анализировать безопасность процессов. Следующая команда отображает свойства всех запущенных программ в вашей системе (для этого нужно использовать опцию --proc-all):

$ checksec --proc-all

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

$ checksec --proc=bash

Проверка ядра


Аналогично вы можете анализировать уязвимости в ядре вашей системы.

$ checksec --kernel

Предупреждён значит вооружён


Подробно изучите свойства безопасности и попытайтесь понять, на что именно влияет каждое из них и какие виды атак это позволяет предотвратить. Checksec вам в помощь!



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

БудниDevOpscобираемgcc9.3.1подCentOS8

21.04.2021 16:08:09 | Автор: admin

В Северстали внедрены большие корпоративные системы, такие какSAPилиQMET,но есть и много разных задач, которые закрывает собственная разработка,и задачи у этой разработки редко бывают простыми. А значит, и требования к инструментам разработки бывают достаточно специфическими.Что делать, если вашим разработчикам потребовалсяgcc-9подCentOS,а его нет в общедоступных репозиториях? Конечно, засучить рукава исоздать требуемые пакеты. Вот только задача эта выглядит просто только на первый взгляд.

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

Stage1.Собственно сборкаgcc

Здесь казалось бывсё просто: берёмgcc.specот пакетаgcc-8.3.1, меняем 8 на 9, запускаемrpmbuildbb,долго ждём? Да, но нет. Для начала придётся пересмотреть и поправить все патчи, а заодно ещё и поставитьbinutilsпосвежее,благо этонесложно. Потом, мы же не просто так компилятор меняем, нам же ещё какие-нибудьnvptx-toolsподавай, а это значит, что когда сборка закончитсяи начнется тестирование, тесты вlibgomp,завязанные на выгрузку кода, начнут виснуть и застревать в разных странных позах.

Решения тут может быть два:

  1. Консервативное: сказать разработчикам извините, нешмогла и отключитьnvptx-tools.

  1. Экспериментальное: предупредить разработчиков, чтоnvptxони используют на свой страх и риск, после чего запуститьrpmbuild, дождаться застревания в тестах и отстреливать их руками. Вы получите массовыеtestsfailedв результатах, но искомые пакеты будут собраны.

Stage 2. Packagelibgcc.i686 has inferiorarchitecture

Итак,мыскладываемвсеэтизамечательныеgcc-9.3.1-3.el8.x86_64.rpm, gcc-offload-nvptx-9.3.1-3.el8.x86_64.rpmит.д.ит.п.вотдельныйрепозиторий,индексируемего,подключаемв/etc/yum.repos.d,говоримdnfupdateиНет, ну вы правда думали, что он возьмёт и сразупоставится? Как бы не так. Как известно, 64-разрядные дистрибутивы семействDebianиRedHatдля семейств процессоровx86 в глубине души немножечко 32-разрядные (то есть, по умолчанию существуют в режимеmultilib), и поэтому вам потребуется илиобъявитьmultilibпережитком прошлогои снести 32-разрядные библиотеки системного компилятора, или создать соответствующие пакеты (libgcc.i686,libgfortran.i686,libgomp.i686,libquadmath.i686 иlibstdc++.i686) для новой версии. Как ни забавно, но решения тут тоже может быть два:

  1. Рекомендованное производителем: поставитьmockи выполнитьполнуюсборку дляi686, наступив на все сопутствующие грабли (nvptx,например, лучше сразу выключить).

  1. Ленивое на скорую руку: дело в том, что 32-битные версии библиотек на самом деле собираются вместе с 64-битными,ноникудав итогене попадают. В стандартной версииgcc.specони вообще тупо удаляются, но это как раз недолго изакомментировать. А потом скопироватьgcc.specвlibgcc-i686.spec, вымарать из него всю секцию %build, а в %installнаписать примерно следующее:

%install rm -rf %{buildroot} mkdir -p %{buildroot} tar cf - -C %{_buildrootdir}/%{name}-%{version}-%{release}.x86_64 usr | tar xf - -C %{buildroot}  FULLPATH=%{buildroot}%{_prefix}/lib/gcc/%{gcc_target_platform}/%{gcc_major} FULLEPATH=%{buildroot}%{_prefix}/libexec/gcc/%{gcc_target_platform}/%{gcc_major}  # fix some things mkdir -p %{buildroot}/%{_lib} mv -f %{buildroot}%{_prefix}/%{_lib}/libgcc_s.so.1 %{buildroot}/%{_lib}/libgcc_s-%{gcc_major}-%{DATE}.so.1 chmod 755 %{buildroot}/%{_lib}/libgcc_s-%{gcc_major}-%{DATE}.so.1 ln -sf libgcc_s-%{gcc_major}-%{DATE}.so.1 %{buildroot}/%{_lib}/libgcc_s.so.1  mkdir -p %{buildroot}%{_datadir}/gdb/auto-load/%{_prefix}/%{_lib} mv -f %{buildroot}%{_prefix}/%{_lib}/libstdc++*gdb.py* \       %{buildroot}%{_datadir}/gdb/auto-load/%{_prefix}/%{_lib}/ pushd %{name}-%{version}-%{DATE}/libstdc++-v3/python  for i in `find . -name \*.py`; do   touch -r $i %{buildroot}%{_prefix}/share/gcc-%{gcc_major}/python/$i done touch -r hook.in %{buildroot}%{_datadir}/gdb/auto-load/%{_prefix}/%{_lib}/libstdc++*gdb.py popd  for f in `find %{buildroot}%{_prefix}/share/gcc-%{gcc_major}/python/ \        %{buildroot}%{_datadir}/gdb/auto-load/%{_prefix}/%{_lib}/ -name \*.py`; do   r=${f/$RPM_BUILD_ROOT/}   %{__python3} -c 'import py_compile; py_compile.compile("'$f'", dfile="'$r'")'   %{__python3} -O -c 'import py_compile; py_compile.compile("'$f'", dfile="'$r'")' done  rm -rf %{buildroot}%{_prefix}/%{_lib}/%{name} 

Теперь достаточно сказатьrpmbuildbblibgcc-i686.specгде-то в соседнем терминале, покаgccразвлекается своимtorture,ивуаля, наши32-битные пакеты у нас в кармане (в смысле, в $RPM_BUILD_ROOT/RPMS/i686).Мы копируем их в наш репозиторий, индексируем его, запускаемdnfmakecacherepogcc-9 &&dnfupdateи Нет, обновить компилятор всё ещё нельзя.

Stage3.Annobinиlibtool

Те, кто внимательно смотрит на параметры сборкина линуксах линеекRHELиCentOS, могли заметить, что по умолчанию вgccподключен плагинannobin.У этого плагина есть неприятная привычка привязываться к версии компилятора, поэтому его придется пересобрать. Детали в принципе изложены в самомannobin.specв комментариях, поэтому отметим только, что пересобрать его придётся минимум дважды: сперва, используя ещё системныйgcc8.3.1,поправить требование к версииgcc,чтобыgcc< %{gcc_next}превратилось вgcc<=%{gcc_next}, потом, уже заменивgcc,пересобрать заново, вернув требованиеgcc< %{gcc_next}ираскомментировавстроку%undefine_annotated_build иначе вообще ничего собираться не будет. Ну и для чистоты можно пересобрать в третий раз, вернув_annotated_buildна место, а предыдущие две версии пакетов (переходную и без аннотациибинарей) из репозитория удалить.

Остаетсяlibtool.Этот джентльмен тоже жёстко привязывается к версииgcc,но к счастью, зависимость эта односторонняя, поэтомуlibtoolможно просто удалить перед заменойgcc,затем собрать его заново новымgccи добавить соответствующий пакет в наш репозиторийgcc-9.

Как ни странно, на этом всё. У нас есть отдельный репозиторий, подключив который, можно поставить требуемую версию компилятора, а при необходимости вернуть обратно системную версию (командойdnfdowngradegcc),хотя необходимости такой у нас не возникало.

Хабравчане-девопсы, а какие у вас бывали нестандартные запросы от разработчиков?

Подробнее..

Сравнение компиляторов ARMCC, IAR и GCC

13.11.2020 00:08:03 | Автор: admin
image

Я работаю над одним проектом, который с большим трудом вмещается в 64к памяти микроконтроллера. И подумалось мне, что возможно, пора взглянуть на разные компиляторы, чтобы выбрать какой более агрессивно может уменьшить размер программы.

Представляю вашему вниманию небольшое сравнение.

Для теста я сделал проект в Cube MX, который включает в себя USB_DEVICE и Mass Storage Class. Это довольно большие библиотеки для теста.

Получившийся main.c выглядит примерно так:

int main() {  HAL_Init();  SystemClock_Config();  MX_GPIO_Init();  MX_USB_DEVICE_Init();}

Дефайн USBD_DEBUG_LEVEL установлен в 0, чтобы отладочные сообщения USB стека не требовали наличия printf

Подопытные компиляторы:

  • IAR EWARM 8.32.1
  • arm-none-eab-gcc 7-2018-q2-update (среда STM32 Cube IDE 1.4.2)
  • ARMCC v5.06 update 7 (среда Keil uVision 5.32)
  • ARMCC v6.14.1 (среда Keil uVision 5.32)

Настройки IAR:

  • Оптимизация по размеру
  • Run-time библиотека NORMAL
  • Без low level IO (отключен printf)
  • Включены оптимизации линкера: Inline small routines, merge duplicate sections

Настройки GCC:

  • Reduced runtime library --specs=nano.specs
  • Optimize for size -Os
  • Place functions in their own sections --ffunction-sections
  • Place data in their own sections --ffdata-sections
  • Discard unused sections -Wl, --gc-sections

Настройки armcc5:

  • Use micro lib
  • Use cross module optimization
  • Optimization -O3
  • One ELF section per function --split_sections

Настройки armcc6:

  • Use micro lib
  • Optimization image size -Oz
  • One ELF section per function --split_sections


GCC armcc5 IAR armcc6
Размер прошивки 14036 13548 12997 12984


Как видно, armcc6 на самую малость лучше IAR. За ним идет armcc5 с отставанием на 4%, а gcc отстает от лидера на 8%.

Надо отметить, что опция KEIL Use cross module optimization Значительно увеличила время компиляции, но ни чуть не уменьшила размер кода.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru